diff --git a/java-spiffe-core/build.gradle b/java-spiffe-core/build.gradle index 013766d..046e318 100644 --- a/java-spiffe-core/build.gradle +++ b/java-spiffe-core/build.gradle @@ -54,8 +54,12 @@ dependencies { implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: "${nettyVersion}", classifier: 'osx-x86_64' compileOnly group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2' + // library for processing JWT tokens implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.1' implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.1' implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.1' + + // library for processing JOSE JWK bundles + implementation group: 'com.nimbusds', name: 'nimbus-jose-jwt', version: '5.7' } diff --git a/java-spiffe-core/src/main/java/spiffe/bundle/jwtbundle/JwtBundle.java b/java-spiffe-core/src/main/java/spiffe/bundle/jwtbundle/JwtBundle.java index e0468f4..82c95f9 100644 --- a/java-spiffe-core/src/main/java/spiffe/bundle/jwtbundle/JwtBundle.java +++ b/java-spiffe-core/src/main/java/spiffe/bundle/jwtbundle/JwtBundle.java @@ -1,15 +1,24 @@ package spiffe.bundle.jwtbundle; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; import lombok.NonNull; import lombok.Value; -import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.StringUtils; import spiffe.exception.AuthorityNotFoundException; import spiffe.exception.BundleNotFoundException; +import spiffe.exception.JwtBundleException; import spiffe.spiffeid.TrustDomain; +import java.io.IOException; import java.nio.file.Path; +import java.security.KeyException; import java.security.PublicKey; +import java.text.ParseException; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -23,38 +32,45 @@ public class JwtBundle implements JwtBundleSource { Map jwtAuthorities; - public JwtBundle(@NonNull TrustDomain trustDomain, @NonNull Map jwtAuthorities) { - this.trustDomain = trustDomain; - this.jwtAuthorities = new ConcurrentHashMap<>(jwtAuthorities); - } - + /** + * Creates a new JWT bundle for a trust domain. + * + * @param trustDomain a {@link TrustDomain} to associate to the JwtBundle + */ public JwtBundle(@NonNull TrustDomain trustDomain) { this.trustDomain = trustDomain; this.jwtAuthorities = new ConcurrentHashMap<>(); } /** - * Creates a new bundle from JWT public keys. + * Creates a new JWT bundle for a trust domain with JWT Authorities (public keys associated to keyIds). * - * @param trustDomain a {@link TrustDomain} to associate to the JwtBundle - * @param jwtAuthorities a Map of public Keys - * @return a new {@link JwtBundle}. + * @param trustDomain a {@link TrustDomain} to associate to the JwtBundle + * @param jwtAuthorities a Map of public Keys */ - public static JwtBundle fromJWTAuthorities(@NonNull TrustDomain trustDomain, Map jwtAuthorities) { - throw new NotImplementedException("Not implemented"); + public JwtBundle(@NonNull TrustDomain trustDomain, @NonNull Map jwtAuthorities) { + this.trustDomain = trustDomain; + this.jwtAuthorities = new ConcurrentHashMap<>(jwtAuthorities); } /** - * Loads a bundle from a file on disk. + * Loads a bundle from a file on disk. The file must contain a standard RFC 7517 JWKS document. + *

+ * Key Types supported are EC and RSA. * * @param trustDomain a {@link TrustDomain} to associate to the JWT bundle. - * @param bundlePath a path to a file containing the JWT bundle. + * @param bundlePath a path to a file containing the JWT authorities (public keys). * @return a instance of a {@link JwtBundle} + * @throws JwtBundleException if there is an error reading or parsing the file, or if a keyId is empty + * @throws KeyException if the bundle file contains a key type that is not supported */ - public static JwtBundle load( - @NonNull final TrustDomain trustDomain, - @NonNull final Path bundlePath) { - throw new NotImplementedException("Not implemented"); + public static JwtBundle load(@NonNull final TrustDomain trustDomain, @NonNull final Path bundlePath) throws KeyException, JwtBundleException { + try { + JWKSet jwkSet = JWKSet.load(bundlePath.toFile()); + return toJwtBundle(trustDomain, jwkSet); + } catch (IOException | ParseException | JOSEException e) { + throw new JwtBundleException(String.format("Could not load bundle from file: %s", bundlePath.toString()), e); + } } /** @@ -66,8 +82,13 @@ public class JwtBundle implements JwtBundleSource { */ public static JwtBundle parse( @NonNull final TrustDomain trustDomain, - @NonNull final byte[] bundleBytes) { - throw new NotImplementedException("Not implemented"); + @NonNull final byte[] bundleBytes) throws KeyException, JwtBundleException { + try { + JWKSet jwkSet = JWKSet.parse(new String(bundleBytes)); + return toJwtBundle(trustDomain, jwkSet); + } catch (ParseException | JOSEException e) { + throw new JwtBundleException("Could not parse bundle from bytes", e); + } } /** @@ -75,7 +96,6 @@ public class JwtBundle implements JwtBundleSource { * * @param trustDomain a {@link TrustDomain} * @return a {@link JwtBundle} for the trust domain - * * @throws BundleNotFoundException if there is no bundle for the given trust domain */ @Override @@ -86,12 +106,18 @@ public class JwtBundle implements JwtBundleSource { throw new BundleNotFoundException(String.format("No JWT bundle found for trust domain %s", trustDomain)); } + /** + * Returns the JWT authorities in the bundle, keyed by key ID. + */ + public Map getJwtAuthorities() { + return new HashMap<>(jwtAuthorities); + } + /** * Finds the JWT key with the given key id from the bundle. * * @param keyId the Key ID * @return {@link PublicKey} representing the Authority associated to the KeyID. - * * @throws AuthorityNotFoundException if no Authority is found associated to the Key ID */ public PublicKey findJwtAuthority(String keyId) throws AuthorityNotFoundException { @@ -102,11 +128,59 @@ public class JwtBundle implements JwtBundleSource { throw new AuthorityNotFoundException(String.format("No authority found for the trust domain %s and key id %s", this.trustDomain, keyId)); } - public void addJWTAuthority(String keyId, PublicKey jwtAuthority) { + /** + * Returns true if the bundle has a JWT authority with the given key ID. + */ + public boolean hasJwtAuthority(String keyId) { + return jwtAuthorities.containsKey(keyId); + } + + /** + * Adds a JWT authority to the bundle. If a JWT authority already exists + * under the given key ID, it is replaced. A key ID must be specified. + * + * @param keyId Key ID to associate to the jwtAuthority + * @param jwtAuthority a PublicKey + */ + public void addJwtAuthority(@NonNull String keyId, @NonNull PublicKey jwtAuthority) { if (StringUtils.isBlank(keyId)) { throw new IllegalArgumentException("KeyId cannot be empty"); } - jwtAuthorities.put(keyId, jwtAuthority); } + + /** + * Removes the JWT authority identified by the key ID from the bundle. + */ + public void removeJwtAuthority(String keyId) { + jwtAuthorities.remove(keyId); + } + + private static JwtBundle toJwtBundle(TrustDomain trustDomain, JWKSet jwkSet) throws JwtBundleException, JOSEException, ParseException, KeyException { + Map authorities = new HashMap<>(); + for (JWK jwk : jwkSet.getKeys()) { + String keyId = getKeyId(jwk); + PublicKey publicKey = getPublicKey(jwk); + authorities.put(keyId, publicKey); + } + return new JwtBundle(trustDomain, authorities); + } + + private static String getKeyId(JWK jwk) throws JwtBundleException { + String keyId = jwk.getKeyID(); + if (StringUtils.isBlank(keyId)) { + throw new JwtBundleException("Error adding authority of JWKS: keyID cannot be empty"); + } + return keyId; + } + + private static PublicKey getPublicKey(JWK jwk) throws JOSEException, ParseException, KeyException { + if ("EC".equals(jwk.getKeyType().getValue())) { + return ECKey.parse(jwk.toJSONString()).toPublicKey(); + } + if ("RSA".equals(jwk.getKeyType().getValue())) { + return RSAKey.parse(jwk.toJSONString()).toPublicKey(); + } + throw new KeyException(String.format("Key Type not supported: %s", jwk.getKeyType().getValue())); + } } diff --git a/java-spiffe-core/src/main/java/spiffe/bundle/jwtbundle/JwtBundleSet.java b/java-spiffe-core/src/main/java/spiffe/bundle/jwtbundle/JwtBundleSet.java index e6e5f27..4fdf862 100644 --- a/java-spiffe-core/src/main/java/spiffe/bundle/jwtbundle/JwtBundleSet.java +++ b/java-spiffe-core/src/main/java/spiffe/bundle/jwtbundle/JwtBundleSet.java @@ -3,11 +3,12 @@ package spiffe.bundle.jwtbundle; import lombok.NonNull; import lombok.Value; import lombok.val; -import org.apache.commons.lang3.NotImplementedException; import spiffe.exception.BundleNotFoundException; import spiffe.spiffeid.TrustDomain; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** @@ -18,8 +19,8 @@ public class JwtBundleSet implements JwtBundleSource { ConcurrentHashMap bundles; - private JwtBundleSet(ConcurrentHashMap bundles) { - this.bundles = bundles; + private JwtBundleSet(Map bundles) { + this.bundles = new ConcurrentHashMap<>(bundles); } /** @@ -29,7 +30,11 @@ public class JwtBundleSet implements JwtBundleSource { * @return a {@link JwtBundleSet} */ public static JwtBundleSet of(@NonNull final List bundles) { - throw new NotImplementedException("Not implemented"); + Map bundleMap = new HashMap<>(); + for (JwtBundle bundle : bundles) { + bundleMap.put(bundle.getTrustDomain(), bundle); + } + return new JwtBundleSet(bundleMap); } /** @@ -40,7 +45,7 @@ public class JwtBundleSet implements JwtBundleSource { * @throws BundleNotFoundException if no bundle could be found for the given trust domain */ @Override - public JwtBundle getJwtBundleForTrustDomain(final TrustDomain trustDomain) throws BundleNotFoundException { + public JwtBundle getJwtBundleForTrustDomain(@NonNull final TrustDomain trustDomain) throws BundleNotFoundException { val bundle = bundles.get(trustDomain); if (bundle == null) { throw new BundleNotFoundException(String.format("No JWT bundle for trust domain %s", trustDomain)); @@ -48,13 +53,20 @@ public class JwtBundleSet implements JwtBundleSource { return bundles.get(trustDomain); } + /** + * Returns the map of JWT bundles keyed by trust domain. + */ + public Map getBundles() { + return new HashMap<>(bundles); + } + /** * Add JWT bundle to this set, if the trust domain already exists * replace the bundle. * * @param jwtBundle an instance of a JwtBundle. */ - public void add(JwtBundle jwtBundle){ - throw new NotImplementedException("Not implemented"); + public void add(@NonNull JwtBundle jwtBundle){ + bundles.put(jwtBundle.getTrustDomain(), jwtBundle); } } diff --git a/java-spiffe-core/src/main/java/spiffe/bundle/x509bundle/X509Bundle.java b/java-spiffe-core/src/main/java/spiffe/bundle/x509bundle/X509Bundle.java index d4e1b0d..135040c 100644 --- a/java-spiffe-core/src/main/java/spiffe/bundle/x509bundle/X509Bundle.java +++ b/java-spiffe-core/src/main/java/spiffe/bundle/x509bundle/X509Bundle.java @@ -26,11 +26,22 @@ public class X509Bundle implements X509BundleSource { TrustDomain trustDomain; Set x509Authorities; + /** + * Creates a new X.509 bundle for a trust domain. + * + * @param trustDomain a {@link TrustDomain} to associate to the JwtBundle + */ public X509Bundle(@NonNull final TrustDomain trustDomain) { this.trustDomain = trustDomain; this.x509Authorities = ConcurrentHashMap.newKeySet(); } + /** + * Creates a new JWT bundle for a trust domain with X.509 Authorities. + * + * @param trustDomain a {@link TrustDomain} to associate to the JwtBundle + * @param x509Authorities a Map of X.509 Certificates + */ public X509Bundle(@NonNull final TrustDomain trustDomain, @NonNull final Set x509Authorities) { this.trustDomain = trustDomain; this.x509Authorities = ConcurrentHashMap.newKeySet(); @@ -49,7 +60,7 @@ public class X509Bundle implements X509BundleSource { * @throws CertificateException if the bundle cannot be parsed */ public static X509Bundle load(@NonNull final TrustDomain trustDomain, @NonNull final Path bundlePath) throws IOException, CertificateException { - byte[] bundleBytes = new byte[0]; + byte[] bundleBytes; try { bundleBytes = Files.readAllBytes(bundlePath); } catch (NoSuchFileException e) { @@ -93,6 +104,13 @@ public class X509Bundle implements X509BundleSource { throw new BundleNotFoundException(String.format("No X509 bundle found for trust domain %s", trustDomain)); } + /** + * Returns the X.509 x509Authorities in the bundle. + */ + public Set getX509Authorities() { + return new HashSet<>(x509Authorities); + } + /** * Checks if the given X.509 authority exists in the bundle. */ diff --git a/java-spiffe-core/src/main/java/spiffe/bundle/x509bundle/X509BundleSet.java b/java-spiffe-core/src/main/java/spiffe/bundle/x509bundle/X509BundleSet.java index 509559b..a43bf74 100644 --- a/java-spiffe-core/src/main/java/spiffe/bundle/x509bundle/X509BundleSet.java +++ b/java-spiffe-core/src/main/java/spiffe/bundle/x509bundle/X509BundleSet.java @@ -6,7 +6,9 @@ import lombok.val; import spiffe.exception.BundleNotFoundException; import spiffe.spiffeid.TrustDomain; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** @@ -17,8 +19,8 @@ public class X509BundleSet implements X509BundleSource { ConcurrentHashMap bundles; - private X509BundleSet(final ConcurrentHashMap bundles) { - this.bundles = bundles; + private X509BundleSet(final Map bundles) { + this.bundles = new ConcurrentHashMap<>(bundles); } /** @@ -28,7 +30,7 @@ public class X509BundleSet implements X509BundleSource { * @return a {@link X509BundleSet} initialized with the list of bundles */ public static X509BundleSet of(@NonNull final List bundles) { - ConcurrentHashMap bundleMap = new ConcurrentHashMap<>(); + Map bundleMap = new HashMap<>(); for (X509Bundle bundle : bundles) { bundleMap.put(bundle.getTrustDomain(), bundle); } @@ -60,4 +62,8 @@ public class X509BundleSet implements X509BundleSource { } return bundles.get(trustDomain); } + + public Map getBundles() { + return new HashMap<>(bundles); + } } diff --git a/java-spiffe-core/src/main/java/spiffe/exception/JwtBundleException.java b/java-spiffe-core/src/main/java/spiffe/exception/JwtBundleException.java new file mode 100644 index 0000000..296b3b9 --- /dev/null +++ b/java-spiffe-core/src/main/java/spiffe/exception/JwtBundleException.java @@ -0,0 +1,18 @@ +package spiffe.exception; + +/** + * Checked exception thrown when there is an error creating a JwtBundle + */ +public class JwtBundleException extends Exception { + public JwtBundleException(String message) { + super(message); + } + + public JwtBundleException(String message, Throwable cause) { + super(message, cause); + } + + public JwtBundleException(Throwable cause) { + super(cause); + } +} diff --git a/java-spiffe-core/src/test/java/spiffe/bundle/jwtbundle/JwtBundleSetTest.java b/java-spiffe-core/src/test/java/spiffe/bundle/jwtbundle/JwtBundleSetTest.java new file mode 100644 index 0000000..23ef173 --- /dev/null +++ b/java-spiffe-core/src/test/java/spiffe/bundle/jwtbundle/JwtBundleSetTest.java @@ -0,0 +1,135 @@ +package spiffe.bundle.jwtbundle; + +import org.junit.jupiter.api.Test; +import spiffe.exception.BundleNotFoundException; +import spiffe.internal.DummyPublicKey; +import spiffe.spiffeid.TrustDomain; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class JwtBundleSetTest { + + @Test + void testOfListOfBundles() { + JwtBundle jwtBundle1 = new JwtBundle(TrustDomain.of("example.org")); + JwtBundle jwtBundle2 = new JwtBundle(TrustDomain.of("other.org")); + + List bundles = Arrays.asList(jwtBundle1, jwtBundle2); + + JwtBundleSet bundleSet = JwtBundleSet.of(bundles); + + assertNotNull(bundleSet); + assertEquals(2, bundleSet.getBundles().size()); + assertEquals(jwtBundle1, bundleSet.getBundles().get(TrustDomain.of("example.org"))); + assertEquals(jwtBundle2, bundleSet.getBundles().get(TrustDomain.of("other.org"))); + } + + @Test + void getJwtBundleForTrustDomain_Success() { + JwtBundle jwtBundle1 = new JwtBundle(TrustDomain.of("example.org")); + JwtBundle jwtBundle2 = new JwtBundle(TrustDomain.of("other.org")); + List bundles = Arrays.asList(jwtBundle1, jwtBundle2); + JwtBundleSet bundleSet = JwtBundleSet.of(bundles); + + JwtBundle bundle = null; + try { + bundle = bundleSet.getJwtBundleForTrustDomain(TrustDomain.of("example.org")); + } catch (BundleNotFoundException e) { + fail(e); + } + + assertEquals(jwtBundle1, bundle); + } + + @Test + void testOf_null_throwsNullPointerException() { + try { + JwtBundleSet.of(null); + fail("should have thrown exception"); + } catch (NullPointerException e) { + assertEquals("bundles is marked non-null but is null", e.getMessage()); + } + } + + @Test + void testGetJwtBundleForTrustDomain_TrustDomainNotInSet_ThrowsBundleNotFoundException() { + JwtBundle jwtBundle1 = new JwtBundle(TrustDomain.of("example.org")); + JwtBundle jwtBundle2 = new JwtBundle(TrustDomain.of("other.org")); + List bundles = Arrays.asList(jwtBundle1, jwtBundle2); + JwtBundleSet bundleSet = JwtBundleSet.of(bundles); + + try { + bundleSet.getJwtBundleForTrustDomain(TrustDomain.of("domain.test")); + fail("exception expected"); + } catch (BundleNotFoundException e) { + assertEquals("No JWT bundle for trust domain domain.test", e.getMessage()); + } + } + + @Test + void testGetJwtBundleForTrustDomain_null_throwsNullPointerException() throws BundleNotFoundException { + JwtBundle jwtBundle1 = new JwtBundle(TrustDomain.of("example.org")); + List bundleList = Collections.singletonList(jwtBundle1); + JwtBundleSet bundleSet = JwtBundleSet.of(bundleList); + try { + bundleSet.getJwtBundleForTrustDomain(null); + } catch (NullPointerException e) { + assertEquals("trustDomain is marked non-null but is null", e.getMessage()); + } + } + + @Test + void testAdd() { + JwtBundle jwtBundle1 = new JwtBundle(TrustDomain.of("example.org")); + List bundleList = Collections.singletonList(jwtBundle1); + JwtBundleSet bundleSet = JwtBundleSet.of(bundleList); + + JwtBundle jwtBundle2 = new JwtBundle(TrustDomain.of("other.org")); + bundleSet.add(jwtBundle2); + + assertTrue(bundleSet.getBundles().containsValue(jwtBundle1)); + assertTrue(bundleSet.getBundles().containsValue(jwtBundle2)); + } + + @Test + void testAdd_sameBundleAgain_noDuplicate() { + JwtBundle jwtBundle1 = new JwtBundle(TrustDomain.of("example.org")); + List bundleList = Collections.singletonList(jwtBundle1); + JwtBundleSet bundleSet = JwtBundleSet.of(bundleList); + + bundleSet.add(jwtBundle1); + + assertEquals(1, bundleSet.getBundles().size()); + assertTrue(bundleSet.getBundles().containsValue(jwtBundle1)); + } + + @Test + void testAdd_aDifferentBundleForSameTrustDomain_replacesWithNewBundle() { + JwtBundle jwtBundle1 = new JwtBundle(TrustDomain.of("example.org")); + List bundleList = Collections.singletonList(jwtBundle1); + JwtBundleSet bundleSet = JwtBundleSet.of(bundleList); + + JwtBundle jwtBundle2 = new JwtBundle(TrustDomain.of("example.org")); + jwtBundle2.addJwtAuthority("key1", new DummyPublicKey()); + bundleSet.add(jwtBundle2); + + assertTrue(bundleSet.getBundles().containsValue(jwtBundle2)); + assertFalse(bundleSet.getBundles().containsValue(jwtBundle1)); + } + + @Test + void add_null_throwsNullPointerException() { + JwtBundle jwtBundle1 = new JwtBundle(TrustDomain.of("example.org")); + List bundleList = Collections.singletonList(jwtBundle1); + JwtBundleSet bundleSet = JwtBundleSet.of(bundleList); + try { + bundleSet.add(null); + } catch (NullPointerException e) { + assertEquals("jwtBundle is marked non-null but is null", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/java-spiffe-core/src/test/java/spiffe/bundle/jwtbundle/JwtBundleTest.java b/java-spiffe-core/src/test/java/spiffe/bundle/jwtbundle/JwtBundleTest.java new file mode 100644 index 0000000..a61b94a --- /dev/null +++ b/java-spiffe-core/src/test/java/spiffe/bundle/jwtbundle/JwtBundleTest.java @@ -0,0 +1,350 @@ +package spiffe.bundle.jwtbundle; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.junit.jupiter.api.Test; +import spiffe.exception.AuthorityNotFoundException; +import spiffe.exception.BundleNotFoundException; +import spiffe.exception.JwtBundleException; +import spiffe.internal.DummyPublicKey; +import spiffe.spiffeid.TrustDomain; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyException; +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.*; + +class JwtBundleTest { + + @Test + void testNewJwtBundleWithTrustDomain_Success() { + JwtBundle jwtBundle = new JwtBundle(TrustDomain.of("example.org")); + assertNotNull(jwtBundle); + assertEquals(TrustDomain.of("example.org"), jwtBundle.getTrustDomain()); + } + + @Test + void testNewJwtBundleWithTrustDomainAndAuthorities_Success() { + HashMap authorities = new HashMap<>(); + KeyPair key1 = Keys.keyPairFor(SignatureAlgorithm.ES384); + KeyPair key2 = Keys.keyPairFor(SignatureAlgorithm.PS256); + + authorities.put("authority1", key1.getPublic()); + authorities.put("authority2", key2.getPublic()); + + JwtBundle jwtBundle = new JwtBundle(TrustDomain.of("example.org"), authorities); + + // change a key in the map, to test that the bundle has its own copy + authorities.put("authority1", key2.getPublic()); + + assertNotNull(jwtBundle); + assertEquals(TrustDomain.of("example.org"), jwtBundle.getTrustDomain()); + assertEquals(2, jwtBundle.getJwtAuthorities().size()); + assertEquals(key1.getPublic(), jwtBundle.getJwtAuthorities().get("authority1")); + assertEquals(key2.getPublic(), jwtBundle.getJwtAuthorities().get("authority2")); + } + + @Test + void testNewJwtBundle_TrustDomainIsNull_ThrowsNullPointerException() { + try { + HashMap authorities = new HashMap<>(); + new JwtBundle(null, authorities); + fail("NullPointerException was expected"); + } catch (NullPointerException e) { + assertEquals("trustDomain is marked non-null but is null", e.getMessage()); + } + } + + @Test + void testNewJwtBundleWithTrustDomain_AuthoritiesIsNull_ThrowsNullPointerException() { + try { + new JwtBundle(TrustDomain.of("example.org"), null); + fail("NullPointerException was expected"); + } catch (NullPointerException e) { + assertEquals("jwtAuthorities is marked non-null but is null", e.getMessage()); + } + } + + @Test + void testNewJwtBundleWithAuthorities_TrustDomainIsNull_ThrowsNullPointerException() { + try { + new JwtBundle(null); + fail("NullPointerException was expected"); + } catch (NullPointerException e) { + assertEquals("trustDomain is marked non-null but is null", e.getMessage()); + } + } + + @Test + void testLoadFileWithEcKey_Success() throws URISyntaxException { + Path path = Paths.get(toUri("testdata/jwtbundle/jwks_valid_EC_1.json")); + TrustDomain trustDomain = TrustDomain.of("example.org"); + + JwtBundle jwtBundle = null; + try { + jwtBundle = JwtBundle.load(trustDomain, path); + } catch (KeyException | JwtBundleException e) { + fail(); + } + + assertNotNull(jwtBundle); + assertEquals(TrustDomain.of("example.org"), jwtBundle.getTrustDomain()); + assertEquals(1, jwtBundle.getJwtAuthorities().size()); + assertNotNull(jwtBundle.getJwtAuthorities().get("C6vs25welZOx6WksNYfbMfiw9l96pMnD")); + } + + @Test + void testLoadFileWithRsaKey_Success() throws URISyntaxException { + Path path = Paths.get(toUri("testdata/jwtbundle/jwks_valid_RSA_1.json")); + TrustDomain trustDomain = TrustDomain.of("domain.test"); + + JwtBundle jwtBundle = null; + try { + jwtBundle = JwtBundle.load(trustDomain, path); + } catch (KeyException | JwtBundleException e) { + fail(e); + } + + assertNotNull(jwtBundle); + assertEquals(TrustDomain.of("domain.test"), jwtBundle.getTrustDomain()); + assertEquals(1, jwtBundle.getJwtAuthorities().size()); + assertNotNull(jwtBundle.getJwtAuthorities().get("14cc39cd-838d-426d-9bb1-77f3468fba96")); + } + + @Test + void testLoadFileWithRsaAndEc_Success() throws URISyntaxException { + Path path = Paths.get(toUri("testdata/jwtbundle/jwks_valid_RSA_EC.json")); + TrustDomain trustDomain = TrustDomain.of("domain.test"); + + JwtBundle jwtBundle = null; + try { + jwtBundle = JwtBundle.load(trustDomain, path); + } catch (KeyException | JwtBundleException e) { + fail(e); + } + + assertNotNull(jwtBundle); + assertEquals(TrustDomain.of("domain.test"), jwtBundle.getTrustDomain()); + assertEquals(2, jwtBundle.getJwtAuthorities().size()); + assertNotNull(jwtBundle.getJwtAuthorities().get("14cc39cd-838d-426d-9bb1-77f3468fba96")); + assertNotNull(jwtBundle.getJwtAuthorities().get("C6vs25welZOx6WksNYfbMfiw9l96pMnD")); + } + + @Test + void testLoadFile_MissingKid_ThrowsJwtBundleException() throws URISyntaxException, KeyException { + Path path = Paths.get(toUri("testdata/jwtbundle/jwks_missing_kid.json")); + TrustDomain trustDomain = TrustDomain.of("domain.test"); + + try { + JwtBundle.load(trustDomain, path); + fail("should have thrown exception"); + } catch (JwtBundleException e) { + assertEquals("Error adding authority of JWKS: keyID cannot be empty", e.getMessage()); + } + } + + @Test + void testLoadFile_InvalidKeyType_ThrowsKeyException() throws URISyntaxException, JwtBundleException { + Path path = Paths.get(toUri("testdata/jwtbundle/jwks_invalid_keytype.json")); + TrustDomain trustDomain = TrustDomain.of("domain.test"); + + try { + JwtBundle.load(trustDomain, path); + fail("should have thrown exception"); + } catch (KeyException e) { + assertEquals("Key Type not supported: OKP", e.getMessage()); + } + } + + @Test + void testLoadFile_NonExistentFile_ThrowsException() throws KeyException { + Path path = Paths.get("testdata/jwtbundle/non-existen.json"); + TrustDomain trustDomain = TrustDomain.of("domain.test"); + + try { + JwtBundle.load(trustDomain, path); + fail("should have thrown exception"); + } catch (JwtBundleException e) { + assertEquals("Could not load bundle from file: testdata/jwtbundle/non-existen.json", e.getMessage()); + } + } + + @Test + void testLoad_NullTrustDomain_ThrowsNullPointerException() throws KeyException, JwtBundleException { + try { + JwtBundle.load(null, Paths.get("path-to-file")); + } catch (NullPointerException e) { + assertEquals("trustDomain is marked non-null but is null", e.getMessage()); + } + } + + @Test + void testLoad_NullBundlePath_ThrowsNullPointerException() throws KeyException, JwtBundleException { + try { + JwtBundle.load(TrustDomain.of("example.org"), null); + } catch (NullPointerException e) { + assertEquals("bundlePath is marked non-null but is null", e.getMessage()); + } + } + + @Test + void testParseJsonWithRsaAndEcKeys_Success() throws URISyntaxException, IOException { + Path path = Paths.get(toUri("testdata/jwtbundle/jwks_valid_RSA_EC.json")); + byte[] bundleBytes = Files.readAllBytes(path); + + JwtBundle jwtBundle = null; + try { + jwtBundle = JwtBundle.parse(TrustDomain.of("domain.test"), bundleBytes); + } catch (KeyException | JwtBundleException e) { + fail(e); + } + + assertNotNull(jwtBundle); + assertEquals(TrustDomain.of("domain.test"), jwtBundle.getTrustDomain()); + assertEquals(2, jwtBundle.getJwtAuthorities().size()); + assertNotNull(jwtBundle.getJwtAuthorities().get("14cc39cd-838d-426d-9bb1-77f3468fba96")); + assertNotNull(jwtBundle.getJwtAuthorities().get("C6vs25welZOx6WksNYfbMfiw9l96pMnD")); + } + + @Test + void testParse_MissingKid_Fails() throws URISyntaxException, IOException { + Path path = Paths.get(toUri("testdata/jwtbundle/jwks_missing_kid.json")); + byte[] bundleBytes = Files.readAllBytes(path); + TrustDomain trustDomain = TrustDomain.of("domain.test"); + + try { + JwtBundle.parse(trustDomain, bundleBytes); + fail("should have thrown exception"); + } catch (KeyException | JwtBundleException e) { + assertEquals("Error adding authority of JWKS: keyID cannot be empty", e.getMessage()); + } + } + + @Test + void testParseInvalidJson() throws KeyException { + try { + JwtBundle.parse(TrustDomain.of("example.org"), "invalid json".getBytes()); + fail("exception is expected"); + } catch (JwtBundleException e) { + assertEquals("Could not parse bundle from bytes", e.getMessage()); + } + } + + @Test + void testParse_NullTrustDomain_ThrowsNullPointerException() throws KeyException, JwtBundleException { + try { + JwtBundle.parse(null, "json".getBytes()); + } catch (NullPointerException e) { + assertEquals("trustDomain is marked non-null but is null", e.getMessage()); + } + } + + @Test + void testParse_NullBundleBytes_ThrowsNullPointerException() throws KeyException, JwtBundleException { + try { + JwtBundle.parse(TrustDomain.of("example.org"), null); + } catch (NullPointerException e) { + assertEquals("bundleBytes is marked non-null but is null", e.getMessage()); + } + } + + + @Test + void testGetJwtBundleForTrustDomain_Success() { + JwtBundle jwtBundle = new JwtBundle(TrustDomain.of("example.org")); + try { + JwtBundle bundle = jwtBundle.getJwtBundleForTrustDomain(TrustDomain.of("example.org")); + assertEquals(jwtBundle, bundle); + } catch (BundleNotFoundException e) { + fail(e); + } + } + + @Test + void testGetJwtBundleForTrustDomain_doesNotExiste_ThrowsBundleNotFoundException() { + JwtBundle jwtBundle = new JwtBundle(TrustDomain.of("example.org")); + try { + jwtBundle.getJwtBundleForTrustDomain(TrustDomain.of("other.org")); + fail("exception expected"); + } catch (BundleNotFoundException e) { + assertEquals("No JWT bundle found for trust domain other.org", e.getMessage()); + } + } + + @Test + void testJWTAuthoritiesCRUD() { + JwtBundle jwtBundle = new JwtBundle(TrustDomain.of("example.org")); + + // Test addJWTAuthority + DummyPublicKey jwtAuthority1 = new DummyPublicKey(); + DummyPublicKey jwtAuthority2 = new DummyPublicKey(); + jwtBundle.addJwtAuthority("key1", jwtAuthority1); + jwtBundle.addJwtAuthority("key2", jwtAuthority2); + + assertEquals(2, jwtBundle.getJwtAuthorities().size()); + + // Test findJwtAuthority + PublicKey key1 = null; + PublicKey key2 = null; + try { + key1 = jwtBundle.findJwtAuthority("key1"); + key2 = jwtBundle.findJwtAuthority("key2"); + } catch (AuthorityNotFoundException e) { + fail(e); + } + assertEquals(key1, jwtAuthority1 ); + assertEquals(key2, jwtAuthority2 ); + + // Test RemoveJwtAuthority + jwtBundle.removeJwtAuthority("key1"); + assertFalse(jwtBundle.hasJwtAuthority("key1")); + assertTrue(jwtBundle.hasJwtAuthority("key2")); + + // Test update + jwtBundle.addJwtAuthority("key2", jwtAuthority1); + assertEquals(jwtAuthority1, jwtBundle.getJwtAuthorities().get("key2")); + assertEquals(1, jwtBundle.getJwtAuthorities().size()); + } + + @Test + void testAddJwtAuthority_emtpyKeyId_throwsIllegalArgumentException() { + JwtBundle jwtBundle = new JwtBundle(TrustDomain.of("example.org")); + try { + jwtBundle.addJwtAuthority("", new DummyPublicKey()); + } catch (IllegalArgumentException e) { + assertEquals("KeyId cannot be empty", e.getMessage()); + } + } + + @Test + void testAddJwtAuthority_nullKeyId_throwsNullPointerException() { + JwtBundle jwtBundle = new JwtBundle(TrustDomain.of("example.org")); + try { + jwtBundle.addJwtAuthority(null, new DummyPublicKey()); + } catch (NullPointerException e) { + assertEquals("keyId is marked non-null but is null", e.getMessage()); + } + } + + @Test + void testAddJwtAuthority_nullJwtAuthority_throwsNullPointerException() { + JwtBundle jwtBundle = new JwtBundle(TrustDomain.of("example.org")); + try { + jwtBundle.addJwtAuthority("key1", null); + } catch (NullPointerException e) { + assertEquals("jwtAuthority is marked non-null but is null", e.getMessage()); + } + } + + private URI toUri(String path) throws URISyntaxException { + return getClass().getClassLoader().getResource(path).toURI(); + } +} \ No newline at end of file diff --git a/java-spiffe-core/src/test/java/spiffe/bundle/x509bundle/X509BundleSetTest.java b/java-spiffe-core/src/test/java/spiffe/bundle/x509bundle/X509BundleSetTest.java index 1fccfc3..8683754 100644 --- a/java-spiffe-core/src/test/java/spiffe/bundle/x509bundle/X509BundleSetTest.java +++ b/java-spiffe-core/src/test/java/spiffe/bundle/x509bundle/X509BundleSetTest.java @@ -20,8 +20,8 @@ class X509BundleSetTest { List bundleList = Arrays.asList(x509Bundle1, x509Bundle2); X509BundleSet bundleSet = X509BundleSet.of(bundleList); - assertTrue(bundleSet.getBundles().contains(x509Bundle1)); - assertTrue(bundleSet.getBundles().contains(x509Bundle2)); + assertTrue(bundleSet.getBundles().containsValue(x509Bundle1)); + assertTrue(bundleSet.getBundles().containsValue(x509Bundle2)); } @Test @@ -43,8 +43,8 @@ class X509BundleSetTest { X509Bundle x509Bundle2 = new X509Bundle(TrustDomain.of("other.org")); bundleSet.add(x509Bundle2); - assertTrue(bundleSet.getBundles().contains(x509Bundle1)); - assertTrue(bundleSet.getBundles().contains(x509Bundle2)); + assertTrue(bundleSet.getBundles().containsValue(x509Bundle1)); + assertTrue(bundleSet.getBundles().containsValue(x509Bundle2)); } @Test @@ -55,7 +55,7 @@ class X509BundleSetTest { bundleSet.add(x509Bundle1); - assertTrue(bundleSet.getBundles().contains(x509Bundle1)); + assertTrue(bundleSet.getBundles().containsValue(x509Bundle1)); assertEquals(1, bundleSet.getBundles().size()); } @@ -69,8 +69,8 @@ class X509BundleSetTest { x509Bundle2.addX509Authority(new DummyX509Certificate()); bundleSet.add(x509Bundle2); - assertTrue(bundleSet.getBundles().contains(x509Bundle2)); - assertFalse(bundleSet.getBundles().contains(x509Bundle1)); + assertTrue(bundleSet.getBundles().containsValue(x509Bundle2)); + assertFalse(bundleSet.getBundles().containsValue(x509Bundle1)); assertEquals(1, bundleSet.getBundles().size()); } @@ -128,8 +128,4 @@ class X509BundleSetTest { assertEquals("trustDomain is marked non-null but is null", e.getMessage()); } } - - @Test - void getBundles() { - } } \ No newline at end of file diff --git a/java-spiffe-core/src/test/java/spiffe/internal/DummyPublicKey.java b/java-spiffe-core/src/test/java/spiffe/internal/DummyPublicKey.java new file mode 100644 index 0000000..37559dd --- /dev/null +++ b/java-spiffe-core/src/test/java/spiffe/internal/DummyPublicKey.java @@ -0,0 +1,20 @@ +package spiffe.internal; + +import java.security.PublicKey; + +public class DummyPublicKey implements PublicKey { + @Override + public String getAlgorithm() { + return null; + } + + @Override + public String getFormat() { + return null; + } + + @Override + public byte[] getEncoded() { + return new byte[0]; + } +} diff --git a/java-spiffe-core/src/test/java/spiffe/svid/jwtsvid/JwtSvidParseAndValidateTest.java b/java-spiffe-core/src/test/java/spiffe/svid/jwtsvid/JwtSvidParseAndValidateTest.java index 44866aa..c8d710f 100644 --- a/java-spiffe-core/src/test/java/spiffe/svid/jwtsvid/JwtSvidParseAndValidateTest.java +++ b/java-spiffe-core/src/test/java/spiffe/svid/jwtsvid/JwtSvidParseAndValidateTest.java @@ -108,8 +108,8 @@ class JwtSvidParseAndValidateTest { TrustDomain trustDomain = TrustDomain.of("test.domain"); JwtBundle jwtBundle = new JwtBundle(trustDomain); - jwtBundle.addJWTAuthority("authority1", key1.getPublic()); - jwtBundle.addJWTAuthority("authority2", key2.getPublic()); + jwtBundle.addJwtAuthority("authority1", key1.getPublic()); + jwtBundle.addJwtAuthority("authority2", key2.getPublic()); SpiffeId spiffeId = trustDomain.newSpiffeId("host"); Date expiration = new Date(System.currentTimeMillis() + 3600000); diff --git a/java-spiffe-core/src/test/java/spiffe/svid/jwtsvid/JwtSvidParseInsecureTest.java b/java-spiffe-core/src/test/java/spiffe/svid/jwtsvid/JwtSvidParseInsecureTest.java index cd6c54b..91c69a9 100644 --- a/java-spiffe-core/src/test/java/spiffe/svid/jwtsvid/JwtSvidParseInsecureTest.java +++ b/java-spiffe-core/src/test/java/spiffe/svid/jwtsvid/JwtSvidParseInsecureTest.java @@ -94,8 +94,8 @@ class JwtSvidParseInsecureTest { TrustDomain trustDomain = TrustDomain.of("test.domain"); JwtBundle jwtBundle = new JwtBundle(trustDomain); - jwtBundle.addJWTAuthority("authority1", key1.getPublic()); - jwtBundle.addJWTAuthority("authority2", key2.getPublic()); + jwtBundle.addJwtAuthority("authority1", key1.getPublic()); + jwtBundle.addJwtAuthority("authority2", key2.getPublic()); SpiffeId spiffeId = trustDomain.newSpiffeId("host"); Date expiration = new Date(System.currentTimeMillis() + 3600000); diff --git a/java-spiffe-core/src/test/resources/testdata/jwtbundle/jwks_invalid_keytype.json b/java-spiffe-core/src/test/resources/testdata/jwtbundle/jwks_invalid_keytype.json new file mode 100644 index 0000000..6921a64 --- /dev/null +++ b/java-spiffe-core/src/test/resources/testdata/jwtbundle/jwks_invalid_keytype.json @@ -0,0 +1,11 @@ +{ + "keys": [ + { + "kty": "OKP", + "crv": "Ed25519", + "kid": "1", + "x": "c2Rmc2Zk", + "alg": "EdDSA" + } + ] +} \ No newline at end of file diff --git a/java-spiffe-core/src/test/resources/testdata/jwtbundle/jwks_missing_kid.json b/java-spiffe-core/src/test/resources/testdata/jwtbundle/jwks_missing_kid.json new file mode 100644 index 0000000..04808a3 --- /dev/null +++ b/java-spiffe-core/src/test/resources/testdata/jwtbundle/jwks_missing_kid.json @@ -0,0 +1,17 @@ +{ + "keys": [ + { + "kty": "EC", + "kid": "C6vs25welZOx6WksNYfbMfiw9l96pMnD", + "crv": "P-256", + "x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k", + "y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM" + }, + { + "kty": "EC", + "crv": "P-256", + "x": "7MGOl06DP9df2u8oHY6lqYFIoQWzCj9UYlp-MFeEYeY", + "y": "PSLLy5Pg0_kNGFFXq_eeq9kYcGDM3MPHJ6ncteNOr6w" + } + ] +} \ No newline at end of file diff --git a/java-spiffe-core/src/test/resources/testdata/jwtbundle/jwks_valid_EC_1.json b/java-spiffe-core/src/test/resources/testdata/jwtbundle/jwks_valid_EC_1.json new file mode 100644 index 0000000..9315a56 --- /dev/null +++ b/java-spiffe-core/src/test/resources/testdata/jwtbundle/jwks_valid_EC_1.json @@ -0,0 +1,11 @@ +{ + "keys": [ + { + "kty": "EC", + "kid": "C6vs25welZOx6WksNYfbMfiw9l96pMnD", + "crv": "P-256", + "x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k", + "y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM" + } + ] +} \ No newline at end of file diff --git a/java-spiffe-core/src/test/resources/testdata/jwtbundle/jwks_valid_RSA_1.json b/java-spiffe-core/src/test/resources/testdata/jwtbundle/jwks_valid_RSA_1.json new file mode 100644 index 0000000..2d519d9 --- /dev/null +++ b/java-spiffe-core/src/test/resources/testdata/jwtbundle/jwks_valid_RSA_1.json @@ -0,0 +1,17 @@ +{ + "keys": [ + { + "p": "4aCuWvrpN7CeAqmXUWoQHex9l0ePELKe9AzJ2jcnDPEqP_CO9jlL7V_ZlMFLXg8oSFMkx59woD3BCjdZqCDaaiWA8L3fFmcLXLj9LrTz0X_p_Tb1cPbCa3Z5MqtuhC5yg5Po8uuDOUrF4FdKcU_YtCYbFwsr4HfViakKlk64Qt8", + "kty": "RSA", + "q": "o7Enkd4yxw9tl-jHQXw38GjItzdbFLmkeppHSIzv02Zs5XS0CQyb-jFrUXStpO3UiNrRXm1YUCSUwRDj81ATilrIpPTTH6qC2Fu_E5eEEDx79W5p0oj3SbN2BQS5-MkhVvMrrDGkIAmRmLcE9SH-eIreYgB6XW0yrbHTwbUWV98", + "d": "EpfAjuifddpKISal0znpNhlfVkRDyEdd6_CBlh6lLJdU8dflcqyWFhmJ8pEXsnwC4-DBkkDKt57HcIq3MQ8Q_IKlhLPexugNr3QJxbA1DCbXagsIvh4-QcBQp-4LOZv1T3T4-lywkRzX0qp9yjySIGkT6OmAfN97Q-_NPhOdlHJn5feQfJhxWIMaWhQnomjmMP40FApRdk-gOmNx-w2pLWHVjnibbfI9SijUKg4ZqW3MvwNbSuM-EPyetRuaJRW892h7kxGE2wV_oqGRbqasPqVJN9SiNAEWwzcs645NwPEC48XUK0Q4eUyd9ra_YFv9HGgZ1Yo0gaD0BnRMLxcm-Q", + "e": "AQAB", + "use": "sig", + "kid": "14cc39cd-838d-426d-9bb1-77f3468fba96", + "qi": "kly84cu6D3sy64HfRpXfIYuNZTcJxlfdkLcLY-ZHkY5oVUiDBb6VrKwkQq3e5UbxF0a6qzwCk_B0Bn6ChHXLorVLnlx8cUG2TP3C72J_iHpE8Bn3wend7ZqLfcceh19zPOe13352RurPXbewzf1tYVji-OPzJr05fUDrqmesXvI", + "dp": "XTZnsbiFDvfNX6Y2mHDr9aDVBeGPTkOs_YAnCBrO7D7ZyI6WUUy8fHWjyxvMCjDS4IZQ5JOPEPRSQuk5BgeElGOoEE0w0-2AOS9Hkbs6G2vv_CdvYNrg2UqZqYA_aSZTMt6xV2JK1Sl59EO7wnJNQaeYe32nA9YeBwAqGoys_Rs", + "dq": "GeTJLKLoh2KiZHhXJL3An5ADyC_CganIIfjLs-dPfLJkIXvvisrq1Y4BuvXpDgDtMOTkX7qOUMconM3OMUwGe0lXGfj8eLLhVdZViITcSDE5Lp7TsJEoBQmVbr_Lp1Yxpu56hxenLcY1uOGisCA7f9f_y6LluGewr5dEtwytRyM", + "n": "kEVx_IUSr30PMuJTWWg4QYxhOWU80Zl_OKHnGp6PxLqD0wuOmLQv2a0xprzXMIxROl3iYyAY_PQmiLQn4aMR_c55V6N6O_qP5d09tArAipMsVyAICW0RRtJtAL-n9_ktghmYUaVHhq5AXS7flxO2b8KKZWZuKJ1f8clbK16eOJ_-NURjqz90zpuafIA1nzCdNk9AE1NOuXdW81FIjC-82abgDhgyNgNnL33z8BXbwKdog0Hu9BFoyIBXIA8HLzaj-KmyqTY9ewu2JVI8XVLuM_zy-6FmzKGanw0bhZRAFNHc-eraMP0HLJuWf0R9waiKmrPy0i6zISayo4_rm2YJQQ" + } + ] +} diff --git a/java-spiffe-core/src/test/resources/testdata/jwtbundle/jwks_valid_RSA_EC.json b/java-spiffe-core/src/test/resources/testdata/jwtbundle/jwks_valid_RSA_EC.json new file mode 100644 index 0000000..ce08214 --- /dev/null +++ b/java-spiffe-core/src/test/resources/testdata/jwtbundle/jwks_valid_RSA_EC.json @@ -0,0 +1,24 @@ +{ + "keys": [ + { + "p": "4aCuWvrpN7CeAqmXUWoQHex9l0ePELKe9AzJ2jcnDPEqP_CO9jlL7V_ZlMFLXg8oSFMkx59woD3BCjdZqCDaaiWA8L3fFmcLXLj9LrTz0X_p_Tb1cPbCa3Z5MqtuhC5yg5Po8uuDOUrF4FdKcU_YtCYbFwsr4HfViakKlk64Qt8", + "kty": "RSA", + "q": "o7Enkd4yxw9tl-jHQXw38GjItzdbFLmkeppHSIzv02Zs5XS0CQyb-jFrUXStpO3UiNrRXm1YUCSUwRDj81ATilrIpPTTH6qC2Fu_E5eEEDx79W5p0oj3SbN2BQS5-MkhVvMrrDGkIAmRmLcE9SH-eIreYgB6XW0yrbHTwbUWV98", + "d": "EpfAjuifddpKISal0znpNhlfVkRDyEdd6_CBlh6lLJdU8dflcqyWFhmJ8pEXsnwC4-DBkkDKt57HcIq3MQ8Q_IKlhLPexugNr3QJxbA1DCbXagsIvh4-QcBQp-4LOZv1T3T4-lywkRzX0qp9yjySIGkT6OmAfN97Q-_NPhOdlHJn5feQfJhxWIMaWhQnomjmMP40FApRdk-gOmNx-w2pLWHVjnibbfI9SijUKg4ZqW3MvwNbSuM-EPyetRuaJRW892h7kxGE2wV_oqGRbqasPqVJN9SiNAEWwzcs645NwPEC48XUK0Q4eUyd9ra_YFv9HGgZ1Yo0gaD0BnRMLxcm-Q", + "e": "AQAB", + "use": "sig", + "kid": "14cc39cd-838d-426d-9bb1-77f3468fba96", + "qi": "kly84cu6D3sy64HfRpXfIYuNZTcJxlfdkLcLY-ZHkY5oVUiDBb6VrKwkQq3e5UbxF0a6qzwCk_B0Bn6ChHXLorVLnlx8cUG2TP3C72J_iHpE8Bn3wend7ZqLfcceh19zPOe13352RurPXbewzf1tYVji-OPzJr05fUDrqmesXvI", + "dp": "XTZnsbiFDvfNX6Y2mHDr9aDVBeGPTkOs_YAnCBrO7D7ZyI6WUUy8fHWjyxvMCjDS4IZQ5JOPEPRSQuk5BgeElGOoEE0w0-2AOS9Hkbs6G2vv_CdvYNrg2UqZqYA_aSZTMt6xV2JK1Sl59EO7wnJNQaeYe32nA9YeBwAqGoys_Rs", + "dq": "GeTJLKLoh2KiZHhXJL3An5ADyC_CganIIfjLs-dPfLJkIXvvisrq1Y4BuvXpDgDtMOTkX7qOUMconM3OMUwGe0lXGfj8eLLhVdZViITcSDE5Lp7TsJEoBQmVbr_Lp1Yxpu56hxenLcY1uOGisCA7f9f_y6LluGewr5dEtwytRyM", + "n": "kEVx_IUSr30PMuJTWWg4QYxhOWU80Zl_OKHnGp6PxLqD0wuOmLQv2a0xprzXMIxROl3iYyAY_PQmiLQn4aMR_c55V6N6O_qP5d09tArAipMsVyAICW0RRtJtAL-n9_ktghmYUaVHhq5AXS7flxO2b8KKZWZuKJ1f8clbK16eOJ_-NURjqz90zpuafIA1nzCdNk9AE1NOuXdW81FIjC-82abgDhgyNgNnL33z8BXbwKdog0Hu9BFoyIBXIA8HLzaj-KmyqTY9ewu2JVI8XVLuM_zy-6FmzKGanw0bhZRAFNHc-eraMP0HLJuWf0R9waiKmrPy0i6zISayo4_rm2YJQQ" + }, + { + "kty": "EC", + "kid": "C6vs25welZOx6WksNYfbMfiw9l96pMnD", + "crv": "P-256", + "x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k", + "y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM" + } + ] +}