Repository navigation
Report which check refused a device-signature request - #407
Merged
Merged
Conversation
#353's four tiers are merged, but they were scoped before this crate's verification path was looked at closely, and it has the same shape tier 1 found in DPoP: the adapters record *that* verification failed, and nothing records which of nineteen checks did it. Not all of them were silent, and the distinction matters. The issuer, device-status and key-binding rejections each already had an event, and the binding one says something a boundary log cannot — that the failure is the specific forgery the check exists to catch — so those stay. What had nothing was every rejection in signature.rs from the request- signature check onward: bad signature, expired, lifetime too long, and the method, path, audience, query and body mismatches. Eight of the nineteen, and a caller sees one flattened VerifyError that the adapters map onto a single 401, so for those eight the reason existed nowhere at all. Reported once at the `verify` boundary rather than at eight new call sites, following `verify_dpop_proof`'s pattern exactly, including the split into an `_inner`. Doing it there also means the set cannot drift: a check added later is reported without anyone remembering to report it. The field is `VerifyError::code()`, whose own doc already offered it as "suitable for metrics labels and log fields" and which nothing was using. The replay pair keeps its own events despite the boundary, because each carries something it cannot see — the jti that was replayed, and the store's own error, which flattens into a plain ReplayDetected. Without that second one a fail-closed outage would be indistinguishable in the logs from a genuine replay, and they are different kinds of event: a client being refused versus this service degrading. Neither credential is logged, and a test asserts it rather than trusting the review: a rejection that echoed the signature or the attestation would put a live proof, or a reusable bearer-ish attestation, into whatever collects the logs. The other two tests assert the reason reaches the logs at all, and that two different rejections are distinguishable from them alone — which over HTTP they are not, both being 401. `tests/support` gains `#![allow(dead_code)]`: dead code is per target, so a second target using half the fixtures turns the other half into errors under `-D warnings`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XaoYHiT9Vb85XhBdVCg9Xu
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Merged
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.
Picks up #353. Its four tiers are all merged, but they were scoped before
authkestra-devsig's verification path was read closely — and it has the same shape tier 1 found in DPoP: the adapters record that verification failed, and nothing records which of nineteen checks did it.What was actually silent
I measured this twice, because the first answer was wrong. A crude scan suggested most rejections were unlogged and that
KeyNotBoundwas the flagship case. It isn't —KeyNotBoundalready has a good event, and so do the issuer and device-status rejections. Those stay, and the binding one says something a boundary log cannot: that the failure is the specific forgery the check exists to catch.What genuinely had nothing is every rejection in
signature.rsfrom the request-signature check onward:Eight of the nineteen. A caller sees one flattened
VerifyErrorand the adapters map the whole family onto a single 401, so for those eight the reason existed nowhere at all — debugging meant reading the source and guessing, which is exactly what #353 says about DPoP.The fix
One report at the
verifyboundary, followingverify_dpop_proof's pattern including the split into an_inner. Eight new call sites would have worked too; the boundary is better because the set cannot drift — a check added later is reported without anyone remembering to report it.The field is
VerifyError::code(), whose own doc already offered it as "suitable for metrics labels and log fields" and which nothing was using.The replay pair keeps its own events despite the boundary, because each carries something it cannot see: the
jtithat was replayed, and the store's own error — which flattens into a plainReplayDetected. Without that second one, a fail-closed outage would be indistinguishable in the logs from a genuine replay, and they are different kinds of event: a client being refused versus this service degrading.Tests
Three, in a new
tests/rejection_logging.rs:a_request_binding_rejection_names_the_check_that_faileddifferent_rejections_are_told_aparta_rejection_never_logs_the_credentialsThat last one is asserted rather than left to review. #353 notes no existing instrumentation leaks credential material and that any new work must hold that line; a rejection echoing either JWS would put a live proof, or a reusable attestation, into whatever collects the logs.
tests/supportgains#![allow(dead_code)]— dead code is checked per target, so a second target using half the fixtures turns the other half into errors under-D warnings.What's left in #353
The tiers are done and the crates that landed after it was written are the remaining gap. Current measurement, for whoever picks it up:
authkestra-ssfauthkestra-policyInvalidRequestreturns are silentauthkestra-resourceinfo, thin spans — tier 3 improved it but did not finishWorth noting #353 itself should probably be closed and re-filed against those, rather than staying open indefinitely against a table that no longer describes the code.
Checks
cargo fmt --all -- --checkcargo clippy --workspace --all-features --all-targets -- -D warningscargo test --workspace --all-features— exit 0, 75 suitescargo deny check—advisories ok, bans ok, licenses ok, sources ok🤖 Generated with Claude Code
https://claude.ai/code/session_01XaoYHiT9Vb85XhBdVCg9Xu