Compare commits

..

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
14 changed files with 317 additions and 44 deletions

12
.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,10 +32,11 @@ 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
.claude
e2e-tests/**/*tfstate* e2e-tests/**/*tfstate*
e2e-tests/**/providers e2e-tests/**/providers
e2e-tests/**/.terraform.lock.hcl e2e-tests/**/.terraform.lock.hcl

View file

@ -58,6 +58,8 @@ resource "local_sensitive_file" "family_key" {
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
@ -80,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

@ -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

@ -26,6 +26,13 @@ resource "tor_obfs4_state" "bridge" {
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"
@ -46,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

View file

@ -17,6 +17,10 @@ resource "tor_family_identity" "this" {
family_name = "MyFamily" family_name = "MyFamily"
} }
resource "tor_relay_identity_rsa" "this" {}
resource "tor_relay_identity_ed25519" "this" {}
resource "local_sensitive_file" "family_key" { resource "local_sensitive_file" "family_key" {
content_base64 = tor_family_identity.this.secret_key content_base64 = tor_family_identity.this.secret_key
filename = "./data/keys/MyKey.secret_family_key" filename = "./data/keys/MyKey.secret_family_key"
@ -42,3 +46,34 @@ output "family_id" {
description = "Family ID for the bridge" description = "Family ID for the bridge"
value = tor_family_identity.this.id 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

@ -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:

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"
@ -36,6 +37,7 @@ type TorRelayIdentityRsaResourceModel struct {
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" {}
` `