Compare commits

..

6 commits

Author SHA1 Message Date
4eadc8416e Add hashed fingerprint functionality to RSA relay identity keys
Some checks failed
CI / ci (push) Failing after 3m10s
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
10 changed files with 127 additions and 32 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

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

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

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

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

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

View file

@ -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}$`)),
), ),
}, },
}, },

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" {}
` `