Skip to content

feat(bundle): add marshal methods to X509Bundle and JwtBundle - #509

Merged
maxlambrecht merged 1 commit into
spiffe:mainfrom
NopeCharon:feat/bundle-marshal
Nov 12, 2026
Merged

maxlambrecht merged 1 commit into
spiffe:mainfrom
NopeCharon:feat/bundle-marshal

Conversation

@NopeCharon

Copy link
Copy Markdown
Contributor

What

Adds marshal() to both bundle types, the reverse of their existing parse methods:

  • X509Bundle.marshal() returns the X.509 authorities as PEM CERTIFICATE blocks, wrapped at 64 characters.
  • JwtBundle.marshal() returns the JWT authorities as an RFC 7517 JWKS document in UTF-8 JSON. Each authority becomes one public JWK with its key ID. Keys are ordered by key ID, so the same bundle always gives the same output.
Files.write(Paths.get("/tmp/bundle.pem"), x509Source.getBundleForTrustDomain(td).marshal());
Files.write(Paths.get("/tmp/bundle.jwks"), jwtSource.getBundleForTrustDomain(td).marshal());

Details:

  • JwtBundle.marshal() supports EC keys on a named JWK curve, and RSA keys. These are the key types parse accepts. Any other key type or curve throws JwtBundleException, and the message names the key ID.
  • X509Bundle.marshal() throws X509BundleException if an authority can't be DER-encoded.
  • parse reads back the output of both methods. One exception: a bundle with no X.509 authorities marshals to an empty array, and X509Bundle.parse still rejects that. The Javadoc says so.
  • JwtBundle.parse now decodes its input as UTF-8, not with the platform default charset. JSON is UTF-8. Before this, on Java 8–17 with a non-UTF-8 default charset, non-ASCII key IDs were corrupted and a marshaled bundle didn't round-trip.
  • The core README has a short "Marshal bundles" section, and the changelog lists the methods under Unreleased.

Fixes #508

Why

A bundle from an X509Source or JwtSource couldn't be serialized again. To store it, pass it to another process, or serve it as a JWKS, callers had to write the encoding themselves. For JWT bundles that included the Nimbus curve lookup for EC keys. go-spiffe offers the same operation as Bundle.Marshal() in x509bundle and jwtbundle.

Compatibility: this only adds methods to two final classes. japicmp and the binary-compatibility suite pass.

How tested

  • X509BundleTest:
    • a bundle loaded from certs.pem round-trips through marshal/parse
    • the output has one well-formed PEM block per authority, with no line over 64 characters
    • an empty bundle marshals to an empty array
    • an encoding failure is wrapped in X509BundleException
  • JwtBundleTest:
    • P-256, P-384, P-521 and RSA keys round-trip
    • a bundle parsed from the existing jwks_valid_RSA_EC.json fixture round-trips
    • the JWKS holds public keys only, ordered by key ID, with the right kty/crv
    • a non-ASCII key ID round-trips
    • an empty bundle gives {"keys":[]}
    • an unsupported key type, and an EC key on an unnamed curve, both throw JwtBundleException
  • Commands, on JDK 17: ./gradlew :java-spiffe-core:build :java-spiffe-provider:build binaryCompatibilityTest javadoc.

The non-ASCII key ID test checks the round trip. It can't fail on JDK 18+, where UTF-8 is already the default charset.

@maxlambrecht maxlambrecht left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

X509Bundle and JwtBundle could be parsed from PEM certificates and from
an RFC 7517 JWKS document, but there was no way to serialize them back.
Code that obtains a bundle from an X509Source or a JwtSource and needs
to store it, pass it to another process, or serve it as a JWKS had to
write the encoding by hand, including the JWK curve lookup for EC keys.
go-spiffe exposes the same operation as Bundle.Marshal in its x509bundle
and jwtbundle packages.

Add X509Bundle.marshal(), which writes each X.509 authority as a PEM
CERTIFICATE block wrapped at 64 characters, and JwtBundle.marshal(),
which writes the JWT authorities as a JWKS document in UTF-8 JSON with
one public JWK per authority, carrying its key ID and ordered by key ID
so the output is stable. EC keys on a curve that has a JWK name and RSA
keys are supported, matching the key types parse accepts; any other key
raises JwtBundleException. An authority that cannot be DER-encoded
raises X509BundleException. The output of both methods is read back by
the existing parse methods. A bundle without X.509 authorities marshals
to an empty array, which X509Bundle.parse rejects as before; this is
documented on the method.

JwtBundle.parse decoded its input with the platform default charset.
JSON is UTF-8, so decode it as UTF-8: on Java 8 to 17 with a non-UTF-8
default charset, non-ASCII key IDs were corrupted and a marshaled bundle
would not round-trip.

The change only adds methods to final classes, so it is binary
compatible. The core README documents the new methods and the changelog
lists them under Unreleased.

Fixes spiffe#508

Signed-off-by: NopeCharon <115794254+NopeCharon@users.noreply.github.com>
@maxlambrecht
maxlambrecht merged commit 1f11e44 into spiffe:main Nov 12, 2026
8 checks passed
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.

Add marshal methods to X509Bundle and JwtBundle

2 participants