Compare commits
No commits in common. "ba5761e973d1cbd1fff0948d7f0b896f48918787" and "0951242b3245ebb223d6e536e775a5a16e8e6834" have entirely different histories.
ba5761e973
...
0951242b32
22 changed files with 67 additions and 575 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -39,9 +39,5 @@ CLAUDE.md
|
||||||
extra
|
extra
|
||||||
.direnv
|
.direnv
|
||||||
.claude
|
.claude
|
||||||
e2e-tests/**/*tfstate*
|
e2e-test/*tfstate*
|
||||||
e2e-tests/**/providers
|
e2e-test/.terraformrc
|
||||||
e2e-tests/**/.terraform.lock.hcl
|
|
||||||
e2e-tests/**/torrc
|
|
||||||
e2e-tests/**/*.secret_family_key
|
|
||||||
e2e-tests/**/data
|
|
||||||
|
|
|
@ -38,14 +38,6 @@ make lint
|
||||||
|
|
||||||
# Run acceptance tests (creates real resources)
|
# Run acceptance tests (creates real resources)
|
||||||
make testacc
|
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
|
## Project Structure
|
||||||
|
@ -57,7 +49,6 @@ make e2e # Run all e2e tests
|
||||||
│ ├── provider/ # Provider configuration examples
|
│ ├── provider/ # Provider configuration examples
|
||||||
│ └── resources/ # Resource examples
|
│ └── resources/ # Resource examples
|
||||||
├── docs/ # Generated documentation
|
├── docs/ # Generated documentation
|
||||||
├── e2e-tests/ # End-to-end tests
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Security Considerations
|
## Security Considerations
|
||||||
|
|
27
GNUmakefile
27
GNUmakefile
|
@ -1,19 +1,13 @@
|
||||||
default: fmt lint install generate
|
default: fmt lint install generate
|
||||||
|
|
||||||
dist-clean:
|
clean:
|
||||||
go clean -cache -modcache -testcache
|
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:
|
build:
|
||||||
go build
|
go build -v ./...
|
||||||
|
|
||||||
install: build
|
install: build
|
||||||
go install
|
go install -v ./...
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
go mod tidy
|
go mod tidy
|
||||||
|
@ -39,11 +33,16 @@ test:
|
||||||
testacc:
|
testacc:
|
||||||
TF_ACC=1 go test -v -cover -timeout 120m ./...
|
TF_ACC=1 go test -v -cover -timeout 120m ./...
|
||||||
|
|
||||||
e2e/%:
|
e2e:
|
||||||
@echo "Executing end-to-end test: $*"
|
@echo "Running end-to-end test..."
|
||||||
cd e2e-tests/$* && ./test.sh
|
cd e2e-test && \
|
||||||
|
rm -rf .terraform/ .terraform.lock.hcl terraform.tfstate* && \
|
||||||
e2e: clean build e2e/obfs4 e2e/tor-family
|
./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"
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
@echo "Running CI pipeline..."
|
@echo "Running CI pipeline..."
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
---
|
|
||||||
# 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,7 +1,7 @@
|
||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
tor = {
|
tor = {
|
||||||
source = "guardianproject/tor"
|
source = "guardianproject/tor"
|
||||||
version = "99.0.0"
|
version = "99.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,11 +15,6 @@ resource "tor_relay_identity_rsa" "bridge" {}
|
||||||
# Generate Ed25519 identity key for the bridge
|
# Generate Ed25519 identity key for the bridge
|
||||||
resource "tor_relay_identity_ed25519" "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
|
# Generate obfs4 state using the identity keys
|
||||||
resource "tor_obfs4_state" "bridge" {
|
resource "tor_obfs4_state" "bridge" {
|
||||||
rsa_identity_private_key = tor_relay_identity_rsa.bridge.private_key_pem
|
rsa_identity_private_key = tor_relay_identity_rsa.bridge.private_key_pem
|
||||||
|
@ -65,8 +60,3 @@ output "bridge_line" {
|
||||||
description = "Complete bridge line for clients"
|
description = "Complete bridge line for clients"
|
||||||
value = data.tor_obfs4_bridge_line.bridge.bridge_line
|
value = data.tor_obfs4_bridge_line.bridge.bridge_line
|
||||||
}
|
}
|
||||||
|
|
||||||
output "family_id" {
|
|
||||||
description = "Family ID for the bridge"
|
|
||||||
value = tor_family_identity.bridge.id
|
|
||||||
}
|
|
48
e2e-test/setup-dev.sh
Executable file
48
e2e-test/setup-dev.sh
Executable file
|
@ -0,0 +1,48 @@
|
||||||
|
#!/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,4 +1,5 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Wrapper script to run terraform with local config
|
# Wrapper script to run terraform with local config
|
||||||
export TF_CLI_CONFIG_FILE=terraformrc
|
|
||||||
|
export TF_CLI_CONFIG_FILE=.terraformrc
|
||||||
exec tofu "$@"
|
exec tofu "$@"
|
|
@ -1,15 +0,0 @@
|
||||||
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/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
#!/usr/bin/env sh
|
|
||||||
set -e
|
|
||||||
../setup.sh
|
|
||||||
rm -f terraform.tfstate*
|
|
||||||
./tf init
|
|
||||||
./tf plan
|
|
||||||
./tf apply -auto-approve
|
|
|
@ -1,3 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
export TF_CLI_CONFIG_FILE=terraformrc
|
|
||||||
exec tofu "$@"
|
|
|
@ -1,14 +0,0 @@
|
||||||
#!/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"
|
|
|
@ -1,44 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
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/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
#!/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,17 +0,0 @@
|
||||||
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,7 +57,6 @@
|
||||||
default = pkgs.mkShell {
|
default = pkgs.mkShell {
|
||||||
packages = [
|
packages = [
|
||||||
pkgs.go
|
pkgs.go
|
||||||
pkgs.gopls
|
|
||||||
pkgs.golangci-lint
|
pkgs.golangci-lint
|
||||||
pkgs.obfs4
|
pkgs.obfs4
|
||||||
pkgs.gnumake
|
pkgs.gnumake
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -5,7 +5,6 @@ go 1.23.0
|
||||||
toolchain go1.24.2
|
toolchain go1.24.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0
|
|
||||||
github.com/hashicorp/terraform-plugin-framework v1.12.0
|
github.com/hashicorp/terraform-plugin-framework v1.12.0
|
||||||
github.com/hashicorp/terraform-plugin-go v0.24.0
|
github.com/hashicorp/terraform-plugin-go v0.24.0
|
||||||
github.com/hashicorp/terraform-plugin-log v0.9.0
|
github.com/hashicorp/terraform-plugin-log v0.9.0
|
||||||
|
@ -14,6 +13,7 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect
|
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect
|
||||||
github.com/agext/levenshtein v1.2.3 // indirect
|
github.com/agext/levenshtein v1.2.3 // indirect
|
||||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||||
|
|
|
@ -61,7 +61,6 @@ func (p *ScaffoldingProvider) Resources(ctx context.Context) []func() resource.R
|
||||||
NewTorObfs4StateResource,
|
NewTorObfs4StateResource,
|
||||||
NewTorRelayIdentityRsaResource,
|
NewTorRelayIdentityRsaResource,
|
||||||
NewTorRelayIdentityEd25519Resource,
|
NewTorRelayIdentityEd25519Resource,
|
||||||
NewTorFamilyIdentityResource,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,166 +0,0 @@
|
||||||
// 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) {
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
// 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])
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
// 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