// Package sign implements the Ed25519 signing envelope used for offline // signing of endpoint and notice distribution documents (doc/06 §3 密码学口径). // // Trust model: // - The signing private key lives OFFLINE, in two physically separate // locations. It must never enter the server, CI, or this repository. // - The verifying public key is embedded in the client install package. // - key_id selects which public key verifies a document; a KeyRing may hold // more than one key so a new signing key can be rolled out before the old // one is retired (双公钥轮换过渡). // // Document shape (the on-disk JSON): // // { // "version": , // anti-rollback: clients only accept larger // "issued_at": "", // "key_id": "", // "payload": { ... arbitrary JSON ... }, // "sig": "" // } // // The signature covers the canonical JSON encoding of the document WITHOUT the // "sig" field, i.e. {version, issued_at, key_id, payload}. Because version, // key_id and issued_at are all inside the signed bytes, an attacker cannot // downgrade the version or swap the key without breaking the signature. package sign import ( "bytes" "crypto/ed25519" "encoding/base64" "encoding/json" "errors" "fmt" "sort" ) // Errors returned by Verify. var ( ErrUnknownKeyID = errors.New("sign: unknown key_id (no matching public key in ring)") ErrBadSignature = errors.New("sign: signature verification failed") ErrMissingSig = errors.New("sign: document has no signature") ErrEmptyKeyID = errors.New("sign: key_id is empty") ErrBadPayload = errors.New("sign: payload is not valid JSON") ) // Envelope is the signed distribution document. type Envelope struct { Version uint64 `json:"version"` IssuedAt string `json:"issued_at"` KeyID string `json:"key_id"` Payload json.RawMessage `json:"payload"` Sig string `json:"sig,omitempty"` } // KeyRing maps key_id -> public key. Holding more than one entry enables a // rotation window where documents signed by either key validate. type KeyRing map[string]ed25519.PublicKey // signingBytes returns the canonical bytes that are signed/verified: the // envelope without its signature. func (e Envelope) signingBytes() ([]byte, error) { if e.KeyID == "" { return nil, ErrEmptyKeyID } if !json.Valid(e.Payload) { return nil, ErrBadPayload } unsigned := Envelope{ Version: e.Version, IssuedAt: e.IssuedAt, KeyID: e.KeyID, Payload: e.Payload, // Sig intentionally empty -> omitted by omitempty. } raw, err := json.Marshal(unsigned) if err != nil { return nil, err } return Canonicalize(raw) } // Sign computes the Ed25519 signature over e's canonical bytes and stores it in // e.Sig (base64). The private key is supplied by the caller (loaded from the // offline key file) and is never persisted by this package. func Sign(priv ed25519.PrivateKey, e *Envelope) error { if len(priv) != ed25519.PrivateKeySize { return fmt.Errorf("sign: invalid private key size %d", len(priv)) } msg, err := e.signingBytes() if err != nil { return err } sig := ed25519.Sign(priv, msg) e.Sig = base64.StdEncoding.EncodeToString(sig) return nil } // Verify checks e's signature against the public key selected by e.KeyID from // ring. It returns nil only if the key is known and the signature is valid. func Verify(e Envelope, ring KeyRing) error { if e.Sig == "" { return ErrMissingSig } pub, ok := ring[e.KeyID] if !ok { return ErrUnknownKeyID } sig, err := base64.StdEncoding.DecodeString(e.Sig) if err != nil { return fmt.Errorf("sign: signature is not valid base64: %w", err) } msg, err := e.signingBytes() if err != nil { return err } if !ed25519.Verify(pub, msg, sig) { return ErrBadSignature } return nil } // Marshal renders the signed envelope as indented JSON suitable for publishing. func Marshal(e Envelope) ([]byte, error) { if e.Sig == "" { return nil, ErrMissingSig } return json.MarshalIndent(e, "", " ") } // Parse decodes a published document into an Envelope. func Parse(raw []byte) (Envelope, error) { var e Envelope if err := json.Unmarshal(raw, &e); err != nil { return Envelope{}, fmt.Errorf("sign: cannot parse document: %w", err) } return e, nil } // Canonicalize returns a deterministic JSON encoding of raw: object keys sorted // lexicographically, no insignificant whitespace, array order preserved. This // guarantees signer and verifier hash identical bytes regardless of field order // or formatting. func Canonicalize(raw []byte) ([]byte, error) { dec := json.NewDecoder(bytes.NewReader(raw)) dec.UseNumber() // keep integers exact; never widen to float64 var v any if err := dec.Decode(&v); err != nil { return nil, fmt.Errorf("sign: canonicalize decode: %w", err) } var buf bytes.Buffer if err := writeCanonical(&buf, v); err != nil { return nil, err } return buf.Bytes(), nil } func writeCanonical(buf *bytes.Buffer, v any) error { switch t := v.(type) { case map[string]any: keys := make([]string, 0, len(t)) for k := range t { keys = append(keys, k) } sort.Strings(keys) buf.WriteByte('{') for i, k := range keys { if i > 0 { buf.WriteByte(',') } kb, err := json.Marshal(k) if err != nil { return err } buf.Write(kb) buf.WriteByte(':') if err := writeCanonical(buf, t[k]); err != nil { return err } } buf.WriteByte('}') case []any: buf.WriteByte('[') for i, e := range t { if i > 0 { buf.WriteByte(',') } if err := writeCanonical(buf, e); err != nil { return err } } buf.WriteByte(']') case string: b, err := json.Marshal(t) if err != nil { return err } buf.Write(b) case json.Number: buf.WriteString(t.String()) case bool: if t { buf.WriteString("true") } else { buf.WriteString("false") } case nil: buf.WriteString("null") default: return fmt.Errorf("sign: unsupported JSON type %T in canonicalization", v) } return nil }