// Package signing implements descriptor and policy signatures. // // ArchitectureBlueprint.md section 35 requires the revision router to accept // only signed or otherwise authenticated published descriptors. Section 28 // explains why: FLUID assumes generated or adaptive behaviour is untrusted // until verified, and a signature is what carries the result of verification // across a process boundary to the runtime that must act on it. package signing import ( "crypto/ed25519" "encoding/base64" "encoding/json" "errors" "fmt" "sort" "strings" ) // Algorithm is the only signature algorithm FLUID defines. // // One algorithm is deliberate. Negotiable algorithms invite downgrade, and a // framework whose safety barrier can be talked down to a weaker primitive has // no safety barrier. const Algorithm = "ed25519" var ( // ErrUnsigned reports a document carrying no signature. ErrUnsigned = errors.New("document is not signed") // ErrUnknownKey reports a signature from a key the verifier does not hold. ErrUnknownKey = errors.New("signing key is not trusted") // ErrBadSignature reports a signature that does not verify. ErrBadSignature = errors.New("signature does not verify") // ErrUnsupportedAlgorithm reports anything other than ed25519. ErrUnsupportedAlgorithm = errors.New("unsupported signature algorithm") ) // Signature is the detached signature attached to a signed document. type Signature struct { Algorithm string `json:"algorithm"` KeyID string `json:"key_id"` Value string `json:"value"` SignedAt string `json:"signed_at,omitempty"` } // Canonicalize renders the signable form of a document. // // The signature member is removed before hashing, so a document signs its own // content rather than its own signature. Map keys are sorted by Go's JSON // encoder, which makes the byte sequence reproducible across processes and // languages — a requirement, since an adapter or Daimon in another language // must be able to produce a signature this verifier accepts. func Canonicalize(document any) ([]byte, error) { raw, err := json.Marshal(document) if err != nil { return nil, fmt.Errorf("canonicalize: %w", err) } var generic any if err := json.Unmarshal(raw, &generic); err != nil { return nil, fmt.Errorf("canonicalize: %w", err) } stripped := stripSignature(generic) return json.Marshal(stripped) } // stripSignature removes every "signature" member, at any depth. // // Descriptors nest the payload under a "revision" key, and policies under // "routing_policy", so the signature may sit one level down. Removing it // wherever it appears keeps canonicalization independent of that shape. func stripSignature(v any) any { switch t := v.(type) { case map[string]any: out := make(map[string]any, len(t)) keys := make([]string, 0, len(t)) for k := range t { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { if k == "signature" { continue } out[k] = stripSignature(t[k]) } return out case []any: out := make([]any, len(t)) for i, item := range t { out[i] = stripSignature(item) } return out default: return v } } // Signer produces signatures. // // The signer holds a private key and nothing else. Blueprint section 28.1 // separates the builder identity from promotion rights: being able to sign an // artifact is not the same authority as being able to publish one, and keeping // this type free of any publishing capability is what makes that separable. type Signer struct { keyID string key ed25519.PrivateKey } // NewSigner returns a signer for the given key. func NewSigner(keyID string, key ed25519.PrivateKey) (*Signer, error) { if strings.TrimSpace(keyID) == "" { return nil, errors.New("signing key needs an id") } if len(key) != ed25519.PrivateKeySize { return nil, fmt.Errorf("private key is %d bytes, want %d", len(key), ed25519.PrivateKeySize) } return &Signer{keyID: keyID, key: key}, nil } // KeyID reports which key this signer uses. func (s *Signer) KeyID() string { return s.keyID } // PublicKey returns the verifying half of the key pair. func (s *Signer) PublicKey() ed25519.PublicKey { return s.key.Public().(ed25519.PublicKey) } // Sign signs a document's canonical form. func (s *Signer) Sign(document any) (Signature, error) { payload, err := Canonicalize(document) if err != nil { return Signature{}, err } return Signature{ Algorithm: Algorithm, KeyID: s.keyID, Value: base64.StdEncoding.EncodeToString(ed25519.Sign(s.key, payload)), }, nil } // Verifier checks signatures against a set of trusted keys. type Verifier struct { keys map[string]ed25519.PublicKey } // NewVerifier returns a verifier trusting the given keys, indexed by key id. func NewVerifier(keys map[string]ed25519.PublicKey) *Verifier { copied := make(map[string]ed25519.PublicKey, len(keys)) for id, k := range keys { copied[id] = k } return &Verifier{keys: copied} } // Trust adds a key. func (v *Verifier) Trust(keyID string, key ed25519.PublicKey) { if v.keys == nil { v.keys = map[string]ed25519.PublicKey{} } v.keys[keyID] = key } // Verify checks a document against its signature. // // An absent signature is refused rather than treated as "nothing to check". // The distinction matters: a verifier that passes unsigned input is worse than // no verifier, because it reports success. func (v *Verifier) Verify(document any, sig *Signature) error { if sig == nil { return ErrUnsigned } if sig.Algorithm != Algorithm { return fmt.Errorf("%w: %q", ErrUnsupportedAlgorithm, sig.Algorithm) } key, ok := v.keys[sig.KeyID] if !ok { return fmt.Errorf("%w: %q", ErrUnknownKey, sig.KeyID) } raw, err := base64.StdEncoding.DecodeString(sig.Value) if err != nil { return fmt.Errorf("%w: signature is not valid base64", ErrBadSignature) } payload, err := Canonicalize(document) if err != nil { return err } if !ed25519.Verify(key, payload, raw) { return ErrBadSignature } return nil } // GenerateKey produces a new signing key pair. Intended for development and // tests; production keys should come from the deployment's key management. func GenerateKey(keyID string) (*Signer, ed25519.PublicKey, error) { pub, priv, err := ed25519.GenerateKey(nil) if err != nil { return nil, nil, err } s, err := NewSigner(keyID, priv) if err != nil { return nil, nil, err } return s, pub, nil }