Add scoped execution and settlement proof verification#28
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 92d7dbfb4e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (receipt.settlement !== undefined && !proofs.some((proof) => proof.type === "settlement" && proof.ok)) { | ||
| errors.push("ERR_MISSING_SETTLEMENT_PROOF"); |
There was a problem hiding this comment.
Require an execution proof before returning VERIFIED
For a clas.execution.receipt.v1 receipt that contains a valid settlement proof but no execution proof, this code only checks that settlement has an OK proof, so proofs.every(...) can still return VERIFIED while verb, agent, and action are completely unsigned and can be arbitrary or tampered. Since scoped proofs intentionally separate settlement from execution, require at least one OK execution proof before accepting the receipt.
Useful? React with 👍 / 👎.
| const covered = Array.isArray(proof?.covers) ? [...proof.covers] : []; | ||
| const result: VerifyScopedProofResult = { | ||
| type: typeof proof?.type === "string" ? proof.type : "", | ||
| signer: proof ? getScopedProofSigner(proof) : undefined, |
There was a problem hiding this comment.
Return INVALID when scoped signatures are missing
When a proof object omits signature, result construction calls getScopedProofSigner(proof) before the validation below can add ERR_MISSING_SIGNATURE, and that helper dereferences proof.signature.signer. A malformed scoped proof with no signature therefore throws a TypeError from verifyScopedProofs() instead of failing closed with an INVALID result.
Useful? React with 👍 / 👎.
| const key = opts.resolvePublicKey?.(proof) ?? opts.publicKeysByKid?.[proof.signature.kid] ?? opts.publicKeyPemOrDer; | ||
| if (!key) { | ||
| errors.push("ERR_MISSING_PUBLIC_KEY"); | ||
| } else if (verifyCanonical(canonical, proof.signature.value, key)) { |
There was a problem hiding this comment.
Catch malformed scoped signatures during verification
If the covered-payload hash matches but signature.value is malformed base64 or decodes to the wrong length, verifyCanonical() throws before returning false; this call is not caught, so attacker-supplied malformed proofs can make verifyScopedProofs() throw rather than return INVALID. Wrap this verification path and convert verification errors to ERR_SIGNATURE_INVALID or a malformed-signature error.
Useful? React with 👍 / 👎.
| } | ||
| } | ||
|
|
||
| result.ok = errors.length === 0 && result.hash_matches && result.signature_valid; |
There was a problem hiding this comment.
Validate scoped proof signers before accepting proofs
For an execution proof whose signature.signer/signer_id does not match the signed receipt.agent identity, this still sets ok from only hash and signature validity, so a receipt can be reported VERIFIED as long as the provided kid/key signs the covered fields even though the declared signer is not the agent. Scoped verification should fail closed on signer mismatch, e.g. by comparing execution proof signer to receipt.agent/receipt.agent.id or accepting an expected signer constraint as the older verifier does.
Useful? React with 👍 / 👎.
| if (!opts.publicKeyPemOrDer && !opts.publicKeysByKid && !opts.resolvePublicKey) { | ||
| throw new Error("verifyScopedProofs requires publicKeyPemOrDer, publicKeysByKid, or resolvePublicKey"); | ||
| } | ||
| if (!Array.isArray(receipt.proofs) || receipt.proofs.length === 0) { |
There was a problem hiding this comment.
Return INVALID for null scoped receipts
When the verifier is called on an untrusted parsed body that is null or undefined, this dereferences receipt.proofs before any structure check, so verifyScopedProofs() throws a TypeError instead of failing closed with an INVALID result. Guard receipt itself before reading proofs so malformed receipt inputs cannot crash callers.
Useful? React with 👍 / 👎.
Motivation
clas.execution.receipt.v1receipts that can contain multiple scopedproofs[]where each proof signs only the fields listed inproof.covers[]so execution and settlement attestations can coexist on one receipt.metadata.proofbehavior for backward compatibility and avoid introducing network calls or rail-specific logic in core verification.Description
CommandLayerScopedProof,ScopedProofType,SCOPED_PROOF_COVERS,VerifyScopedProofResult, andVerifyScopedProofsResultto modelexecutionandsettlementproofs and their exact ordered coverage requirements.buildCoveredPayload(receipt, proof)which materializes only the top-level fields named byproof.covers[], validates exact ordered covers perSCOPED_PROOF_COVERS, and fails closed on malformed/unsupported covers or missing fields.verifyScopedProof()andverifyScopedProofs()to canonicalize the covered payload with the existingjson.sorted_keys.v1, recompute SHA-256, and verify Ed25519 signatures, returning per-proof results (signature_valid,hash_matches,covered,signer,ok,errors).test/scoped-proofs.test.tsplus a README note documenting the scoped proof model and backward compatibility withverifyCommandLayerReceipt().Testing
npm test, which executed the full suite including the newCLAS scoped proofstests and reported86tests passing and0failures.npm run buildand a typecheck withnpm run typecheck, both completed successfully.covers[]handling.Codex Task