- module path renamed (no external importers); golangci local-prefixes - ACCESS_ENGINE_RUN_TOPAZ_INTEGRATION replaces FLEX_AUTH_RUN_TOPAZ_INTEGRATION - README title and identity paragraph reflect the landed renames - docs/runtime-rename-inventory.md: per-consumer references by kind and order - chart name, image package and /opt paths deliberately left for T05 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 735967@bnt-lap001 Assistant-Session: 5971967f-77f8-4ced-9e02-c6ef67040ffc
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/access-engine/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
|
|
}
|