Compare commits
6 commits
e1dc9e1ffa
...
4eadc8416e
Author | SHA1 | Date | |
---|---|---|---|
4eadc8416e | |||
62b243c8e4 | |||
83df31ec80 | |||
005634ff1c | |||
f60d0d655b | |||
53f019906e |
31 changed files with 718 additions and 99 deletions
20
.gitignore
vendored
20
.gitignore
vendored
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
27
GNUmakefile
27
GNUmakefile
|
@ -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..."
|
||||||
|
|
21
README.md
21
README.md
|
@ -38,6 +38,22 @@ 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" {}
|
||||||
|
|
||||||
|
@ -64,6 +80,11 @@ 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"
|
||||||
value = tor_relay_identity_ed25519.bridge.private_key_pem
|
value = tor_relay_identity_ed25519.bridge.private_key_pem
|
||||||
|
|
51
docs/resources/family_identity.md
Normal file
51
docs/resources/family_identity.md
Normal 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)
|
|
@ -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
|
||||||
|
|
|
@ -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."
|
|
|
@ -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,6 +15,11 @@ 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
|
||||||
|
@ -60,3 +65,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
|
||||||
|
}
|
15
e2e-tests/obfs4/terraformrc
Normal file
15
e2e-tests/obfs4/terraformrc
Normal 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
7
e2e-tests/obfs4/test.sh
Executable 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
3
e2e-tests/obfs4/tf
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
export TF_CLI_CONFIG_FILE=terraformrc
|
||||||
|
exec tofu "$@"
|
14
e2e-tests/setup.sh
Executable file
14
e2e-tests/setup.sh
Executable 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"
|
44
e2e-tests/tor-family/main.tf
Normal file
44
e2e-tests/tor-family/main.tf
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
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 "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
|
||||||
|
}
|
15
e2e-tests/tor-family/terraformrc
Normal file
15
e2e-tests/tor-family/terraformrc
Normal 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
34
e2e-tests/tor-family/test.sh
Executable 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
|
|
@ -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 "$@"
|
17
examples/resources/tor_family_identity/resource.tf
Normal file
17
examples/resources/tor_family_identity/resource.tf
Normal 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
|
||||||
|
}
|
|
@ -33,4 +33,9 @@ output "public_key_fingerprint_sha1" {
|
||||||
output "public_key_fingerprint_sha256" {
|
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
|
||||||
}
|
}
|
|
@ -57,6 +57,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
2
go.mod
|
@ -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
|
||||||
|
|
|
@ -61,6 +61,7 @@ func (p *ScaffoldingProvider) Resources(ctx context.Context) []func() resource.R
|
||||||
NewTorObfs4StateResource,
|
NewTorObfs4StateResource,
|
||||||
NewTorRelayIdentityRsaResource,
|
NewTorRelayIdentityRsaResource,
|
||||||
NewTorRelayIdentityEd25519Resource,
|
NewTorRelayIdentityEd25519Resource,
|
||||||
|
NewTorFamilyIdentityResource,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
166
internal/provider/tor_family_identity_resource.go
Normal file
166
internal/provider/tor_family_identity_resource.go
Normal 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) {
|
||||||
|
}
|
|
@ -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])
|
||||||
|
}
|
||||||
|
}
|
85
internal/provider/tor_family_identity_resource_test.go
Normal file
85
internal/provider/tor_family_identity_resource_test.go
Normal 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)
|
||||||
|
}
|
|
@ -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"),
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -184,5 +184,5 @@ func (r *TorRelayIdentityEd25519Resource) encodePublicKeyPEM(publicKey ed25519.P
|
||||||
func (r *TorRelayIdentityEd25519Resource) generateSha256Fingerprint(publicKey ed25519.PublicKey) string {
|
func (r *TorRelayIdentityEd25519Resource) generateSha256Fingerprint(publicKey ed25519.PublicKey) string {
|
||||||
publicKeyBytes, _ := x509.MarshalPKIXPublicKey(publicKey)
|
publicKeyBytes, _ := x509.MarshalPKIXPublicKey(publicKey)
|
||||||
sha256Sum := sha256.Sum256(publicKeyBytes)
|
sha256Sum := sha256.Sum256(publicKeyBytes)
|
||||||
return fmt.Sprintf("%x", sha256Sum)
|
return fmt.Sprintf("%X", sha256Sum)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,7 @@ 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(`^[0-9A-F]{64}$`)),
|
||||||
resource.TestMatchResourceAttr("tor_relay_identity_ed25519.test", "public_key_fingerprint_sha256", regexp.MustCompile(`^[0-9a-f]{64}$`)),
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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" {}
|
||||||
`
|
`
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue