// Package operator verifies private administrative requests independently of // browser identities and instance bearer credentials. package operator import ( "bytes" "crypto/elliptic" "crypto/sha256" "encoding/base64" "encoding/hex" "errors" "net/url" "strings" "time" "unicode/utf8" family "amadan.net/rastrillo/rastrillo/crypto" "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" ) const ( BodyLimit = 64 << 10 InputLimit = 48 << 10 AuthorizationLimit = 16 << 10 InputContext = "oficina/home/operator-input/v1" OwnerContext = "oficina/home/owner-seed/v1" ) var ( ErrRefused = errors.New("operator: refused") ErrUnavailable = errors.New("operator: unavailable") ) type Authorization struct { Version int `json:"version"` Purpose string `json:"purpose"` Role string `json:"role"` Origin string `json:"origin"` Installation string `json:"installation"` Registry string `json:"registry"` Device string `json:"device"` Sequence int64 `json:"sequence"` Initial bool `json:"initial_authority"` Previous string `json:"previous"` JournalSequence int64 `json:"journal_sequence"` OperationID string `json:"operation_id"` Action string `json:"action"` InputDigest string `json:"input_digest"` Issued int64 `json:"issued_at"` Expires int64 `json:"expires_at"` } type Pins struct { Role, Origin, Installation, Registry, Device string PublicKey string Actions []string } type envelope struct { Version int `json:"v"` KeyID string `json:"kid"` Payload string `json:"payload"` Signature string `json:"signature"` } type wrapper struct { Version int `json:"version"` Authorization jsontext.Value `json:"signed_authorization"` Input jsontext.Value `json:"operation_input"` } type Verifier struct { pins Pins key []byte kid string actions map[string]bool } // Request's authority and input bytes can only be produced by Verify. Returned // copies cannot change the verified request retained by the transaction layer. type Request struct { authorization Authorization input []byte head string valid bool } func (r Request) Authorization() Authorization { return r.authorization } func (r Request) Input() []byte { return append([]byte(nil), r.input...) } func (r Request) Head() string { return r.head } func (r Request) Valid() bool { return r.valid } func (r Request) Fresh(now time.Time) bool { n := now.Unix() a := r.authorization return r.valid && n > 0 && a.Issued <= n && n < a.Expires } func CanonicalToken(s string) bool { _, ok := canonical(s, 32); return ok } func canonical(s string, n int) ([]byte, bool) { if len(s) > BodyLimit { return nil, false } b, e := base64.RawURLEncoding.Strict().DecodeString(s) return b, e == nil && (n < 0 || len(b) == n) && base64.RawURLEncoding.EncodeToString(b) == s } func Origin(s string) bool { if len(s) == 0 || len(s) > 253 { return false } u, e := url.Parse(s) return e == nil && u.Scheme == "https" && u.Host != "" && u.Host == strings.ToLower(u.Host) && u.User == nil && u.Path == "" && u.RawPath == "" && u.RawQuery == "" && !u.ForceQuery && u.Fragment == "" && u.RawFragment == "" && !strings.ContainsAny(u.Host, "%\\:") && !strings.HasSuffix(u.Host, ".") && u.String() == s } func digest(b []byte) string { h := sha256.Sum256(b) return base64.RawURLEncoding.EncodeToString(h[:]) } func keyID(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) } func public(encoded string) ([]byte, error) { b, ok := canonical(encoded, 65) if !ok { return nil, ErrRefused } x, _ := elliptic.Unmarshal(elliptic.P256(), b) if x == nil { return nil, ErrRefused } return b, nil } func allowedAction(role, a string) bool { if role == "credential" { return a == "credential.owner-seed" } if role == "directory" { switch a { case "directory.inspect", "directory.initialize", "directory.seed-team", "directory.provision-consumer", "directory.verify-consumer", "directory.activate-consumer": return true } } return false } func NewVerifier(p Pins) (*Verifier, error) { if !Origin(p.Origin) || !CanonicalToken(p.Installation) || !CanonicalToken(p.Registry) || !CanonicalToken(p.Device) || len(p.Actions) == 0 || len(p.Actions) > 8 { return nil, ErrRefused } k, e := public(p.PublicKey) if e != nil { return nil, ErrRefused } v := &Verifier{pins: p, key: k, kid: keyID(k), actions: map[string]bool{}} v.pins.Actions = append([]string(nil), p.Actions...) for _, a := range p.Actions { if !allowedAction(p.Role, a) || v.actions[a] { return nil, ErrRefused } v.actions[a] = true } return v, nil } func strict(b []byte, d any) error { if !utf8.Valid(b) || json.Unmarshal(b, d, json.RejectUnknownMembers(true)) != nil { return ErrRefused } return nil } func encode(v any) ([]byte, error) { b, e := json.Marshal(v, json.Deterministic(true)) if e != nil { return nil, ErrRefused } return b, nil } func strictCanonical(b []byte, d any) error { if strict(b, d) != nil { return ErrRefused } canonical, e := encode(d) if e != nil || !bytes.Equal(b, canonical) { return ErrRefused } return nil } func (a Authorization) valid() bool { return a.Version == 1 && a.Purpose == "operator-input" && allowedAction(a.Role, a.Action) && Origin(a.Origin) && CanonicalToken(a.Installation) && CanonicalToken(a.Registry) && CanonicalToken(a.Device) && CanonicalToken(a.OperationID) && CanonicalToken(a.InputDigest) && a.Sequence > 0 && a.Initial == (a.Sequence == 1) && a.JournalSequence > 0 && ((a.Sequence == 1 && a.Previous == "") || (a.Sequence > 1 && CanonicalToken(a.Previous))) && a.Issued > 0 && a.Expires > a.Issued && a.Expires-a.Issued <= 300 } // Verify deliberately retains an expired but authentic request for exact result // recovery. A storage mutation must also require Fresh after checking replay. func (v *Verifier) Verify(raw []byte) (Request, error) { if v == nil || len(raw) == 0 || len(raw) > BodyLimit { return Request{}, ErrRefused } var w wrapper if strictCanonical(raw, &w) != nil || w.Version != 1 || len(w.Authorization) == 0 || len(w.Authorization) > AuthorizationLimit || len(w.Input) == 0 || len(w.Input) > InputLimit { return Request{}, ErrRefused } var e envelope if strictCanonical(w.Authorization, &e) != nil || e.Version != 1 || e.KeyID != v.kid { return Request{}, ErrRefused } payload, ok := canonical(e.Payload, -1) sig, ok2 := canonical(e.Signature, 64) if !ok || !ok2 || len(payload) > AuthorizationLimit || !family.Verify(v.key, InputContext, payload, sig) { return Request{}, ErrRefused } var a Authorization if strictCanonical(payload, &a) != nil || !a.valid() || !v.actions[a.Action] || a.Role != v.pins.Role || a.Origin != v.pins.Origin || a.Installation != v.pins.Installation || a.Registry != v.pins.Registry || a.Device != v.pins.Device || a.InputDigest != digest(w.Input) { return Request{}, ErrRefused } return Request{a, append([]byte(nil), w.Input...), digest(w.Authorization), true}, nil } func DecodeInput(r Request, dst any) error { if !r.valid || strictCanonical(r.input, dst) != nil { return ErrRefused } return nil } // SignRequest is used by the separate provisioner. It does not grant execution // authority; the platform still requires fresh owner approval and a held fence. func SignRequest(k *family.Keypair, a Authorization, input any) ([]byte, error) { if !signingKey(k) { return nil, ErrRefused } b, e := encode(input) if e != nil || len(b) > InputLimit { return nil, ErrRefused } a.InputDigest = digest(b) if !a.valid() { return nil, ErrRefused } p, e := encode(a) if e != nil { return nil, e } signature, e := family.Sign(k, InputContext, p) if e != nil { return nil, ErrUnavailable } env, e := encode(envelope{1, keyID(k.SignPub()), base64.RawURLEncoding.EncodeToString(p), base64.RawURLEncoding.EncodeToString(signature)}) if e != nil || len(env) > AuthorizationLimit { return nil, ErrRefused } out, e := encode(wrapper{1, env, b}) if e != nil || len(out) > BodyLimit { return nil, ErrRefused } return out, nil } func signingKey(k *family.Keypair) bool { if k == nil || k.SignPriv == nil { return false } p := k.SignPriv return p.Curve == elliptic.P256() && p.D != nil && p.D.Sign() > 0 && p.D.Cmp(elliptic.P256().Params().N) < 0 && p.X != nil && p.Y != nil && elliptic.P256().IsOnCurve(p.X, p.Y) }