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
76 lines
2 KiB
Go
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
|
|
}
|