carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "bytes"
5 "crypto/ed25519"
6 "encoding/base64"
7 "encoding/json"
8 "errors"
9 "io"
10)
11
12// MaxRootEnvelopeBytes bounds both the signed wire object and its decoded
13// payload. Loss declarations can contain timer snapshots, so the worker
14// control record's smaller bound is not suitable here.
15const MaxRootEnvelopeBytes = 1 << 20
16
17var ErrRootEnvelope = errors.New("release: root envelope refused")
18
19// RootPublicKey returns a copy of the verification anchor carried by a
20// signer. It lets create-only callers resolve an ambiguous retry by reading
21// and verifying the object they may already have created.
22func (s Signer) RootPublicKey() ed25519.PublicKey {
23 return append(ed25519.PublicKey(nil), s.root...)
24}
25
26// SignRootEnvelope writes the existing root-signed {payload,sig} envelope.
27// Scoped release keys are deliberately refused: these records are deployment
28// control authority, not channel publication.
29func SignRootEnvelope(sg Signer, payload any) ([]byte, error) {
30 if sg.usable() != nil || sg.scope != nil || sg.grant != nil {
31 return nil, ErrRootEnvelope
32 }
33 raw, err := json.Marshal(payload)
34 if err != nil || len(raw) == 0 || len(raw) > MaxRootEnvelopeBytes/2 {
35 return nil, ErrRootEnvelope
36 }
37 b64 := base64.StdEncoding.EncodeToString(raw)
38 sig := ed25519.Sign(sg.key, []byte(b64))
39 wire, err := json.Marshal(signedWrapper{Payload: b64, Sig: base64.StdEncoding.EncodeToString(sig)})
40 if err != nil || len(wire) > MaxRootEnvelopeBytes {
41 return nil, ErrRootEnvelope
42 }
43 return wire, nil
44}
45
46// VerifyRootEnvelope verifies an envelope against the deployment root and
47// requires canonical JSON for both wrapper and typed payload. dst must be a
48// non-nil pointer to the exact expected payload type.
49func VerifyRootEnvelope(pub ed25519.PublicKey, wire []byte, dst any) error {
50 if len(pub) != ed25519.PublicKeySize || len(wire) == 0 || len(wire) > MaxRootEnvelopeBytes || dst == nil {
51 return ErrRootEnvelope
52 }
53 var wrapper signedWrapper
54 if strictCanonicalJSON(wire, &wrapper) != nil || wrapper.Grant != nil {
55 return ErrRootEnvelope
56 }
57 sig, err := base64.StdEncoding.Strict().DecodeString(wrapper.Sig)
58 if err != nil || base64.StdEncoding.EncodeToString(sig) != wrapper.Sig || len(sig) != ed25519.SignatureSize ||
59 !ed25519.Verify(pub, []byte(wrapper.Payload), sig) {
60 return ErrRootEnvelope
61 }
62 raw, err := base64.StdEncoding.Strict().DecodeString(wrapper.Payload)
63 if err != nil || base64.StdEncoding.EncodeToString(raw) != wrapper.Payload || len(raw) == 0 || len(raw) > MaxRootEnvelopeBytes/2 {
64 return ErrRootEnvelope
65 }
66 if strictCanonicalJSON(raw, dst) != nil {
67 return ErrRootEnvelope
68 }
69 return nil
70}
71
72func strictCanonicalJSON(raw []byte, dst any) error {
73 dec := json.NewDecoder(bytes.NewReader(raw))
74 dec.DisallowUnknownFields()
75 if err := dec.Decode(dst); err != nil {
76 return err
77 }
78 var trailing any
79 if err := dec.Decode(&trailing); err != io.EOF {
80 return ErrRootEnvelope
81 }
82 canonical, err := json.Marshal(dst)
83 if err != nil {
84 return err
85 }
86 var compact bytes.Buffer
87 if err := json.Compact(&compact, raw); err != nil || !bytes.Equal(compact.Bytes(), canonical) {
88 return ErrRootEnvelope
89 }
90 return nil
91}
92