Conversation
doHTTPRequest pushes the Replay-Nonce of every response onto c.nonces,
including the newNonce response whose header Client.nonce() returns
directly. One nonce therefore gets two owners: the caller signs a JWS with
it, and the next nonce() call pops the same value from the pool. The
server rejects the second use with badNonce, which costs a round trip
through httpPostJWS's retry loop.
Measured with three sequential nonce() calls against a test server that
tags each nonce:
before: handed out [nonce1 nonce1 nonce2], 2 newNonce requests,
"nonce2" also left in the pool
after: handed out [nonce1 nonce2 nonce3], 3 newNonce requests,
pool empty
Skip the push for a newNonce request. Nonces from every other response are
still remembered, which is what §6.5 and §7.2 are about: those are nonces
the client is not consuming at the time. x/crypto/acme draws the same
line, its fetchNonce never calls addNonce.
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.
The bug
doHTTPRequestpushes theReplay-Nonceof every response ontoc.nonces(acme/http.go:294). That includes theHEAD newNonceresponse, whose headerClient.nonce()then returns to its caller (acme/client.go:151). One nonce ends up with two owners: the caller signs a JWS with it, and the nextnonce()call pops the same value out of the pool. The server rejects the second use withbadNonce.Measured with three sequential
nonce()calls against a test server that tags each nonce:[nonce1 nonce1 nonce2]nonce2(also handed out)[nonce1 nonce2 nonce3]Impact is bounded, which is why I am not filing this as a security issue:
nonce()has one caller, insidehttpPostJWS's retry loop, and thebadNoncebranch there retries with the fresh nonce from the error response. So the cost is a wasted round trip per duplicate, and the LIFO pool hides it in the common sequential case — the duplicate sinks to the bottom of the stack. It surfaces when the pool drains (two requests in a row both popping the same stale value) and under concurrent use of one client, which thestacktype is explicitly built for ("a simple thread-safe stack").The fix
Skip the push when the request is the
newNonceHEAD. Nonces from every other response are still remembered, which is what §6.5 and §7.2 ask for: those are nonces the client is not consuming at the time.x/crypto/acme, the ancestor of this file, draws the same line — itsfetchNoncedoes the HEAD and returns the header without ever callingaddNonce, andaddNonceis called only from the directory GET and from POST responses.I chose this over having
nonce()pop the value it just pushed: with a concurrent caller the pool can be empty at that moment, andnonce()has no retry (httpPostJWSreturns its error straight to the caller), so that shape turns a harmless duplicate into a failed request.Tests
New
acme/nonce_test.go:TestNonceIsHandedOutOnce— threenonce()calls must return three distinct nonces, hit the endpoint three times, and leave the pool empty.TestNonceFromOtherResponsesIsRemembered— a POST response's nonce is still pooled and served to the nextnonce()call, so the fix does not stop the client remembering nonces generally.go test ./...passes on this branch. Reverting onlyacme/http.gofails the first test with the numbers in the table above, and the second test passes either way.One thing I left alone
nonce()returnsresp.Header.Get(replayNonce)without checking for an empty value, so a server that omits the header (a MUST in §6.5) gets a JWS with an empty nonce and abadNonceretry instead of a clear error.x/crypto/acmeerrors there ("acme: nonce not found"). Happy to add that here or in a separate PR, but it changes behaviour for a spec-violating server, so I kept it out of this one.