flex-auth/internal/sign/sign.go
tegwick 127f83da4d
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 53s
Sign decision envelopes and close FLEX-WP-0024.
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
2026-09-14 09:57:50 +02:00

76 lines
2 KiB
Go

// 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
}