Make the x509_alias_fallback certificate overlap work in both directions - #612
Open
JakeHuneau wants to merge 1 commit into
Open
JakeHuneau wants to merge 1 commit into
JakeHuneau wants to merge 1 commit into
Conversation
JakeHuneau
force-pushed
the
feature/outbound-cert-fallback
branch
from
September 16, 2026 10:30
1924cdd to
14ccc4f
Compare
The fallback certificate was only consulted when receiving: decrypting an inbound message and verifying an inbound signature. Three of the six places a certificate is resolved had no fallback, so an overlap only half worked. A partner rotating their certificate still broke sending, and rotating our own broke the MDN we return for a message we had otherwise accepted. Receiving can fall back locally because the operation throws when the wrong certificate is used. Sending cannot: signing and encrypting succeed locally whichever certificate is chosen, and the only signal that the wrong one was used is the partner reporting it in the MDN disposition. The three gaps are therefore closed in three different ways. Verifying the MDN a partner returns now falls back locally, the same way inbound verification already did. Both the synchronous and asynchronous MDN paths go through this one place. The MDN returned for an inbound message is now signed with whichever of our certificates actually decrypted it, rather than always the primary. The decrypt step already recorded that alias and nothing read it. A partner still holding our previous certificate can now verify the MDN for a message we accepted with it. Where the inbound message was not encrypted there is nothing to indicate which certificate they hold, so the primary is still used. Sending maps the partner's rejection back to the side whose certificate is implicated and flags the message so its resend uses that side's fallback: "decryption-failed" means they could not decrypt, so we encrypted to a certificate of theirs they no longer hold; "authentication-failed" or "integrity-check-failed" means they could not verify our signature, so we signed with one they do not have yet. This costs one rejected round trip per message until the primary is switched over, which is the price of there being no local signal. The side and its flag are paired in one place so the code that decides to switch and the code that acts on it cannot drift apart. Some implementations report a MIC mismatch as "integrity-check-failed" rather than a signature problem, so the switch only happens when a fallback is actually configured for that side, and the fallback is tried once. If it is rejected as well the certificate was not the problem, so the message goes back to the primary alias for whatever retries remain rather than spending all of them on a certificate the partner has also refused, and it is never switched again. A message failing for any other reason behaves exactly as it did before. A partnership that requests no MDN gets no fallback when sending, because there is no response to act on. The state is carried explicitly onto the message a resend restores from disk. The stored copy is written on the first send only, so it predates any fallback decision: without carrying it forward a resend would quietly revert to the primary certificate while the logging said it was using the fallback. Where the fallback cannot verify a returned MDN either, or is not in the keystore at all, the original verification failure is reported rather than the fallback's. The primary is the configured certificate so its error is the meaningful diagnostic, and an unusable fallback alias is logged separately as the configuration problem it is. Reporting is the part an operator acts on, so all six paths now report the same two messages and a single log search finds every overlap still in progress. Sending cannot report at the time it signs or encrypts because the certificate is not known to be correct until the partner accepts the message, so it logs the attempt at info and the confirmation at warn once the MDN comes back clean. Both inbound messages also gained the message ID that the rest of the logging carries, and the one reporting that our own certificate was switched now names the partner to coordinate with: on an inbound partnership the receiver is this server, so it was reporting our own name. The HowTo already documented that message as naming the partner, so this aligns the code with what was always described. Documentation is updated in both places. The HowTo section "Overlapping Old And New Certificates When Changing" stated that the mechanism only worked when receiving and that a sending switchover had to be synchronised with the partner, which is no longer true, so it now describes how each direction behaves, the round trip that sending costs, the no-MDN limitation and what the log reports. The attribute was also undocumented in partnerships.xml despite being in the HowTo, so the rollover procedure is now described where it is configured. The shipped PDF is regenerated from the updated ODT. It was exported with LibreOffice 26.8.0.3 on x86_64 where the previous one came from 26.2.5.2 on AARCH64, so line and page breaks shift slightly throughout even though the page count is unchanged at 81 and the text is identical apart from this section. A word by word comparison of the two renders accounts for every difference as either the sentence deliberately removed here or a long token wrapping at a different point, so nothing else in the manual changed.
JakeHuneau
force-pushed
the
feature/outbound-cert-fallback
branch
from
September 16, 2026 10:51
14ccc4f to
68bd52c
Compare
Contributor
|
I will only be able to get to your PR's early next week. |
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.
x509_alias_fallbacklets an old and a new certificate overlap while one side rotates, but it was only consulted when receiving. Three of the six places a certificate is resolved had no fallback, so an overlap only half worked: a partner rotating their certificate still broke sending, and rotating your own broke the MDN you return for a message you had otherwise accepted.The six places a certificate is resolved
AS2ReceiverHandler:344AS2ReceiverHandler:399AS2ReceiverHandler:650AS2Util:540AS2SenderModule:356AS2SenderModule:394The three gaps are closed in three different ways, because the situations are not symmetric.
Receiving can fall back in place, sending cannot
Decrypting and verifying throw locally when the wrong certificate is used, so the existing paths catch and retry. Verifying the MDN a partner returns is the same shape, so it now does the same. Both the synchronous and asynchronous MDN paths go through that one place. There is no cost and nothing changes on the wire.
Signing and encrypting always succeed locally whichever certificate is used, so there is nothing to catch. The only signal is the partner reporting it in the MDN disposition, so a rejection is mapped back to the side whose certificate is implicated and the message is flagged so its resend uses that side's fallback:
decryption-failedmeans the partner could not decrypt, so we encrypted to a certificate of theirs they no longer hold, and the receiver fallback is usedauthentication-failedorintegrity-check-failedmeans they could not verify our signature, so we signed with one they do not have yet, and the sender fallback is usedThis costs one rejected round trip per message until the primary alias is switched over, which is the price of there being no local signal.
The MDN we return for an inbound message is now signed with whichever of our certificates actually decrypted it. The decrypt step already recorded that alias and nothing read it. A partner still holding our previous certificate can now verify the MDN for a message we accepted with it. Where the inbound message was not encrypted there is nothing to indicate which certificate they hold, so the primary is used.
Safety
integrity-check-failedis reported by some implementations for a MIC mismatch rather than a signature problem. The fallback is therefore tried once: if it is rejected as well the certificate was not the problem, so the message goes back to the primary for any remaining retries and is never switched again. The two aliases cannot ping-pong.Logging
Reporting is what an operator acts on, so all paths now emit the same two messages, hoisted to constants with the wording unchanged, and a single search for
Switch the fallback aliasfinds every overlap still in progress.Sending cannot report at the time it signs or encrypts, because the certificate is not known to be right until the partner accepts the message. It therefore logs the attempt at info and the confirmation at warn once the MDN comes back clean.
Two small corrections to the existing inbound logging came with this. Both messages gained the message ID that the rest of the logging carries, and the one reporting that our own certificate was switched now names the partner to coordinate with: on an inbound partnership the receiver is this server, so it was reporting our own name. The HowTo already documented that message as naming the partner, so this aligns the code with what was always described.
Documentation
The HowTo section "Overlapping Old And New Certificates When Changing" stated that the mechanism only worked when receiving and that a sending switchover had to be synchronised with the partner, which is no longer true. It now describes how each direction behaves, the round trip that sending costs, the no-MDN limitation and what the log reports. The attribute was also undocumented in partnerships.xml despite being in the HowTo, so the rollover procedure is now described where it is configured.
The shipped PDF is regenerated from the updated ODT. It was exported with LibreOffice 26.8.0.3 on x86_64 where the previous one came from 26.2.5.2 on AARCH64, so line and page breaks shift slightly throughout even though the page count is unchanged at 81. A word by word comparison of the two renders accounts for every difference as either the sentence deliberately removed here or a long token wrapping at a different point, so nothing else in the manual changed. Happy to drop the PDF from this branch and leave it for a release export if you would rather it came off your own toolchain.
Testing
194 tests pass, 21 new. Mutation checks: cross wiring the side to flag pairing fails 5, removing the revert fails 1, swapping which side each disposition maps to fails 3.
Two gaps I would rather state than paper over. The
resendcall site that carries the fallback state forward has no test, because reaching it needs a session, a resender module and pendinginfo files on disk; the state machine itself is covered directly. The two local try/catch fallbacks also have no dedicated test, as that needs a validly signedmultipart/reportMDN built with a non-primary key. Both are worth adding and I am happy to do so if you want them in before this merges.