// Package sign attaches a detached Ed25519 signature to a decision envelope // (FLEX-WP-0024). The signed material is json.Marshal of the envelope with // the signature field omitted. package sign import ( "crypto/ed25519" "encoding/base64" "encoding/json" "fmt" "github.com/netkingdom/flex-auth/pkg/api" ) const AlgorithmEd25519 = "ed25519" // CanonicalBytes is the exact payload that is signed and verified: the // envelope as emitted, with signature absent. func CanonicalBytes(envelope api.DecisionEnvelope) ([]byte, error) { envelope.Signature = nil data, err := json.Marshal(envelope) if err != nil { return nil, fmt.Errorf("marshal canonical envelope: %w", err) } return data, nil } func Sign(envelope *api.DecisionEnvelope, private ed25519.PrivateKey, kid string) error { if envelope == nil { return fmt.Errorf("envelope is required") } if len(private) != ed25519.PrivateKeySize { return fmt.Errorf("ed25519 private key has length %d", len(private)) } if kid == "" { return fmt.Errorf("kid is required") } payload, err := CanonicalBytes(*envelope) if err != nil { return err } sig := ed25519.Sign(private, payload) envelope.Signature = &api.EnvelopeSignature{ Mode: api.EnvelopeSignatureSigned, Alg: AlgorithmEd25519, Kid: kid, Value: base64.RawURLEncoding.EncodeToString(sig), } return nil } func Verify(envelope api.DecisionEnvelope, keys map[string]ed25519.PublicKey) error { sig := envelope.Signature if sig == nil || sig.Mode != api.EnvelopeSignatureSigned { return fmt.Errorf("envelope is not signed") } if sig.Alg != AlgorithmEd25519 { return fmt.Errorf("unsupported signature alg %q", sig.Alg) } pub, ok := keys[sig.Kid] if !ok { return fmt.Errorf("unknown signature kid %q", sig.Kid) } raw, err := base64.RawURLEncoding.DecodeString(sig.Value) if err != nil { return fmt.Errorf("decode signature: %w", err) } payload, err := CanonicalBytes(envelope) if err != nil { return err } if !ed25519.Verify(pub, payload, raw) { return fmt.Errorf("signature does not match envelope") } return nil }