package protocol import ( "fmt" "git.n1ko.dev/Niko/niko_trust/pkg/address" "git.n1ko.dev/Niko/niko_trust/pkg/tce" ) // Encoders for the six protocol objects. // // Each encoder writes the fields in exactly the order given in PROTOCOL.md // section 8 and enforces that section's limits on top of the primitive // constraints that internal/tce already applies. There are no optional fields: // every field is always present, an empty string encodes as a single 0x00 and // an empty map as a single 0x00, so the encoding cannot drift between call // sites. // fieldErr annotates a sentinel error with the field that failed, without // including any input data. func fieldErr(field string, err error) error { if err == nil { return nil } return fmt.Errorf("%s: %w", field, err) } // finishEncode returns the encoder's bytes after applying the per-object whole // limit from PROTOCOL.md section 6.3. func finishEncode(e *tce.Encoder, limit int) ([]byte, error) { b, err := e.Bytes() if err != nil { return nil, err } if len(b) > limit { return nil, tce.ErrObjectTooLarge } return b, nil } // EncodeIdentity encodes an IdentityRegistration, object tag 0x01. // // Field order: identity, alias, created_at. The alias appears in this object // and in no other (INV-7). The key is validated as a curve point before it is // written, so a degenerate key can never be signed. func EncodeIdentity(o *Identity) ([]byte, error) { if o == nil { return nil, ErrNil } if err := address.ValidatePubKey(o.PubKey); err != nil { return nil, fieldErr("identity", err) } e := tce.NewEncoder() e.Header(tce.TagIdentity) e.Identity("identity", o.PubKey) e.String("alias", o.Alias, tce.MaxAliasLen) e.Timestamp("created_at", o.CreatedAt, false) return finishEncode(e, tce.MaxIdentityTCE) } // EncodeClaim encodes a Claim, object tag 0x02. // // Field order: issuer, subject, claims, created_at, expires_at, serial, // nonce. expires_at of 0 means "does not expire"; otherwise it must be // strictly after created_at. func EncodeClaim(o *Claim) ([]byte, error) { if o == nil { return nil, ErrNil } if err := address.ValidatePubKey(o.Issuer); err != nil { return nil, fieldErr("issuer", err) } if err := address.ValidatePubKey(o.Subject); err != nil { return nil, fieldErr("subject", err) } if len(o.Nonce) != tce.NonceSize { return nil, fieldErr("nonce", tce.ErrFieldSize) } if o.ExpiresAt != 0 && o.ExpiresAt <= o.CreatedAt { return nil, fieldErr("expires_at", tce.ErrExpiry) } e := tce.NewEncoder() e.Header(tce.TagClaim) e.Identity("issuer", o.Issuer) e.Identity("subject", o.Subject) e.Map("claims", o.Claims, 1) e.Timestamp("created_at", o.CreatedAt, false) e.Timestamp("expires_at", o.ExpiresAt, true) e.Uvarint(o.Serial) e.FixedBytes("nonce", o.Nonce, tce.NonceSize) return finishEncode(e, tce.MaxClaimTCE) } // EncodeRevocation encodes a Revocation, object tag 0x03. // // Field order: issuer, claim_id, reason, created_at, nonce. The binding // between a revocation and the claim it withdraws is enforced by // VerifyRevocationOf once both objects are verified. func EncodeRevocation(o *Revocation) ([]byte, error) { if o == nil { return nil, ErrNil } if err := address.ValidatePubKey(o.Issuer); err != nil { return nil, fieldErr("issuer", err) } if len(o.Nonce) != tce.NonceSize { return nil, fieldErr("nonce", tce.ErrFieldSize) } e := tce.NewEncoder() e.Header(tce.TagRevocation) e.Identity("issuer", o.Issuer) e.FixedBytes("claim_id", o.ClaimID[:], tce.HashSize) e.String("reason", o.Reason, tce.MaxReasonLen) e.Timestamp("created_at", o.CreatedAt, false) e.FixedBytes("nonce", o.Nonce, tce.NonceSize) return finishEncode(e, tce.MaxRevocTCE) } // EncodeApprovalRequest encodes an ApprovalRequest, object tag 0x04. // // Field order: sender, recipient, action, payload, message, created_at, // expires_at, nonce. expires_at must be after created_at by at most 60 // seconds; the bound is part of the format so an over-long request is invalid // everywhere rather than merely refused by one server. func EncodeApprovalRequest(o *ApprovalRequest) ([]byte, error) { if o == nil { return nil, ErrNil } if err := address.ValidatePubKey(o.Sender); err != nil { return nil, fieldErr("sender", err) } if err := address.ValidatePubKey(o.Recipient); err != nil { return nil, fieldErr("recipient", err) } if len(o.Nonce) != tce.NonceSize { return nil, fieldErr("nonce", tce.ErrFieldSize) } if o.ExpiresAt <= o.CreatedAt { return nil, fieldErr("expires_at", tce.ErrExpiry) } if o.ExpiresAt-o.CreatedAt > tce.MaxApprovalLifetime { return nil, fieldErr("expires_at", tce.ErrLifetime) } e := tce.NewEncoder() e.Header(tce.TagApprovalRequest) e.Identity("sender", o.Sender) e.Identity("recipient", o.Recipient) e.String("action", o.Action, tce.MaxActionLen) e.Map("payload", o.Payload, 0) e.String("message", o.Message, tce.MaxMessageLen) e.Timestamp("created_at", o.CreatedAt, false) e.Timestamp("expires_at", o.ExpiresAt, false) e.FixedBytes("nonce", o.Nonce, tce.NonceSize) return finishEncode(e, tce.MaxRequestTCE) } // EncodeApprovalResponse encodes an ApprovalResponse, object tag 0x05. // // Field order: request_hash, responder, decision, created_at, nonce. // request_hash comes first because it is the field that gives the object its // meaning. The decision may be only deny (0) or allow (1); a verifier cannot // be left with an outcome it has no rule for. func EncodeApprovalResponse(o *ApprovalResponse) ([]byte, error) { if o == nil { return nil, ErrNil } if !o.Decision.Valid() { return nil, fieldErr("decision", tce.ErrDecision) } if err := address.ValidatePubKey(o.Responder); err != nil { return nil, fieldErr("responder", err) } if len(o.Nonce) != tce.NonceSize { return nil, fieldErr("nonce", tce.ErrFieldSize) } e := tce.NewEncoder() e.Header(tce.TagApprovalResponse) e.FixedBytes("request_hash", o.RequestHash[:], tce.HashSize) e.Identity("responder", o.Responder) e.Uvarint(uint64(o.Decision)) e.Timestamp("created_at", o.CreatedAt, false) e.FixedBytes("nonce", o.Nonce, tce.NonceSize) return finishEncode(e, tce.MaxResponseTCE) } // EncodeAuthAssertion encodes an AuthAssertion, object tag 0x06. // // Field order: identity, challenge, scope, audience, created_at. The audience // is signed so an assertion produced for one server cannot be replayed to // another; its binding is checked by VerifyAuthAssertion. func EncodeAuthAssertion(o *AuthAssertion) ([]byte, error) { if o == nil { return nil, ErrNil } if err := address.ValidatePubKey(o.PubKey); err != nil { return nil, fieldErr("identity", err) } if len(o.Challenge) != tce.ChallengeSize { return nil, fieldErr("challenge", tce.ErrFieldSize) } e := tce.NewEncoder() e.Header(tce.TagAuthAssertion) e.Identity("identity", o.PubKey) e.FixedBytes("challenge", o.Challenge, tce.ChallengeSize) e.String("scope", o.Scope, tce.MaxScopeLen) e.String("audience", o.Audience, tce.MaxAudienceLen) e.Timestamp("created_at", o.CreatedAt, false) return finishEncode(e, tce.MaxAuthTCE) } // EncodeDelegationClaim encodes a DelegationClaim, object tag 0x07. // // Field order: granter, grantee, predicates, max_depth, created_at, // expires_at, serial, nonce. Every predicate value must be the boolean true: // a delegation covers a key or it does not, and any other spelling would // make two implementations disagree about coverage while both saw a valid // signature. func EncodeDelegationClaim(o *DelegationClaim) ([]byte, error) { if o == nil { return nil, ErrNil } if err := address.ValidatePubKey(o.Granter); err != nil { return nil, fieldErr("granter", err) } if err := address.ValidatePubKey(o.Grantee); err != nil { return nil, fieldErr("grantee", err) } if len(o.Nonce) != tce.NonceSize { return nil, fieldErr("nonce", tce.ErrFieldSize) } if o.ExpiresAt != 0 && o.ExpiresAt <= o.CreatedAt { return nil, fieldErr("expires_at", tce.ErrExpiry) } if len(o.Predicates) < 1 || len(o.Predicates) > tce.MaxMapEntries { return nil, fieldErr("predicates", tce.ErrEmptyMap) } for k, v := range o.Predicates { b, ok := v.Bool() if !ok || !b { return nil, fieldErr("predicates:"+k, tce.ErrValueTag) } } e := tce.NewEncoder() e.Header(tce.TagDelegation) e.Identity("granter", o.Granter) e.Identity("grantee", o.Grantee) e.Map("predicates", o.Predicates, 1) e.Uvarint(o.MaxDepth) e.Timestamp("created_at", o.CreatedAt, false) e.Timestamp("expires_at", o.ExpiresAt, true) e.Uvarint(o.Serial) e.FixedBytes("nonce", o.Nonce, tce.NonceSize) return finishEncode(e, tce.MaxDelegTCE) }