Common Login Portal

AVA not a sequence: a Java 17 SAML cert bug

Ashish Bhagat · 13 Aug 2026

The exception was one line:

AVA not a sequence

No stack trace worth reproducing beyond it — just that string, thrown the first time our SAML service provider tried to load the real Ministry-issued identity provider metadata for Common Login Portal (CLP), the identity and single-sign-on layer I built solo for Oman's National Port Community System. Not the port system itself — the layer in front of it that authenticates users and silently signs them into whichever downstream application they're entitled to. Every environment below that point had been running against a Keycloak instance standing in for the government IdP. This was the first time anything in the pipeline touched the ministry's actual certificate.

AVA not a sequence

My first read was that the certificate itself was bad. A wrong export, a truncated file, something dropped incorrectly into the metadata bundle the ministry had sent over. That's the natural reading of a parsing failure phrased like this one — "not a sequence" sounds like you've been handed bytes that don't parse as anything at all.

It wasn't corrupted. It was a normal, valid X.509 certificate — just encoded in a way that wasn't the strict canonical form Java's own parser had started insisting on. That's a common enough situation with government-issued PKI, where the certificate authority's tooling doesn't always produce byte-for-byte textbook ASN.1, and every consumer of that metadata up to that point had accepted it without complaint.

This wasn't a cosmetic failure. The certificate in that metadata document is what the service provider uses to verify the signature on every SAML response the IdP sends back — without it, there's no way to trust that an assertion actually came from the ministry and wasn't forged. CLP does have an OTP email fallback for cases where SAML isn't available, gated behind its own feature flag, but that exists for degraded operation, not as a substitute for PKI-based sign-in. The entire reason to build against a government identity provider in the first place was to authenticate users against ministry-issued credentials, not a one-time code sent to an inbox. Falling back to OTP because the SAML certificate wouldn't parse would have quietly defeated the point of the integration.

What changed in the parser, not the certificate

Java 17 tightened how its bundled X.509 parser validates the ASN.1 structure of a certificate's distinguished names — specifically the attribute-value assertions (AVAs) that make up each relative distinguished name in the subject and issuer fields. A certificate can be entirely valid under the X.509 spec and still fail that stricter check if it was built with an encoder that took a legal but non-canonical shortcut somewhere in that structure.

RFC 5280 defines a Name as a SEQUENCE OF RelativeDistinguishedName, where each RDN is itself a SET OF AttributeTypeAndValue, and each of those AVAs is a two-element SEQUENCE — attribute type, attribute value. "AVA not a sequence" is exactly what it says: the parser walked into that innermost structure expecting a two-element SEQUENCE and found something else — a different tag, different nesting, whatever the ministry's CA tooling had actually produced for that field. Plenty of real-world certificates get this slightly wrong, or at least differently-right, and plenty of parsers have historically shrugged and accepted it anyway. The JDK's stopped shrugging.

That's exactly what was happening here. The certificate wasn't broken. The parser had gotten stricter than the certificate needed it to be, and there was nothing I could do about how the ministry's CA had encoded it — it wasn't ours to change, and it was fine everywhere else it was used.

Parsing it with BouncyCastle instead

Once it was clear the problem was which parser we were using rather than the certificate itself, the fix was to stop asking the JDK's default CertificateFactory to do the parsing and hand it to BouncyCastle's instead. BouncyCastle ships its own ASN.1 decoder and X.509 implementation as a JCE provider, entirely separate from the JDK's sun.security internals, and it's meaningfully more permissive about this kind of encoding variance. It isn't ignoring the spec — it's just not enforcing the same strict-canonical reading the JDK's own implementation had moved to.

This is the shape of the fix — reconstructed for this post, not the literal production code — wired into the metadata resolution path that Spring Security's SAML2 support uses to load the identity provider's signing certificate:

// Illustrative — shows the approach, not the production implementation
Security.addProvider(new BouncyCastleProvider());

CertificateFactory certificateFactory =
    CertificateFactory.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME);

X509Certificate idpSigningCert =
    (X509Certificate) certificateFactory.generateCertificate(metadataCertificateStream);

Same bytes, different parser. The certificate that the JDK's default factory rejected outright loads cleanly through BouncyCastle's, and Spring Security's SAML2 metadata handling doesn't care which CertificateFactory produced the X509Certificate it ends up holding — it just needs a valid one.

Where this landed matters as much as what it does. CLP's backend is built as a hexagonal architecture — domain logic with zero Spring or JPA imports, verified the boring way, by grepping the domain package and getting nothing back. The SAML handler lives in the inbound web adapter, not the domain, which is exactly where a JDK-version-specific parsing workaround belongs. The domain layer never needed to know the certificate parser had a problem, let alone that the fix for it was a different JCE provider. It just needed a valid signing certificate to verify assertions against, and the adapter's job was to make sure it got one.

None of this is really about SAML. It's what happens whenever a runtime you don't control tightens a spec-compliance check that a piece of infrastructure you don't control was quietly relying on being lenient about. Nobody shipped a bug — the ministry's CA produced a valid certificate, and the JDK enforced the spec more strictly than it used to. The fix isn't a workaround bolted onto the calling code that happened to break; it's swapping out the specific component doing the parsing, at the boundary that's supposed to absorb exactly this kind of external-system behavior you don't get to change.

What I'd do differently

The rest of CLP's external dependencies — the labour ministry API, the commercial registry API, the payment gateway — all sat behind mock and real adapter pairs, toggled by Spring profile, specifically so QA and UAT could run full end-to-end flows without the live government systems being available, which they frequently weren't. SAML followed the same pattern: Keycloak underneath in the lower environments, the real government IdP only in the environment that mattered for this. Which meant the one thing that pattern couldn't produce a realistic stand-in for — the actual encoding quirks of a ministry-issued certificate — only showed up the first time we pointed at the real thing.

If I did this again, I'd get a real, or at least realistically malformed, copy of the government's certificate into a lower environment as early as it existed, instead of finding out how it was encoded the same week the integration needed to work.

The fix above also only solves the certificate I had in front of me that day. A hand-parsed certificate loaded once at startup doesn't know what to do the next time the ministry rotates it — and they do rotate it. Making that self-heal instead of paging someone is a separate problem, and a separate post.


More from this series:

Hot-reloading a SAML signing certificate in prod · Hexagonal architecture you can verify with grep