terraform-provider-tor/internal/provider/tor_family_identity_resource.go
Abel Luck ba5761e973
Some checks failed
CI / ci (push) Has been cancelled
Update documentation
2025-06-06 13:02:05 +02:00

166 lines
5.3 KiB
Go

// 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) {
}