166 lines
5.3 KiB
Go
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) {
|
|
}
|