package protocol import ( "crypto/ed25519" "crypto/subtle" "git.n1ko.dev/Niko/niko_trust/pkg/tce" ) // Verification of the six protocol objects, following the order of // PROTOCOL.md section 7.3: a decoded object is an object whose canonical // bytes were read strictly and whose public keys are valid curve points, and // only then is the Ed25519 signature checked over those exact bytes. // // Nothing here re-encodes a decoded object and verifies the result. The // verified bytes are the received bytes, retained by the decode step // (docs/IMPLEMENTATION_NOTES.md property 2). // // Scope of this package: it establishes who signed a statement and that the // statement is unchanged and well-formed. Whether the statement should be // believed is the caller's policy (INV-5), and object-vs-now timing is a // separate, explicit step (ValidateCurrent) so that verification stays // deterministic. // verifySignature checks an Ed25519 signature over the exact canonical bytes // the decoded object was read from. // // crypto/ed25519.Verify performs no key validation, which is why the decode // path already validated every public key through address.ValidatePubKey. func verifySignature(pub, tceBytes, sig []byte) error { if len(sig) != tce.SignatureSize { return ErrSignatureSize } if !ed25519.Verify(ed25519.PublicKey(pub), tceBytes, sig) { return ErrSignature } return nil } // VerifyIdentity decodes the canonical bytes, checks the signature under the // registration's public key and returns the registered identity. func VerifyIdentity(tceBytes, sig []byte) (*Identity, error) { o, err := DecodeIdentity(tceBytes) if err != nil { return nil, err } if err := verifySignature(o.PubKey, o.tce, sig); err != nil { return nil, err } o.sig = make([]byte, len(sig)) copy(o.sig, sig) return o, nil } // VerifyClaim decodes the canonical bytes, checks the signature under the // issuer's public key and returns the claim. func VerifyClaim(tceBytes, sig []byte) (*Claim, error) { o, err := DecodeClaim(tceBytes) if err != nil { return nil, err } if err := verifySignature(o.Issuer, o.tce, sig); err != nil { return nil, err } o.sig = make([]byte, len(sig)) copy(o.sig, sig) return o, nil } // VerifyRevocation decodes the canonical bytes and checks the signature under // the issuer's public key. Whether the revocation actually withdraws a // particular claim is a separate check, VerifyRevocationOf. func VerifyRevocation(tceBytes, sig []byte) (*Revocation, error) { o, err := DecodeRevocation(tceBytes) if err != nil { return nil, err } if err := verifySignature(o.Issuer, o.tce, sig); err != nil { return nil, err } o.sig = make([]byte, len(sig)) copy(o.sig, sig) return o, nil } // VerifyApprovalRequest decodes the canonical bytes and checks the signature // under the sender's public key. func VerifyApprovalRequest(tceBytes, sig []byte) (*ApprovalRequest, error) { o, err := DecodeApprovalRequest(tceBytes) if err != nil { return nil, err } if err := verifySignature(o.Sender, o.tce, sig); err != nil { return nil, err } o.sig = make([]byte, len(sig)) copy(o.sig, sig) return o, nil } // VerifyAuthAssertion decodes the canonical bytes, checks the signature under // the asserting identity's public key, and requires the signed audience to // equal expectedAudience exactly (INV-4). // // There is no default audience, no empty-means-any case and no substring or // suffix matching: an assertion produced for one server must never // authenticate a connection to another. An empty expectedAudience is an // error. func VerifyAuthAssertion(tceBytes, sig []byte, expectedAudience string) (*AuthAssertion, error) { if expectedAudience == "" { return nil, ErrEmptyAudience } o, err := DecodeAuthAssertion(tceBytes) if err != nil { return nil, err } if err := verifySignature(o.PubKey, o.tce, sig); err != nil { return nil, err } // Constant-time comparison so that the byte length difference revealed by // a length check is the only thing an observer learns. if subtle.ConstantTimeCompare([]byte(o.Audience), []byte(expectedAudience)) != 1 { return nil, ErrAudience } o.sig = make([]byte, len(sig)) copy(o.sig, sig) return o, nil } // VerifyApprovalResponse verifies an approval response against the exact // request it claims to answer. // // Both the request and the response must verify. There is deliberately no // function that verifies a response on its own, because a response is only // meaningful relative to the request it commits to (INV-3): this is the // request_hash binding that makes a signed decision impossible to move to a // different request. // // The checks, on top of both signatures, are those of PROTOCOL.md section // 8.5: SHA-256(received_request_tce) equals response.request_hash; the // responder equals the request's recipient; and the response is dated within // the request's validity window with the clock-skew allowance. // // Replay state — the one-response-per-request rule — is kept by the caller, // because it cannot be reproduced from two byte strings alone and is a // storage concern. func VerifyApprovalResponse(requestTCE, requestSig, responseTCE, responseSig []byte) (*ApprovalResponse, error) { req, err := VerifyApprovalRequest(requestTCE, requestSig) if err != nil { return nil, err } resp, err := DecodeApprovalResponse(responseTCE) if err != nil { return nil, err } if err := verifySignature(resp.Responder, resp.tce, responseSig); err != nil { return nil, err } // 1. The response commits to the exact request bytes, compared in // constant time with no parsing involved. reqID := tce.ComputeID(req.tce) if !resp.RequestHash.Equal(reqID) { return nil, ErrRequestMismatch } // 2. Only the recipient of the request may answer it. if subtle.ConstantTimeCompare(resp.Responder, req.Recipient) != 1 { return nil, ErrWrongResponder } // 3. The response must fall inside the request's window. Both timestamps // come from different signers with independent clocks, so the clock-skew // allowance of section 13.1 applies to each bound. if err := checkResponseWindow(req, resp); err != nil { return nil, err } resp.sig = make([]byte, len(responseSig)) copy(resp.sig, responseSig) return resp, nil } // VerifyApprovalResponseStandalone decodes the canonical bytes and checks the // signature under the responder's public key, without binding to a request. // Use VerifyApprovalResponse when the request is available; this is for the // transport layer and other contexts that only hold the response. func VerifyApprovalResponseStandalone(tceBytes, sig []byte) (*ApprovalResponse, error) { o, err := DecodeApprovalResponse(tceBytes) if err != nil { return nil, err } if err := verifySignature(o.Responder, o.tce, sig); err != nil { return nil, err } o.sig = make([]byte, len(sig)) copy(o.sig, sig) return o, nil } // VerifyAuthAssertionSignature decodes the canonical bytes and checks the // signature under the asserting identity's public key, without checking the // audience. Use VerifyAuthAssertion when the expected audience is known; this // is for the transport layer which has no server context. func VerifyAuthAssertionSignature(tceBytes, sig []byte) (*AuthAssertion, error) { o, err := DecodeAuthAssertion(tceBytes) if err != nil { return nil, err } if err := verifySignature(o.PubKey, o.tce, sig); err != nil { return nil, err } o.sig = make([]byte, len(sig)) copy(o.sig, sig) return o, nil } // checkResponseWindow enforces // request.created_at - skew <= response.created_at <= request.expires_at + skew. func checkResponseWindow(req *ApprovalRequest, resp *ApprovalResponse) error { if req.CreatedAt > resp.CreatedAt { if req.CreatedAt-resp.CreatedAt > MaxClockSkew { return ErrResponseTiming } } else if resp.CreatedAt > req.ExpiresAt { if resp.CreatedAt-req.ExpiresAt > MaxClockSkew { return ErrResponseTiming } } return nil } // VerifyRevocationOf checks that a verified revocation withdraws a verified // claim: it must target the claim's object ID and be signed by the claim's // issuer. A revocation signed by anyone else is meaningless and is rejected // (PROTOCOL.md section 8.3). // // Both objects must have been verified first, since both the issuer tie and // the content hash are only trustworthy once the signatures hold. func VerifyRevocationOf(rev *Revocation, claim *Claim) error { if rev == nil || claim == nil { return ErrNil } want := tce.ComputeID(claim.tce) if subtle.ConstantTimeCompare(rev.ClaimID[:], want[:]) != 1 { return ErrWrongClaim } if subtle.ConstantTimeCompare(rev.Issuer, claim.Issuer) != 1 { return ErrWrongIssuer } return nil } // ValidateCurrent reports whether an object dated createdAt with optional // expiry expiresAt is valid at time now, applying the clock-skew allowance of // PROTOCOL.md section 13.1. // // A timestamp cannot be trusted absolutely and there is no ordering between // different signers' clocks, so an object whose created_at is no more than the // allowance in the future is accepted, and an object accepted no more than the // allowance after its expiry is still current. Anything further away is an // error: ErrNotYetValid for the future, ErrExpired for the past. func ValidateCurrent(createdAt, expiresAt, now uint64) error { if createdAt > now && createdAt-now > MaxClockSkew { return ErrNotYetValid } if expiresAt != 0 && now > expiresAt && now-expiresAt > MaxClockSkew { return ErrExpired } return nil } // ClaimStatusAt returns the current validity of a claim at time now, without // consulting any revocation store. // // There are exactly three outcomes and there is deliberately no // "not found" status: absence of a claim is not a protocol state, because a // relay can withhold anything and a consumer that treated silence as a // negative answer could be manipulated by censorship // (docs/IMPLEMENTATION_NOTES.md property 6). An application decides what an // absent claim means. // // Revoked is not returned here precisely because the caller holds the // revocation store: whether a verified revocation exists is up to the // application, which finds StatusRevoked by combining this with its own // evidence. func ClaimStatusAt(o *Claim, now uint64) ClaimStatus { if o == nil { return StatusActive } if o.ExpiresAt != 0 && now > o.ExpiresAt && now-o.ExpiresAt > MaxClockSkew { return StatusExpired } return StatusActive } // VerifyDelegationClaim decodes the canonical bytes and checks the signature // under the granter's public key. func VerifyDelegationClaim(tceBytes, sig []byte) (*DelegationClaim, error) { o, err := DecodeDelegationClaim(tceBytes) if err != nil { return nil, err } if err := verifySignature(o.Granter, o.tce, sig); err != nil { return nil, err } o.sig = make([]byte, len(sig)) copy(o.sig, sig) return o, nil } // VerifyRevocationOfDelegation reports whether rev withdraws the delegation // cp: only the granter may revoke its own grant, and the revocation must // target this exact object by content ID. func VerifyRevocationOfDelegation(rev *Revocation, cp *DelegationClaim) error { if rev == nil || cp == nil { return ErrNil } if subtle.ConstantTimeCompare(rev.Issuer, cp.Granter) != 1 { return ErrWrongIssuer } if rev.ClaimID.String() != tce.ComputeID(cp.tce).String() { return ErrWrongClaim } return nil } // DelegationStatusAt returns the current validity of a grant at time now, // without consulting any revocation store. Same three-valued semantics as // ClaimStatusAt: absence is not a protocol state. func DelegationStatusAt(cp *DelegationClaim, now uint64) ClaimStatus { if cp == nil { return StatusActive } if cp.ExpiresAt != 0 && now > cp.ExpiresAt && now-cp.ExpiresAt > MaxClockSkew { return StatusExpired } return StatusActive }