Skip to content

Make the x509_alias_fallback certificate overlap work in both directions - #612

Open
JakeHuneau wants to merge 1 commit into
OpenAS2:masterfrom
JakeHuneau:feature/outbound-cert-fallback
Open

JakeHuneau wants to merge 1 commit into
OpenAS2:masterfrom
JakeHuneau:feature/outbound-cert-fallback

Conversation

@JakeHuneau

Copy link
Copy Markdown
Contributor

x509_alias_fallback lets 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

Where Operation Whose certificate Before
AS2ReceiverHandler:344 inbound, decrypt payload ours had a fallback
AS2ReceiverHandler:399 inbound, verify their signature theirs had a fallback
AS2ReceiverHandler:650 inbound, sign the MDN we return ours none
AS2Util:540 outbound, verify the MDN they return theirs none
AS2SenderModule:356 outbound, sign payload ours none
AS2SenderModule:394 outbound, encrypt to them theirs none

The 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-failed means the partner could not decrypt, so we encrypted to a certificate of theirs they no longer hold, and the receiver fallback is used
  • authentication-failed or integrity-check-failed means they could not verify our signature, so we signed with one they do not have yet, and the sender fallback is used

This 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

  • A partnership with no fallback configured behaves exactly as before. Every new path is gated on a fallback existing.
  • integrity-check-failed is 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.
  • 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 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, and the unusable alias is logged separately as the configuration problem it is.
  • One retry of the partnership's budget is spent on the fallback probe while a fallback is configured.

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 alias finds 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 resend call 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 signed multipart/report MDN 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.

@JakeHuneau
JakeHuneau force-pushed the feature/outbound-cert-fallback branch from 1924cdd to 14ccc4f Compare September 16, 2026 10:30
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
JakeHuneau force-pushed the feature/outbound-cert-fallback branch from 14ccc4f to 68bd52c Compare September 16, 2026 10:51
@uhurusurfa

Copy link
Copy Markdown
Contributor

I will only be able to get to your PR's early next week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants