Compare commits
2 commits
0951242b32
...
ba5761e973
Author | SHA1 | Date | |
---|---|---|---|
ba5761e973 | |||
ec57a47ba2 |
22 changed files with 575 additions and 67 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -39,5 +39,9 @@ CLAUDE.md
|
|||
extra
|
||||
.direnv
|
||||
.claude
|
||||
e2e-test/*tfstate*
|
||||
e2e-test/.terraformrc
|
||||
e2e-tests/**/*tfstate*
|
||||
e2e-tests/**/providers
|
||||
e2e-tests/**/.terraform.lock.hcl
|
||||
e2e-tests/**/torrc
|
||||
e2e-tests/**/*.secret_family_key
|
||||
e2e-tests/**/data
|
||||
|
|
|
@ -38,6 +38,14 @@ make lint
|
|||
|
||||
# Run acceptance tests (creates real resources)
|
||||
make testacc
|
||||
|
||||
# Run e2e tests
|
||||
make clean build e2e/tor-family # Run the tor-family test with latest binary
|
||||
make clean build e2e/obfs4 # Run the obfs4 e2e test with latest binary
|
||||
make e2e # Run all e2e tests
|
||||
|
||||
# Important: Always use 'make clean build' before e2e tests to ensure
|
||||
# the test uses the latest provider binary
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
@ -49,6 +57,7 @@ make testacc
|
|||
│ ├── provider/ # Provider configuration examples
|
||||
│ └── resources/ # Resource examples
|
||||
├── docs/ # Generated documentation
|
||||
├── e2e-tests/ # End-to-end tests
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
|
27
GNUmakefile
27
GNUmakefile
|
@ -1,13 +1,19 @@
|
|||
default: fmt lint install generate
|
||||
|
||||
clean:
|
||||
dist-clean:
|
||||
go clean -cache -modcache -testcache
|
||||
|
||||
clean:
|
||||
rm -f ./terraform-provider-tor ./provider.test
|
||||
find e2e-tests -type f -name '*.tfstate*' -delete
|
||||
find e2e-tests -type d -name '.terraform' -prune -exec rm -rf ./{} \;
|
||||
find e2e-tests -type d -name 'providers' -prune -exec rm -rf ./{} \;
|
||||
|
||||
build:
|
||||
go build -v ./...
|
||||
go build
|
||||
|
||||
install: build
|
||||
go install -v ./...
|
||||
go install
|
||||
|
||||
lint:
|
||||
go mod tidy
|
||||
|
@ -33,16 +39,11 @@ test:
|
|||
testacc:
|
||||
TF_ACC=1 go test -v -cover -timeout 120m ./...
|
||||
|
||||
e2e:
|
||||
@echo "Running end-to-end test..."
|
||||
cd e2e-test && \
|
||||
rm -rf .terraform/ .terraform.lock.hcl terraform.tfstate* && \
|
||||
./setup-dev.sh && \
|
||||
TF_CLI_CONFIG_FILE=.terraformrc tofu plan && \
|
||||
TF_CLI_CONFIG_FILE=.terraformrc tofu apply -auto-approve && \
|
||||
echo "✓ E2E test passed! Cleaning up..." && \
|
||||
TF_CLI_CONFIG_FILE=.terraformrc tofu destroy -auto-approve && \
|
||||
echo "✓ E2E test completed successfully"
|
||||
e2e/%:
|
||||
@echo "Executing end-to-end test: $*"
|
||||
cd e2e-tests/$* && ./test.sh
|
||||
|
||||
e2e: clean build e2e/obfs4 e2e/tor-family
|
||||
|
||||
ci:
|
||||
@echo "Running CI pipeline..."
|
||||
|
|
51
docs/resources/family_identity.md
Normal file
51
docs/resources/family_identity.md
Normal file
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "tor_family_identity Resource - tor"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
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
|
||||
---
|
||||
|
||||
# tor_family_identity (Resource)
|
||||
|
||||
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
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
resource "tor_family_identity" "example" {
|
||||
family_name = "MyFamily"
|
||||
}
|
||||
|
||||
|
||||
resource "local_sensitive_file" "family_key" {
|
||||
content_base64 = tor_family_identity.example.secret_key
|
||||
filename = "MyFamily.secret_family_key"
|
||||
}
|
||||
|
||||
resource "local_file" "torrc" {
|
||||
filename = "./torrc"
|
||||
content = <<EOF
|
||||
FamilyId ${tor_family_identity.example.id}
|
||||
... other torrc configuration ...
|
||||
EOF
|
||||
}
|
||||
```
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `family_name` (String) Name of the family
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `id` (String) Base64-encoded public key (as stored in public_family_id file)
|
||||
- `secret_key` (String, Sensitive) Binary contents of the secret family key file (base64 encoded)
|
|
@ -1,48 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "Setting up development environment for terraform-provider-tor..."
|
||||
|
||||
# Get the Go bin path
|
||||
GOBIN=$(go env GOPATH)/bin
|
||||
if [ -z "$GOBIN" ]; then
|
||||
GOBIN=$(go env GOROOT)/bin
|
||||
fi
|
||||
|
||||
echo "Go bin path: $GOBIN"
|
||||
|
||||
# Create local .terraformrc with dev overrides
|
||||
TERRAFORMRC="$(pwd)/.terraformrc"
|
||||
echo "Creating $TERRAFORMRC..."
|
||||
|
||||
# Create local .terraformrc with dev overrides
|
||||
cat > "$TERRAFORMRC" << EOF
|
||||
provider_installation {
|
||||
dev_overrides {
|
||||
"guardianproject/tor" = "$GOBIN"
|
||||
}
|
||||
direct {}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "✓ Created local $TERRAFORMRC with dev overrides"
|
||||
|
||||
# Build and install the provider
|
||||
echo "Building and installing provider..."
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# Build with proper naming for dev overrides
|
||||
go build -o "$GOBIN/terraform-provider-tor_v99.0.0"
|
||||
|
||||
echo "✓ Provider built and installed to $GOBIN/terraform-provider-tor_v99.0.0"
|
||||
|
||||
echo ""
|
||||
echo "Setup complete! You can now run:"
|
||||
echo " cd e2e-test"
|
||||
echo " ./tf plan"
|
||||
echo " ./tf apply"
|
||||
echo ""
|
||||
echo "Or use the full command:"
|
||||
echo " TF_CLI_CONFIG_FILE=.terraformrc tofu plan"
|
||||
echo ""
|
||||
echo "Note: Using local .terraformrc to avoid modifying your global configuration."
|
|
@ -1,7 +1,7 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
tor = {
|
||||
source = "guardianproject/tor"
|
||||
source = "guardianproject/tor"
|
||||
version = "99.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,11 @@ resource "tor_relay_identity_rsa" "bridge" {}
|
|||
# Generate Ed25519 identity key for the bridge
|
||||
resource "tor_relay_identity_ed25519" "bridge" {}
|
||||
|
||||
# Generate family identity for the bridge
|
||||
resource "tor_family_identity" "bridge" {
|
||||
family_name = "MyBridgeFamily"
|
||||
}
|
||||
|
||||
# Generate obfs4 state using the identity keys
|
||||
resource "tor_obfs4_state" "bridge" {
|
||||
rsa_identity_private_key = tor_relay_identity_rsa.bridge.private_key_pem
|
||||
|
@ -60,3 +65,8 @@ output "bridge_line" {
|
|||
description = "Complete bridge line for clients"
|
||||
value = data.tor_obfs4_bridge_line.bridge.bridge_line
|
||||
}
|
||||
|
||||
output "family_id" {
|
||||
description = "Family ID for the bridge"
|
||||
value = tor_family_identity.bridge.id
|
||||
}
|
15
e2e-tests/obfs4/terraformrc
Normal file
15
e2e-tests/obfs4/terraformrc
Normal file
|
@ -0,0 +1,15 @@
|
|||
provider_installation {
|
||||
filesystem_mirror {
|
||||
path = "./providers"
|
||||
include = [
|
||||
"registry.terraform.io/guardianproject/*",
|
||||
"registry.opentofu.org/guardianproject/*"
|
||||
]
|
||||
}
|
||||
direct {
|
||||
exclude = [
|
||||
"registry.terraform.io/guardianproject/*",
|
||||
"registry.opentofu.org/guardianproject/*"
|
||||
]
|
||||
}
|
||||
}
|
7
e2e-tests/obfs4/test.sh
Executable file
7
e2e-tests/obfs4/test.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env sh
|
||||
set -e
|
||||
../setup.sh
|
||||
rm -f terraform.tfstate*
|
||||
./tf init
|
||||
./tf plan
|
||||
./tf apply -auto-approve
|
3
e2e-tests/obfs4/tf
Executable file
3
e2e-tests/obfs4/tf
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
export TF_CLI_CONFIG_FILE=terraformrc
|
||||
exec tofu "$@"
|
14
e2e-tests/setup.sh
Executable file
14
e2e-tests/setup.sh
Executable file
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env bash
|
||||
set -e
|
||||
rm -f ./.terraform.lock.hcl
|
||||
|
||||
mkdir -p providers/registry.terraform.io/guardianproject/tor/99.0.0/linux_amd64
|
||||
mkdir -p providers/registry.opentofu.org/guardianproject/tor/99.0.0/linux_amd64
|
||||
cp ../../terraform-provider-tor providers/registry.terraform.io/guardianproject/tor/99.0.0/linux_amd64
|
||||
cp ../../terraform-provider-tor providers/registry.opentofu.org/guardianproject/tor/99.0.0/linux_amd64
|
||||
|
||||
echo ""
|
||||
echo "Setup complete! You can now run:"
|
||||
echo " ./tf init"
|
||||
echo " ./tf plan"
|
||||
echo " ./tf apply"
|
44
e2e-tests/tor-family/main.tf
Normal file
44
e2e-tests/tor-family/main.tf
Normal file
|
@ -0,0 +1,44 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
tor = {
|
||||
source = "guardianproject/tor"
|
||||
version = "99.0.0"
|
||||
}
|
||||
local = {
|
||||
source = "hashicorp/local"
|
||||
version = "2.5.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "tor" {}
|
||||
|
||||
resource "tor_family_identity" "this" {
|
||||
family_name = "MyFamily"
|
||||
}
|
||||
|
||||
resource "local_sensitive_file" "family_key" {
|
||||
content_base64 = tor_family_identity.this.secret_key
|
||||
filename = "./data/keys/MyKey.secret_family_key"
|
||||
file_permission = "0600"
|
||||
}
|
||||
|
||||
resource "local_file" "this" {
|
||||
filename = "./torrc"
|
||||
content = <<EOF
|
||||
FamilyId ${tor_family_identity.this.id}
|
||||
BridgeRelay 1
|
||||
DataDirectory data
|
||||
ORPort 3333
|
||||
ServerTransportListenAddr obfs4 0.0.0.0:3334
|
||||
ExtORPort auto
|
||||
ContactInfo <address@email.com>
|
||||
Nickname PickANickname
|
||||
EOF
|
||||
}
|
||||
|
||||
|
||||
output "family_id" {
|
||||
description = "Family ID for the bridge"
|
||||
value = tor_family_identity.this.id
|
||||
}
|
15
e2e-tests/tor-family/terraformrc
Normal file
15
e2e-tests/tor-family/terraformrc
Normal file
|
@ -0,0 +1,15 @@
|
|||
provider_installation {
|
||||
filesystem_mirror {
|
||||
path = "./providers"
|
||||
include = [
|
||||
"registry.terraform.io/guardianproject/*",
|
||||
"registry.opentofu.org/guardianproject/*"
|
||||
]
|
||||
}
|
||||
direct {
|
||||
exclude = [
|
||||
"registry.terraform.io/guardianproject/*",
|
||||
"registry.opentofu.org/guardianproject/*"
|
||||
]
|
||||
}
|
||||
}
|
34
e2e-tests/tor-family/test.sh
Executable file
34
e2e-tests/tor-family/test.sh
Executable file
|
@ -0,0 +1,34 @@
|
|||
#!/usr/bin/env sh
|
||||
set -e
|
||||
../setup.sh
|
||||
rm -f terraform.tfstate*
|
||||
./tf init
|
||||
./tf plan
|
||||
./tf apply -auto-approve
|
||||
|
||||
set +e
|
||||
|
||||
# Start tor and let it run for a few seconds
|
||||
echo "Starting Tor to verify family key..."
|
||||
timeout 5 tor -f ./torrc >tor.log 2>&1
|
||||
TOR_EXIT_CODE=$?
|
||||
|
||||
set -e
|
||||
|
||||
# Check if tor exited with an error (not due to timeout)
|
||||
# timeout returns 124 when it kills the process
|
||||
if [ $TOR_EXIT_CODE -ne 0 ] && [ $TOR_EXIT_CODE -ne 124 ]; then
|
||||
echo "ERROR: Tor exited with error code $TOR_EXIT_CODE"
|
||||
cat tor.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if tor started bootstrapping (indicates successful key loading)
|
||||
if grep -q "Bootstrapped [0-9]" tor.log; then
|
||||
echo "SUCCESS: Tor started bootstrapping with generated family key"
|
||||
exit 0
|
||||
else
|
||||
echo "ERROR: Tor did not start bootstrapping"
|
||||
cat tor.log
|
||||
exit 1
|
||||
fi
|
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
# Wrapper script to run terraform with local config
|
||||
|
||||
export TF_CLI_CONFIG_FILE=.terraformrc
|
||||
export TF_CLI_CONFIG_FILE=terraformrc
|
||||
exec tofu "$@"
|
17
examples/resources/tor_family_identity/resource.tf
Normal file
17
examples/resources/tor_family_identity/resource.tf
Normal file
|
@ -0,0 +1,17 @@
|
|||
resource "tor_family_identity" "example" {
|
||||
family_name = "MyFamily"
|
||||
}
|
||||
|
||||
|
||||
resource "local_sensitive_file" "family_key" {
|
||||
content_base64 = tor_family_identity.example.secret_key
|
||||
filename = "MyFamily.secret_family_key"
|
||||
}
|
||||
|
||||
resource "local_file" "torrc" {
|
||||
filename = "./torrc"
|
||||
content = <<EOF
|
||||
FamilyId ${tor_family_identity.example.id}
|
||||
... other torrc configuration ...
|
||||
EOF
|
||||
}
|
|
@ -57,6 +57,7 @@
|
|||
default = pkgs.mkShell {
|
||||
packages = [
|
||||
pkgs.go
|
||||
pkgs.gopls
|
||||
pkgs.golangci-lint
|
||||
pkgs.obfs4
|
||||
pkgs.gnumake
|
||||
|
|
2
go.mod
2
go.mod
|
@ -5,6 +5,7 @@ go 1.23.0
|
|||
toolchain go1.24.2
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0
|
||||
github.com/hashicorp/terraform-plugin-framework v1.12.0
|
||||
github.com/hashicorp/terraform-plugin-go v0.24.0
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0
|
||||
|
@ -13,7 +14,6 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
|
|
|
@ -61,6 +61,7 @@ func (p *ScaffoldingProvider) Resources(ctx context.Context) []func() resource.R
|
|||
NewTorObfs4StateResource,
|
||||
NewTorRelayIdentityRsaResource,
|
||||
NewTorRelayIdentityEd25519Resource,
|
||||
NewTorFamilyIdentityResource,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
166
internal/provider/tor_family_identity_resource.go
Normal file
166
internal/provider/tor_family_identity_resource.go
Normal file
|
@ -0,0 +1,166 @@
|
|||
// 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) {
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (C) 2025 Abel Luck <abel@guardianproject.info>
|
||||
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test data from a secret key generated by Tor version 0.4.9.2-alpha
|
||||
// To reproduce:
|
||||
// 1. Run: tor --keygen-family MyKey
|
||||
// 2. This creates MyKey.secret_family_key and MyKey.public_family_id
|
||||
// 3. Convert secret key to hex: xxd -p MyKey.secret_family_key | tr -d '\n'
|
||||
const torGeneratedSecretKeyHex = "3d3d206564323535313976312d7365637265743a20666d6c792d6964203d3d00f84c620e227fcc5085eb538a29a11ac25abb052b6a36ddae008b307cca67fe792ecd73a67c0a7a28b2b747be2a59e5ef0c155e33217add6dac42dbc6f85a5162"
|
||||
const torGeneratedPublicFamilyID = "xiHyFHgSU4/aaqPfRCrUk61jVBDQAv1hlHQd7lKIlI8"
|
||||
|
||||
func TestTorFamilyIdentity_SerializationAgainstTorGenerated(t *testing.T) {
|
||||
secretKeyBytes, err := hex.DecodeString(torGeneratedSecretKeyHex)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to decode secret key hex: %v", err)
|
||||
}
|
||||
|
||||
const expectedHeader = "== ed25519v1-secret: fmly-id ==\x00"
|
||||
if len(secretKeyBytes) < len(expectedHeader) {
|
||||
t.Fatalf("Secret key file too short: %d bytes", len(secretKeyBytes))
|
||||
}
|
||||
|
||||
header := string(secretKeyBytes[:len(expectedHeader)])
|
||||
if header != expectedHeader {
|
||||
t.Fatalf("Invalid header: got %q, want %q", header, expectedHeader)
|
||||
}
|
||||
|
||||
expandedSK := secretKeyBytes[len(expectedHeader):]
|
||||
if len(expandedSK) != 64 {
|
||||
t.Fatalf("Invalid expanded secret key length: got %d, want 64", len(expandedSK))
|
||||
}
|
||||
|
||||
publicKey, err := torComputePublicKey(expandedSK)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to compute public key: %v", err)
|
||||
}
|
||||
|
||||
computedFamilyID := base64.RawStdEncoding.EncodeToString(publicKey)
|
||||
|
||||
if computedFamilyID != torGeneratedPublicFamilyID {
|
||||
t.Errorf("Family ID mismatch:\n computed: %s\n expected: %s",
|
||||
computedFamilyID, torGeneratedPublicFamilyID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTorFamilyIdentity_ExpandSeed(t *testing.T) {
|
||||
// Test that our seed expansion produces the same result as Tor
|
||||
// We'll need to reverse-engineer the seed from the expanded key
|
||||
// This is a secondary test to verify our torExpandSeed function
|
||||
secretKeyBytes, err := hex.DecodeString(torGeneratedSecretKeyHex)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to decode secret key hex: %v", err)
|
||||
}
|
||||
|
||||
const expectedHeader = "== ed25519v1-secret: fmly-id ==\x00"
|
||||
expandedSK := secretKeyBytes[len(expectedHeader):]
|
||||
|
||||
// Verify the key is properly clamped according to ed25519 standards
|
||||
// For ed25519, the clamping is:
|
||||
// - bits 0,1,2 of first byte cleared (make it a multiple of 8)
|
||||
// - bit 6 of byte 31 set
|
||||
// - bit 7 of byte 31 cleared
|
||||
if expandedSK[0]&0x07 != 0 {
|
||||
t.Errorf("First byte not properly clamped: %02x (bits 0,1,2 should be cleared)", expandedSK[0])
|
||||
}
|
||||
if expandedSK[31]&0x40 == 0 {
|
||||
t.Errorf("Byte 31 bit 6 should be set: %02x", expandedSK[31])
|
||||
}
|
||||
if expandedSK[31]&0x80 != 0 {
|
||||
t.Errorf("Byte 31 bit 7 should be cleared: %02x", expandedSK[31])
|
||||
}
|
||||
}
|
85
internal/provider/tor_family_identity_resource_test.go
Normal file
85
internal/provider/tor_family_identity_resource_test.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (c) 2025 Abel Luck <abel@guardianproject.info>
|
||||
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
"github.com/hashicorp/terraform-plugin-testing/terraform"
|
||||
)
|
||||
|
||||
func TestAccTorFamilyIdentityResource(t *testing.T) {
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccTorFamilyIdentityResourceConfig("MyFamily"),
|
||||
Check: resource.ComposeAggregateTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("tor_family_identity.test", "family_name", "MyFamily"),
|
||||
resource.TestMatchResourceAttr("tor_family_identity.test", "id", regexp.MustCompile(`^[A-Za-z0-9+/]{43}$`)),
|
||||
resource.TestCheckResourceAttrSet("tor_family_identity.test", "secret_key"),
|
||||
testAccCheckTorFamilyIdentitySecretKeyFormat("tor_family_identity.test", "secret_key"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccCheckTorFamilyIdentitySecretKeyFormat(resourceName, attributeName string) resource.TestCheckFunc {
|
||||
return func(s *terraform.State) error {
|
||||
rs, ok := s.RootModule().Resources[resourceName]
|
||||
if !ok {
|
||||
return fmt.Errorf("Resource not found: %s", resourceName)
|
||||
}
|
||||
|
||||
secretKeyBase64 := rs.Primary.Attributes[attributeName]
|
||||
if secretKeyBase64 == "" {
|
||||
return fmt.Errorf("secret_key attribute is empty")
|
||||
}
|
||||
|
||||
secretKey, err := base64.StdEncoding.DecodeString(secretKeyBase64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to decode secret_key: %v", err)
|
||||
}
|
||||
|
||||
if len(secretKey) != 96 {
|
||||
return fmt.Errorf("secret_key has wrong size: expected 96 bytes, got %d", len(secretKey))
|
||||
}
|
||||
|
||||
expectedHeader := "== ed25519v1-secret: fmly-id =="
|
||||
actualHeader := string(secretKey[:32])
|
||||
if !strings.HasPrefix(actualHeader, expectedHeader) {
|
||||
return fmt.Errorf("secret_key has wrong header: expected %q, got %q", expectedHeader, actualHeader[:len(expectedHeader)])
|
||||
}
|
||||
|
||||
if secretKey[31] != 0 {
|
||||
return fmt.Errorf("secret_key header should end with null byte")
|
||||
}
|
||||
|
||||
id := rs.Primary.Attributes["id"]
|
||||
if _, err := base64.RawStdEncoding.DecodeString(id); err != nil {
|
||||
return fmt.Errorf("id should be a valid base64 string without padding: %s", id)
|
||||
}
|
||||
|
||||
if strings.HasSuffix(id, "=") {
|
||||
return fmt.Errorf("id should not have padding characters: %s", id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testAccTorFamilyIdentityResourceConfig(familyName string) string {
|
||||
return fmt.Sprintf(`
|
||||
resource "tor_family_identity" "test" {
|
||||
family_name = %[1]q
|
||||
}
|
||||
`, familyName)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue