Detached Ed25519 over the canonical envelope with signature omitted. Unsigned is stated, not implied. Testdata fixtures prove verify and tamper failure without minting a production key. FLEX-WP-0025 is finished with the validate check from the previous commit. Assistant: grok Assistant-Session: 01a09dc1-b21e-77e1-919e-fcad2f82b267
108 lines
3.5 KiB
Go
108 lines
3.5 KiB
Go
package sign_test
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/netkingdom/flex-auth/internal/sign"
|
|
"github.com/netkingdom/flex-auth/pkg/api"
|
|
)
|
|
|
|
// testdataSeed is a well-known non-production key. It exists so fixtures can
|
|
// prove verification without an OpenBao lane (FLEX-WP-0024-T02).
|
|
var testdataSeed = bytes32(0x42)
|
|
|
|
func bytes32(b byte) []byte {
|
|
out := make([]byte, ed25519.SeedSize)
|
|
for i := range out {
|
|
out[i] = b
|
|
}
|
|
return out
|
|
}
|
|
|
|
func fixtureKey() (ed25519.PublicKey, ed25519.PrivateKey) {
|
|
priv := ed25519.NewKeyFromSeed(testdataSeed)
|
|
return priv.Public().(ed25519.PublicKey), priv
|
|
}
|
|
|
|
func TestSignAndVerifyRoundTrip(t *testing.T) {
|
|
pub, priv := fixtureKey()
|
|
envelope := sampleEnvelope("allow")
|
|
digest := envelope.Binding.RequestDigest
|
|
if err := sign.Sign(&envelope, priv, "testdata-ed25519"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if envelope.Binding.RequestDigest != digest {
|
|
t.Fatalf("request_digest moved from %s to %s", digest, envelope.Binding.RequestDigest)
|
|
}
|
|
if envelope.Signature == nil || envelope.Signature.Mode != api.EnvelopeSignatureSigned {
|
|
t.Fatalf("signature = %+v", envelope.Signature)
|
|
}
|
|
if err := sign.Verify(envelope, map[string]ed25519.PublicKey{"testdata-ed25519": pub}); err != nil {
|
|
t.Fatalf("Verify: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAlteredEnvelopeFailsVerification(t *testing.T) {
|
|
pub, priv := fixtureKey()
|
|
envelope := sampleEnvelope("allow")
|
|
if err := sign.Sign(&envelope, priv, "testdata-ed25519"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
envelope.Effect = api.DecisionEffectDeny
|
|
if err := sign.Verify(envelope, map[string]ed25519.PublicKey{"testdata-ed25519": pub}); err == nil {
|
|
t.Fatal("tampered envelope verified")
|
|
}
|
|
}
|
|
|
|
func TestUnsignedEnvelopeDoesNotVerify(t *testing.T) {
|
|
pub, _ := fixtureKey()
|
|
envelope := sampleEnvelope("allow")
|
|
envelope.Signature = api.UnsignedEnvelopeSignature()
|
|
if err := sign.Verify(envelope, map[string]ed25519.PublicKey{"testdata-ed25519": pub}); err == nil {
|
|
t.Fatal("unsigned envelope verified")
|
|
}
|
|
}
|
|
|
|
func TestReplayFixturesProveSuccessAndFailure(t *testing.T) {
|
|
pub, _ := fixtureKey()
|
|
keys := map[string]ed25519.PublicKey{"testdata-ed25519": pub}
|
|
|
|
signed := loadEnvelope(t, filepath.Join("..", "..", "examples", "secrets-engine", "replay", "decision_rotate_signed.json"))
|
|
if err := sign.Verify(signed, keys); err != nil {
|
|
t.Fatalf("genuine fixture: %v", err)
|
|
}
|
|
tampered := loadEnvelope(t, filepath.Join("..", "..", "examples", "secrets-engine", "replay", "decision_rotate_signed_tampered.json"))
|
|
if err := sign.Verify(tampered, keys); err == nil {
|
|
t.Fatal("tampered fixture verified")
|
|
}
|
|
}
|
|
|
|
func sampleEnvelope(effect string) api.DecisionEnvelope {
|
|
return api.DecisionEnvelope{
|
|
ID: "decision:test",
|
|
ContractVersion: api.DecisionRecordContractV1,
|
|
Effect: api.DecisionEffect(effect),
|
|
Resource: api.ResourceRef{ID: "lane:test", System: "secrets-engine"},
|
|
Subject: api.SubjectRef{ID: "secrets-engine", Type: "service"},
|
|
Binding: &api.DecisionBinding{RequestDigest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Action: "rotate"},
|
|
Provenance: api.DecisionProvenance{Evaluator: "flex-auth/local", Mode: "standalone"},
|
|
Signature: api.UnsignedEnvelopeSignature(),
|
|
}
|
|
}
|
|
|
|
func loadEnvelope(t *testing.T, path string) api.DecisionEnvelope {
|
|
t.Helper()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var envelope api.DecisionEnvelope
|
|
if err := json.Unmarshal(data, &envelope); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return envelope
|
|
}
|