diff --git a/README.md b/README.md index 5a7a157..a8fefa5 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ resource "local_sensitive_file" "family_key" { resource "tor_relay_identity_rsa" "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" { rsa_identity_private_key = tor_relay_identity_rsa.bridge.private_key_pem @@ -86,11 +88,17 @@ output "rsa_fingerprint_hashed" { } 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 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" { description = "Complete obfs4 state for bridge runtime" value = tor_obfs4_state.bridge.state_json diff --git a/docs/resources/relay_identity_ed25519.md b/docs/resources/relay_identity_ed25519.md index d3d8f33..79b32df 100644 --- a/docs/resources/relay_identity_ed25519.md +++ b/docs/resources/relay_identity_ed25519.md @@ -52,5 +52,7 @@ output "public_key_fingerprint_sha256" { - `algorithm` (String) Name of the algorithm used when generating the private key (always 'Ed25519') - `id` (String) Unique identifier based on public key fingerprint - `private_key_pem` (String, Sensitive) Private key data in PEM (RFC 1421) 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_tor` (String) Public key data in Tor's binary format, base64 encoded diff --git a/internal/provider/tor_relay_identity_ed25519_resource.go b/internal/provider/tor_relay_identity_ed25519_resource.go index 5d42bb1..f513097 100644 --- a/internal/provider/tor_relay_identity_ed25519_resource.go +++ b/internal/provider/tor_relay_identity_ed25519_resource.go @@ -34,6 +34,8 @@ type TorRelayIdentityEd25519ResourceModel struct { Algorithm types.String `tfsdk:"algorithm"` PrivateKeyPem types.String `tfsdk:"private_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"` } @@ -75,6 +77,21 @@ func (r *TorRelayIdentityEd25519Resource) Schema(ctx context.Context, req resour 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{ Computed: true, MarkdownDescription: "Base64 encoded public key bytes (32 bytes) without padding, used as the Tor Ed25519 fingerprint", @@ -126,6 +143,13 @@ func (r *TorRelayIdentityEd25519Resource) Create(ctx context.Context, req resour } data.PublicKeyPem = types.StringValue(publicKeyPem) + // Encode keys in Tor format + privateKeyTor := r.encodeTorPrivateKey(privateKey) + data.PrivateKeyTor = types.StringValue(privateKeyTor) + + publicKeyTor := r.encodeTorPublicKey(publicKey) + 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) @@ -186,3 +210,33 @@ func (r *TorRelayIdentityEd25519Resource) generateEd25519Fingerprint(publicKey e fingerprint := base64.StdEncoding.EncodeToString(publicKey) return strings.TrimRight(fingerprint, "=") } + +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) +} diff --git a/internal/provider/tor_relay_identity_ed25519_resource_test.go b/internal/provider/tor_relay_identity_ed25519_resource_test.go index 1d19f84..f293060 100644 --- a/internal/provider/tor_relay_identity_ed25519_resource_test.go +++ b/internal/provider/tor_relay_identity_ed25519_resource_test.go @@ -52,6 +52,43 @@ func TestTorEd25519FingerprintGeneration(t *testing.T) { } } +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 = ` resource "tor_relay_identity_ed25519" "test" {} `