feat: signature verification, keyid resolution, and endorsements (JS/Go/PHP)#1
Merged
Conversation
Adds README scaffolds for two new language bindings of the HTMLTrust canonicalization library. Both MUST produce byte-identical output to the existing JS, Go, and PHP implementations for every test vector in a shared conformance suite (TBD). Python uses stdlib unicodedata plus beautifulsoup4/lxml for the extract_canonical_text HTML parser. Rust uses unicode-normalization plus scraper/html5ever. Implementation to follow in separate commits; test vectors must be centralized before implementation to enforce cross-language parity. See TODO-Cleanup.md at the umbrella project root for implementation task tracking.
Brings JS, Go, and PHP bindings to parity with spec §2.1, §2.2, and §2.5.
The canonicalization library is now the cross-platform reference for
clients performing local signature verification per spec §3.1, not just
text canonicalization.
New surface in all three bindings:
- buildSignatureBinding({contentHash, claimsHash, domain, signedAt})
builds the canonical "{content-hash}:{claims-hash}:{domain}:{signed-at}"
string per spec §2.1
- verifySignature(message, signatureB64, publicKeyPem, algorithm)
supports ed25519, ecdsa (SHA-256), and rsa (RSA-SHA256); accepts both
padded and unpadded base64; algorithm matching is case-insensitive
- KeyResolver chain with three implementations (didWebResolver,
directUrlResolver, trustDirectoryResolver) and resolveKey() walking
the chain. None is privileged per spec §2.2; callers compose them.
- canonicalizeClaims for sorted name=value serialization
- buildEndorsementBinding + verifyEndorsement for §2.5 endorsements as
standalone signed JSON blobs
Cross-platform decisions:
- Endorsement signature algorithm: resolver-declared algorithm wins
over the endorsement.algorithm field. The key knows what it is; the
endorsement field is a hint. Aligned across JS and Go.
- JS library detects environment: prefers SubtleCrypto in browsers,
falls back to node:crypto in Node.js
- Go uses crypto/ed25519, crypto/ecdsa, crypto/rsa from stdlib; no new
module deps
- PHP uses sodium_crypto_sign_verify_detached for ed25519, openssl_verify
for ecdsa/rsa; resolvers accept an injected fetcher callable for
testability
Tests: JS 29/29, Go 38/38, PHP written but unverified locally
(PHP toolchain not installed on this dev machine).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings the JS, Go, and PHP canonicalization bindings to parity with spec §2.1, §2.2, and §2.5. After this PR, the library is the cross-platform reference for clients performing local signature verification per spec §3.1, not just text canonicalization.
New surface (all three bindings)
buildSignatureBinding({contentHash, claimsHash, domain, signedAt})— canonical{content-hash}:{claims-hash}:{domain}:{signed-at}per §2.1verifySignature(message, signatureB64, publicKeyPem, algorithm)— ed25519, ecdsa (SHA-256), rsa (RSA-SHA256); padded or unpadded base64; case-insensitive algorithmdidWebResolver,directUrlResolver,trustDirectoryResolver, withresolveKey()walking the chain. None is privileged per §2.2; callers compose them.canonicalizeClaimsfor sorted name=value serializationbuildEndorsementBinding+verifyEndorsementfor §2.5 endorsementsCross-platform decisions
endorsement.algorithm. The key knows what it is; the field is a hint. Aligned across JS and Go (Go agent flagged it; JS updated to match).node:crypto.crypto/ed25519,crypto/ecdsa,crypto/rsa); no new module deps. RE2 has no backreferences, so excluded-elements regex is split into per-tag-name compiled REs (output equivalence verified by extraction tests).sodium_crypto_sign_verify_detachedfor ed25519,openssl_verifyfor ecdsa/rsa. Resolvers accept an injected fetcher callable for testability.Test plan
cd javascript && node test.js— 29/29 passingcd go && go test ./...— 38/38 passingcd php && composer install && vendor/bin/phpunit— needs verification; PHP not installed on the dev machine where this was written. Tests written carefully against PHP 7.2+ contract with PSR-4 + injected fetchers; please run locally and report any failures.🤖 Generated with Claude Code