package release import ( "bytes" "crypto/ed25519" "encoding/base64" "encoding/json" "errors" "io" ) // MaxRootEnvelopeBytes bounds both the signed wire object and its decoded // payload. Loss declarations can contain timer snapshots, so the worker // control record's smaller bound is not suitable here. const MaxRootEnvelopeBytes = 1 << 20 var ErrRootEnvelope = errors.New("release: root envelope refused") // RootPublicKey returns a copy of the verification anchor carried by a // signer. It lets create-only callers resolve an ambiguous retry by reading // and verifying the object they may already have created. func (s Signer) RootPublicKey() ed25519.PublicKey { return append(ed25519.PublicKey(nil), s.root...) } // SignRootEnvelope writes the existing root-signed {payload,sig} envelope. // Scoped release keys are deliberately refused: these records are deployment // control authority, not channel publication. func SignRootEnvelope(sg Signer, payload any) ([]byte, error) { if sg.usable() != nil || sg.scope != nil || sg.grant != nil { return nil, ErrRootEnvelope } raw, err := json.Marshal(payload) if err != nil || len(raw) == 0 || len(raw) > MaxRootEnvelopeBytes/2 { return nil, ErrRootEnvelope } b64 := base64.StdEncoding.EncodeToString(raw) sig := ed25519.Sign(sg.key, []byte(b64)) wire, err := json.Marshal(signedWrapper{Payload: b64, Sig: base64.StdEncoding.EncodeToString(sig)}) if err != nil || len(wire) > MaxRootEnvelopeBytes { return nil, ErrRootEnvelope } return wire, nil } // VerifyRootEnvelope verifies an envelope against the deployment root and // requires canonical JSON for both wrapper and typed payload. dst must be a // non-nil pointer to the exact expected payload type. func VerifyRootEnvelope(pub ed25519.PublicKey, wire []byte, dst any) error { if len(pub) != ed25519.PublicKeySize || len(wire) == 0 || len(wire) > MaxRootEnvelopeBytes || dst == nil { return ErrRootEnvelope } var wrapper signedWrapper if strictCanonicalJSON(wire, &wrapper) != nil || wrapper.Grant != nil { return ErrRootEnvelope } sig, err := base64.StdEncoding.Strict().DecodeString(wrapper.Sig) if err != nil || base64.StdEncoding.EncodeToString(sig) != wrapper.Sig || len(sig) != ed25519.SignatureSize || !ed25519.Verify(pub, []byte(wrapper.Payload), sig) { return ErrRootEnvelope } raw, err := base64.StdEncoding.Strict().DecodeString(wrapper.Payload) if err != nil || base64.StdEncoding.EncodeToString(raw) != wrapper.Payload || len(raw) == 0 || len(raw) > MaxRootEnvelopeBytes/2 { return ErrRootEnvelope } if strictCanonicalJSON(raw, dst) != nil { return ErrRootEnvelope } return nil } func strictCanonicalJSON(raw []byte, dst any) error { dec := json.NewDecoder(bytes.NewReader(raw)) dec.DisallowUnknownFields() if err := dec.Decode(dst); err != nil { return err } var trailing any if err := dec.Decode(&trailing); err != io.EOF { return ErrRootEnvelope } canonical, err := json.Marshal(dst) if err != nil { return err } var compact bytes.Buffer if err := json.Compact(&compact, raw); err != nil || !bytes.Equal(compact.Bytes(), canonical) { return ErrRootEnvelope } return nil }