terraform-provider-tor/internal/provider/tor_obfs4_state_resource.go

426 lines
14 KiB
Go
Raw Normal View History

2025-06-03 13:23:45 +02:00
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (c) 2025 Abel Luck <abel@guardianproject.info>
package provider
import (
"context"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"strings"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"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"
"gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/lyrebird/common/drbg"
"gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/lyrebird/common/ntor"
)
// Ensure provider defined types fully satisfy framework interfaces.
var _ resource.Resource = &TorObfs4StateResource{}
func NewTorObfs4StateResource() resource.Resource {
return &TorObfs4StateResource{}
}
// TorObfs4StateResource defines the resource implementation.
type TorObfs4StateResource struct{}
// TorObfs4StateResourceModel describes the resource data model.
type TorObfs4StateResourceModel struct {
2025-06-03 13:24:01 +02:00
Id types.String `tfsdk:"id"`
RsaIdentityPrivateKey types.String `tfsdk:"rsa_identity_private_key"`
2025-06-03 13:23:45 +02:00
Ed25519IdentityPrivateKey types.String `tfsdk:"ed25519_identity_private_key"`
2025-06-03 13:24:01 +02:00
NodeId types.String `tfsdk:"node_id"`
PrivateKey types.String `tfsdk:"private_key"`
PublicKey types.String `tfsdk:"public_key"`
DrbgSeed types.String `tfsdk:"drbg_seed"`
IatMode types.Int64 `tfsdk:"iat_mode"`
Certificate types.String `tfsdk:"certificate"`
StateJson types.String `tfsdk:"state_json"`
BridgeLine types.String `tfsdk:"bridge_line"`
2025-06-03 13:23:45 +02:00
}
// obfs4StateJson represents the JSON structure for the state file
type obfs4StateJson struct {
NodeId string `json:"node-id"`
PrivateKey string `json:"private-key"`
PublicKey string `json:"public-key"`
DrbgSeed string `json:"drbg-seed"`
IatMode int `json:"iat-mode"`
}
func (r *TorObfs4StateResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_obfs4_state"
}
func (r *TorObfs4StateResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Generates obfs4 state and certificate for Tor bridges using external relay identity keys",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Resource identifier",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"rsa_identity_private_key": schema.StringAttribute{
Required: true,
Sensitive: true,
MarkdownDescription: "RSA identity private key in PEM format (from tor_relay_identity_rsa resource)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"ed25519_identity_private_key": schema.StringAttribute{
Required: true,
Sensitive: true,
MarkdownDescription: "Ed25519 identity private key in PEM format (from tor_relay_identity_ed25519 resource)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"node_id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "20-byte node ID in hex format",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"private_key": schema.StringAttribute{
Computed: true,
Sensitive: true,
MarkdownDescription: "32-byte Curve25519 private key in hex format",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"public_key": schema.StringAttribute{
Computed: true,
MarkdownDescription: "32-byte Curve25519 public key in hex format",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"drbg_seed": schema.StringAttribute{
Computed: true,
Sensitive: true,
MarkdownDescription: "24-byte DRBG seed in hex format",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"iat_mode": schema.Int64Attribute{
Optional: true,
Computed: true,
Default: int64default.StaticInt64(0),
MarkdownDescription: "Inter-Arrival Time mode (0=none, 1=enabled, 2=paranoid)",
},
"certificate": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Base64-encoded certificate for bridge lines",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"state_json": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Complete obfs4 state in JSON format",
},
"bridge_line": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Complete bridge line ready for client use (placeholder IP and fingerprint)",
},
},
}
}
func (r *TorObfs4StateResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// No configuration needed for this crypto generation resource
}
func (r *TorObfs4StateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data TorObfs4StateResourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Generate obfs4 state
err := r.generateObfs4State(ctx, &data)
if err != nil {
2025-06-03 13:24:01 +02:00
resp.Diagnostics.AddError("obfs4 State Generation Error",
2025-06-03 13:23:45 +02:00
fmt.Sprintf("Unable to generate obfs4 state: %s", err))
return
}
// Set ID to a hash of the public key for uniqueness
data.Id = types.StringValue(fmt.Sprintf("obfs4-%s", data.PublicKey.ValueString()[:16]))
tflog.Trace(ctx, "created obfs4 state resource")
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *TorObfs4StateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data TorObfs4StateResourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// For this resource, the state is static once created, so no external API calls needed
// Just ensure the computed fields are still consistent
if !data.NodeId.IsNull() && !data.PublicKey.IsNull() {
// Regenerate certificate to ensure consistency
cert, err := r.generateCertificate(data.NodeId.ValueString(), data.PublicKey.ValueString())
if err != nil {
resp.Diagnostics.AddError("Certificate Generation Error",
fmt.Sprintf("Unable to regenerate certificate: %s", err))
return
}
data.Certificate = types.StringValue(cert)
// Regenerate state JSON
stateJson, err := r.generateStateJson(&data)
if err != nil {
resp.Diagnostics.AddError("State JSON Generation Error",
fmt.Sprintf("Unable to regenerate state JSON: %s", err))
return
}
data.StateJson = types.StringValue(stateJson)
// Regenerate bridge line
bridgeLine := r.generateBridgeLine(data.Certificate.ValueString(), data.IatMode.ValueInt64())
data.BridgeLine = types.StringValue(bridgeLine)
}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *TorObfs4StateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var currentState TorObfs4StateResourceModel
var plannedState TorObfs4StateResourceModel
// Read current state
resp.Diagnostics.Append(req.State.Get(ctx, &currentState)...)
if resp.Diagnostics.HasError() {
return
}
// Read planned state
resp.Diagnostics.Append(req.Plan.Get(ctx, &plannedState)...)
if resp.Diagnostics.HasError() {
return
}
// Only iat_mode can be updated; preserve all crypto fields from current state
currentState.IatMode = plannedState.IatMode
// Regenerate computed fields with new iat_mode
stateJson, err := r.generateStateJson(&currentState)
if err != nil {
resp.Diagnostics.AddError("State JSON Generation Error",
fmt.Sprintf("Unable to regenerate state JSON: %s", err))
return
}
currentState.StateJson = types.StringValue(stateJson)
// Regenerate bridge line with new iat_mode
bridgeLine := r.generateBridgeLine(currentState.Certificate.ValueString(), currentState.IatMode.ValueInt64())
currentState.BridgeLine = types.StringValue(bridgeLine)
tflog.Trace(ctx, "updated obfs4 state resource")
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &currentState)...)
}
func (r *TorObfs4StateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
// No external resources to clean up - this is a stateless resource
tflog.Trace(ctx, "deleted obfs4 state resource")
}
// generateObfs4State generates all the obfs4 state fields
func (r *TorObfs4StateResource) generateObfs4State(ctx context.Context, data *TorObfs4StateResourceModel) error {
// Derive node ID from RSA identity key (required)
nodeId, err := r.deriveNodeIdFromRsaKey(data.RsaIdentityPrivateKey.ValueString())
if err != nil {
return fmt.Errorf("failed to derive node ID from RSA key: %w", err)
}
data.NodeId = types.StringValue(nodeId.Hex())
// Generate Curve25519 keypair
keypair, err := ntor.NewKeypair(false)
if err != nil {
return fmt.Errorf("failed to generate keypair: %w", err)
}
data.PrivateKey = types.StringValue(keypair.Private().Hex())
data.PublicKey = types.StringValue(keypair.Public().Hex())
// Generate DRBG seed
seed, err := drbg.NewSeed()
if err != nil {
return fmt.Errorf("failed to generate DRBG seed: %w", err)
}
data.DrbgSeed = types.StringValue(seed.Hex())
// Set default iat_mode if not specified
if data.IatMode.IsNull() {
data.IatMode = types.Int64Value(0)
}
// Generate certificate
cert, err := r.generateCertificate(data.NodeId.ValueString(), data.PublicKey.ValueString())
if err != nil {
return fmt.Errorf("failed to generate certificate: %w", err)
}
data.Certificate = types.StringValue(cert)
// Generate state JSON
stateJson, err := r.generateStateJson(data)
if err != nil {
return fmt.Errorf("failed to generate state JSON: %w", err)
}
data.StateJson = types.StringValue(stateJson)
// Generate bridge line
bridgeLine := r.generateBridgeLine(data.Certificate.ValueString(), data.IatMode.ValueInt64())
data.BridgeLine = types.StringValue(bridgeLine)
return nil
}
// generateCertificate creates the base64-encoded certificate from node ID and public key
func (r *TorObfs4StateResource) generateCertificate(nodeIdHex, publicKeyHex string) (string, error) {
// Decode hex strings to bytes
nodeIdBytes, err := hex.DecodeString(nodeIdHex)
if err != nil {
return "", fmt.Errorf("failed to decode node ID: %w", err)
}
2025-06-03 13:24:01 +02:00
2025-06-03 13:23:45 +02:00
publicKeyBytes, err := hex.DecodeString(publicKeyHex)
if err != nil {
return "", fmt.Errorf("failed to decode public key: %w", err)
}
// Validate lengths
if len(nodeIdBytes) != 20 {
return "", fmt.Errorf("node ID must be 20 bytes, got %d", len(nodeIdBytes))
}
if len(publicKeyBytes) != 32 {
return "", fmt.Errorf("public key must be 32 bytes, got %d", len(publicKeyBytes))
}
// Concatenate node ID + public key (52 bytes total)
certBytes := make([]byte, 52)
copy(certBytes[:20], nodeIdBytes)
copy(certBytes[20:], publicKeyBytes)
// Base64 encode and remove padding
cert := base64.StdEncoding.EncodeToString(certBytes)
cert = strings.TrimSuffix(cert, "==")
2025-06-03 13:24:01 +02:00
2025-06-03 13:23:45 +02:00
return cert, nil
}
// generateStateJson creates the JSON representation of the obfs4 state
func (r *TorObfs4StateResource) generateStateJson(data *TorObfs4StateResourceModel) (string, error) {
state := obfs4StateJson{
NodeId: data.NodeId.ValueString(),
PrivateKey: data.PrivateKey.ValueString(),
PublicKey: data.PublicKey.ValueString(),
DrbgSeed: data.DrbgSeed.ValueString(),
IatMode: int(data.IatMode.ValueInt64()),
}
jsonBytes, err := json.Marshal(state)
if err != nil {
return "", fmt.Errorf("failed to marshal state JSON: %w", err)
}
return string(jsonBytes), nil
}
// generateBridgeLine creates a complete bridge line with placeholder IP and fingerprint
func (r *TorObfs4StateResource) generateBridgeLine(certificate string, iatMode int64) string {
// Use placeholder values that users can easily replace
placeholderIP := "<IP:PORT>"
placeholderFingerprint := "<FINGERPRINT>"
2025-06-03 13:24:01 +02:00
return fmt.Sprintf("Bridge obfs4 %s %s cert=%s iat-mode=%d",
2025-06-03 13:23:45 +02:00
placeholderIP, placeholderFingerprint, certificate, iatMode)
}
// deriveNodeIdFromRsaKey derives a 20-byte node ID from an RSA private key PEM
// This follows the Tor specification for relay identity
func (r *TorObfs4StateResource) deriveNodeIdFromRsaKey(rsaPrivateKeyPem string) (*ntor.NodeID, error) {
// Parse the PEM-encoded RSA private key
block, _ := pem.Decode([]byte(rsaPrivateKeyPem))
if block == nil {
return nil, fmt.Errorf("failed to parse PEM block")
}
var privateKey *rsa.PrivateKey
var err error
// Try different RSA private key formats
2025-06-03 13:28:44 +02:00
switch block.Type {
case "RSA PRIVATE KEY":
2025-06-03 13:23:45 +02:00
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
2025-06-03 13:28:44 +02:00
case "PRIVATE KEY":
2025-06-03 13:23:45 +02:00
parsedKey, err2 := x509.ParsePKCS8PrivateKey(block.Bytes)
if err2 == nil {
var ok bool
privateKey, ok = parsedKey.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("parsed key is not an RSA private key")
}
} else {
err = err2
}
2025-06-03 13:28:44 +02:00
default:
2025-06-03 13:23:45 +02:00
return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type)
}
if err != nil {
return nil, fmt.Errorf("failed to parse RSA private key: %w", err)
}
// Extract the public key and encode it
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)
hash := sha1.Sum(publicKeyBytes)
2025-06-03 13:24:01 +02:00
2025-06-03 13:23:45 +02:00
// Create node ID from the first 20 bytes (which is all of SHA1)
nodeId, err := ntor.NewNodeID(hash[:])
if err != nil {
return nil, fmt.Errorf("failed to create node ID: %w", err)
}
return nodeId, nil
2025-06-03 13:24:01 +02:00
}