You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This is an issue I have asked Claude to put together as a result of its audit of Boulder against the RFCs Boulder implements.
Checklist of cases where Boulder emits a field the RFC defines but its
meaning, status code, or population logic diverges from the spec.
Distinct from the "non-standard fields to remove" and "missing fields to
add" lists — every item here is a field Boulder already implements and
already sends on the wire, but with the wrong semantics.
unauthorized problem returns HTTP 403 instead of 401 for
deactivated accounts. RFC 8555 §7.3.6: "If a server receives a
POST or POST-as-GET from a deactivated account, it MUST return an
error response with status code 401 (Unauthorized) and type urn:ietf:params:acme:error:unauthorized." Boulder's Unauthorized helper hardcodes http.StatusForbidden, and the JWS
verification layer raises berrors.UnauthorizedError for
deactivated accounts, which the web layer maps to HTTP 403. The
type URN is correct; the status code is not. Deactivated-account
rejections are the specific case the RFC calls out as MUST-401. probs.Unauthorized
· wfe2/verify.go deactivated-account check
· web/probs.go berrors→HTTP mapping
NotFound helper emits malformed problem type on 404
responses. RFC 8555 §6.7 defines malformed as "The request
message was malformed" — a 400-class validation error. Boulder's probs.NotFound builds a ProblemDetails with Type: MalformedProblem and HTTPStatus: 404. Every 404 raised
from WFE2 (finalize, certificate download, authorization lookup,
order lookup, revocation) carries a type URN that tells the
client "your request was syntactically bad" when the actual
condition is "that resource does not exist". probs.NotFound
prepChallengeForDisplay marks every challenge invalid when
the authz is invalid. RFC 8555 §7.1.4: "For invalid
authorizations, the challenge that was attempted and failed." The
RFC models a single failing challenge driving the authz to invalid. Boulder instead overwrites the status of every
challenge on the authz to invalid, including challenges the
client never attempted. The error field is still only populated
on the actually-attempted challenge, so a client cannot tell from status alone which challenge failed, and the status/error fields
become inconsistent across the challenge array. prepChallengeForDisplay invalid broadcast
order.expires serializes zero-valued time when unset. RFC
8555 §7.1.3: "expires (optional, string): … This field is
REQUIRED for objects with 'pending' or 'valid' in the status
field." Optional fields that are unset should be absent from the
response, not serialized as a placeholder. Boulder's orderJSON.Expires is a bare time.Time with no omitempty and
no pointer indirection, so any code path producing an order
without an expiry emits "expires": "0001-01-01T00:00:00Z",
claiming the order expired in year 1 AD. In practice every
Boulder order has a real expiry; the type permits a state that
would silently publish nonsense. orderJSON.Expires
updateAccount returns 200 OK for contact updates it silently
discards. RFC 8555 §7.3.2 describes the account-update flow: a
client POSTs changed fields and the server returns 200 (OK) with
"the resulting account object". A 200 with the updated object
implies the update was accepted. Boulder's updateAccount
dispatches on status: for deactivated it deactivates, for valid or empty it returns the account unchanged with the
explanatory comment "They probably intended to update their
contact address, but we don't do that anymore, so simply return
their account as-is. We don't error out here because it would
break too many clients." A client comparing the echoed account
against what it sent has no signal that its contact update was
ignored. updateAccount valid branch
KeyRollover 409 uses the non-standard conflict URN. RFC
8555 §7.3.5: "If there is an existing account with the new key
provided, then the server SHOULD use status code 409 (Conflict)
and provide the URL of that account in the Location header
field." The RFC pins the HTTP status and the Location header
but does not define a problem type for this condition, and §6.7
has no registered URN for it. Boulder calls probs.Conflict on
both the pre-check and the post-update-race branch of KeyRollover, emitting urn:ietf:params:acme:error:conflict — a
URN not in the §6.7 registry. The 409 status and Location header
are correct; the URN is semantically a Boulder invention for a
well-defined RFC condition. KeyRollover pre-check
· KeyRollover race branch
· probs.ConflictProblem
RenewalInfo endpoint accepts POST in addition to GET. RFC 9773
§4.1: "the client sends an unauthenticated GET request to a path
under the server's renewalInfo URL." §6 reinforces: "RenewalInfo
resources are exposed and accessed via unauthenticated GET
requests, a departure from the requirement in RFC 8555 that
clients send POST-as-GET requests to fetch resources from the
server." The RFC's request-method contract is singular: GET only,
unauthenticated. Boulder registers the renewalInfo endpoint for
both GET and POST. Accepting POST isn't explicitly forbidden, but
it departs from the stated method contract, and the POST path
bypasses the caching-friendliness rationale §6 gives for making
the endpoint GET-only. RenewalInfo handler registration
Certificate download ignores the Accept header. RFC 8555
§7.4.2: "The ACME client MAY request other formats by including
an Accept header field in its request… Server support for
alternate formats is OPTIONAL." HTTP content negotiation
semantics (RFC 9110) additionally imply that a server which cannot
satisfy a specific Accept value should respond 406 (Not
Acceptable). Boulder's Certificate handler ignores the Accept
header entirely and unconditionally sets Content-Type: application/pem-certificate-chain. A client
sending Accept: application/pkix-cert (or any other value
excluding pem-certificate-chain) receives a 200 with PEM anyway,
with no signal that its stated preference was ignored.
Alternate-format support being optional is fine; silently
overriding the client's explicit Accept value is not the
content-negotiation contract the RFC points at. Certificate handler Content-Type
This is an issue I have asked Claude to put together as a result of its audit of Boulder against the RFCs Boulder implements.
Checklist of cases where Boulder emits a field the RFC defines but its
meaning, status code, or population logic diverges from the spec.
Distinct from the "non-standard fields to remove" and "missing fields to
add" lists — every item here is a field Boulder already implements and
already sends on the wire, but with the wrong semantics.
unauthorizedproblem returns HTTP 403 instead of 401 fordeactivated accounts. RFC 8555 §7.3.6: "If a server receives a
POST or POST-as-GET from a deactivated account, it MUST return an
error response with status code 401 (Unauthorized) and type
urn:ietf:params:acme:error:unauthorized." Boulder'sUnauthorizedhelper hardcodeshttp.StatusForbidden, and the JWSverification layer raises
berrors.UnauthorizedErrorfordeactivated accounts, which the web layer maps to HTTP 403. The
type URN is correct; the status code is not. Deactivated-account
rejections are the specific case the RFC calls out as MUST-401.
probs.Unauthorized·
wfe2/verify.godeactivated-account check·
web/probs.goberrors→HTTP mappingNotFoundhelper emitsmalformedproblem type on 404responses. RFC 8555 §6.7 defines
malformedas "The requestmessage was malformed" — a 400-class validation error. Boulder's
probs.NotFoundbuilds aProblemDetailswithType: MalformedProblemandHTTPStatus: 404. Every 404 raisedfrom WFE2 (finalize, certificate download, authorization lookup,
order lookup, revocation) carries a
typeURN that tells theclient "your request was syntactically bad" when the actual
condition is "that resource does not exist".
probs.NotFoundprepChallengeForDisplaymarks every challengeinvalidwhenthe authz is invalid. RFC 8555 §7.1.4: "For invalid
authorizations, the challenge that was attempted and failed." The
RFC models a single failing challenge driving the authz to
invalid. Boulder instead overwrites the status of everychallenge on the authz to
invalid, including challenges theclient never attempted. The
errorfield is still only populatedon the actually-attempted challenge, so a client cannot tell from
statusalone which challenge failed, and the status/error fieldsbecome inconsistent across the challenge array.
prepChallengeForDisplayinvalid broadcastorder.expiresserializes zero-valued time when unset. RFC8555 §7.1.3: "expires (optional, string): … This field is
REQUIRED for objects with 'pending' or 'valid' in the status
field." Optional fields that are unset should be absent from the
response, not serialized as a placeholder. Boulder's
orderJSON.Expiresis a baretime.Timewith noomitemptyandno pointer indirection, so any code path producing an order
without an expiry emits
"expires": "0001-01-01T00:00:00Z",claiming the order expired in year 1 AD. In practice every
Boulder order has a real expiry; the type permits a state that
would silently publish nonsense.
orderJSON.ExpiresupdateAccountreturns 200 OK for contact updates it silentlydiscards. RFC 8555 §7.3.2 describes the account-update flow: a
client POSTs changed fields and the server returns 200 (OK) with
"the resulting account object". A 200 with the updated object
implies the update was accepted. Boulder's
updateAccountdispatches on
status: fordeactivatedit deactivates, forvalidor empty it returns the account unchanged with theexplanatory comment "They probably intended to update their
contact address, but we don't do that anymore, so simply return
their account as-is. We don't error out here because it would
break too many clients." A client comparing the echoed account
against what it sent has no signal that its contact update was
ignored.
updateAccountvalid branchKeyRollover 409 uses the non-standard
conflictURN. RFC8555 §7.3.5: "If there is an existing account with the new key
provided, then the server SHOULD use status code 409 (Conflict)
and provide the URL of that account in the Location header
field." The RFC pins the HTTP status and the
Locationheaderbut does not define a problem type for this condition, and §6.7
has no registered URN for it. Boulder calls
probs.Conflictonboth the pre-check and the post-update-race branch of
KeyRollover, emittingurn:ietf:params:acme:error:conflict— aURN not in the §6.7 registry. The 409 status and Location header
are correct; the URN is semantically a Boulder invention for a
well-defined RFC condition.
KeyRolloverpre-check·
KeyRolloverrace branch·
probs.ConflictProblemRenewalInfo endpoint accepts POST in addition to GET. RFC 9773
§4.1: "the client sends an unauthenticated GET request to a path
under the server's renewalInfo URL." §6 reinforces: "RenewalInfo
resources are exposed and accessed via unauthenticated GET
requests, a departure from the requirement in RFC 8555 that
clients send POST-as-GET requests to fetch resources from the
server." The RFC's request-method contract is singular: GET only,
unauthenticated. Boulder registers the renewalInfo endpoint for
both GET and POST. Accepting POST isn't explicitly forbidden, but
it departs from the stated method contract, and the POST path
bypasses the caching-friendliness rationale §6 gives for making
the endpoint GET-only.
RenewalInfohandler registrationCertificate download ignores the
Acceptheader. RFC 8555§7.4.2: "The ACME client MAY request other formats by including
an Accept header field in its request… Server support for
alternate formats is OPTIONAL." HTTP content negotiation
semantics (RFC 9110) additionally imply that a server which cannot
satisfy a specific Accept value should respond 406 (Not
Acceptable). Boulder's
Certificatehandler ignores theAcceptheader entirely and unconditionally sets
Content-Type: application/pem-certificate-chain. A clientsending
Accept: application/pkix-cert(or any other valueexcluding pem-certificate-chain) receives a 200 with PEM anyway,
with no signal that its stated preference was ignored.
Alternate-format support being optional is fine; silently
overriding the client's explicit Accept value is not the
content-negotiation contract the RFC points at.
Certificatehandler Content-Type