Files
zeekay 65979bf7a0 feat(internal): cosigners + secrets resolver — layered cosigner SDK Go port
Brings the SDK-v1.0.3 layered cosigner pattern from app/server/src/domain/cosigners.ts
across to Go. Adds:

  internal/secrets/        URI-scheme secret resolver (literal:/env:/file:/kms:)
                           with pluggable KMS providers (init-time Register).
                           Single source of truth for mpcd bearer tokens,
                           cosigner PEMs, and any future secret material.

  internal/cosigners/      Layered cosigner SDK — per-swap external custodian
                           attestation on TOP of the native MPC threshold sig.
                           Wire shape mirrors the TS reference exactly (snake-
                           case, `kind` discriminator). Families:
                             - fireblocks.go      Real REST integration; RAW
                                                  transaction approval flow,
                                                  per-tenant JWT (RSA), polls
                                                  /v1/transactions/{id} to
                                                  terminal status.
                             - utila.go           Connect-RPC scaffold; mirrors
                                                  FireblocksRESTFamily shape so
                                                  the future port is one
                                                  contained method swap.
                             - stub_family.go     Catch-all for unknown kinds.

Security invariant baked in: SECRET MATERIAL NEVER CROSSES THE WIRE. The
SDK sends only public identifiers (org_id, client_id, api_key, vault ids).
ValidateIntents rejects SECRET_FIELD_NAMES (api_secret, private_key, jwt,
token, …) as a defensive layer. Secrets are fetched at dispatch time via
secrets.Resolver, which is also where KMS plugs in.

Tests: cosigners (2.0s) + secrets (0.3s) all green. Self-contained — no
out-of-tree imports beyond stdlib and within-repo packages.

Extracted from origin/whispers/bridgev2 as the first of a series of focused
slices integrating that branch onto main without dragging in the noise
(export.txt transcript dump, bridge binary diff). Per "x.x.x+1" rule the
next bridge release tag is v1.2.0 — this is the first additive feature
that warrants the minor bump (new internal packages, no consumer break).
2026-05-28 20:14:51 -07:00

212 lines
5.7 KiB
Go

package secrets
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
)
func TestResolver_LiteralScheme(t *testing.T) {
got, err := Default().Resolve(context.Background(), "literal:hello-world")
if err != nil {
t.Fatalf("err: %v", err)
}
if got != "hello-world" {
t.Errorf("got %q, want %q", got, "hello-world")
}
}
func TestResolver_UnprefixedIsLiteral_BackCompat(t *testing.T) {
// Every pre-secrets flag value is bare (e.g. --mpc-token=abc123).
// Bare strings must be treated as literal so existing deploys
// keep working without flag changes.
got, err := Default().Resolve(context.Background(), "bare-token-value")
if err != nil {
t.Fatalf("err: %v", err)
}
if got != "bare-token-value" {
t.Errorf("got %q, want %q", got, "bare-token-value")
}
}
func TestResolver_EnvScheme(t *testing.T) {
t.Setenv("BRIDGE_TEST_SECRET", "the-shared-token")
got, err := Default().Resolve(context.Background(), "env:BRIDGE_TEST_SECRET")
if err != nil {
t.Fatalf("err: %v", err)
}
if got != "the-shared-token" {
t.Errorf("got %q, want %q", got, "the-shared-token")
}
}
func TestResolver_EnvScheme_UnsetVar(t *testing.T) {
_, err := Default().Resolve(context.Background(), "env:DOES_NOT_EXIST_xx_BRIDGE_TEST")
if err == nil {
t.Fatal("expected error for unset env var")
}
if !strings.Contains(err.Error(), "unset") {
t.Errorf("error %q should mention 'unset'", err.Error())
}
}
func TestResolver_EnvScheme_EmptyName(t *testing.T) {
_, err := Default().Resolve(context.Background(), "env:")
if err == nil {
t.Fatal("expected error for empty env name")
}
}
func TestResolver_FileScheme(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "secret.pem")
if err := os.WriteFile(path, []byte("-----BEGIN-----\nbody\n-----END-----\n"), 0o600); err != nil {
t.Fatal(err)
}
got, err := Default().Resolve(context.Background(), "file:"+path)
if err != nil {
t.Fatalf("err: %v", err)
}
want := "-----BEGIN-----\nbody\n-----END-----"
if got != want {
t.Errorf("got %q, want %q (trailing \\n should be stripped)", got, want)
}
}
func TestResolver_FileScheme_CRLF(t *testing.T) {
// Windows-style files: trailing \r\n should be stripped fully.
dir := t.TempDir()
path := filepath.Join(dir, "secret.crlf")
if err := os.WriteFile(path, []byte("token-value\r\n"), 0o600); err != nil {
t.Fatal(err)
}
got, err := Default().Resolve(context.Background(), "file:"+path)
if err != nil {
t.Fatalf("err: %v", err)
}
if got != "token-value" {
t.Errorf("got %q, want %q", got, "token-value")
}
}
func TestResolver_FileScheme_MissingPath(t *testing.T) {
_, err := Default().Resolve(context.Background(), "file:")
if err == nil {
t.Fatal("expected error for empty path")
}
}
func TestResolver_FileScheme_NotExist(t *testing.T) {
_, err := Default().Resolve(context.Background(), "file:/no/such/path/xyz123")
if err == nil {
t.Fatal("expected error for missing file")
}
}
func TestResolver_KMSScheme_NoProvider(t *testing.T) {
_, err := Default().Resolve(context.Background(), "kms:aws:us-east-1:cipher:xxx")
if err == nil {
t.Fatal("expected ErrKMSNotRegistered")
}
if !errors.Is(err, ErrKMSNotRegistered) {
t.Errorf("err = %v, want ErrKMSNotRegistered", err)
}
}
func TestResolver_KMSScheme_MalformedNoFamily(t *testing.T) {
_, err := Default().Resolve(context.Background(), "kms:onlyfamily")
if err == nil {
t.Fatal("expected error — kms URI must be kms:<family>:<opaque>")
}
}
// fakeKMS is a test KMSProvider that maps opaque strings back to plaintext.
type fakeKMS struct {
plaintext map[string]string
}
func (f *fakeKMS) Decrypt(_ context.Context, opaque string) (string, error) {
pt, ok := f.plaintext[opaque]
if !ok {
return "", errors.New("fakeKMS: opaque not found")
}
return pt, nil
}
func TestResolver_KMSScheme_HappyPath(t *testing.T) {
prev := kmsRegistry["fake"]
t.Cleanup(func() {
if prev == nil {
delete(kmsRegistry, "fake")
} else {
kmsRegistry["fake"] = prev
}
})
RegisterKMS("fake", &fakeKMS{plaintext: map[string]string{
"cipher-abc": "decrypted-token",
}})
got, err := Default().Resolve(context.Background(), "kms:fake:cipher-abc")
if err != nil {
t.Fatalf("err: %v", err)
}
if got != "decrypted-token" {
t.Errorf("got %q, want %q", got, "decrypted-token")
}
}
func TestResolver_UnknownScheme(t *testing.T) {
_, err := Default().Resolve(context.Background(), "vault:secret/data/bridge/mpc-token")
if err == nil {
t.Fatal("expected ErrUnknownScheme")
}
if !errors.Is(err, ErrUnknownScheme) {
t.Errorf("err = %v, want ErrUnknownScheme", err)
}
}
func TestSplitScheme_RejectsWindowsPath(t *testing.T) {
// `C:/path/to/file` is a bare Windows path, NOT a `C:` scheme.
// splitScheme should reject it (uppercase scheme chars not allowed).
scheme, _, ok := splitScheme("C:/path/to/file")
if ok {
t.Errorf("splitScheme accepted Windows path as scheme=%q", scheme)
}
}
func TestSplitScheme_RejectsBareIP(t *testing.T) {
// "10:20:30" should not be parsed as scheme=10.
scheme, _, ok := splitScheme("1.2.3.4:5000")
if ok {
t.Errorf("splitScheme accepted IP:port as scheme=%q", scheme)
}
}
func TestRegisterKMS_RejectsNilOrEmpty(t *testing.T) {
prevCount := len(kmsRegistry)
RegisterKMS("", &fakeKMS{})
RegisterKMS("validname", nil)
if len(kmsRegistry) != prevCount {
t.Errorf("registry grew after invalid Register calls — want size unchanged")
}
}
func TestMustResolve_PanicsOnError(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("MustResolve should panic on error")
}
}()
MustResolve(context.Background(), Default(), "env:DOES_NOT_EXIST_xx")
}
func TestMustResolve_HappyPath(t *testing.T) {
got := MustResolve(context.Background(), Default(), "literal:abc")
if got != "abc" {
t.Errorf("got %q, want %q", got, "abc")
}
}