feat(signing): L1 signing — RFC 9421, KMS providers, JWKS, webhook verification#32
feat(signing): L1 signing — RFC 9421, KMS providers, JWKS, webhook verification#32MichielDean wants to merge 27 commits into
Conversation
…ntext, VerificationKeyResolver)
…verification round-trip tests Track 4 (L1 Signing) implementation: - InProcessSigningProvider: JCA Ed25519/ES256/ES384 signing via SigningProvider SPI, delegates to Rfc9421Canonicalizer for signature base construction - InProcessKeyGenerator: test keypair generation utility (Ed25519, ES256, ES384) for development/testing - InProcessVerificationProvider: verification path using Rfc9421Verifier with AdCP profile validation - WebhookSigner/DefaultWebhookSigner: seam interface for Track 6 (async-l3) webhook signing, applies AdCP webhook-signing profile - WebhookSigningResult: record for signed webhook headers - Tests: signing/verification round-trip for Ed25519 and ES256, key generation, webhook signer seam
…wire, and ECDSA DER conversion
…wire, and ECDSA DER conversion
…lidation, and brand.json walk - JwkParser: parse Ed25519, EC (P-256/P-384), and RSA JWKs into VerificationKey - StaticJwksResolver: in-memory kid lookup from pre-loaded JWKS - CachingJwksResolver: HTTP-fetching with 30s cooldown, single-flight dedup - BrandJsonJwksResolver: 3-hop resolution (brand.json → agent → jwks_uri) - SsrfJwksUriValidator: HTTPS enforcement, private IP rejection, redirect:manual - JwksDocument: parsed JWKS with per-kid index and lastFetched timestamp - JwksResolutionException: unchecked exception for JWKS fetch failures - SsrfBlockedException: made constructor public for JWKS validator tests
…r nonce dedup and key revocation
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 34069021 | Triggered | Generic High Entropy Secret | f3d9c7b | adcp-server/src/test/java/org/adcontextprotocol/adcp/server/signing/webhook/StandardWebhooksVerifierTest.java | View secret |
| 34069022 | Triggered | Generic High Entropy Secret | f3d9c7b | adcp-server/src/test/java/org/adcontextprotocol/adcp/server/signing/webhook/StandardWebhooksVerifierTest.java | View secret |
| 34109707 | Triggered | Generic High Entropy Secret | f53b5a5 | adcp-server/src/test/java/org/adcontextprotocol/adcp/server/signing/webhook/StandardWebhooksVerifierTest.java | View secret |
| 34069023 | Triggered | Generic High Entropy Secret | f53b5a5 | adcp-server/src/test/java/org/adcontextprotocol/adcp/server/signing/webhook/StandardWebhooksVerifierTest.java | View secret |
| 34109706 | Triggered | Generic High Entropy Secret | f53b5a5 | adcp-server/src/test/java/org/adcontextprotocol/adcp/server/signing/webhook/StandardWebhooksVerifierTest.java | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
The compliance test vectors in adcp-server/src/test/resources/compliance/ contain intentionally public test keys with _private_d_for_test_only fields. These are for signer/verifier round-trip conformance testing and MUST NOT be used in production. The keys.json files carry explicit warnings in their _WARNING and fields.
|
The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final This is an automated message from the Argus AI review workflow. |
…h presence) The .gitguardian file only takes effect when present on the default branch. For now, the test key findings will be marked as false positives via the GitGuardian dashboard. The keys.json files contain intentionally public test keys from the AdCP compliance suite with explicit warnings.
|
The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final This is an automated message from the Argus AI review workflow. |
bokelley
left a comment
There was a problem hiding this comment.
Thanks for the large signing implementation. I reviewed the verifier, signing providers, JWKS/brand.json resolution, revocation handling, and webhook helpers. I think this needs another pass before merge; the targeted tests pass locally, but there are several correctness/security issues that the current coverage does not catch.
Findings:
-
[P1] Malformed signature inputs can throw instead of returning
VerificationResult.Invalid.
adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/Rfc9421Verifier.java:100parsescreated/expireswithLong.parseLongoutside a catch, andextractSignatureBytescan throw fromBase64.getUrlDecoder().decode(...)at line 364. A bad remote header can therefore become a verifier exception/500 instead of a signature rejection. Please keep all wire-format failures on theVerificationResult.Invalidpath. -
[P1] Empty webhook bodies cannot be signed correctly.
Webhook signing requirescontent-digest, butDefaultWebhookSigner.java:44andInProcessSigningProvider.java:77only add it whenbody.length > 0. ForWEBHOOK_SIGNING, the covered component list still includescontent-digest, so empty-payload webhooks either fail canonicalization or skip digest validation. The digest of an empty byte array is still a valid requiredContent-Digestvalue. -
[P1]
brand.jsonJWKS URI rotation is broken.
InBrandJsonJwksResolver.java,selectedJwksUri = jwksUriis assigned at line 214 before comparing at line 218, so the resolver replacement condition never detects a changed URI onceinnerResolverexists. Ifbrand.jsonrotates to a newjwks_uri, the process can keep using the stale resolver until restart. Capture the previous selected URI before assignment, or compare against the existing resolver state. -
[P2] Standard Webhooks secret padding is incorrect.
StandardWebhooksVerifier.java:65uses"=".repeat((-payload.length()) % 4). Java preserves the negative remainder, so lengths where padding is needed can throwIllegalArgumentException: count is negative. Use(4 - payload.length() % 4) % 4instead, and add an unpadded secret test for non-multiple-of-4 lengths. -
[P2] Revocation stale handling does not match the result API.
CachingRevocationChecker.checkreturnsRevocationResult, and the class docs say stale lists returnRevocationResult.Stale, butensureFresh()throwsRevocationListStaleExceptionfrom line 132. Callers using the sealed result API will not see the advertisedStaleresult. Please either returnRevocationResult.Stalefromcheckor change the API/docs/tests to make stale an exception path consistently.
Local verification run:
./gradlew :adcp-server:test :adcp-signing-aws-kms:test :adcp-signing-gcp-kms:test
BUILD SUCCESSFUL in 2m 57s
… empty body digest, URI rotation, padding, stale API
|
The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final This is an automated message from the Argus AI review workflow. |
bokelley
left a comment
There was a problem hiding this comment.
Findings:
- P1: malformed Signature-Input can be parsed and verified against a synthesized value.
Rfc9421VerifierrebuildsSignature-Inputfor the signature base instead of preserving the parsedsig1item semantics, and the parser accepts bare string params and silently picks the first duplicatesig1. This misses committed negative vectors like unquotedkeyidand duplicate labels.
References:
adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/Rfc9421Verifier.java:139adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/Rfc9421Verifier.java:206adcp-server/src/test/resources/compliance/request-signing/negative/024-unquoted-string-param.json:10adcp-server/src/test/resources/compliance/request-signing/negative/021-duplicate-signature-input-label.json:10
- P1: JWKS key parsing does not validate JWK
alg/kty/crvagainst the signature algorithm.
JwkParser.parse()validatesadcp_useandkey_ops, but ignores the JWKalgand has no access to the inboundSignature-Inputalg. The repo includes a negative vector expectingrequest_signature_key_purpose_invalidfor this exact mismatch, but the current path would either parse the wrong key shape or fail later with the wrong family/code.
References:
adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/jwks/JwkParser.java:40adcp-server/src/test/resources/compliance/request-signing/negative/025-jwk-alg-crv-mismatch.json:22
- P2: JWKS errors are hard-coded as
webhook_signature_*even for request signing.
JwkParserthrowswebhook_signature_invalid/webhook_signature_key_purpose_invalid, and resolvers pass that through unchanged forVerificationInput.expectedUse() == REQUEST_SIGNING. This makes request-signing conformance failures report the wrong error taxonomy.
References:
adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/jwks/JwkParser.java:54adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/jwks/CachingJwksResolver.java:143adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/jwks/StaticJwksResolver.java:41
- P2: KMS providers can emit algorithms this SDK rejects.
AWS/GCP map P-384 KMS keys toecdsa-p384-sha384, butAdcpSignatureProfile.ALLOWED_ALGORITHMSonly includes Ed25519 and P-256, andRfc9421Verifierrejects anything else. A P-384-configured provider can sign messages that this verifier reports asalg_not_allowed.
References:
adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/AdcpSignatureProfile.java:23adcp-signing-aws-kms/src/main/java/org/adcontextprotocol/adcp/signing/aws/AwsKmsSigningProvider.java:237adcp-signing-gcp-kms/src/main/java/org/adcontextprotocol/adcp/signing/gcp/GcpKmsSigningProvider.java:250
- P2: AWS/GCP KMS webhook signing fails for empty bodies unless callers pre-add
content-digest.
Both providers covercontent-digestforWEBHOOK_SIGNING, but only add it whenbody.length > 0. Empty webhook bodies should still sign the SHA-256 digest ofbyte[0], asInProcessSigningProviderdoes.
References:
adcp-signing-aws-kms/src/main/java/org/adcontextprotocol/adcp/signing/aws/AwsKmsSigningProvider.java:99adcp-signing-gcp-kms/src/main/java/org/adcontextprotocol/adcp/signing/gcp/GcpKmsSigningProvider.java:99
- P2: GCP KMS signs all algorithms via
setData.
The provider sends raw data for Ed25519 and ECDSA alike. Google Cloud KMS documents Ed25519 as raw-data input, while P-256/P-384 signing algorithms are digest-based; Google’s Java sample computes a SHA-256 digest and passesDigesttoasymmetricSign. The provider should branch by algorithm.
Reference:
adcp-signing-gcp-kms/src/main/java/org/adcontextprotocol/adcp/signing/gcp/GcpKmsSigningProvider.java:230
Verification:
./gradlew testpassed in.context/pr32-worktree(BUILD SUCCESSFUL).
|
The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final This is an automated message from the Argus AI review workflow. |
…erns Replace legacy v1 .gitguardian.yml (paths-ignore/matches-ignore) with documented v2 .gitguardian.yaml (version: 2 + secret.ignored_paths), which is what ggshield actually loads (docs.gitguardian.com/ggshield-docs/configuration). Drop the **/*hmac* pattern: no fixture is named *hmac* today, so the rule is a no-op; keeping it would silently exclude unrelated future files whose names happen to contain 'hmac'. Scope ignores to the exact public test-vector key files instead of the compliance/** glob. Note in the file that this configures ggshield (local/CI CLI scans), not the 'GitGuardian Security Checks' GitHub App check-run, which is driven by dashboard filepath exclusion rules and is unaffected by any repo-local config. Supersedes PR #33, which added a duplicate .gitguardian JSON file with the same v1 patterns.
|
Updated the ggshield config in this PR (commit 4d09593). What changed
Heads up: this config does not suppress the failing The config file now documents this and mirrors the two exact paths. To make the check green, a workspace admin needs to add those same paths under GitGuardian dashboard → Secrets Detection → Exclusion rules → Filepath. Supersedes #33 (closed as duplicate; it added a second, divergent |
|
The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final This is an automated message from the Argus AI review workflow. |
…K alg validation, KMS digest branching - Rfc9421Verifier: reject duplicate sig1 labels (RFC 8941 dict duplicate-key) and unquoted string sig-params (RFC 8941 §3.3); validate content-digest for empty-body webhooks - JwkParser: validate JWK alg/kty/crv consistency per RFC 8037 before key-material parsing; use request_signature_* taxonomy for request signing - AdcpSignatureProfile: add ecdsa-p384-sha384 to ALLOWED_ALGORITHMS - AdcpUse.fromWireName: accept long-form wire names (request-signing, webhook-signing) from published conformance vectors - AwsKms/GcpKmsSigningProvider: emit content-digest for empty webhook bodies - GcpKmsSigningProvider: branch by algorithm — setData for Ed25519, setDigest (SHA-256/384) for ECDSA per GCP KMS docs - Add RequestSigningConformanceTest wiring negative vectors 021/024/025 - Add GCP KMS tests asserting setData vs setDigest branching
|
All 6 findings addressed in 6ea9036. F1 (P1) — Parser differential: F2 (P1) — JWK alg/kty/crv: F3 (P2) — Error taxonomy: F4 (P2) — P-384 allowlist: F5 (P2) — Empty webhook body digest: AWS/GCP KMS providers now emit F6 (P2) — GCP KMS digest branching: Additional: |
|
The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final This is an automated message from the Argus AI review workflow. |
Changesets CLI does not recognize Gradle module names as npm workspace packages. Follow the existing pattern (block-agent-pr-title-prefixes.md) with empty frontmatter — the changeset is release-notes documentation only.
|
The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final This is an automated message from the Argus AI review workflow. |
bokelley
left a comment
There was a problem hiding this comment.
Thanks for the follow-up fixes. I reviewed the latest head (fcecf25) across the signing providers, verifier/canonicalizer, JWKS/brand.json resolution, webhook helpers, revocation, and the new CLI. The focused signing/KMS test suite passes locally, but I found a few remaining issues.
Findings:
- [P1]
signing-probecannot call the KMS provider probe methods.
SigningProbeCommandusesbuilt.getClass().getMethod("initialize")andgetMethod("getAlgorithms"), which only find public methods. Both AWS and GCP providers declare those methods package-private, and the CLI is in a different package, so every real probe reportsNoSuchMethodExceptionfor signing/purpose even when the provider and key are valid. Either make the probe methods public, avoid reflection from the CLI, or reflect declared methods with accessibility set intentionally.
References:
adcp-cli/src/main/java/org/adcontextprotocol/adcp/cli/SigningProbeCommand.java:131adcp-cli/src/main/java/org/adcontextprotocol/adcp/cli/SigningProbeCommand.java:145adcp-signing-aws-kms/src/main/java/org/adcontextprotocol/adcp/signing/aws/AwsKmsSigningProvider.java:211adcp-signing-aws-kms/src/main/java/org/adcontextprotocol/adcp/signing/aws/AwsKmsSigningProvider.java:219adcp-signing-gcp-kms/src/main/java/org/adcontextprotocol/adcp/signing/gcp/GcpKmsSigningProvider.java:221adcp-signing-gcp-kms/src/main/java/org/adcontextprotocol/adcp/signing/gcp/GcpKmsSigningProvider.java:229
- [P2] Low-level
Rfc9421Signerstill breaks empty webhook bodies and P-384.
The provider implementations were updated to addContent-Digestfor empty webhook bodies, but the public/staticRfc9421Signer.sign(...)helper still only inserts the digest whenbody.length > 0. WithWEBHOOK_SIGNING, the covered component list always includescontent-digest, so empty-body signing through this API fails during canonicalization. The same helper also rejectsecdsa-p384-sha384even though the profile and verifier now allow it. Please keep this helper aligned with the provider-backed path or remove it from the supported surface.
References:
adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/Rfc9421Signer.java:57adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/Rfc9421Signer.java:74adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/Rfc9421Signer.java:103adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/Rfc9421Signer.java:108
- [P2]
CachingRevocationChecker.checkstill throws fetch failures through the result API.
RevocationResultincludesFetchFailed, andcheck(kid)documents/returns sealed results, but it only catchesRevocationListStaleException. Cold-cache network failures, non-200 responses, empty bodies, and invalid JWS all throwRevocationFetchExceptionout ofcheckinstead of returningRevocationResult.FetchFailed. Callers using the sealed API will miss the advertised failure state.
References:
adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/revocation/CachingRevocationChecker.java:83adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/revocation/CachingRevocationChecker.java:86adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/revocation/CachingRevocationChecker.java:163adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/revocation/CachingRevocationChecker.java:187adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/revocation/CachingRevocationChecker.java:193adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/revocation/CachingRevocationChecker.java:198adcp-server/src/main/java/org/adcontextprotocol/adcp/server/signing/revocation/RevocationResult.java:43
Local verification:
./gradlew :adcp-server:test :adcp-signing-aws-kms:test :adcp-signing-gcp-kms:test :adcp-cli:test
BUILD SUCCESSFUL in 54s
…evocation fetch-failed result - AwsKms/GcpKmsSigningProvider: make initialize() and getAlgorithms() public so SigningProbeCommand reflection can invoke them across packages - Rfc9421Signer: emit content-digest for empty webhook bodies; accept ecdsa-p384-sha384 (align with AdcpSignatureProfile.ALLOWED_ALGORITHMS) - CachingRevocationChecker.check: catch RevocationFetchException and return RevocationResult.FetchFailed instead of throwing through the sealed API - Add Rfc9421SignerTest covering empty webhook body, empty request body, P-384 acceptance, and unsupported-algorithm rejection - Add CachingRevocationCheckerTest.check_fetchFailure_returnsFetchFailedNotException
|
The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final This is an automated message from the Argus AI review workflow. |
|
All 3 round-3 findings addressed in 3f47af0. F1 (P1) — signing-probe reflection: F2 (P2) — Rfc9421Signer empty body + P-384: The static F3 (P2) — CachingRevocationChecker fetch failures: Tests: Added |
bokelley
left a comment
There was a problem hiding this comment.
Thanks for the CI context. I re-checked latest head 3f47af0 and confirmed the new commit addresses the three issues from my previous review:
- AWS/GCP KMS probe methods are now public, so
SigningProbeCommandcan invoke them across packages. Rfc9421Signernow emitsContent-Digestfor empty webhook bodies and acceptsecdsa-p384-sha384.CachingRevocationChecker.check(...)now returnsRevocationResult.FetchFailedfor fetch failures instead of throwing through the sealed result API.
Local verification on 3f47af0:
./gradlew :adcp-server:test :adcp-signing-aws-kms:test :adcp-signing-gcp-kms:test :adcp-cli:test
BUILD SUCCESSFUL in 9s
I also checked the PR status rollup: the latest head currently shows Argus/IPR and GitGuardian, but not the pull_request-triggered build/commitlint/changeset/storyboard checks. That matches your note; those still need maintainer approval or a re-trigger in Actions, but my requested code changes are resolved.
Summary
Track 4 (L1 Signing) — complete RFC 9421 message signing and verification for the AdCP Java SDK.
Core SPI (
adcpmodule)SigningProvider/VerificationKeyResolver— SPI interfaces viaMETA-INF/services/SigningContext— tenant-aware key selection (D22):AdcpUse use(),@Nullable TenantId tenant(),@Nullable PrincipalRef principal()AdcpUseenum —REQUEST_SIGNING,WEBHOOK_SIGNINGwith wire name mappingVerificationKeyLookup— sealed interface:Found(key, tenant, principal)/Missing(kid)VerificationResult— sealed interface:Valid/Invalid(errorCode, reason, failedStep)VerificationException— typed errors matchingwebhook_signature_*taxonomyRFC 9421 Canonicalizer (
adcp-servermodule)org.tomitribe:http-signaturesdependency)Rfc9421Signer— producesSignature-Input,Signature,Content-DigestheadersRfc9421Verifier— full 13-step verification checklistAdcpSignatureProfile— required components, algorithms, tags, replay window enforcementHeaderNormalizer— RFC 9421 header name/value normalizationContentDigest— RFC 9530 SHA-256/512 content digestSigning Providers
InProcessSigningProvider— JCA Ed25519, ES256, ES384 (no Bouncy Castle in core)InProcessKeyGenerator— Ed25519/ES256/ES384 keypair generationAwsKmsSigningProvider— AWS KMS with lazy-init, tripwire verification, ECDSA DER-to-raw conversionGcpKmsSigningProvider— GCP KMS with lazy-init, tripwire, service account credentialsBouncyCastleSigningProvider— stub (FIPS environments only,UnsupportedOperationException)Verification & Key Resolution
JwksVerificationKeyResolver— SPI entry pointStaticJwksResolver— in-memory from pre-loaded JWKSCachingJwksResolver— HTTP-fetching with 30s cooldown, single-flight dedupBrandJsonJwksResolver— 3-hop resolution (brand.json → agent → jwks_uri)SsrfJwksUriValidator— HTTPS enforcement, private IP rejection, DNS rebinding defenseJwkParser— Ed25519, EC (P-256/P-384), RSA JWK parsing withadcp_useandkey_opsvalidationWebhook Signing
WebhookSignerseam interface for Track 6 (async-l3)DefaultWebhookSigner— implementation usingRfc9421SignerLegacyHmacWebhookVerifier— deprecated HMAC-SHA256 (removed in AdCP 4.0), timing-safe comparison, rotation supportStandardWebhooksVerifier— Svix/Resend v1 interopWebhookChallenge+WebhookChallengeVerifier— endpoint proof-of-controlKey Management
InMemoryReplayStore— nonce dedup with 300s TTL evictionInMemoryRevocationStore— revoked kid tracking with staleness detectionCachingRevocationChecker— JWS-verified revocation list fetcher with grace windowSigningKeyGenerator— Ed25519/ES256/ES384 keypair generation with JWK outputKeyOriginConsistencyCheck— ADCP #3690 eTLD+1 key origin verificationETldPlusOne— public suffix extraction utilityJWS Verification
JwsVerifier— compact and JSON general JWS verificationJwsDocument— parsed JWS recordCLI
signing-probesubcommand — pre-deploy KMS connectivity and key usability checkModules
adcp— core SPI interfacesadcp-server— canonicalizer, providers, verification, webhookadcp-signing-aws-kms— AWS KMS provider (real implementation)adcp-signing-gcp-kms— GCP KMS provider (real implementation)adcp-signing-bouncycastle— BC FIPS provider (stub)adcp-cli— signing-probe commandTests
226+ tests across all signing modules, including full conformance coverage against AdCP webhook signing and request signing test vectors.
Design Decisions
org.tomitribe:http-signatures) — Cavage draft ≠ RFC 9421SigningContext-based API (D22) — no single-argforUse(); tenant and principal nullable until v0.3Known Limitations
AccountStore→SigningContexttenant resolution wiring deferred to Track 5 (v0.3)ReplayStoredeferred to Track 6 (async-l3)adcp-keygenCLI wrapper not yet added (trivial, can land anytime)