Compare commits

...
Sign in to create a new pull request.

12 commits
main ... main

Author SHA1 Message Date
139f5f6d9c run nix fmt 2025-09-10 14:14:13 +02:00
7f6d7cc0c4 Enhance tor-family e2e test to validate RSA and ED25519 relay identity fixes 2025-09-10 14:13:18 +02:00
43c442ad20 Update obfs4 e2e test to include new ED25519 Tor format and RSA hashed fingerprint outputs 2025-09-10 14:06:28 +02:00
f1cccbe22b Add Tor-specific binary format for ED25519 keys
fixes #5
2025-09-10 14:00:12 +02:00
5bf771ac96 Fix Ed25519 fingerprint to use base64 encoded public key bytes
fixes #4
2025-09-10 13:49:21 +02:00
b406226f0f add aarch64-darwin to supported systems for dev shell 2025-09-10 13:49:21 +02:00
4eadc8416e Add hashed fingerprint functionality to RSA relay identity keys
fixes #3
2025-09-10 13:45:39 +02:00
62b243c8e4 Fix RSA fingerprint generation to use PKCS1 encoding and uppercase format
related #2
2025-09-10 13:45:39 +02:00
83df31ec80 Fix incorrect RSA identity key fingerprint generation
The fingerprint calculation was using PKIX encoding instead of the
required PKCS1 DER encoding for RSA public keys. This affected both
the relay identity resource and obfs4 node ID derivation.

- Use x509.MarshalPKCS1PublicKey instead of x509.MarshalPKIXPublicKey
- Add test case with known fingerprint vector to prevent regression
- Update both generateFingerprints and deriveNodeIdFromRsaKey functions

fixes #2
2025-09-10 13:45:39 +02:00
005634ff1c Update README 2025-09-10 13:45:39 +02:00
f60d0d655b Update documentation 2025-09-10 13:45:39 +02:00
53f019906e Implement the tor_family_identity resource 2025-09-10 13:45:39 +02:00
32 changed files with 908 additions and 111 deletions

20
.gitignore vendored
View file

@ -1,6 +1,6 @@
.*
*.dll *.dll
*.exe *.exe
.DS_Store
example.tf example.tf
terraform.tfplan terraform.tfplan
terraform.tfstate terraform.tfstate
@ -15,12 +15,9 @@ website/node_modules
.vagrant/ .vagrant/
*.backup *.backup
./*.tfstate ./*.tfstate
.terraform/
*.log *.log
*.bak *.bak
*~ *~
.*.swp
.idea
*.iml *.iml
*.test *.test
*.iml *.iml
@ -35,9 +32,14 @@ website/vendor
*.winfile eol=crlf *.winfile eol=crlf
terraform-provider-tor terraform-provider-tor
dev/ dev/
CLAUDE.md *.md
!README.md
!CHANGELOG.md
!CONTRIBUTING.md
extra extra
.direnv e2e-tests/**/*tfstate*
.claude e2e-tests/**/providers
e2e-test/*tfstate* e2e-tests/**/.terraform.lock.hcl
e2e-test/.terraformrc e2e-tests/**/torrc
e2e-tests/**/*.secret_family_key
e2e-tests/**/data

View file

@ -38,6 +38,14 @@ make lint
# Run acceptance tests (creates real resources) # Run acceptance tests (creates real resources)
make testacc make testacc
# Run e2e tests
make clean build e2e/tor-family # Run the tor-family test with latest binary
make clean build e2e/obfs4 # Run the obfs4 e2e test with latest binary
make e2e # Run all e2e tests
# Important: Always use 'make clean build' before e2e tests to ensure
# the test uses the latest provider binary
``` ```
## Project Structure ## Project Structure
@ -49,6 +57,7 @@ make testacc
│ ├── provider/ # Provider configuration examples │ ├── provider/ # Provider configuration examples
│ └── resources/ # Resource examples │ └── resources/ # Resource examples
├── docs/ # Generated documentation ├── docs/ # Generated documentation
├── e2e-tests/ # End-to-end tests
``` ```
## Security Considerations ## Security Considerations

View file

@ -1,13 +1,19 @@
default: fmt lint install generate default: fmt lint install generate
clean: dist-clean:
go clean -cache -modcache -testcache go clean -cache -modcache -testcache
clean:
rm -f ./terraform-provider-tor ./provider.test
find e2e-tests -type f -name '*.tfstate*' -delete
find e2e-tests -type d -name '.terraform' -prune -exec rm -rf ./{} \;
find e2e-tests -type d -name 'providers' -prune -exec rm -rf ./{} \;
build: build:
go build -v ./... go build
install: build install: build
go install -v ./... go install
lint: lint:
go mod tidy go mod tidy
@ -33,16 +39,11 @@ test:
testacc: testacc:
TF_ACC=1 go test -v -cover -timeout 120m ./... TF_ACC=1 go test -v -cover -timeout 120m ./...
e2e: e2e/%:
@echo "Running end-to-end test..." @echo "Executing end-to-end test: $*"
cd e2e-test && \ cd e2e-tests/$* && ./test.sh
rm -rf .terraform/ .terraform.lock.hcl terraform.tfstate* && \
./setup-dev.sh && \ e2e: clean build e2e/obfs4 e2e/tor-family
TF_CLI_CONFIG_FILE=.terraformrc tofu plan && \
TF_CLI_CONFIG_FILE=.terraformrc tofu apply -auto-approve && \
echo "✓ E2E test passed! Cleaning up..." && \
TF_CLI_CONFIG_FILE=.terraformrc tofu destroy -auto-approve && \
echo "✓ E2E test completed successfully"
ci: ci:
@echo "Running CI pipeline..." @echo "Running CI pipeline..."

View file

@ -38,10 +38,28 @@ terraform {
provider "tor" {} provider "tor" {}
# Generate family (tor 0.4.9.2-alpha or later)
resource "tor_family_identity" "example" {
family_name = "MyFamily"
}
output "family_id" {
description = "Family ID for the bridge"
value = tor_family_identity.this.id
}
resource "local_sensitive_file" "family_key" {
content_base64 = tor_family_identity.this.secret_key
filename = "./data/keys/MyKey.secret_family_key"
file_permission = "0600"
}
# Generate relay identity keys # Generate relay identity keys
resource "tor_relay_identity_rsa" "bridge" {} resource "tor_relay_identity_rsa" "bridge" {}
resource "tor_relay_identity_ed25519" "bridge" {} resource "tor_relay_identity_ed25519" "bridge" {}
# Note: Ed25519 keys are available in both PEM format (private_key_pem, public_key_pem)
# and Tor's binary format (private_key_tor, public_key_tor)
resource "tor_obfs4_state" "bridge" { resource "tor_obfs4_state" "bridge" {
rsa_identity_private_key = tor_relay_identity_rsa.bridge.private_key_pem rsa_identity_private_key = tor_relay_identity_rsa.bridge.private_key_pem
@ -64,12 +82,23 @@ output "rsa_identity_pem" {
sensitive = true sensitive = true
} }
output "rsa_fingerprint_hashed" {
description = "Hashed RSA fingerprint for privacy in monitoring systems"
value = tor_relay_identity_rsa.bridge.public_key_fingerprint_sha1_hashed
}
output "ed25519_identity_pem" { output "ed25519_identity_pem" {
description = "Ed25519 identity private key for bridge configuration" description = "Ed25519 identity private key for bridge configuration (PEM format)"
value = tor_relay_identity_ed25519.bridge.private_key_pem value = tor_relay_identity_ed25519.bridge.private_key_pem
sensitive = true sensitive = true
} }
output "ed25519_identity_tor" {
description = "Ed25519 identity private key in Tor's binary format (base64 encoded)"
value = tor_relay_identity_ed25519.bridge.private_key_tor
sensitive = true
}
output "obfs4_state_json" { output "obfs4_state_json" {
description = "Complete obfs4 state for bridge runtime" description = "Complete obfs4 state for bridge runtime"
value = tor_obfs4_state.bridge.state_json value = tor_obfs4_state.bridge.state_json

View file

@ -0,0 +1,51 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "tor_family_identity Resource - tor"
subcategory: ""
description: |-
Generates a Tor family identity key as described in proposal 321 (Happy Families).
References
https://community.torproject.org/relay/setup/post-install/family-ids/https://gitlab.torproject.org/tpo/core/torspec/-/blob/main/proposals/321-happy-families.md
---
# tor_family_identity (Resource)
Generates a Tor family identity key as described in proposal 321 (Happy Families).
### References
- https://community.torproject.org/relay/setup/post-install/family-ids/
- https://gitlab.torproject.org/tpo/core/torspec/-/blob/main/proposals/321-happy-families.md
## Example Usage
```terraform
resource "tor_family_identity" "example" {
family_name = "MyFamily"
}
resource "local_sensitive_file" "family_key" {
content_base64 = tor_family_identity.example.secret_key
filename = "MyFamily.secret_family_key"
}
resource "local_file" "torrc" {
filename = "./torrc"
content = <<EOF
FamilyId ${tor_family_identity.example.id}
... other torrc configuration ...
EOF
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `family_name` (String) Name of the family
### Read-Only
- `id` (String) Base64-encoded public key (as stored in public_family_id file)
- `secret_key` (String, Sensitive) Binary contents of the secret family key file (base64 encoded)

View file

@ -52,5 +52,7 @@ output "public_key_fingerprint_sha256" {
- `algorithm` (String) Name of the algorithm used when generating the private key (always 'Ed25519') - `algorithm` (String) Name of the algorithm used when generating the private key (always 'Ed25519')
- `id` (String) Unique identifier based on public key fingerprint - `id` (String) Unique identifier based on public key fingerprint
- `private_key_pem` (String, Sensitive) Private key data in PEM (RFC 1421) format - `private_key_pem` (String, Sensitive) Private key data in PEM (RFC 1421) format
- `public_key_fingerprint_sha256` (String) SHA256 fingerprint of the public key in hex format - `private_key_tor` (String, Sensitive) Private key data in Tor's binary format, base64 encoded
- `public_key_fingerprint_sha256` (String) Base64 encoded public key bytes (32 bytes) without padding, used as the Tor Ed25519 fingerprint
- `public_key_pem` (String) Public key data in PEM (RFC 1421) format - `public_key_pem` (String) Public key data in PEM (RFC 1421) format
- `public_key_tor` (String) Public key data in Tor's binary format, base64 encoded

View file

@ -49,6 +49,11 @@ output "public_key_fingerprint_sha256" {
description = "SHA256 fingerprint of the RSA public key" description = "SHA256 fingerprint of the RSA public key"
value = tor_relay_identity_rsa.example.public_key_fingerprint_sha256 value = tor_relay_identity_rsa.example.public_key_fingerprint_sha256
} }
output "public_key_fingerprint_sha1_hashed" {
description = "Hashed SHA1 fingerprint of the RSA public key for privacy in monitoring systems"
value = tor_relay_identity_rsa.example.public_key_fingerprint_sha1_hashed
}
``` ```
<!-- schema generated by tfplugindocs --> <!-- schema generated by tfplugindocs -->
@ -60,5 +65,6 @@ output "public_key_fingerprint_sha256" {
- `id` (String) Unique identifier based on public key fingerprint - `id` (String) Unique identifier based on public key fingerprint
- `private_key_pem` (String, Sensitive) Private key data in PEM (RFC 1421) format - `private_key_pem` (String, Sensitive) Private key data in PEM (RFC 1421) format
- `public_key_fingerprint_sha1` (String) SHA1 fingerprint of the public key in hex format - `public_key_fingerprint_sha1` (String) SHA1 fingerprint of the public key in hex format
- `public_key_fingerprint_sha1_hashed` (String) SHA1 hash of the binary form of the SHA1 fingerprint in hex format
- `public_key_fingerprint_sha256` (String) SHA256 fingerprint of the public key in hex format - `public_key_fingerprint_sha256` (String) SHA256 fingerprint of the public key in hex format
- `public_key_pem` (String) Public key data in PEM (RFC 1421) format - `public_key_pem` (String) Public key data in PEM (RFC 1421) format

View file

@ -1,48 +0,0 @@
#!/usr/bin/env bash
set -e
echo "Setting up development environment for terraform-provider-tor..."
# Get the Go bin path
GOBIN=$(go env GOPATH)/bin
if [ -z "$GOBIN" ]; then
GOBIN=$(go env GOROOT)/bin
fi
echo "Go bin path: $GOBIN"
# Create local .terraformrc with dev overrides
TERRAFORMRC="$(pwd)/.terraformrc"
echo "Creating $TERRAFORMRC..."
# Create local .terraformrc with dev overrides
cat > "$TERRAFORMRC" << EOF
provider_installation {
dev_overrides {
"guardianproject/tor" = "$GOBIN"
}
direct {}
}
EOF
echo "✓ Created local $TERRAFORMRC with dev overrides"
# Build and install the provider
echo "Building and installing provider..."
cd "$(dirname "$0")/.."
# Build with proper naming for dev overrides
go build -o "$GOBIN/terraform-provider-tor_v99.0.0"
echo "✓ Provider built and installed to $GOBIN/terraform-provider-tor_v99.0.0"
echo ""
echo "Setup complete! You can now run:"
echo " cd e2e-test"
echo " ./tf plan"
echo " ./tf apply"
echo ""
echo "Or use the full command:"
echo " TF_CLI_CONFIG_FILE=.terraformrc tofu plan"
echo ""
echo "Note: Using local .terraformrc to avoid modifying your global configuration."

View file

@ -1,7 +1,7 @@
terraform { terraform {
required_providers { required_providers {
tor = { tor = {
source = "guardianproject/tor" source = "guardianproject/tor"
version = "99.0.0" version = "99.0.0"
} }
} }
@ -15,12 +15,24 @@ resource "tor_relay_identity_rsa" "bridge" {}
# Generate Ed25519 identity key for the bridge # Generate Ed25519 identity key for the bridge
resource "tor_relay_identity_ed25519" "bridge" {} resource "tor_relay_identity_ed25519" "bridge" {}
# Generate family identity for the bridge
resource "tor_family_identity" "bridge" {
family_name = "MyBridgeFamily"
}
# Generate obfs4 state using the identity keys # Generate obfs4 state using the identity keys
resource "tor_obfs4_state" "bridge" { resource "tor_obfs4_state" "bridge" {
rsa_identity_private_key = tor_relay_identity_rsa.bridge.private_key_pem rsa_identity_private_key = tor_relay_identity_rsa.bridge.private_key_pem
ed25519_identity_private_key = tor_relay_identity_ed25519.bridge.private_key_pem ed25519_identity_private_key = tor_relay_identity_ed25519.bridge.private_key_pem
} }
# Alternative: obfs4 state could also use Tor format keys (demonstration only)
# resource "tor_obfs4_state" "bridge_alt" {
# rsa_identity_private_key = tor_relay_identity_rsa.bridge.private_key_pem
# ed25519_identity_private_key = tor_relay_identity_ed25519.bridge.private_key_pem
# # Note: private_key_tor could be used here as well for ed25519 keys
# }
# Generate bridge line for client distribution # Generate bridge line for client distribution
data "tor_obfs4_bridge_line" "bridge" { data "tor_obfs4_bridge_line" "bridge" {
ip_address = "203.0.113.1" ip_address = "203.0.113.1"
@ -41,11 +53,27 @@ output "rsa_fingerprint_sha256" {
value = tor_relay_identity_rsa.bridge.public_key_fingerprint_sha256 value = tor_relay_identity_rsa.bridge.public_key_fingerprint_sha256
} }
output "rsa_fingerprint_sha1_hashed" {
description = "RSA identity fingerprint (SHA1) hashed for privacy"
value = tor_relay_identity_rsa.bridge.public_key_fingerprint_sha1_hashed
}
output "ed25519_fingerprint_sha256" { output "ed25519_fingerprint_sha256" {
description = "Ed25519 identity fingerprint (SHA256)" description = "Ed25519 identity fingerprint (SHA256)"
value = tor_relay_identity_ed25519.bridge.public_key_fingerprint_sha256 value = tor_relay_identity_ed25519.bridge.public_key_fingerprint_sha256
} }
output "ed25519_private_key_tor" {
description = "Ed25519 private key in Tor binary format (base64)"
value = tor_relay_identity_ed25519.bridge.private_key_tor
sensitive = true
}
output "ed25519_public_key_tor" {
description = "Ed25519 public key in Tor binary format (base64)"
value = tor_relay_identity_ed25519.bridge.public_key_tor
}
output "obfs4_certificate" { output "obfs4_certificate" {
description = "obfs4 certificate for bridge line" description = "obfs4 certificate for bridge line"
value = tor_obfs4_state.bridge.certificate value = tor_obfs4_state.bridge.certificate
@ -60,3 +88,8 @@ output "bridge_line" {
description = "Complete bridge line for clients" description = "Complete bridge line for clients"
value = data.tor_obfs4_bridge_line.bridge.bridge_line value = data.tor_obfs4_bridge_line.bridge.bridge_line
} }
output "family_id" {
description = "Family ID for the bridge"
value = tor_family_identity.bridge.id
}

View file

@ -0,0 +1,15 @@
provider_installation {
filesystem_mirror {
path = "./providers"
include = [
"registry.terraform.io/guardianproject/*",
"registry.opentofu.org/guardianproject/*"
]
}
direct {
exclude = [
"registry.terraform.io/guardianproject/*",
"registry.opentofu.org/guardianproject/*"
]
}
}

7
e2e-tests/obfs4/test.sh Executable file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env sh
set -e
../setup.sh
rm -f terraform.tfstate*
./tf init
./tf plan
./tf apply -auto-approve

3
e2e-tests/obfs4/tf Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
export TF_CLI_CONFIG_FILE=terraformrc
exec tofu "$@"

14
e2e-tests/setup.sh Executable file
View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -e
rm -f ./.terraform.lock.hcl
mkdir -p providers/registry.terraform.io/guardianproject/tor/99.0.0/linux_amd64
mkdir -p providers/registry.opentofu.org/guardianproject/tor/99.0.0/linux_amd64
cp ../../terraform-provider-tor providers/registry.terraform.io/guardianproject/tor/99.0.0/linux_amd64
cp ../../terraform-provider-tor providers/registry.opentofu.org/guardianproject/tor/99.0.0/linux_amd64
echo ""
echo "Setup complete! You can now run:"
echo " ./tf init"
echo " ./tf plan"
echo " ./tf apply"

View file

@ -0,0 +1,79 @@
terraform {
required_providers {
tor = {
source = "guardianproject/tor"
version = "99.0.0"
}
local = {
source = "hashicorp/local"
version = "2.5.3"
}
}
}
provider "tor" {}
resource "tor_family_identity" "this" {
family_name = "MyFamily"
}
resource "tor_relay_identity_rsa" "this" {}
resource "tor_relay_identity_ed25519" "this" {}
resource "local_sensitive_file" "family_key" {
content_base64 = tor_family_identity.this.secret_key
filename = "./data/keys/MyKey.secret_family_key"
file_permission = "0600"
}
resource "local_file" "this" {
filename = "./torrc"
content = <<EOF
FamilyId ${tor_family_identity.this.id}
BridgeRelay 1
DataDirectory data
ORPort 3333
ServerTransportListenAddr obfs4 0.0.0.0:3334
ExtORPort auto
ContactInfo <address@email.com>
Nickname PickANickname
EOF
}
output "family_id" {
description = "Family ID for the bridge"
value = tor_family_identity.this.id
}
output "rsa_fingerprint_sha1" {
description = "RSA identity fingerprint (SHA1) - should be uppercase hex"
value = tor_relay_identity_rsa.this.public_key_fingerprint_sha1
}
output "rsa_fingerprint_sha1_hashed" {
description = "RSA identity fingerprint (SHA1) hashed for privacy monitoring"
value = tor_relay_identity_rsa.this.public_key_fingerprint_sha1_hashed
}
output "rsa_fingerprint_sha256" {
description = "RSA identity fingerprint (SHA256)"
value = tor_relay_identity_rsa.this.public_key_fingerprint_sha256
}
output "ed25519_fingerprint_sha256" {
description = "ED25519 identity fingerprint (base64 encoded public key bytes)"
value = tor_relay_identity_ed25519.this.public_key_fingerprint_sha256
}
output "ed25519_private_key_tor" {
description = "ED25519 private key in Tor binary format (base64 encoded)"
value = tor_relay_identity_ed25519.this.private_key_tor
sensitive = true
}
output "ed25519_public_key_tor" {
description = "ED25519 public key in Tor binary format (base64 encoded)"
value = tor_relay_identity_ed25519.this.public_key_tor
}

View file

@ -0,0 +1,15 @@
provider_installation {
filesystem_mirror {
path = "./providers"
include = [
"registry.terraform.io/guardianproject/*",
"registry.opentofu.org/guardianproject/*"
]
}
direct {
exclude = [
"registry.terraform.io/guardianproject/*",
"registry.opentofu.org/guardianproject/*"
]
}
}

34
e2e-tests/tor-family/test.sh Executable file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env sh
set -e
../setup.sh
rm -f terraform.tfstate*
./tf init
./tf plan
./tf apply -auto-approve
set +e
# Start tor and let it run for a few seconds
echo "Starting Tor to verify family key..."
timeout 5 tor -f ./torrc >tor.log 2>&1
TOR_EXIT_CODE=$?
set -e
# Check if tor exited with an error (not due to timeout)
# timeout returns 124 when it kills the process
if [ $TOR_EXIT_CODE -ne 0 ] && [ $TOR_EXIT_CODE -ne 124 ]; then
echo "ERROR: Tor exited with error code $TOR_EXIT_CODE"
cat tor.log
exit 1
fi
# Check if tor started bootstrapping (indicates successful key loading)
if grep -q "Bootstrapped [0-9]" tor.log; then
echo "SUCCESS: Tor started bootstrapping with generated family key"
exit 0
else
echo "ERROR: Tor did not start bootstrapping"
cat tor.log
exit 1
fi

View file

@ -1,5 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Wrapper script to run terraform with local config # Wrapper script to run terraform with local config
export TF_CLI_CONFIG_FILE=terraformrc
export TF_CLI_CONFIG_FILE=.terraformrc
exec tofu "$@" exec tofu "$@"

View file

@ -0,0 +1,17 @@
resource "tor_family_identity" "example" {
family_name = "MyFamily"
}
resource "local_sensitive_file" "family_key" {
content_base64 = tor_family_identity.example.secret_key
filename = "MyFamily.secret_family_key"
}
resource "local_file" "torrc" {
filename = "./torrc"
content = <<EOF
FamilyId ${tor_family_identity.example.id}
... other torrc configuration ...
EOF
}

View file

@ -34,3 +34,8 @@ output "public_key_fingerprint_sha256" {
description = "SHA256 fingerprint of the RSA public key" description = "SHA256 fingerprint of the RSA public key"
value = tor_relay_identity_rsa.example.public_key_fingerprint_sha256 value = tor_relay_identity_rsa.example.public_key_fingerprint_sha256
} }
output "public_key_fingerprint_sha1_hashed" {
description = "Hashed SHA1 fingerprint of the RSA public key for privacy in monitoring systems"
value = tor_relay_identity_rsa.example.public_key_fingerprint_sha1_hashed
}

View file

@ -8,6 +8,7 @@
let let
supportedSystems = [ supportedSystems = [
"x86_64-linux" "x86_64-linux"
"aarch64-darwin"
]; ];
forEachSupportedSystem = forEachSupportedSystem =
f: f:
@ -57,6 +58,7 @@
default = pkgs.mkShell { default = pkgs.mkShell {
packages = [ packages = [
pkgs.go pkgs.go
pkgs.gopls
pkgs.golangci-lint pkgs.golangci-lint
pkgs.obfs4 pkgs.obfs4
pkgs.gnumake pkgs.gnumake

2
go.mod
View file

@ -5,6 +5,7 @@ go 1.23.0
toolchain go1.24.2 toolchain go1.24.2
require ( require (
filippo.io/edwards25519 v1.1.0
github.com/hashicorp/terraform-plugin-framework v1.12.0 github.com/hashicorp/terraform-plugin-framework v1.12.0
github.com/hashicorp/terraform-plugin-go v0.24.0 github.com/hashicorp/terraform-plugin-go v0.24.0
github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-log v0.9.0
@ -13,7 +14,6 @@ require (
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect
github.com/agext/levenshtein v1.2.3 // indirect github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect

View file

@ -61,6 +61,7 @@ func (p *ScaffoldingProvider) Resources(ctx context.Context) []func() resource.R
NewTorObfs4StateResource, NewTorObfs4StateResource,
NewTorRelayIdentityRsaResource, NewTorRelayIdentityRsaResource,
NewTorRelayIdentityEd25519Resource, NewTorRelayIdentityEd25519Resource,
NewTorFamilyIdentityResource,
} }
} }

View file

@ -0,0 +1,166 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (c) 2025 Abel Luck <abel@guardianproject.info>
package provider
import (
"context"
"crypto/rand"
"crypto/sha512"
"encoding/base64"
"fmt"
"filippo.io/edwards25519"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
var _ resource.Resource = &TorFamilyIdentityResource{}
func NewTorFamilyIdentityResource() resource.Resource {
return &TorFamilyIdentityResource{}
}
type TorFamilyIdentityResource struct{}
type TorFamilyIdentityResourceModel struct {
Id types.String `tfsdk:"id"`
FamilyName types.String `tfsdk:"family_name"`
SecretKey types.String `tfsdk:"secret_key"`
}
func (r *TorFamilyIdentityResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_family_identity"
}
func (r *TorFamilyIdentityResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: `Generates a Tor family identity key as described in proposal 321 (Happy Families).
### References
- https://community.torproject.org/relay/setup/post-install/family-ids/
- https://gitlab.torproject.org/tpo/core/torspec/-/blob/main/proposals/321-happy-families.md
`,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Base64-encoded public key (as stored in public_family_id file)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"family_name": schema.StringAttribute{
Required: true,
MarkdownDescription: "Name of the family",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"secret_key": schema.StringAttribute{
Computed: true,
Sensitive: true,
MarkdownDescription: "Binary contents of the secret family key file (base64 encoded)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}
func (r *TorFamilyIdentityResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
}
// torExpandSeed mimics Tor's ed25519_extsk function
func torExpandSeed(seed []byte) []byte {
h := sha512.Sum512(seed)
// Apply ed25519 bit manipulation to first 32 bytes
h[0] &= 248
h[31] &= 127
h[31] |= 64
return h[:]
}
// torComputePublicKey mimics how Tor computes public key from expanded secret key
func torComputePublicKey(expandedSK []byte) ([]byte, error) {
// Tor uses only the first 32 bytes (the scalar) to compute public key
scalar := expandedSK[:32]
var scalarBytes [32]byte
copy(scalarBytes[:], scalar)
// The scalar is already clamped, so use SetBytesWithClamping
s, err := edwards25519.NewScalar().SetBytesWithClamping(scalarBytes[:])
if err != nil {
return nil, fmt.Errorf("failed to create scalar: %v", err)
}
publicKey := edwards25519.NewIdentityPoint().ScalarBaseMult(s)
return publicKey.Bytes(), nil
}
func (r *TorFamilyIdentityResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data TorFamilyIdentityResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
seed := make([]byte, 32)
if _, err := rand.Read(seed); err != nil {
resp.Diagnostics.AddError("Failed to generate random seed", err.Error())
return
}
expandedSK := torExpandSeed(seed)
publicKey, err := torComputePublicKey(expandedSK)
if err != nil {
resp.Diagnostics.AddError("Failed to compute public key", err.Error())
return
}
// Format secret key file content following Tor's tagged format
const FAMILY_KEY_FILE_TAG = "fmly-id"
header := fmt.Sprintf("== ed25519v1-secret: %s ==\x00", FAMILY_KEY_FILE_TAG)
secretKeyContent := append([]byte(header), expandedSK...)
// Encode public key as base64 without padding (matching Tor's format)
publicKeyBase64 := base64.RawStdEncoding.EncodeToString(publicKey)
data.Id = types.StringValue(publicKeyBase64)
data.SecretKey = types.StringValue(base64.StdEncoding.EncodeToString(secretKeyContent))
tflog.Trace(ctx, "created a family identity resource")
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *TorFamilyIdentityResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data TorFamilyIdentityResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *TorFamilyIdentityResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
resp.Diagnostics.AddError("Update not supported", "All changes to family identity require resource replacement")
}
func (r *TorFamilyIdentityResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
}

View file

@ -0,0 +1,80 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2025 Abel Luck <abel@guardianproject.info>
package provider
import (
"encoding/base64"
"encoding/hex"
"testing"
)
// Test data from a secret key generated by Tor version 0.4.9.2-alpha
// To reproduce:
// 1. Run: tor --keygen-family MyKey
// 2. This creates MyKey.secret_family_key and MyKey.public_family_id
// 3. Convert secret key to hex: xxd -p MyKey.secret_family_key | tr -d '\n'
const torGeneratedSecretKeyHex = "3d3d206564323535313976312d7365637265743a20666d6c792d6964203d3d00f84c620e227fcc5085eb538a29a11ac25abb052b6a36ddae008b307cca67fe792ecd73a67c0a7a28b2b747be2a59e5ef0c155e33217add6dac42dbc6f85a5162"
const torGeneratedPublicFamilyID = "xiHyFHgSU4/aaqPfRCrUk61jVBDQAv1hlHQd7lKIlI8"
func TestTorFamilyIdentity_SerializationAgainstTorGenerated(t *testing.T) {
secretKeyBytes, err := hex.DecodeString(torGeneratedSecretKeyHex)
if err != nil {
t.Fatalf("Failed to decode secret key hex: %v", err)
}
const expectedHeader = "== ed25519v1-secret: fmly-id ==\x00"
if len(secretKeyBytes) < len(expectedHeader) {
t.Fatalf("Secret key file too short: %d bytes", len(secretKeyBytes))
}
header := string(secretKeyBytes[:len(expectedHeader)])
if header != expectedHeader {
t.Fatalf("Invalid header: got %q, want %q", header, expectedHeader)
}
expandedSK := secretKeyBytes[len(expectedHeader):]
if len(expandedSK) != 64 {
t.Fatalf("Invalid expanded secret key length: got %d, want 64", len(expandedSK))
}
publicKey, err := torComputePublicKey(expandedSK)
if err != nil {
t.Fatalf("Failed to compute public key: %v", err)
}
computedFamilyID := base64.RawStdEncoding.EncodeToString(publicKey)
if computedFamilyID != torGeneratedPublicFamilyID {
t.Errorf("Family ID mismatch:\n computed: %s\n expected: %s",
computedFamilyID, torGeneratedPublicFamilyID)
}
}
func TestTorFamilyIdentity_ExpandSeed(t *testing.T) {
// Test that our seed expansion produces the same result as Tor
// We'll need to reverse-engineer the seed from the expanded key
// This is a secondary test to verify our torExpandSeed function
secretKeyBytes, err := hex.DecodeString(torGeneratedSecretKeyHex)
if err != nil {
t.Fatalf("Failed to decode secret key hex: %v", err)
}
const expectedHeader = "== ed25519v1-secret: fmly-id ==\x00"
expandedSK := secretKeyBytes[len(expectedHeader):]
// Verify the key is properly clamped according to ed25519 standards
// For ed25519, the clamping is:
// - bits 0,1,2 of first byte cleared (make it a multiple of 8)
// - bit 6 of byte 31 set
// - bit 7 of byte 31 cleared
if expandedSK[0]&0x07 != 0 {
t.Errorf("First byte not properly clamped: %02x (bits 0,1,2 should be cleared)", expandedSK[0])
}
if expandedSK[31]&0x40 == 0 {
t.Errorf("Byte 31 bit 6 should be set: %02x", expandedSK[31])
}
if expandedSK[31]&0x80 != 0 {
t.Errorf("Byte 31 bit 7 should be cleared: %02x", expandedSK[31])
}
}

View file

@ -0,0 +1,85 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (c) 2025 Abel Luck <abel@guardianproject.info>
package provider
import (
"encoding/base64"
"fmt"
"regexp"
"strings"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
)
func TestAccTorFamilyIdentityResource(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccTorFamilyIdentityResourceConfig("MyFamily"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("tor_family_identity.test", "family_name", "MyFamily"),
resource.TestMatchResourceAttr("tor_family_identity.test", "id", regexp.MustCompile(`^[A-Za-z0-9+/]{43}$`)),
resource.TestCheckResourceAttrSet("tor_family_identity.test", "secret_key"),
testAccCheckTorFamilyIdentitySecretKeyFormat("tor_family_identity.test", "secret_key"),
),
},
},
})
}
func testAccCheckTorFamilyIdentitySecretKeyFormat(resourceName, attributeName string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[resourceName]
if !ok {
return fmt.Errorf("Resource not found: %s", resourceName)
}
secretKeyBase64 := rs.Primary.Attributes[attributeName]
if secretKeyBase64 == "" {
return fmt.Errorf("secret_key attribute is empty")
}
secretKey, err := base64.StdEncoding.DecodeString(secretKeyBase64)
if err != nil {
return fmt.Errorf("Failed to decode secret_key: %v", err)
}
if len(secretKey) != 96 {
return fmt.Errorf("secret_key has wrong size: expected 96 bytes, got %d", len(secretKey))
}
expectedHeader := "== ed25519v1-secret: fmly-id =="
actualHeader := string(secretKey[:32])
if !strings.HasPrefix(actualHeader, expectedHeader) {
return fmt.Errorf("secret_key has wrong header: expected %q, got %q", expectedHeader, actualHeader[:len(expectedHeader)])
}
if secretKey[31] != 0 {
return fmt.Errorf("secret_key header should end with null byte")
}
id := rs.Primary.Attributes["id"]
if _, err := base64.RawStdEncoding.DecodeString(id); err != nil {
return fmt.Errorf("id should be a valid base64 string without padding: %s", id)
}
if strings.HasSuffix(id, "=") {
return fmt.Errorf("id should not have padding characters: %s", id)
}
return nil
}
}
func testAccTorFamilyIdentityResourceConfig(familyName string) string {
return fmt.Sprintf(`
resource "tor_family_identity" "test" {
family_name = %[1]q
}
`, familyName)
}

View file

@ -31,9 +31,8 @@ func TestTorObfs4BridgeLineDataSource(t *testing.T) {
// Check computed values are generated // Check computed values are generated
resource.TestCheckResourceAttrSet("data.tor_obfs4_bridge_line.test", "bridge_line"), resource.TestCheckResourceAttrSet("data.tor_obfs4_bridge_line.test", "bridge_line"),
// Check bridge line format
resource.TestMatchResourceAttr("data.tor_obfs4_bridge_line.test", "bridge_line", resource.TestMatchResourceAttr("data.tor_obfs4_bridge_line.test", "bridge_line",
regexp.MustCompile(`^Bridge obfs4 192\.0\.2\.1:443 [0-9a-f]{40} cert=[A-Za-z0-9+/]+ iat-mode=[0-2]$`)), regexp.MustCompile(`^Bridge obfs4 192\.0\.2\.1:443 [0-9A-F]{40} cert=[A-Za-z0-9+/]+ iat-mode=[0-2]$`)),
// Check that input values are used correctly // Check that input values are used correctly
resource.TestCheckResourceAttrPair("data.tor_obfs4_bridge_line.test", "identity_fingerprint_sha1", "tor_relay_identity_rsa.test", "public_key_fingerprint_sha1"), resource.TestCheckResourceAttrPair("data.tor_obfs4_bridge_line.test", "identity_fingerprint_sha1", "tor_relay_identity_rsa.test", "public_key_fingerprint_sha1"),

View file

@ -406,11 +406,7 @@ func (r *TorObfs4StateResource) deriveNodeIdFromRsaKey(rsaPrivateKeyPem string)
return nil, fmt.Errorf("failed to parse RSA private key: %w", err) return nil, fmt.Errorf("failed to parse RSA private key: %w", err)
} }
// Extract the public key and encode it publicKeyBytes := x509.MarshalPKCS1PublicKey(&privateKey.PublicKey)
publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
if err != nil {
return nil, fmt.Errorf("failed to marshal public key: %w", err)
}
// Generate SHA1 hash of public key (this is the relay fingerprint/node ID) // Generate SHA1 hash of public key (this is the relay fingerprint/node ID)
hash := sha1.Sum(publicKeyBytes) hash := sha1.Sum(publicKeyBytes)

View file

@ -7,10 +7,11 @@ import (
"context" "context"
"crypto/ed25519" "crypto/ed25519"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/base64"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"strings"
"github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema"
@ -33,6 +34,8 @@ type TorRelayIdentityEd25519ResourceModel struct {
Algorithm types.String `tfsdk:"algorithm"` Algorithm types.String `tfsdk:"algorithm"`
PrivateKeyPem types.String `tfsdk:"private_key_pem"` PrivateKeyPem types.String `tfsdk:"private_key_pem"`
PublicKeyPem types.String `tfsdk:"public_key_pem"` PublicKeyPem types.String `tfsdk:"public_key_pem"`
PrivateKeyTor types.String `tfsdk:"private_key_tor"`
PublicKeyTor types.String `tfsdk:"public_key_tor"`
PublicKeyFingerprintSha256 types.String `tfsdk:"public_key_fingerprint_sha256"` PublicKeyFingerprintSha256 types.String `tfsdk:"public_key_fingerprint_sha256"`
} }
@ -74,9 +77,24 @@ func (r *TorRelayIdentityEd25519Resource) Schema(ctx context.Context, req resour
stringplanmodifier.UseStateForUnknown(), stringplanmodifier.UseStateForUnknown(),
}, },
}, },
"private_key_tor": schema.StringAttribute{
Computed: true,
Sensitive: true,
MarkdownDescription: "Private key data in Tor's binary format, base64 encoded",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"public_key_tor": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Public key data in Tor's binary format, base64 encoded",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"public_key_fingerprint_sha256": schema.StringAttribute{ "public_key_fingerprint_sha256": schema.StringAttribute{
Computed: true, Computed: true,
MarkdownDescription: "SHA256 fingerprint of the public key in hex format", MarkdownDescription: "Base64 encoded public key bytes (32 bytes) without padding, used as the Tor Ed25519 fingerprint",
PlanModifiers: []planmodifier.String{ PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(), stringplanmodifier.UseStateForUnknown(),
}, },
@ -125,12 +143,19 @@ func (r *TorRelayIdentityEd25519Resource) Create(ctx context.Context, req resour
} }
data.PublicKeyPem = types.StringValue(publicKeyPem) data.PublicKeyPem = types.StringValue(publicKeyPem)
// Generate SHA256 fingerprint // Encode keys in Tor format
sha256Fingerprint := r.generateSha256Fingerprint(publicKey) privateKeyTor := r.encodeTorPrivateKey(privateKey)
data.PublicKeyFingerprintSha256 = types.StringValue(sha256Fingerprint) data.PrivateKeyTor = types.StringValue(privateKeyTor)
// Generate ID from SHA256 fingerprint publicKeyTor := r.encodeTorPublicKey(publicKey)
data.Id = types.StringValue(fmt.Sprintf("ed25519-%s", sha256Fingerprint[:16])) data.PublicKeyTor = types.StringValue(publicKeyTor)
// Generate Tor Ed25519 fingerprint (base64 encoded public key bytes without padding)
ed25519Fingerprint := r.generateEd25519Fingerprint(publicKey)
data.PublicKeyFingerprintSha256 = types.StringValue(ed25519Fingerprint)
// Generate ID from Ed25519 fingerprint
data.Id = types.StringValue(fmt.Sprintf("ed25519-%s", ed25519Fingerprint[:16]))
tflog.Trace(ctx, "created tor relay identity Ed25519 resource") tflog.Trace(ctx, "created tor relay identity Ed25519 resource")
@ -181,8 +206,37 @@ func (r *TorRelayIdentityEd25519Resource) encodePublicKeyPEM(publicKey ed25519.P
return string(publicKeyPem), nil return string(publicKeyPem), nil
} }
func (r *TorRelayIdentityEd25519Resource) generateSha256Fingerprint(publicKey ed25519.PublicKey) string { func (r *TorRelayIdentityEd25519Resource) generateEd25519Fingerprint(publicKey ed25519.PublicKey) string {
publicKeyBytes, _ := x509.MarshalPKIXPublicKey(publicKey) fingerprint := base64.StdEncoding.EncodeToString(publicKey)
sha256Sum := sha256.Sum256(publicKeyBytes) return strings.TrimRight(fingerprint, "=")
return fmt.Sprintf("%x", sha256Sum) }
func (r *TorRelayIdentityEd25519Resource) encodeTorPrivateKey(privateKey ed25519.PrivateKey) string {
// Tor Ed25519 private key format:
// "== ed25519v1-secret: type0 ==" + null bytes + 64 bytes key data
header := "== ed25519v1-secret: type0 =="
// Create 96-byte buffer: 32 bytes header + null padding + 64 bytes key
torKey := make([]byte, 96)
copy(torKey, header)
// Copy the private key (64 bytes) starting at offset 32
copy(torKey[32:], privateKey)
return base64.StdEncoding.EncodeToString(torKey)
}
func (r *TorRelayIdentityEd25519Resource) encodeTorPublicKey(publicKey ed25519.PublicKey) string {
// Tor Ed25519 public key format:
// "== ed25519v1-public: type0 ==" + null bytes + 32 bytes key data
header := "== ed25519v1-public: type0 =="
// Create 64-byte buffer: 32 bytes header + null padding + 32 bytes key
torKey := make([]byte, 64)
copy(torKey, header)
// Copy the public key (32 bytes) starting at offset 32
copy(torKey[32:], publicKey)
return base64.StdEncoding.EncodeToString(torKey)
} }

View file

@ -4,6 +4,7 @@
package provider package provider
import ( import (
"encoding/base64"
"regexp" "regexp"
"testing" "testing"
@ -27,14 +28,67 @@ func TestAccTorRelayIdentityEd25519Resource(t *testing.T) {
// Verify PEM format // Verify PEM format
resource.TestMatchResourceAttr("tor_relay_identity_ed25519.test", "private_key_pem", regexp.MustCompile(`^-----BEGIN PRIVATE KEY-----`)), resource.TestMatchResourceAttr("tor_relay_identity_ed25519.test", "private_key_pem", regexp.MustCompile(`^-----BEGIN PRIVATE KEY-----`)),
resource.TestMatchResourceAttr("tor_relay_identity_ed25519.test", "public_key_pem", regexp.MustCompile(`^-----BEGIN PUBLIC KEY-----`)), resource.TestMatchResourceAttr("tor_relay_identity_ed25519.test", "public_key_pem", regexp.MustCompile(`^-----BEGIN PUBLIC KEY-----`)),
// Verify fingerprint format (64 hex characters for SHA256) resource.TestMatchResourceAttr("tor_relay_identity_ed25519.test", "public_key_fingerprint_sha256", regexp.MustCompile(`^[A-Za-z0-9+/]{43}$`)),
resource.TestMatchResourceAttr("tor_relay_identity_ed25519.test", "public_key_fingerprint_sha256", regexp.MustCompile(`^[0-9a-f]{64}$`)),
), ),
}, },
}, },
}) })
} }
func TestTorEd25519FingerprintGeneration(t *testing.T) {
testPubKeyBase64 := "PT0gZWQyNTUxOXYxLXB1YmxpYzogdHlwZTAgPT0AAAB6UEqfT3OvqdpNfw/rbOucsc5AXRUw4lcy/SaWxruoYA=="
expectedFingerprint := "elBKn09zr6naTX8P62zrnLHOQF0VMOJXMv0mlsa7qGA"
pubKeyBytes, err := base64.StdEncoding.DecodeString(testPubKeyBase64)
if err != nil {
t.Fatalf("Failed to decode test public key: %v", err)
}
actualFingerprint := base64.StdEncoding.EncodeToString(pubKeyBytes[32:])
actualFingerprint = regexp.MustCompile(`=*$`).ReplaceAllString(actualFingerprint, "")
if actualFingerprint != expectedFingerprint {
t.Errorf("Expected fingerprint %s, got %s", expectedFingerprint, actualFingerprint)
}
}
func TestAccTorRelayIdentityEd25519TorFormat(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccTorRelayIdentityEd25519ResourceConfig,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet("tor_relay_identity_ed25519.test", "private_key_tor"),
resource.TestCheckResourceAttrSet("tor_relay_identity_ed25519.test", "public_key_tor"),
resource.TestMatchResourceAttr("tor_relay_identity_ed25519.test", "private_key_tor", regexp.MustCompile(`^[A-Za-z0-9+/]+=*$`)),
resource.TestMatchResourceAttr("tor_relay_identity_ed25519.test", "public_key_tor", regexp.MustCompile(`^[A-Za-z0-9+/]+=*$`)),
),
},
},
})
}
func TestTorEd25519FormatEncoding(t *testing.T) {
testPubKeyBase64 := "PT0gZWQyNTUxOXYxLXB1YmxpYzogdHlwZTAgPT0AAAB6UEqfT3OvqdpNfw/rbOucsc5AXRUw4lcy/SaWxruoYA=="
pubKeyBytes, err := base64.StdEncoding.DecodeString(testPubKeyBase64)
if err != nil {
t.Fatalf("Failed to decode test public key: %v", err)
}
if len(pubKeyBytes) != 64 {
t.Errorf("Expected public key to be 64 bytes, got %d", len(pubKeyBytes))
}
expectedHeader := "== ed25519v1-public: type0 =="
actualHeader := string(pubKeyBytes[:len(expectedHeader)])
if actualHeader != expectedHeader {
t.Errorf("Expected header %q, got %q", expectedHeader, actualHeader)
}
}
const testAccTorRelayIdentityEd25519ResourceConfig = ` const testAccTorRelayIdentityEd25519ResourceConfig = `
resource "tor_relay_identity_ed25519" "test" {} resource "tor_relay_identity_ed25519" "test" {}
` `

View file

@ -10,6 +10,7 @@ import (
"crypto/sha1" "crypto/sha1"
"crypto/sha256" "crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/hex"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
@ -30,12 +31,13 @@ func NewTorRelayIdentityRsaResource() resource.Resource {
type TorRelayIdentityRsaResource struct{} type TorRelayIdentityRsaResource struct{}
type TorRelayIdentityRsaResourceModel struct { type TorRelayIdentityRsaResourceModel struct {
Id types.String `tfsdk:"id"` Id types.String `tfsdk:"id"`
Algorithm types.String `tfsdk:"algorithm"` Algorithm types.String `tfsdk:"algorithm"`
PrivateKeyPem types.String `tfsdk:"private_key_pem"` PrivateKeyPem types.String `tfsdk:"private_key_pem"`
PublicKeyPem types.String `tfsdk:"public_key_pem"` PublicKeyPem types.String `tfsdk:"public_key_pem"`
PublicKeyFingerprintSha1 types.String `tfsdk:"public_key_fingerprint_sha1"` PublicKeyFingerprintSha1 types.String `tfsdk:"public_key_fingerprint_sha1"`
PublicKeyFingerprintSha256 types.String `tfsdk:"public_key_fingerprint_sha256"` PublicKeyFingerprintSha256 types.String `tfsdk:"public_key_fingerprint_sha256"`
PublicKeyFingerprintSha1Hashed types.String `tfsdk:"public_key_fingerprint_sha1_hashed"`
} }
func (r *TorRelayIdentityRsaResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { func (r *TorRelayIdentityRsaResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
@ -90,6 +92,13 @@ func (r *TorRelayIdentityRsaResource) Schema(ctx context.Context, req resource.S
stringplanmodifier.UseStateForUnknown(), stringplanmodifier.UseStateForUnknown(),
}, },
}, },
"public_key_fingerprint_sha1_hashed": schema.StringAttribute{
Computed: true,
MarkdownDescription: "SHA1 hash of the binary form of the SHA1 fingerprint in hex format",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
}, },
} }
} }
@ -146,6 +155,15 @@ func (r *TorRelayIdentityRsaResource) Create(ctx context.Context, req resource.C
data.PublicKeyFingerprintSha1 = types.StringValue(sha1Fingerprint) data.PublicKeyFingerprintSha1 = types.StringValue(sha1Fingerprint)
data.PublicKeyFingerprintSha256 = types.StringValue(sha256Fingerprint) data.PublicKeyFingerprintSha256 = types.StringValue(sha256Fingerprint)
// Generate hashed fingerprint
hashedFingerprint, err := r.generateHashedFingerprint(sha1Fingerprint)
if err != nil {
resp.Diagnostics.AddError("Hashed Fingerprint Generation Error",
fmt.Sprintf("Unable to generate hashed fingerprint: %s", err))
return
}
data.PublicKeyFingerprintSha1Hashed = types.StringValue(hashedFingerprint)
// Generate ID from SHA1 fingerprint // Generate ID from SHA1 fingerprint
data.Id = types.StringValue(fmt.Sprintf("rsa-%s", sha1Fingerprint[:16])) data.Id = types.StringValue(fmt.Sprintf("rsa-%s", sha1Fingerprint[:16]))
@ -196,16 +214,25 @@ func (r *TorRelayIdentityRsaResource) encodePublicKeyPEM(publicKey *rsa.PublicKe
} }
func (r *TorRelayIdentityRsaResource) generateFingerprints(publicKey *rsa.PublicKey) (string, string, error) { func (r *TorRelayIdentityRsaResource) generateFingerprints(publicKey *rsa.PublicKey) (string, string, error) {
publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey) publicKeyBytes := x509.MarshalPKCS1PublicKey(publicKey)
if err != nil {
return "", "", err
}
sha1Sum := sha1.Sum(publicKeyBytes) sha1Sum := sha1.Sum(publicKeyBytes)
sha256Sum := sha256.Sum256(publicKeyBytes) sha256Sum := sha256.Sum256(publicKeyBytes)
sha1Fingerprint := fmt.Sprintf("%x", sha1Sum) sha1Fingerprint := fmt.Sprintf("%X", sha1Sum)
sha256Fingerprint := fmt.Sprintf("%x", sha256Sum) sha256Fingerprint := fmt.Sprintf("%X", sha256Sum)
return sha1Fingerprint, sha256Fingerprint, nil return sha1Fingerprint, sha256Fingerprint, nil
} }
func (r *TorRelayIdentityRsaResource) generateHashedFingerprint(fingerprint string) (string, error) {
fingerprintBytes, err := hex.DecodeString(fingerprint)
if err != nil {
return "", fmt.Errorf("failed to decode fingerprint hex: %w", err)
}
hashedSum := sha1.Sum(fingerprintBytes)
hashedFingerprint := fmt.Sprintf("%X", hashedSum)
return hashedFingerprint, nil
}

View file

@ -4,6 +4,8 @@
package provider package provider
import ( import (
"crypto/x509"
"encoding/pem"
"regexp" "regexp"
"testing" "testing"
@ -25,18 +27,76 @@ func TestAccTorRelayIdentityRsaResource(t *testing.T) {
resource.TestCheckResourceAttrSet("tor_relay_identity_rsa.test", "public_key_pem"), resource.TestCheckResourceAttrSet("tor_relay_identity_rsa.test", "public_key_pem"),
resource.TestCheckResourceAttrSet("tor_relay_identity_rsa.test", "public_key_fingerprint_sha1"), resource.TestCheckResourceAttrSet("tor_relay_identity_rsa.test", "public_key_fingerprint_sha1"),
resource.TestCheckResourceAttrSet("tor_relay_identity_rsa.test", "public_key_fingerprint_sha256"), resource.TestCheckResourceAttrSet("tor_relay_identity_rsa.test", "public_key_fingerprint_sha256"),
resource.TestCheckResourceAttrSet("tor_relay_identity_rsa.test", "public_key_fingerprint_sha1_hashed"),
// Verify PEM format // Verify PEM format
resource.TestMatchResourceAttr("tor_relay_identity_rsa.test", "private_key_pem", regexp.MustCompile(`^-----BEGIN RSA PRIVATE KEY-----`)), resource.TestMatchResourceAttr("tor_relay_identity_rsa.test", "private_key_pem", regexp.MustCompile(`^-----BEGIN RSA PRIVATE KEY-----`)),
resource.TestMatchResourceAttr("tor_relay_identity_rsa.test", "public_key_pem", regexp.MustCompile(`^-----BEGIN PUBLIC KEY-----`)), resource.TestMatchResourceAttr("tor_relay_identity_rsa.test", "public_key_pem", regexp.MustCompile(`^-----BEGIN PUBLIC KEY-----`)),
// Verify fingerprint formats (hex strings) resource.TestMatchResourceAttr("tor_relay_identity_rsa.test", "public_key_fingerprint_sha1", regexp.MustCompile(`^[0-9A-F]{40}$`)),
resource.TestMatchResourceAttr("tor_relay_identity_rsa.test", "public_key_fingerprint_sha1", regexp.MustCompile(`^[0-9a-f]{40}$`)), resource.TestMatchResourceAttr("tor_relay_identity_rsa.test", "public_key_fingerprint_sha256", regexp.MustCompile(`^[0-9A-F]{64}$`)),
resource.TestMatchResourceAttr("tor_relay_identity_rsa.test", "public_key_fingerprint_sha256", regexp.MustCompile(`^[0-9a-f]{64}$`)), resource.TestMatchResourceAttr("tor_relay_identity_rsa.test", "public_key_fingerprint_sha1_hashed", regexp.MustCompile(`^[0-9A-F]{40}$`)),
), ),
}, },
}, },
}) })
} }
func TestRsaFingerprintGeneration(t *testing.T) {
testRsaPrivateKeyPem := `-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDxZrR1gsB00rE5Rift0y35j/F5Jt03ExlW0H7fUI+4W2MyZ+pE
0CM512o1tshfpDVxtVrnJuI8dFAVgO9Ct7aEKYTNk++g9llF+Q0M4nIrKftv/4Fo
MlFHwa+C41LGiucauaagiatiESNWNK3FhOLx1Who9duUN5SVVFyXpApPrQIDAQAB
AoGAJya2G9zh49CMB7L2JN88NJ6A1lpURGtnj6nu+b7yID9KHlG2MATlwarLQfzs
EH7sYA2+uYCX7qAaoPIxW8u54PLKbgVD3Kh7eXBO0isIx4FPMHD4khv5CLiNVopI
VwStg9Fv3FZ6h2+2FTVfLK4+xuwfyoShUwfEp3eV7c8YRSECQQD1ntdJrokM2zWR
hf+Cl/L62tl58hSYVZoSrR+b0cTQlbN1rnYTvY+1jXbBP8fKFBoAHKZ1xMCY/m67
H7qt+nalAkEA+5o4GQb6YCHYJZ/lhGQFaSGnWnKE16MsW1xLimQY8gOMbg3AYcXk
B8fylmp/XpNG1/PC+M6m5C0DjI85eKYuaQJBAJ2eOPmHj1s4sL+aBcWATOS93CFt
P9oh1KV3g3kyu+I+rtMuCYfRdY9EIJkSnNsI20aHHCsm/5EudVCPo/RRbiECQEQu
psUhfvhOM6T+j9QwxsaWuCNqpVVKgtq/SDlYpunuzD+GunvEhOcW6Eaa1alrf+dF
x7BlUBTFnhCZP5nSbwECQGgUr7jW/xrwbkDAP3+ql6o0yyhLMtvIqAKk3fUWxPXO
OhEqFiIYW5mI//JWsqSZZxy4nMqgejKkrRgOOQbL0NE=
-----END RSA PRIVATE KEY-----`
expectedSha1Fingerprint := "DA5CEC632A9A544394403BD533E1A7BDE2F26EDD"
block, _ := pem.Decode([]byte(testRsaPrivateKeyPem))
if block == nil {
t.Fatal("failed to decode PEM block")
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
t.Fatalf("failed to parse RSA private key: %v", err)
}
resource := &TorRelayIdentityRsaResource{}
sha1Fingerprint, _, err := resource.generateFingerprints(&privateKey.PublicKey)
if err != nil {
t.Fatalf("failed to generate fingerprint: %v", err)
}
if sha1Fingerprint != expectedSha1Fingerprint {
t.Errorf("SHA1 fingerprint mismatch:\nExpected: %s\nActual: %s",
expectedSha1Fingerprint, sha1Fingerprint)
}
}
func TestHashedFingerprintGeneration(t *testing.T) {
testFingerprint := "DA5CEC632A9A544394403BD533E1A7BDE2F26EDD"
expectedHashedFingerprint := "922650D27357BE307B3B322A5ABC3E9585AF776F"
resource := &TorRelayIdentityRsaResource{}
hashedFingerprint, err := resource.generateHashedFingerprint(testFingerprint)
if err != nil {
t.Fatalf("failed to generate hashed fingerprint: %v", err)
}
if hashedFingerprint != expectedHashedFingerprint {
t.Errorf("Hashed fingerprint mismatch:\nExpected: %s\nActual: %s",
expectedHashedFingerprint, hashedFingerprint)
}
}
const testAccTorRelayIdentityRsaResourceConfig = ` const testAccTorRelayIdentityRsaResourceConfig = `
resource "tor_relay_identity_rsa" "test" {} resource "tor_relay_identity_rsa" "test" {}
` `