Implementing JWT bundle and bundle set.

Refactors to X509 bundle and set.
Adding tests.
Adding library for processing JOSE JWK bundles.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
This commit is contained in:
Max Lambrecht 2020-05-19 13:07:07 -03:00
parent a203cf450f
commit 8c5384ee3b
17 changed files with 763 additions and 50 deletions

View File

@ -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'
}

View File

@ -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<String, PublicKey> jwtAuthorities;
public JwtBundle(@NonNull TrustDomain trustDomain, @NonNull Map<String, PublicKey> 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<String, PublicKey> jwtAuthorities) {
throw new NotImplementedException("Not implemented");
public JwtBundle(@NonNull TrustDomain trustDomain, @NonNull Map<String, PublicKey> 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.
* <p>
* 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<String, PublicKey> 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<String, PublicKey> 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()));
}
}

View File

@ -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<TrustDomain, JwtBundle> bundles;
private JwtBundleSet(ConcurrentHashMap<TrustDomain, JwtBundle> bundles) {
this.bundles = bundles;
private JwtBundleSet(Map<TrustDomain, JwtBundle> 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<JwtBundle> bundles) {
throw new NotImplementedException("Not implemented");
Map<TrustDomain, JwtBundle> 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<TrustDomain, JwtBundle> 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);
}
}

View File

@ -26,11 +26,22 @@ public class X509Bundle implements X509BundleSource {
TrustDomain trustDomain;
Set<X509Certificate> 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<X509Certificate> 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<X509Certificate> getX509Authorities() {
return new HashSet<>(x509Authorities);
}
/**
* Checks if the given X.509 authority exists in the bundle.
*/

View File

@ -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<TrustDomain, X509Bundle> bundles;
private X509BundleSet(final ConcurrentHashMap<TrustDomain, X509Bundle> bundles) {
this.bundles = bundles;
private X509BundleSet(final Map<TrustDomain, X509Bundle> 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<X509Bundle> bundles) {
ConcurrentHashMap<TrustDomain, X509Bundle> bundleMap = new ConcurrentHashMap<>();
Map<TrustDomain, X509Bundle> 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<TrustDomain, X509Bundle> getBundles() {
return new HashMap<>(bundles);
}
}

View File

@ -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);
}
}

View File

@ -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<JwtBundle> 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<JwtBundle> 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<JwtBundle> 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<JwtBundle> 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<JwtBundle> 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<JwtBundle> 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<JwtBundle> 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<JwtBundle> 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());
}
}
}

View File

@ -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<String, PublicKey> 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<String, PublicKey> 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();
}
}

View File

@ -20,8 +20,8 @@ class X509BundleSetTest {
List<X509Bundle> 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() {
}
}

View File

@ -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];
}
}

View File

@ -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);

View File

@ -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);

View File

@ -0,0 +1,11 @@
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"kid": "1",
"x": "c2Rmc2Zk",
"alg": "EdDSA"
}
]
}

View File

@ -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"
}
]
}

View File

@ -0,0 +1,11 @@
{
"keys": [
{
"kty": "EC",
"kid": "C6vs25welZOx6WksNYfbMfiw9l96pMnD",
"crv": "P-256",
"x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k",
"y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM"
}
]
}

View File

@ -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"
}
]
}

View File

@ -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"
}
]
}