package protocol import ( "bytes" "git.n1ko.dev/Niko/niko_trust/pkg/address" "git.n1ko.dev/Niko/niko_trust/pkg/tce" ) // Decoders for the six protocol objects. // // Each decoder reads the fields in the exact order PROTOCOL.md section 8 // lists them and enforces the whole-object size limit before touching the // body. Identity public keys are validated as canonical curve points here, // which is the step crypto/ed25519.Verify deliberately does not perform and // therefore the one that must happen before any signature check // (docs/PROTOCOL.md section 7.3). // // Every byte slice a decoder exposes is a copy: keys, nonces and the retained // canonical bytes never alias the caller's buffer, so a decoded object cannot // be altered after the fact by mutating the input. // decodeIdentityField reads an identity field and validates its public key. func decodeIdentityField(d *tce.Decoder, name string) ([]byte, error) { pub, err := d.Identity() if err != nil { return nil, err } if err := address.ValidatePubKey(pub); err != nil { return nil, fieldErr(name, err) } return pub, nil } // checkEnd asserts the object consumed the whole input: any byte after the // last field would give the object a different ID and signature from the // object it appears to contain. func checkEnd(d *tce.Decoder) error { return d.End() } // DecodeIdentity parses an IdentityRegistration from its canonical bytes. func DecodeIdentity(b []byte) (*Identity, error) { if len(b) > tce.MaxIdentityTCE { return nil, tce.ErrObjectTooLarge } d := tce.NewDecoder(b) tag, err := d.Header() if err != nil { return nil, err } if tag != tce.TagIdentity { return nil, ErrWrongObject } pub, err := decodeIdentityField(d, "identity") if err != nil { return nil, err } alias, err := d.String(tce.MaxAliasLen) if err != nil { return nil, fieldErr("alias", err) } createdAt, err := d.Timestamp(false) if err != nil { return nil, fieldErr("created_at", err) } if err := checkEnd(d); err != nil { return nil, err } o := &Identity{PubKey: pub, Alias: alias, CreatedAt: createdAt} o.tce = bytes.Clone(b) return o, nil } // DecodeClaim parses a Claim from its canonical bytes. func DecodeClaim(b []byte) (*Claim, error) { if len(b) > tce.MaxClaimTCE { return nil, tce.ErrObjectTooLarge } d := tce.NewDecoder(b) tag, err := d.Header() if err != nil { return nil, err } if tag != tce.TagClaim { return nil, ErrWrongObject } issuer, err := decodeIdentityField(d, "issuer") if err != nil { return nil, err } subject, err := decodeIdentityField(d, "subject") if err != nil { return nil, err } claims, err := d.Map(1) if err != nil { return nil, fieldErr("claims", err) } createdAt, err := d.Timestamp(false) if err != nil { return nil, fieldErr("created_at", err) } expiresAt, err := d.Timestamp(true) if err != nil { return nil, fieldErr("expires_at", err) } if expiresAt != 0 && expiresAt <= createdAt { return nil, fieldErr("expires_at", tce.ErrExpiry) } serial, err := d.Uvarint() if err != nil { return nil, fieldErr("serial", err) } nonce, err := d.FixedBytes(tce.NonceSize) if err != nil { return nil, fieldErr("nonce", err) } if err := checkEnd(d); err != nil { return nil, err } o := &Claim{ Issuer: issuer, Subject: subject, Claims: claims, CreatedAt: createdAt, ExpiresAt: expiresAt, Serial: serial, Nonce: bytes.Clone(nonce), } o.tce = bytes.Clone(b) return o, nil } // DecodeRevocation parses a Revocation from its canonical bytes. func DecodeRevocation(b []byte) (*Revocation, error) { if len(b) > tce.MaxRevocTCE { return nil, tce.ErrObjectTooLarge } d := tce.NewDecoder(b) tag, err := d.Header() if err != nil { return nil, err } if tag != tce.TagRevocation { return nil, ErrWrongObject } issuer, err := decodeIdentityField(d, "issuer") if err != nil { return nil, err } claimHash, err := d.FixedBytes(tce.HashSize) if err != nil { return nil, fieldErr("claim_id", err) } claimID, err := tce.IDFromBytes(claimHash) if err != nil { return nil, fieldErr("claim_id", err) } reason, err := d.String(tce.MaxReasonLen) if err != nil { return nil, fieldErr("reason", err) } createdAt, err := d.Timestamp(false) if err != nil { return nil, fieldErr("created_at", err) } nonce, err := d.FixedBytes(tce.NonceSize) if err != nil { return nil, fieldErr("nonce", err) } if err := checkEnd(d); err != nil { return nil, err } o := &Revocation{ Issuer: issuer, ClaimID: claimID, Reason: reason, CreatedAt: createdAt, Nonce: bytes.Clone(nonce), } o.tce = bytes.Clone(b) return o, nil } // DecodeApprovalRequest parses an ApprovalRequest from its canonical bytes. func DecodeApprovalRequest(b []byte) (*ApprovalRequest, error) { if len(b) > tce.MaxRequestTCE { return nil, tce.ErrObjectTooLarge } d := tce.NewDecoder(b) tag, err := d.Header() if err != nil { return nil, err } if tag != tce.TagApprovalRequest { return nil, ErrWrongObject } sender, err := decodeIdentityField(d, "sender") if err != nil { return nil, err } recipient, err := decodeIdentityField(d, "recipient") if err != nil { return nil, err } action, err := d.String(tce.MaxActionLen) if err != nil { return nil, fieldErr("action", err) } payload, err := d.Map(0) if err != nil { return nil, fieldErr("payload", err) } message, err := d.String(tce.MaxMessageLen) if err != nil { return nil, fieldErr("message", err) } createdAt, err := d.Timestamp(false) if err != nil { return nil, fieldErr("created_at", err) } expiresAt, err := d.Timestamp(false) if err != nil { return nil, fieldErr("expires_at", err) } if expiresAt <= createdAt { return nil, fieldErr("expires_at", tce.ErrExpiry) } if expiresAt-createdAt > tce.MaxApprovalLifetime { return nil, fieldErr("expires_at", tce.ErrLifetime) } nonce, err := d.FixedBytes(tce.NonceSize) if err != nil { return nil, fieldErr("nonce", err) } if err := checkEnd(d); err != nil { return nil, err } o := &ApprovalRequest{ Sender: sender, Recipient: recipient, Action: action, Payload: payload, Message: message, CreatedAt: createdAt, ExpiresAt: expiresAt, Nonce: bytes.Clone(nonce), } o.tce = bytes.Clone(b) return o, nil } // DecodeApprovalResponse parses an ApprovalResponse from its canonical bytes. // // This only parses. Binding the response to a particular request is the job // of VerifyApprovalResponse, which requires the request. func DecodeApprovalResponse(b []byte) (*ApprovalResponse, error) { if len(b) > tce.MaxResponseTCE { return nil, tce.ErrObjectTooLarge } d := tce.NewDecoder(b) tag, err := d.Header() if err != nil { return nil, err } if tag != tce.TagApprovalResponse { return nil, ErrWrongObject } hashBytes, err := d.FixedBytes(tce.HashSize) if err != nil { return nil, fieldErr("request_hash", err) } requestHash, err := tce.IDFromBytes(hashBytes) if err != nil { return nil, fieldErr("request_hash", err) } responder, err := decodeIdentityField(d, "responder") if err != nil { return nil, err } decisionRaw, err := d.Uvarint() if err != nil { return nil, fieldErr("decision", err) } if Decision(decisionRaw) != Deny && Decision(decisionRaw) != Allow { return nil, fieldErr("decision", tce.ErrDecision) } createdAt, err := d.Timestamp(false) if err != nil { return nil, fieldErr("created_at", err) } nonce, err := d.FixedBytes(tce.NonceSize) if err != nil { return nil, fieldErr("nonce", err) } if err := checkEnd(d); err != nil { return nil, err } o := &ApprovalResponse{ RequestHash: requestHash, Responder: responder, Decision: Decision(decisionRaw), CreatedAt: createdAt, Nonce: bytes.Clone(nonce), } o.tce = bytes.Clone(b) return o, nil } // DecodeAuthAssertion parses an AuthAssertion from its canonical bytes. func DecodeAuthAssertion(b []byte) (*AuthAssertion, error) { if len(b) > tce.MaxAuthTCE { return nil, tce.ErrObjectTooLarge } d := tce.NewDecoder(b) tag, err := d.Header() if err != nil { return nil, err } if tag != tce.TagAuthAssertion { return nil, ErrWrongObject } pub, err := decodeIdentityField(d, "identity") if err != nil { return nil, err } challenge, err := d.FixedBytes(tce.ChallengeSize) if err != nil { return nil, fieldErr("challenge", err) } scope, err := d.String(tce.MaxScopeLen) if err != nil { return nil, fieldErr("scope", err) } audience, err := d.String(tce.MaxAudienceLen) if err != nil { return nil, fieldErr("audience", err) } createdAt, err := d.Timestamp(false) if err != nil { return nil, fieldErr("created_at", err) } if err := checkEnd(d); err != nil { return nil, err } o := &AuthAssertion{ PubKey: pub, Challenge: bytes.Clone(challenge), Scope: scope, Audience: audience, CreatedAt: createdAt, } o.tce = bytes.Clone(b) return o, nil } // DecodeDelegationClaim parses a DelegationClaim from its canonical bytes. func DecodeDelegationClaim(b []byte) (*DelegationClaim, error) { if len(b) > tce.MaxDelegTCE { return nil, tce.ErrObjectTooLarge } d := tce.NewDecoder(b) tag, err := d.Header() if err != nil { return nil, err } if tag != tce.TagDelegation { return nil, ErrWrongObject } granter, err := decodeIdentityField(d, "granter") if err != nil { return nil, err } grantee, err := decodeIdentityField(d, "grantee") if err != nil { return nil, err } predRaw, err := d.Map(1) if err != nil { return nil, fieldErr("predicates", err) } predicates := make(map[string]tce.Value, len(predRaw)) for k, v := range predRaw { bv, ok := v.Bool() if !ok || !bv { return nil, fieldErr("predicates:"+k, tce.ErrValueTag) } predicates[k] = v } maxDepth, err := d.Uvarint() if err != nil { return nil, fieldErr("max_depth", err) } createdAt, err := d.Timestamp(false) if err != nil { return nil, fieldErr("created_at", err) } expiresAt, err := d.Timestamp(true) if err != nil { return nil, fieldErr("expires_at", err) } if expiresAt != 0 && expiresAt <= createdAt { return nil, fieldErr("expires_at", tce.ErrExpiry) } serial, err := d.Uvarint() if err != nil { return nil, fieldErr("serial", err) } nonce, err := d.FixedBytes(tce.NonceSize) if err != nil { return nil, fieldErr("nonce", err) } if err := checkEnd(d); err != nil { return nil, err } o := &DelegationClaim{ Granter: granter, Grantee: grantee, Predicates: predicates, MaxDepth: maxDepth, CreatedAt: createdAt, ExpiresAt: expiresAt, Serial: serial, Nonce: nonce, } o.tce = bytes.Clone(b) return o, nil }