x5t#256 thumbprint respectively.
*
* @param fingerprint the SHA-1 or SHA-256 fingerprint
- * @return an x5t hash.
+ * @return a x5t hash.
*/
public static String convertFingerprintToThumbprint(String fingerprint) {
byte[] bytes = HexUtils.toBytes(fingerprint);
@@ -110,13 +109,43 @@ public static JWT decodePayload(String encodedJWT) {
}
/**
- * Generate a new public / private key pair using a 2048 bit RSA key. This is the minimum key length for use with an
+ * Generate a new public / private key pair using a 2048-bit RSA key. This is the minimum key length for use with an
* RSA signing scheme for JWT.
*
* @return a public and private key PEM in their respective X.509 and PKCS#8 key formats.
*/
public static KeyPair generate2048_RSAKeyPair() {
- return generateKeyPair(2048, KeyType.RSA);
+ return generateKeyPair("RSA", 2048);
+ }
+
+ /**
+ * Generate a new public / private key pair using a 2048-bit RSA PSS key. This is the minimum key length for use with an
+ * RSA PSS signing scheme for JWT.
+ *
+ * @return a public and private key PEM in their respective X.509 and PKCS#8 key formats.
+ */
+ public static KeyPair generate2048_RSAPSSKeyPair() {
+ return generateKeyPair("RSASSA-PSS", 2048);
+ }
+
+ /**
+ * Generate a new public / private key pair using a 3072-bit RSA PSS key. This is the minimum key length for use with an
+ * RSA PSS signing scheme for JWT.
+ *
+ * @return a public and private key PEM in their respective X.509 and PKCS#8 key formats.
+ */
+ public static KeyPair generate3072_RSAPSSKeyPair() {
+ return generateKeyPair("RSASSA-PSS", 3072);
+ }
+
+ /**
+ * Generate a new public / private key pair using a 4096-bit RSA PSS key. This is the minimum key length for use with an
+ * RSA PSS signing scheme for JWT.
+ *
+ * @return a public and private key PEM in their respective X.509 and PKCS#8 key formats.
+ */
+ public static KeyPair generate4096_RSAPSSKeyPair() {
+ return generateKeyPair("RSASSA-PSS", 4096);
}
/**
@@ -125,34 +154,34 @@ public static KeyPair generate2048_RSAKeyPair() {
* @return a public and private key PEM in their respective X.509 and PKCS#8 key formats.
*/
public static KeyPair generate256_ECKeyPair() {
- return generateKeyPair(256, EC);
+ return generateKeyPair("EC", 256);
}
/**
- * Generate a new public / private key pair using a 3072 bit RSA key.
+ * Generate a new public / private key pair using a 3072-bit RSA key.
*
* @return a public and private key PEM in their respective X.509 and PKCS#8 key formats.
*/
public static KeyPair generate3072_RSAKeyPair() {
- return generateKeyPair(3072, KeyType.RSA);
+ return generateKeyPair("RSA", 3072);
}
/**
- * Generate a new public / private key pair using a 384 bit EC key. A 384 bit EC key is roughly equivalent to a 7680 bit RSA key.
+ * Generate a new public / private key pair using a 384-bit EC key. A 384 bit EC key is roughly equivalent to a 7680 bit RSA key.
*
* @return a public and private key PEM in their respective X.509 and PKCS#8 key formats.
*/
public static KeyPair generate384_ECKeyPair() {
- return generateKeyPair(384, EC);
+ return generateKeyPair("EC", 384);
}
/**
- * Generate a new public / private key pair using a 4096 bit RSA key.
+ * Generate a new public / private key pair using a 4096-bit RSA key.
*
* @return a public and private key PEM in their respective X.509 and PKCS#8 key formats.
*/
public static KeyPair generate4096_RSAKeyPair() {
- return generateKeyPair(4096, KeyType.RSA);
+ return generateKeyPair("RSA", 4096);
}
/**
@@ -161,7 +190,25 @@ public static KeyPair generate4096_RSAKeyPair() {
* @return a public and private key PEM in their respective X.509 and PKCS#8 key formats.
*/
public static KeyPair generate521_ECKeyPair() {
- return generateKeyPair(521, EC);
+ return generateKeyPair("EC", 521);
+ }
+
+ /**
+ * Generate a new public / private key pair using the Ed25529 curve.
+ *
+ * @return a public and private key PEM in their respective X.509 and PKCS#8 key formats.
+ */
+ public static KeyPair generate_ed25519_EdDSAKeyPair() {
+ return generateKeyPair("ed25519", null);
+ }
+
+ /**
+ * Generate a new public / private key pair using the Ed448 curve.
+ *
+ * @return a public and private key PEM in their respective X.509 and PKCS#8 key formats.
+ */
+ public static KeyPair generate_ed448_EdDSAKeyPair() {
+ return generateKeyPair("ed448", null);
}
/**
@@ -305,13 +352,16 @@ private static String digest(String algorithm, byte[] bytes) {
/**
* Generate a new Public / Private key pair with a key size of the provided length.
*
- * @param keySize the length of the key in bits
+ * @param algorithm the algorithm to use to generate the key pair
+ * @param keySize the optional key size when applicable
* @return a public and private key in PEM format.
*/
- private static KeyPair generateKeyPair(int keySize, KeyType keyType) {
+ private static KeyPair generateKeyPair(String algorithm, Integer keySize) {
try {
- KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(keyType.name());
- keyPairGenerator.initialize(keySize);
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
+ if (keySize != null) {
+ keyPairGenerator.initialize(keySize);
+ }
java.security.KeyPair keyPair = keyPairGenerator.generateKeyPair();
String privateKey = PEM.encode(keyPair.getPrivate(), keyPair.getPublic());
diff --git a/src/main/java/io/fusionauth/jwt/OpenIDConnect.java b/src/main/java/io/fusionauth/jwt/OpenIDConnect.java
index ce42e744..b4487abd 100644
--- a/src/main/java/io/fusionauth/jwt/OpenIDConnect.java
+++ b/src/main/java/io/fusionauth/jwt/OpenIDConnect.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018-2019, FusionAuth, All Rights Reserved
+ * Copyright (c) 2018-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -58,31 +58,31 @@ private static String generate_hash(String string, Algorithm algorithm) {
Objects.requireNonNull(string);
Objects.requireNonNull(algorithm);
- int leftMostBits;
MessageDigest messageDigest;
- switch (algorithm) {
- case ES256:
- case HS256:
- case RS256:
+ int leftMostBits = switch (algorithm) {
+ case ES256, HS256, RS256 -> {
messageDigest = getDigest("SHA-256");
- leftMostBits = 128;
- break;
- case ES384:
- case HS384:
- case RS384:
+ yield 128; // 32 * 8 / 2 = 256
+ }
+ case ES384, HS384, RS384 -> {
messageDigest = getDigest("SHA-384");
- leftMostBits = 192;
- break;
- case ES512:
- case HS512:
- case RS512:
+ yield 192; // 48 * 8 / 2 = 192
+ }
+ case Ed25519, ES512, HS512, RS512 -> {
messageDigest = getDigest("SHA-512");
- leftMostBits = 256;
- break;
- default:
- throw new IllegalArgumentException("You specified an unsupported algorithm. The algorithm [" + algorithm + "]"
- + " is not supported. You must use ES256, ES384, ES512, HS256, HS384, HS512, RS256, RS384 or RS512.");
- }
+ yield 256; // 64 * 8 / 2 = 256
+ }
+ case Ed448 -> {
+ // Ed448 uses a 114 byte SHAKE256 hash. The recommended hash length here is the same, see discussion thread:
+ // - https://bitbucket.org/openid/connect/issues/1125
+ // The JCA does not ship with SHAKE256, expect this to exception if you have not registered a provider with support for this algorithm (such as BC)
+ messageDigest = getDigest("SHAKE256");
+ yield 456; // 114 * 8 / 2 = 456
+ }
+ default ->
+ throw new IllegalArgumentException("You specified an unsupported algorithm. The algorithm [" + algorithm + "]"
+ + " is not supported. You must use Ed25519, Ed448, ES256, ES384, ES512, HS256, HS384, HS512, RS256, RS384 or RS512.");
+ };
byte[] digest = string.getBytes(StandardCharsets.UTF_8);
digest = messageDigest.digest(digest);
diff --git a/src/main/java/io/fusionauth/jwt/domain/Algorithm.java b/src/main/java/io/fusionauth/jwt/domain/Algorithm.java
index d434fa42..ff977963 100644
--- a/src/main/java/io/fusionauth/jwt/domain/Algorithm.java
+++ b/src/main/java/io/fusionauth/jwt/domain/Algorithm.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016-2019, FusionAuth, All Rights Reserved
+ * Copyright (c) 2016-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,6 +24,18 @@
* @author Daniel DeGroff
*/
public enum Algorithm {
+ /**
+ * Edwards-curve Digital Signature Algorithm (EdDSA) Ed25519
+ * OID: 1.3.101.112
+ */
+ Ed25519("Ed25519"),
+
+ /**
+ * Edwards-curve Digital Signature Algorithm (EdDSA) Ed448
+ * OID: 1.3.101.113
+ */
+ Ed448("Ed448"),
+
/**
* ECDSA using P-256 and SHA-256
* OID: 1.2.840.10045.3.1.7
@@ -64,19 +76,19 @@ public enum Algorithm {
* RSASSA-PSS using SHA-256 and MGF1 with SHA-256
* - SHA256withRSAandMGF1
*/
- PS256("SHA-256"),
+ PS256("SHA256withRSAandMGF1"),
/**
* RSASSA-PSS using SHA-384 and MGF1 with SHA-384
* - SHA384withRSAandMGF1
*/
- PS384("SHA-384"),
+ PS384("SHA384withRSAandMGF1"),
/**
* RSASSA-PSS using SHA-512 and MGF1 with SHA-512
* - SHA512withRSAandMGF1
*/
- PS512("SHA-512"),
+ PS512("SHA512withRSAandMGF1"),
/**
* RSASSA-PKCS1-v1_5 using SHA-256
@@ -118,16 +130,23 @@ public String getName() {
return algorithm;
}
+ public String getDigest() {
+ return switch (this) {
+ case PS256 -> "SHA-256";
+ case PS384 -> "SHA-384";
+ case PS512 -> "SHA-512";
+ default ->
+ throw new IllegalStateException("An incompatible algorithm was provided, this method is only used for RSASSA-PSS algorithms.");
+ };
+ }
+
public int getSaltLength() {
- switch (this) {
- case PS256:
- return 32;
- case PS384:
- return 48;
- case PS512:
- return 64;
- default:
- throw new IllegalStateException("An incompatible algorithm was provided, this method is only used for RSASSA-PSS algorithms.");
- }
+ return switch (this) {
+ case PS256 -> 32;
+ case PS384 -> 48;
+ case PS512 -> 64;
+ default ->
+ throw new IllegalStateException("An incompatible algorithm was provided, this method is only used for RSASSA-PSS algorithms.");
+ };
}
}
diff --git a/src/main/java/io/fusionauth/jwt/domain/KeyType.java b/src/main/java/io/fusionauth/jwt/domain/KeyType.java
index 3b39546b..96ece35e 100644
--- a/src/main/java/io/fusionauth/jwt/domain/KeyType.java
+++ b/src/main/java/io/fusionauth/jwt/domain/KeyType.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016-2019, FusionAuth, All Rights Reserved
+ * Copyright (c) 2016-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,9 @@
import java.util.Objects;
import static io.fusionauth.der.ObjectIdentifier.EC_ENCRYPTION;
+import static io.fusionauth.der.ObjectIdentifier.EdDSA_25519;
+import static io.fusionauth.der.ObjectIdentifier.EdDSA_448;
+import static io.fusionauth.der.ObjectIdentifier.RSASSA_PSS_ENCRYPTION;
import static io.fusionauth.der.ObjectIdentifier.RSA_ENCRYPTION;
/**
@@ -34,19 +37,30 @@
* @author Daniel DeGroff
*/
public enum KeyType {
- RSA,
- EC;
+ RSA("RSA"),
+ RSASSA_PSS("RSASSA-PSS"),
+ EC("EC"),
+ OKP("EdDSA");
+
+ private final String algorithm;
+
+ KeyType(String algorithm) {
+ this.algorithm = algorithm;
+ }
public static KeyType getKeyTypeFromOid(String oid) {
Objects.requireNonNull(oid);
- switch (oid) {
- case EC_ENCRYPTION:
- return EC;
- case RSA_ENCRYPTION:
- return RSA;
- default:
- return null;
- }
+ return switch (oid) {
+ case EC_ENCRYPTION -> EC;
+ case EdDSA_448, EdDSA_25519 -> OKP;
+ case RSA_ENCRYPTION -> RSA;
+ case RSASSA_PSS_ENCRYPTION -> RSASSA_PSS;
+ default -> null;
+ };
+ }
+
+ public String getAlgorithm() {
+ return algorithm;
}
}
diff --git a/src/main/java/io/fusionauth/jwt/ec/ECDSASignature.java b/src/main/java/io/fusionauth/jwt/ec/ECDSASignature.java
index aa208723..94bd6147 100644
--- a/src/main/java/io/fusionauth/jwt/ec/ECDSASignature.java
+++ b/src/main/java/io/fusionauth/jwt/ec/ECDSASignature.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018-2024, FusionAuth, All Rights Reserved
+ * Copyright (c) 2018-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -71,20 +71,13 @@ public byte[] derDecode(Algorithm algorithm) throws IOException {
byte[] s = sequence[1].getPositiveBigInteger().toByteArray();
// The length of the result is fixed and discrete per algorithm.
- byte[] result;
- switch (algorithm) {
- case ES256:
- result = new byte[64];
- break;
- case ES384:
- result = new byte[96];
- break;
- case ES512:
- result = new byte[132];
- break;
- default:
- throw new IllegalArgumentException("Unable to decode the signature for algorithm [" + algorithm.name() + "]");
- }
+ byte[] result = switch (algorithm) {
+ case ES256 -> new byte[64];
+ case ES384 -> new byte[96];
+ case ES512 -> new byte[132];
+ default ->
+ throw new IllegalArgumentException("Unable to decode the signature for algorithm [" + algorithm.name() + "]");
+ };
// Because the response is not encoded, the r and s component must take up an equal amount of the resulting array.
// This allows the consumer of this value to always safely split the value in half based upon an index value since
diff --git a/src/main/java/io/fusionauth/jwt/ec/ECVerifier.java b/src/main/java/io/fusionauth/jwt/ec/ECVerifier.java
index ae9563a4..50da9d56 100644
--- a/src/main/java/io/fusionauth/jwt/ec/ECVerifier.java
+++ b/src/main/java/io/fusionauth/jwt/ec/ECVerifier.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018-2022, FusionAuth, All Rights Reserved
+ * Copyright (c) 2018-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -113,16 +113,11 @@ public static ECVerifier newVerifier(byte[] bytes) {
}
@Override
- @SuppressWarnings("Duplicates")
public boolean canVerify(Algorithm algorithm) {
- switch (algorithm) {
- case ES256:
- case ES384:
- case ES512:
- return true;
- default:
- return false;
- }
+ return switch (algorithm) {
+ case ES256, ES384, ES512 -> true;
+ default -> false;
+ };
}
private void checkFor_CVE_2022_21449(byte[] signature) {
@@ -162,7 +157,7 @@ public void verify(Algorithm algorithm, byte[] message, byte[] signature) {
verifier.update(message);
byte[] derEncoded = new ECDSASignature(signature).derEncode();
- if (!(verifier.verify(derEncoded))) {
+ if (!verifier.verify(derEncoded)) {
throw new InvalidJWTSignatureException();
}
} catch (InvalidKeyException | IOException | NoSuchAlgorithmException | SignatureException | SecurityException e) {
diff --git a/src/main/java/io/fusionauth/jwt/ed/EdDSASigner.java b/src/main/java/io/fusionauth/jwt/ed/EdDSASigner.java
new file mode 100644
index 00000000..08005b32
--- /dev/null
+++ b/src/main/java/io/fusionauth/jwt/ed/EdDSASigner.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2025, FusionAuth, All Rights Reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the License.
+ */
+
+package io.fusionauth.jwt.ed;
+
+import io.fusionauth.jwt.InvalidKeyTypeException;
+import io.fusionauth.jwt.JWTSigningException;
+import io.fusionauth.jwt.MissingPrivateKeyException;
+import io.fusionauth.jwt.Signer;
+import io.fusionauth.jwt.domain.Algorithm;
+import io.fusionauth.pem.domain.PEM;
+
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.interfaces.EdECPrivateKey;
+import java.util.Objects;
+
+/**
+ * @author Daniel DeGroff
+ */
+public class EdDSASigner implements Signer {
+ private final Algorithm algorithm;
+
+ private final String kid;
+
+ private final EdECPrivateKey privateKey;
+
+ private EdDSASigner(PrivateKey privateKey, String kid) {
+ Objects.requireNonNull(privateKey);
+
+ if (!(privateKey instanceof EdECPrivateKey)) {
+ throw new InvalidKeyTypeException("Expecting a private key of type [EdECPrivateKey], but found [" + privateKey.getClass().getSimpleName() + "].");
+ }
+
+ this.kid = kid;
+ this.privateKey = (EdECPrivateKey) privateKey;
+ this.algorithm = Algorithm.fromName(this.privateKey.getParams().getName());
+ if (this.algorithm == null) {
+ throw new InvalidKeyTypeException("Unsupported algorithm reported by the private key. [" + this.privateKey.getParams().getName() + "].");
+ }
+ }
+
+ private EdDSASigner(String privateKey, String kid) {
+ Objects.requireNonNull(privateKey);
+
+ PEM pem = PEM.decode(privateKey);
+ if (pem.privateKey == null) {
+ throw new MissingPrivateKeyException("The provided PEM encoded string did not contain a private key.");
+ }
+
+ if (!(pem.privateKey instanceof EdECPrivateKey)) {
+ throw new InvalidKeyTypeException("Expecting a private key of type [EdECPrivateKey], but found [" + pem.privateKey.getClass().getSimpleName() + "].");
+ }
+
+ this.kid = kid;
+ this.privateKey = pem.getPrivateKey();
+ this.algorithm = Algorithm.fromName(this.privateKey.getParams().getName());
+ if (this.algorithm == null) {
+ throw new InvalidKeyTypeException("Unsupported algorithm reported by the private key. [" + this.privateKey.getParams().getName() + "].");
+ }
+ }
+
+ /**
+ * Build a new EdDSA signer.
+ *
+ * @param privateKey The private key.
+ * @param kid The key identifier. This will be used by the JWTEncoder to write the 'kid' header.
+ * @return a new EdDSA signer.
+ */
+ public static EdDSASigner newSigner(PrivateKey privateKey, String kid) {
+ return new EdDSASigner(privateKey, kid);
+ }
+
+ /**
+ * Build a new EdDSA signer.
+ *
+ * @param privateKey The private key.
+ * @return a new EdDSA signer.
+ */
+ public static EdDSASigner newSigner(PrivateKey privateKey) {
+ return new EdDSASigner(privateKey, null);
+ }
+
+ /**
+ * Build a new EdDSA signer.
+ *
+ * @param privateKey The private key.
+ * @param kid The key identifier. This will be used by the JWTEncoder to write the 'kid' header.
+ * @return a new EdDSA signer.
+ */
+ public static EdDSASigner newSigner(String privateKey, String kid) {
+ return new EdDSASigner(privateKey, kid);
+ }
+
+ /**
+ * Build a new EdDSA signer.
+ *
+ * @param privateKey The private key.
+ * @return a new EdDSA signer.
+ */
+ public static EdDSASigner newSigner(String privateKey) {
+ return new EdDSASigner(privateKey, null);
+ }
+
+ @Override
+ public Algorithm getAlgorithm() {
+ return this.algorithm;
+ }
+
+ @Override
+ public String getKid() {
+ return kid;
+ }
+
+ @Override
+ public byte[] sign(String message) {
+ Objects.requireNonNull(message);
+
+ try {
+ Signature signature = Signature.getInstance(algorithm.getName());
+ signature.initSign(privateKey);
+ signature.update((message).getBytes(StandardCharsets.UTF_8));
+
+ return signature.sign();
+ } catch (InvalidKeyException | NoSuchAlgorithmException | SignatureException e) {
+ throw new JWTSigningException("An unexpected exception occurred when attempting to sign the JWT", e);
+ }
+ }
+}
diff --git a/src/main/java/io/fusionauth/jwt/ed/EdDSAVerifier.java b/src/main/java/io/fusionauth/jwt/ed/EdDSAVerifier.java
new file mode 100644
index 00000000..4b71396d
--- /dev/null
+++ b/src/main/java/io/fusionauth/jwt/ed/EdDSAVerifier.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2025, FusionAuth, All Rights Reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the License.
+ */
+
+package io.fusionauth.jwt.ed;
+
+import io.fusionauth.jwt.InvalidJWTSignatureException;
+import io.fusionauth.jwt.InvalidKeyTypeException;
+import io.fusionauth.jwt.JWTVerifierException;
+import io.fusionauth.jwt.MissingPublicKeyException;
+import io.fusionauth.jwt.Verifier;
+import io.fusionauth.jwt.domain.Algorithm;
+import io.fusionauth.pem.domain.PEM;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.interfaces.EdECPublicKey;
+import java.util.Objects;
+
+/**
+ * @author Daniel DeGroff
+ */
+public class EdDSAVerifier implements Verifier {
+ private final Algorithm algorithm;
+
+ private final EdECPublicKey publicKey;
+
+ private EdDSAVerifier(PublicKey publicKey) {
+ Objects.requireNonNull(publicKey);
+
+ if (!(publicKey instanceof EdECPublicKey)) {
+ throw new InvalidKeyTypeException("Expecting a public key of type [EdECPublicKey], but found [" + publicKey.getClass().getSimpleName() + "].");
+ }
+
+ this.publicKey = (EdECPublicKey) publicKey;
+ this.algorithm = Algorithm.fromName(this.publicKey.getParams().getName());
+ if (this.algorithm == null) {
+ throw new InvalidKeyTypeException("Unsupported algorithm reported by the public key. [" + this.publicKey.getParams().getName() + "].");
+ }
+ }
+
+ private EdDSAVerifier(String publicKey) {
+ Objects.requireNonNull(publicKey);
+
+ PEM pem = PEM.decode(publicKey);
+ if (pem.publicKey == null) {
+ throw new MissingPublicKeyException("The provided PEM encoded string did not contain a public key.");
+ }
+
+ if (!(pem.publicKey instanceof EdECPublicKey)) {
+ throw new InvalidKeyTypeException("Expecting a private key of type [EdECPublicKey], but found [" + pem.privateKey.getClass().getSimpleName() + "].");
+ }
+
+ this.publicKey = pem.getPublicKey();
+ this.algorithm = Algorithm.fromName(this.publicKey.getParams().getName());
+ if (this.algorithm == null) {
+ throw new InvalidKeyTypeException("Unsupported algorithm reported by the public key. [" + this.publicKey.getParams().getName() + "].");
+ }
+ }
+
+ /**
+ * Return a new instance of the EdDSA Verifier with the provided public key.
+ *
+ * @param path The path to the public key PEM.
+ * @return a new instance of the EdDSA verifier.
+ */
+ public static EdDSAVerifier newVerifier(Path path) {
+ Objects.requireNonNull(path);
+
+ try {
+ return new EdDSAVerifier(new String(Files.readAllBytes(path)));
+ } catch (IOException e) {
+ throw new JWTVerifierException("Unable to read the file from path [" + path.toAbsolutePath() + "]", e);
+ }
+ }
+
+ /**
+ * Return a new instance of the EdDSA Verifier with the provided public key.
+ *
+ * @param bytes The bytes of the public key in PEM format.
+ * @return a new instance of the EdDSA verifier.
+ */
+ public static EdDSAVerifier newVerifier(byte[] bytes) {
+ Objects.requireNonNull(bytes);
+ return new EdDSAVerifier(new String(bytes));
+ }
+
+ /**
+ * Return a new instance of the EdDSA Verifier with the provided public key.
+ *
+ * @param publicKey The public key object.
+ * @return a new instance of the EdDSA verifier.
+ */
+ public static EdDSAVerifier newVerifier(PublicKey publicKey) {
+ return new EdDSAVerifier(publicKey);
+ }
+
+ @Override
+ public boolean canVerify(Algorithm algorithm) {
+ return this.algorithm == algorithm;
+ }
+
+ @Override
+ public void verify(Algorithm algorithm, byte[] message, byte[] signature) {
+ Objects.requireNonNull(algorithm);
+ Objects.requireNonNull(message);
+ Objects.requireNonNull(signature);
+
+ try {
+ Signature verifier = Signature.getInstance(algorithm.getName());
+ verifier.initVerify(publicKey);
+ verifier.update(message);
+
+ if (!verifier.verify(signature)) {
+ throw new InvalidJWTSignatureException();
+ }
+ } catch (InvalidKeyException | NoSuchAlgorithmException | SignatureException e) {
+ throw new JWTVerifierException("An unexpected exception occurred when attempting to verify the JWT", e);
+ }
+ }
+}
diff --git a/src/main/java/io/fusionauth/jwt/hmac/HMACVerifier.java b/src/main/java/io/fusionauth/jwt/hmac/HMACVerifier.java
index 64a90248..b1681bca 100644
--- a/src/main/java/io/fusionauth/jwt/hmac/HMACVerifier.java
+++ b/src/main/java/io/fusionauth/jwt/hmac/HMACVerifier.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016-2020, FusionAuth, All Rights Reserved
+ * Copyright (c) 2016-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -91,14 +91,10 @@ public static HMACVerifier newVerifier(byte[] bytes) {
@Override
@SuppressWarnings("Duplicates")
public boolean canVerify(Algorithm algorithm) {
- switch (algorithm) {
- case HS256:
- case HS384:
- case HS512:
- return true;
- default:
- return false;
- }
+ return switch (algorithm) {
+ case HS256, HS384, HS512 -> true;
+ default -> false;
+ };
}
@Override
diff --git a/src/main/java/io/fusionauth/jwt/rsa/RSAPSSSigner.java b/src/main/java/io/fusionauth/jwt/rsa/RSAPSSSigner.java
index 904cba5b..f3caa9f1 100644
--- a/src/main/java/io/fusionauth/jwt/rsa/RSAPSSSigner.java
+++ b/src/main/java/io/fusionauth/jwt/rsa/RSAPSSSigner.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, FusionAuth, All Rights Reserved
+ * Copyright (c) 2020-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -229,7 +229,8 @@ public byte[] sign(String message) {
try {
Signature signature = Signature.getInstance("RSASSA-PSS");
- signature.setParameter(new PSSParameterSpec(algorithm.getName(), "MGF1", new MGF1ParameterSpec(algorithm.getName()), algorithm.getSaltLength(), 1));
+ String digestName = algorithm.getDigest();
+ signature.setParameter(new PSSParameterSpec(digestName, "MGF1", new MGF1ParameterSpec(digestName), algorithm.getSaltLength(), 1));
signature.initSign(privateKey);
signature.update(message.getBytes(StandardCharsets.UTF_8));
return signature.sign();
diff --git a/src/main/java/io/fusionauth/jwt/rsa/RSAPSSVerifier.java b/src/main/java/io/fusionauth/jwt/rsa/RSAPSSVerifier.java
index ae427e1a..bcddded6 100644
--- a/src/main/java/io/fusionauth/jwt/rsa/RSAPSSVerifier.java
+++ b/src/main/java/io/fusionauth/jwt/rsa/RSAPSSVerifier.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, FusionAuth, All Rights Reserved
+ * Copyright (c) 2020-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -122,14 +122,10 @@ public static RSAPSSVerifier newVerifier(byte[] bytes) {
@Override
@SuppressWarnings("Duplicates")
public boolean canVerify(Algorithm algorithm) {
- switch (algorithm) {
- case PS256:
- case PS384:
- case PS512:
- return true;
- default:
- return false;
- }
+ return switch (algorithm) {
+ case PS256, PS384, PS512 -> true;
+ default -> false;
+ };
}
public void verify(Algorithm algorithm, byte[] message, byte[] signature) {
@@ -139,7 +135,8 @@ public void verify(Algorithm algorithm, byte[] message, byte[] signature) {
try {
Signature verifier = Signature.getInstance("RSASSA-PSS");
- verifier.setParameter(new PSSParameterSpec(algorithm.getName(), "MGF1", new MGF1ParameterSpec(algorithm.getName()), algorithm.getSaltLength(), 1));
+ String digestName = algorithm.getDigest();
+ verifier.setParameter(new PSSParameterSpec(digestName, "MGF1", new MGF1ParameterSpec(digestName), algorithm.getSaltLength(), 1));
verifier.initVerify(publicKey);
verifier.update(message);
if (!verifier.verify(signature)) {
diff --git a/src/main/java/io/fusionauth/jwt/rsa/RSAVerifier.java b/src/main/java/io/fusionauth/jwt/rsa/RSAVerifier.java
index c5aee760..f5744042 100644
--- a/src/main/java/io/fusionauth/jwt/rsa/RSAVerifier.java
+++ b/src/main/java/io/fusionauth/jwt/rsa/RSAVerifier.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016-2022, FusionAuth, All Rights Reserved
+ * Copyright (c) 2016-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -119,14 +119,10 @@ public static RSAVerifier newVerifier(byte[] bytes) {
@Override
@SuppressWarnings("Duplicates")
public boolean canVerify(Algorithm algorithm) {
- switch (algorithm) {
- case RS256:
- case RS384:
- case RS512:
- return true;
- default:
- return false;
- }
+ return switch (algorithm) {
+ case RS256, RS384, RS512 -> true;
+ default -> false;
+ };
}
public void verify(Algorithm algorithm, byte[] message, byte[] signature) {
diff --git a/src/main/java/io/fusionauth/pem/PEMDecoder.java b/src/main/java/io/fusionauth/pem/PEMDecoder.java
index fd523341..12976116 100644
--- a/src/main/java/io/fusionauth/pem/PEMDecoder.java
+++ b/src/main/java/io/fusionauth/pem/PEMDecoder.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018-2019, FusionAuth, All Rights Reserved
+ * Copyright (c) 2018-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,12 +23,14 @@
import io.fusionauth.der.Tag;
import io.fusionauth.jwt.domain.KeyType;
import io.fusionauth.pem.domain.PEM;
+import io.fusionauth.security.KeyUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.InvalidParameterException;
import java.security.KeyFactory;
@@ -38,6 +40,7 @@
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.EdECPrivateKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
@@ -79,7 +82,7 @@ public PEM decode(Path path) {
try {
return decode(Files.readAllBytes(path));
} catch (IOException e) {
- throw new PEMDecoderException("Unable to read the file from path [" + path.toAbsolutePath().toString() + "]", e);
+ throw new PEMDecoderException("Unable to read the file from path [" + path.toAbsolutePath() + "]", e);
}
}
@@ -122,7 +125,8 @@ public PEM decode(String encodedKey) {
} else {
throw new PEMDecoderException(new InvalidParameterException("Unexpected PEM Format"));
}
- } catch (CertificateException | InvalidKeyException | InvalidKeySpecException | IOException | NoSuchAlgorithmException e) {
+ } catch (CertificateException | InvalidAlgorithmParameterException | InvalidKeyException | InvalidKeySpecException |
+ IOException | NoSuchAlgorithmException e) {
throw new PEMDecoderException(e);
}
}
@@ -254,7 +258,7 @@ private PEM decode_PKCS_1_Public(String encodedKey) throws IOException, NoSuchAl
return new PEM(KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(modulus, publicExponent)));
}
- private PEM decode_PKCS_8(String encodedKey) throws NoSuchAlgorithmException, IOException, InvalidKeySpecException, InvalidKeyException {
+ private PEM decode_PKCS_8(String encodedKey) throws NoSuchAlgorithmException, IOException, InvalidKeySpecException, InvalidKeyException, InvalidAlgorithmParameterException {
byte[] bytes = getKeyBytes(encodedKey, PKCS_8_PRIVATE_KEY_PREFIX, PKCS_8_PRIVATE_KEY_SUFFIX);
DerValue[] sequence = new DerInputStream(bytes).getSequence();
@@ -271,18 +275,19 @@ private PEM decode_PKCS_8(String encodedKey) throws NoSuchAlgorithmException, IO
// parameters ANY DEFINED BY algorithm OPTIONAL
// }
- if (sequence.length != 3 || !sequence[0].tag.is(Tag.Integer) || !sequence[1].tag.is(Tag.Sequence) || !sequence[2].tag.is(Tag.OctetString)) {
+ // EC and RSA will be length 3, EdDSA will be 4 or 5
+ if (sequence.length < 3 || !sequence[0].tag.is(Tag.Integer) || !sequence[1].tag.is(Tag.Sequence) || !sequence[2].tag.is(Tag.OctetString)) {
// Expect the following format : [ Integer | Sequence | OctetString ]
- throw new InvalidKeyException("Could not decode the private key. Expecting values in the DER encoded sequence in the following format [ Integer | Sequence | OctetString ]");
+ throw new InvalidKeyException("Could not decode the private key. Expecting values in the DER encoded sequence in the following format [ Integer | Sequence | OctetString ] or [ Integer | Sequence | OctetString | Attributes ]");
}
ObjectIdentifier algorithmOID = new DerInputStream(sequence[1].toByteArray()).getOID();
KeyType type = KeyType.getKeyTypeFromOid(algorithmOID.decode());
if (type == null) {
- throw new InvalidKeyException("Could not decode the private key. Expected an EC or RSA key type but found OID [" + algorithmOID.decode() + "] and was unable to match that to a supported algorithm.");
+ throw new InvalidKeyException("Could not decode the private key. Expected an EC, ED or RSA key type but found OID [" + algorithmOID.decode() + "] and was unable to match that to a supported algorithm.");
}
- PrivateKey privateKey = KeyFactory.getInstance(type.name()).generatePrivate(new PKCS8EncodedKeySpec(bytes));
+ PrivateKey privateKey = KeyFactory.getInstance(type.getAlgorithm()).generatePrivate(new PKCS8EncodedKeySpec(bytes));
// Attempt to extract the public key if available
if (privateKey instanceof ECPrivateKey) {
@@ -300,6 +305,33 @@ private PEM decode_PKCS_8(String encodedKey) throws NoSuchAlgorithmException, IO
BigInteger publicExponent = ((RSAPrivateCrtKey) privateKey).getPublicExponent();
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(modulus, publicExponent));
return new PEM(privateKey, publicKey);
+ } else if (privateKey instanceof EdECPrivateKey edECPrivateKey) {
+
+ byte[] algorithmIdentifier = sequence[1].toByteArray();
+ byte[] publicKeyBytes;
+ byte[] derEncodedPublicKey;
+ if (sequence.length >= 4) {
+ int index = sequence.length - 1;
+ publicKeyBytes = sequence[index].toByteArray();
+ derEncodedPublicKey = derEncodePublicKey(algorithmIdentifier, publicKeyBytes);
+ } else {
+ // The private key did not contain the public key. The public key can be derived from the privat key.
+ String curve = KeyUtils.getCurveName(privateKey);
+ publicKeyBytes = KeyUtils.deriveEdDSAPublicKeyFromPrivate(edECPrivateKey.getBytes().orElseThrow(), curve);
+
+ byte[] bitStringKeyBytes = new byte[publicKeyBytes.length + 1];
+ bitStringKeyBytes[0] = 0x0;
+ System.arraycopy(publicKeyBytes, 0, bitStringKeyBytes, 1, publicKeyBytes.length);
+ derEncodedPublicKey = new DerOutputStream()
+ .writeValue(new DerValue(Tag.Sequence, new DerOutputStream()
+ .writeValue(new DerValue(Tag.Sequence, algorithmIdentifier))
+ .writeValue(new DerValue(Tag.BitString, bitStringKeyBytes))))
+ .toByteArray();
+ }
+
+ PublicKey publicKey = KeyFactory.getInstance(edECPrivateKey.getAlgorithm())
+ .generatePublic(new X509EncodedKeySpec(derEncodedPublicKey, edECPrivateKey.getAlgorithm()));
+ return new PEM(privateKey, publicKey);
}
return new PEM(privateKey);
@@ -334,7 +366,7 @@ private PEM decode_X_509(String encodedKey) throws IOException, NoSuchAlgorithmE
throw new InvalidKeyException("Could not decode the X.509 public key. Expected at 2 values in the DER encoded sequence but found [" + sequence.length + "]");
}
- return new PEM(KeyFactory.getInstance(type.name()).generatePublic(new X509EncodedKeySpec(bytes)));
+ return new PEM(KeyFactory.getInstance(type.getAlgorithm()).generatePublic(new X509EncodedKeySpec(bytes)));
}
private byte[] getKeyBytes(String key, String keyPrefix, String keySuffix) {
@@ -345,20 +377,33 @@ private byte[] getKeyBytes(String key, String keyPrefix, String keySuffix) {
return Base64.getDecoder().decode(base64);
}
- private PublicKey getPublicKeyFromPrivateEC(DerValue bitString, ECPrivateKey privateKey) throws InvalidKeySpecException, IOException, NoSuchAlgorithmException {
- // Build an X.509 DER encoded byte array from the provided bitString
+ private byte[] getEncodedPublicKeyFromPrivate(byte[] bitString, byte[] encodedKey) throws IOException {
+ DerValue[] sequence = new DerInputStream(encodedKey).getSequence();
+ return derEncodePublicKey(sequence[1].toByteArray(), bitString);
+ }
+
+ private byte[] derEncodePublicKey(byte[] algorithmIdentifier, byte[] publicKeyBytes) throws IOException {
+ // Build an X.509 DER encoded byte array from the provided byte[]
//
// SubjectPublicKeyInfo ::= SEQUENCE {
// algorithm AlgorithmIdentifier,
// subjectPublicKey BIT STRING
// }
- DerValue[] sequence = new DerInputStream(privateKey.getEncoded()).getSequence();
- byte[] encodedPublicKey = new DerOutputStream()
+ return new DerOutputStream()
.writeValue(new DerValue(Tag.Sequence, new DerOutputStream()
- .writeValue(new DerValue(Tag.Sequence, sequence[1].toByteArray()))
- .writeValue(new DerValue(Tag.BitString, bitString.toByteArray()))))
+ .writeValue(new DerValue(Tag.Sequence, algorithmIdentifier))
+ .writeValue(new DerValue(Tag.BitString, publicKeyBytes))))
.toByteArray();
+ }
+ private PublicKey getPublicKeyFromPrivateEC(DerValue bitString, ECPrivateKey privateKey) throws InvalidKeySpecException, IOException, NoSuchAlgorithmException {
+ // Build an X.509 DER encoded byte array from the provided bitString
+ //
+ // SubjectPublicKeyInfo ::= SEQUENCE {
+ // algorithm AlgorithmIdentifier,
+ // subjectPublicKey BIT STRING
+ // }
+ byte[] encodedPublicKey = getEncodedPublicKeyFromPrivate(bitString.toByteArray(), privateKey.getEncoded());
return KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(encodedPublicKey));
}
}
diff --git a/src/main/java/io/fusionauth/pem/PEMEncoder.java b/src/main/java/io/fusionauth/pem/PEMEncoder.java
index 993e4127..5efc5189 100644
--- a/src/main/java/io/fusionauth/pem/PEMEncoder.java
+++ b/src/main/java/io/fusionauth/pem/PEMEncoder.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018-2019, FusionAuth, All Rights Reserved
+ * Copyright (c) 2018-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,6 +35,7 @@
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
+import java.util.Objects;
import static io.fusionauth.pem.domain.PEM.X509_CERTIFICATE_PREFIX;
import static io.fusionauth.pem.domain.PEM.X509_CERTIFICATE_SUFFIX;
@@ -86,7 +87,7 @@ private String chopIt(String s) {
* If null is passed for one of the two parameters, a PEM will be returned that only includes the non-null
* value.
*
- * Both values may no be null.
+ * Both values may not be null.
*
* @param privateKey the private key
* @param publicKey the public key
@@ -97,13 +98,7 @@ public String encode(PrivateKey privateKey, PublicKey publicKey) {
throw new PEMEncoderException(new InvalidParameterException("At least one key must be provided, they may not both be null"));
}
- Key key;
- if (privateKey == null) {
- key = publicKey;
- } else {
- key = privateKey;
- }
-
+ Key key = Objects.requireNonNullElse(privateKey, publicKey);
StringBuilder sb = new StringBuilder();
addOpeningTag(key, sb);
try {
diff --git a/src/main/java/io/fusionauth/security/KeyUtils.java b/src/main/java/io/fusionauth/security/KeyUtils.java
index 200d432e..3146b064 100644
--- a/src/main/java/io/fusionauth/security/KeyUtils.java
+++ b/src/main/java/io/fusionauth/security/KeyUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, FusionAuth, All Rights Reserved
+ * Copyright (c) 2020-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,16 +16,117 @@
package io.fusionauth.security;
+import io.fusionauth.der.DerInputStream;
+import io.fusionauth.der.DerValue;
+import io.fusionauth.der.ObjectIdentifier;
+
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
import java.security.Key;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
import java.security.interfaces.ECKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.EdECKey;
+import java.security.interfaces.EdECPrivateKey;
+import java.security.interfaces.EdECPublicKey;
import java.security.interfaces.RSAKey;
+import java.security.spec.NamedParameterSpec;
+import java.util.Arrays;
+
+import static io.fusionauth.der.ObjectIdentifier.ECDSA_P256;
+import static io.fusionauth.der.ObjectIdentifier.ECDSA_P384;
+import static io.fusionauth.der.ObjectIdentifier.ECDSA_P521;
+import static io.fusionauth.der.ObjectIdentifier.EdDSA_25519;
+import static io.fusionauth.der.ObjectIdentifier.EdDSA_448;
/**
* @author Daniel DeGroff
*/
public class KeyUtils {
+ /**
+ * @param key the key
+ * @return the name of the curve used by the key or null if it cannot be identified.
+ */
+ public static String getCurveName(Key key) throws IOException {
+ // Match up the Curve Object Identifier to a string value
+ String oid = getCurveOID(key).decode();
+ return switch (oid) {
+ case ECDSA_P256 -> "P-256";
+ case ECDSA_P384 -> "P-384";
+ case ECDSA_P521 -> "P-521";
+ case EdDSA_25519 -> "Ed25519";
+ case EdDSA_448 -> "Ed448";
+ default -> null;
+ };
+ }
+
+ /**
+ * @param key the key
+ * @return the Object Identifier (OID) of the curve used by the key.
+ */
+ public static ObjectIdentifier getCurveOID(Key key) throws IOException {
+ DerValue[] sequence = new DerInputStream(key.getEncoded()).getSequence();
+ if (key instanceof PrivateKey) {
+ if (key instanceof EdECPrivateKey) {
+ return sequence[1].getOID();
+ }
+
+ // Read the first value in the sequence, it is the algorithm OID, the second will be the curve
+ sequence[1].getOID();
+ return sequence[1].getOID();
+ } else {
+ if (key instanceof EdECPublicKey) {
+ return sequence[0].getOID();
+ }
+
+ // Read the first value in the sequence, it is the algorithm OID, the second will be the curve
+ sequence[0].getOID();
+ return sequence[0].getOID();
+ }
+ }
+
+ /**
+ * Calculate the public key for the provided EdDSA private key.
+ *
+ * @param privateKey the private EdDSA key
+ * @param curve the curve used by the private key
+ * @return the public key
+ */
+ public static byte[] deriveEdDSAPublicKeyFromPrivate(byte[] privateKey, String curve) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(curve);
+
+ // Ensure the curve is expected, and we can identify the expected key length.
+ String algorithm = keyPairGenerator.getAlgorithm();
+ int expectedByteLength = switch (algorithm) {
+ case "Ed25519" -> 32;
+ case "Ed448" -> 57;
+ default ->
+ throw new IllegalArgumentException("You specified an unsupported algorithm. The algorithm [" + algorithm + "]"
+ + " is not supported. You must use Ed25519 or Ed448.");
+ };
+
+ // Ensure the caller provided a key of the correct length.
+ if (privateKey.length != expectedByteLength) {
+ throw new IllegalArgumentException("The provided privateKey length is unexpected. Expected [" + expectedByteLength + "] but found [" + privateKey.length + "]");
+ }
+
+ keyPairGenerator.initialize(new NamedParameterSpec(curve), new SecureRandom() {
+ public void nextBytes(byte[] bytes) {
+ // Note that because we pass the curve to the NamedParameterSpec constructor, it would be unexpected that the provided
+ // byte array would not fit the expected key length. As a fail save, ensure it fits.
+ if (bytes.length != privateKey.length) {
+ throw new IllegalStateException("Provided bytes array is not large enough for the key. Expected [" + privateKey.length + "] but found [" + bytes.length + "]");
+ }
+ System.arraycopy(privateKey, 0, bytes, 0, privateKey.length);
+ }
+ });
+ byte[] spki = keyPairGenerator.generateKeyPair().getPublic().getEncoded();
+ return Arrays.copyOfRange(spki, spki.length - privateKey.length, spki.length);
+ }
/**
* Return the length of the key in bits.
@@ -57,6 +158,14 @@ public static int getKeyLength(Key key) {
return ((bytes / 8) * 8) * 8;
} else if (key instanceof RSAKey rsaKey) {
return rsaKey.getModulus().bitLength();
+ } else if (key instanceof EdECKey edECKey) {
+ // Only recognizing Ed25519 and Ed448.
+ String curve = edECKey.getParams().getName();
+ if ("Ed25519".equals(curve)) {
+ return 32;
+ } else if ("Ed448".equals(curve)) {
+ return 57;
+ }
}
throw new IllegalArgumentException();
diff --git a/src/test/java/io/fusionauth/BaseTest.java b/src/test/java/io/fusionauth/BaseTest.java
index 4e8db46a..1b94b55b 100644
--- a/src/test/java/io/fusionauth/BaseTest.java
+++ b/src/test/java/io/fusionauth/BaseTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, FusionAuth, All Rights Reserved
+ * Copyright (c) 2020-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -32,11 +32,24 @@
* @author Daniel DeGroff
*/
public abstract class BaseTest {
+ public static boolean FipsEnabled;
+
public List
+ * Key lengths can differ, and when encoding larger integers in DER encode sequences, or parsing them in and out of
+ * JWK formats, we want to be certain we are not making incorrect assumptions. During development, you may wish to
+ * run some of these with 5-10k invocation counts to ensure these types of anomalies are un-covered and addressed.
+ *
+ * It may be reasonable to reduce the invocation counts if tests take too long to run - once we know that the tests
+ * will pass with a high number of invocations. However, the time is not yet that significant, and there is value to
+ * ensuring that the same result can be expected regardless of the number of times we run the same test.
+ *
* @author Daniel DeGroff
*/
public class JSONWebKeyBuilderTest extends BaseJWTTest {
@@ -120,6 +137,41 @@ public void rsa_private() throws Exception {
assertJSONEquals(JSONWebKey.build(privateKey), "src/test/resources/jwk/rsa_private_key_jwk_control.json");
}
+ @Test
+ public void rsa_pss_private() throws Exception {
+ // RSA PSS private key
+ RSAPrivateKey privateKey = PEM.decode(Paths.get("src/test/resources/rsa_pss_private_key_2048.pem")).getPrivateKey();
+ // Note that the alg property in the JWK is optional, and with an RSA key we don't know the algorithm.
+ // - This key could be used with PS256, PS384 or PS512.
+ assertJSONEquals(JSONWebKey.build(privateKey), "src/test/resources/jwk/rsa_pss_private_key_2048.json");
+
+ // See!
+ Signer signer = RSAPSSSigner.newSHA256Signer(privateKey);
+ String message = "hello world!";
+ byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
+ byte[] signature = signer.sign(message);
+
+ RSAPublicKey publicKey = PEM.decode(Paths.get("src/test/resources/rsa_pss_public_key_2048.pem")).getPublicKey();
+ Verifier verifier = RSAPSSVerifier.newVerifier(publicKey);
+ verifier.canVerify(Algorithm.PS256);
+ verifier.canVerify(Algorithm.PS384);
+ verifier.canVerify(Algorithm.PS512);
+ verifier.verify(Algorithm.PS256, messageBytes, signature);
+ }
+
+ @Test
+ public void rsa_pss_public() throws Exception {
+ // RSA PSS public key
+ RSAPublicKey publicKey = PEM.decode(Paths.get("src/test/resources/rsa_pss_public_key_2048.pem")).getPublicKey();
+ // Note that the alg property in the JWK is optional, and with an RSA key we don't know the algorithm.
+ // - This key could be used with PS256, PS384 or PS512.
+ assertJSONEquals(JSONWebKey.build(publicKey), "src/test/resources/jwk/rsa_pss_public_key_2048.json");
+
+ // X.509 cert, the certificate will contain the algorithm 'SHA256withRSAandMGF1' so we will expect PS256 in the JWK
+ Certificate certificate = PEM.decode(Paths.get("src/test/resources/rsa_pss_public_key_2048_certificate.pem")).certificate;
+ assertJSONEquals(JSONWebKey.build(certificate), "src/test/resources/jwk/rsa_pss_public_key_2048_certificate.json");
+ }
+
@Test
public void embedded_jwk() {
JWT jwt = new JWT();
@@ -164,4 +216,34 @@ public void rsa_public() throws Exception {
Certificate cert2 = PEM.decode(Paths.get("src/test/resources/rsa_certificate_gd_bundle_g2.pem")).certificate;
assertJSONEquals(JSONWebKey.build(cert2), "src/test/resources/jwk/rsa_certificate_gd_bundle_g2.json");
}
+
+ @Test(invocationCount = 100)
+ public void eddsa_private() throws Exception {
+ // ed25519
+ EdECPrivateKey key25519 = PEM.decode(Paths.get("src/test/resources/ed_dsa_ed25519_private_key.pem")).getPrivateKey();
+ assertJSONEquals(JSONWebKey.build(key25519), "src/test/resources/jwk/ed_dsa_ed25519_private_key.json");
+
+ // ed448
+ EdECPrivateKey key448 = PEM.decode(Paths.get("src/test/resources/ed_dsa_ed448_private_key.pem")).getPrivateKey();
+ assertJSONEquals(JSONWebKey.build(key448), "src/test/resources/jwk/ed_dsa_ed448_private_key.json");
+ }
+
+ @Test(invocationCount = 100)
+ public void eddsa_public() throws Exception {
+ // ed25519
+ EdECPublicKey key25519 = PEM.decode(Paths.get("src/test/resources/ed_dsa_ed25519_public_key.pem")).getPublicKey();
+ assertJSONEquals(JSONWebKey.build(key25519), "src/test/resources/jwk/ed_dsa_ed25519_public_key.json");
+
+ // X.509 PEM encoded
+ Certificate cert25519 = PEM.decode(Paths.get("src/test/resources/ed_dsa_ed25519_certificate.pem")).certificate;
+ assertJSONEquals(JSONWebKey.build(cert25519), "src/test/resources/jwk/ed_dsa_ed25519_certificate.json");
+
+ // ed448
+ EdECPublicKey key448 = PEM.decode(Paths.get("src/test/resources/ed_dsa_ed448_public_key.pem")).getPublicKey();
+ assertJSONEquals(JSONWebKey.build(key448), "src/test/resources/jwk/ed_dsa_ed448_public_key.json");
+
+ // X.509 PEM encoded
+ Certificate cert448 = PEM.decode(Paths.get("src/test/resources/ed_dsa_ed448_certificate.pem")).certificate;
+ assertJSONEquals(JSONWebKey.build(cert448), "src/test/resources/jwk/ed_dsa_ed448_certificate.json");
+ }
}
diff --git a/src/test/java/io/fusionauth/jwks/JSONWebKeyParserTest.java b/src/test/java/io/fusionauth/jwks/JSONWebKeyParserTest.java
index 6c0c49b9..b1025e87 100644
--- a/src/test/java/io/fusionauth/jwks/JSONWebKeyParserTest.java
+++ b/src/test/java/io/fusionauth/jwks/JSONWebKeyParserTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018-2019, FusionAuth, All Rights Reserved
+ * Copyright (c) 2018-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,12 +35,22 @@
import static io.fusionauth.jwks.JWKUtils.base64DecodeUint;
import static io.fusionauth.jwks.JWKUtils.base64EncodeUint;
-import static io.fusionauth.jwt.domain.Algorithm.RS256;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
import static org.testng.Assert.fail;
/**
+ * Note that the higher invocationCount parameters are helpful to indentify incorrect assumptions in key parsing.
+ *
+ * Key lengths can differ, and when encoding larger integers in DER encode sequences, or parsing them in and out of
+ * JWK formats, we want to be certain we are not making incorrect assumptions. During development, you may wish to
+ * run some of these with 5-10k invocation counts to ensure these types of anomalies are un-covered and addressed.
+ *
+ * It may be reasonable to reduce the invocation counts if tests take too long to run - once we know that the tests
+ * will pass with a high number of invocations. However, the time is not yet that significant, and there is value to
+ * ensuring that the same result can be expected regardless of the number of times we run the same test.
+ *
* @author Daniel DeGroff
*/
public class JSONWebKeyParserTest extends BaseJWTTest {
@@ -136,7 +146,6 @@ public void parse_ec_keys(String curve, String x, String y) {
assertEquals(JSONWebKey.build(pem.publicKey).y, expected.y);
}
-
@Test(dataProvider = "rsaPublicKeys")
public void parse_well_known(String exponent, String modulus) {
JSONWebKey expected = new JSONWebKey();
@@ -184,7 +193,7 @@ public void unsignedEncodingTest() {
assertEquals(key.e, encodedE);
}
- @Test
+ @Test(invocationCount = 1_000)
public void parse_ec() {
KeyPair keyPair = JWTUtils.generate256_ECKeyPair();
@@ -206,13 +215,60 @@ public void parse_ec() {
assertEquals(JSONWebKey.build(pem.publicKey).y, expected.y);
}
- @Test
+ @DataProvider(name = "EdDSACurves")
+ public Object[][] EdDSACurves() {
+ return new Object[][]{
+ {"Ed25519"},
+ {"Ed448"},
+ };
+ }
+
+ @Test(dataProvider = "EdDSACurves", invocationCount = 1_000)
+ public void parse_eddsa(String curve) {
+ KeyPair keyPair = curve.equals("Ed25519")
+ ? JWTUtils.generate_ed25519_EdDSAKeyPair()
+ : JWTUtils.generate_ed448_EdDSAKeyPair();
+
+ // Build a JSON Web Key from our own EdDSA key pair
+ JSONWebKey expectedPublicJWK = JSONWebKey.build(keyPair.publicKey);
+ expectedPublicJWK.alg = Algorithm.Ed25519;
+ expectedPublicJWK.kty = KeyType.OKP;
+ assertNotNull(expectedPublicJWK.x);
+ assertNull(expectedPublicJWK.y);
+
+ PublicKey publicKey = JSONWebKey.parse(expectedPublicJWK);
+ assertNotNull(publicKey);
+
+ // Compare to the original expected key
+ String encodedPublicPEM = PEM.encode(publicKey);
+ assertEquals(JSONWebKey.build(encodedPublicPEM).x, expectedPublicJWK.x);
+
+ // Get the public key from the PEM, and assert against the expected values
+ PEM pem = PEM.decode(encodedPublicPEM);
+ assertEquals(JSONWebKey.build(pem.publicKey).x, expectedPublicJWK.x);
+
+ // Build a JWK of the private key
+ JSONWebKey expectedPrivateJWK = JSONWebKey.build(keyPair.privateKey);
+ expectedPrivateJWK.alg = Algorithm.Ed25519;
+ expectedPrivateJWK.kty = KeyType.OKP;
+ assertNotNull(expectedPrivateJWK.d);
+ assertNotNull(expectedPrivateJWK.x);
+ assertNull(expectedPrivateJWK.e);
+ assertNull(expectedPrivateJWK.p);
+ assertNull(expectedPrivateJWK.q);
+ assertNull(expectedPrivateJWK.y);
+
+ // x should match between public and private
+ assertEquals(expectedPrivateJWK.x, expectedPublicJWK.x);
+ }
+
+ @Test(invocationCount = 100)
public void parse_rsa() {
KeyPair keyPair = JWTUtils.generate2048_RSAKeyPair();
// Build a JSON Web Key from our own RSA key pair
JSONWebKey expected = JSONWebKey.build(keyPair.publicKey);
- expected.alg = RS256;
+ expected.alg = Algorithm.RS256;
PublicKey publicKey = JSONWebKey.parse(expected);
assertNotNull(publicKey);
diff --git a/src/test/java/io/fusionauth/jwt/JWTTest.java b/src/test/java/io/fusionauth/jwt/JWTTest.java
index affceb23..c0ccbbbb 100644
--- a/src/test/java/io/fusionauth/jwt/JWTTest.java
+++ b/src/test/java/io/fusionauth/jwt/JWTTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016-2024, FusionAuth, All Rights Reserved
+ * Copyright (c) 2016-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -59,6 +59,16 @@
import static org.testng.Assert.assertTrue;
/**
+ * Note that the higher invocationCount parameters are helpful to indentify incorrect assumptions in key parsing.
+ *
+ * Key lengths can differ, and when encoding larger integers in DER encode sequences, or parsing them in and out of
+ * JWK formats, we want to be certain we are not making incorrect assumptions. During development, you may wish to
+ * run some of these with 5-10k invocation counts to ensure these types of anomalies are un-covered and addressed.
+ *
+ * It may be reasonable to reduce the invocation counts if tests take too long to run - once we know that the tests
+ * will pass with a high number of invocations. However, the time is not yet that significant, and there is value to
+ * ensuring that the same result can be expected regardless of the number of times we run the same test.
+ *
* @author Daniel DeGroff
*/
public class JWTTest extends BaseJWTTest {
@@ -179,7 +189,7 @@ public void decoding_performance() throws Exception {
BigDecimal average = durationInMillis.divide(BigDecimal.valueOf(iterationCount), RoundingMode.HALF_DOWN);
long perSecond = iterationCount / (duration.toMillis() / 1000);
- System.out.println("[" + signer.getAlgorithm().getName() + "] " + duration.toMillis() + " milliseconds total. [" + iterationCount + "] iterations. [" + average + "] milliseconds per iteration. Approx. [" + perSecond + "] per second.");
+ System.out.println("[" + signer.getAlgorithm().name() + "] " + duration.toMillis() + " milliseconds total. [" + iterationCount + "] iterations. [" + average + "] milliseconds per iteration. Approx. [" + perSecond + "] per second.");
}
}
@@ -439,7 +449,6 @@ public void test_RS256() {
}
@Test
- @RequiresAlgorithm("RSASSA-PSS")
public void test_PS256() throws IOException {
JWT jwt = new JWT().setSubject("1234567890");
@@ -455,7 +464,6 @@ public void test_PS256() throws IOException {
}
@Test
- @RequiresAlgorithm("RSASSA-PSS")
public void test_PS384() throws IOException {
JWT jwt = new JWT().setSubject("1234567890");
@@ -471,7 +479,6 @@ public void test_PS384() throws IOException {
}
@Test
- @RequiresAlgorithm("RSASSA-PSS")
public void test_PS512() throws IOException {
JWT jwt = new JWT().setSubject("1234567890");
diff --git a/src/test/java/io/fusionauth/jwt/JWTUtilsTest.java b/src/test/java/io/fusionauth/jwt/JWTUtilsTest.java
index 74bdd0d5..e8a40b96 100644
--- a/src/test/java/io/fusionauth/jwt/JWTUtilsTest.java
+++ b/src/test/java/io/fusionauth/jwt/JWTUtilsTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016-2020, FusionAuth, All Rights Reserved
+ * Copyright (c) 2016-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,6 +29,8 @@
import java.nio.charset.StandardCharsets;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.EdECPrivateKey;
+import java.security.interfaces.EdECPublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Base64;
@@ -61,7 +63,7 @@ public void decodePayload() {
@Test
public void generateECKey() {
- // 256 bit key
+ // 256-bit key
KeyPair keyPair256 = JWTUtils.generate256_ECKeyPair();
ECPrivateKey privateKey256 = PEM.decode(keyPair256.privateKey).getPrivateKey();
ECPublicKey publicKey256 = PEM.decode(keyPair256.publicKey).getPublicKey();
@@ -87,7 +89,7 @@ public void generateECKey() {
assertEquals(actualPrivateKey256, keyPair256.privateKey);
assertEquals(actualPublicKey256, keyPair256.publicKey);
- // 384 bit key
+ // 384-bit key
KeyPair keyPair384 = JWTUtils.generate384_ECKeyPair();
ECPrivateKey privateKey384 = PEM.decode(keyPair384.privateKey).getPrivateKey();
ECPublicKey publicKey384 = PEM.decode(keyPair384.publicKey).getPublicKey();
@@ -113,7 +115,7 @@ public void generateECKey() {
assertEquals(actualPrivateKey384, keyPair384.privateKey);
assertEquals(actualPublicKey384, keyPair384.publicKey);
- // 521 bit key
+ // 521-bit key
KeyPair keyPair521 = JWTUtils.generate521_ECKeyPair();
ECPrivateKey privateKey521 = PEM.decode(keyPair521.privateKey).getPrivateKey();
ECPublicKey publicKey521 = PEM.decode(keyPair521.publicKey).getPublicKey();
@@ -140,9 +142,114 @@ public void generateECKey() {
assertEquals(actualPublicKey521, keyPair521.publicKey);
}
+ @Test
+ public void generate_ed25519_EdDSAKeyPair() {
+ KeyPair keyPair = JWTUtils.generate_ed25519_EdDSAKeyPair();
+ EdECPrivateKey privateKey = PEM.decode(keyPair.privateKey).getPrivateKey();
+ EdECPublicKey publicKey = PEM.decode(keyPair.publicKey).getPublicKey();
+
+ assertEquals(privateKey.getAlgorithm(), FipsEnabled ? "Ed25519" : "EdDSA");
+ assertEquals(privateKey.getFormat(), "PKCS#8");
+ assertEquals(privateKey.getParams().getName(), "Ed25519");
+ assertEquals(privateKey.getBytes().orElseThrow().length, 32);
+
+ assertEquals(publicKey.getAlgorithm(), FipsEnabled ? "Ed25519" : "EdDSA");
+ assertEquals(publicKey.getFormat(), "X.509");
+ }
+
+ @Test
+ public void generate_ed448_EdDSAKeyPair() {
+ KeyPair keyPair = JWTUtils.generate_ed448_EdDSAKeyPair();
+ EdECPrivateKey privateKey = PEM.decode(keyPair.privateKey).getPrivateKey();
+ EdECPublicKey publicKey = PEM.decode(keyPair.publicKey).getPublicKey();
+
+ assertEquals(privateKey.getAlgorithm(), FipsEnabled ? "Ed448" : "EdDSA");
+ assertEquals(privateKey.getFormat(), "PKCS#8");
+ assertEquals(privateKey.getParams().getName(), "Ed448");
+ assertEquals(privateKey.getBytes().orElseThrow().length, 57);
+
+ assertEquals(publicKey.getAlgorithm(), FipsEnabled ? "Ed448" : "EdDSA");
+ assertEquals(publicKey.getFormat(), "X.509");
+ }
+
+ @Test
+ public void generateRSAPSS_key() {
+ // 2048-bit key
+ KeyPair keyPair2048 = JWTUtils.generate2048_RSAPSSKeyPair();
+ RSAPrivateKey privateKey2048 = PEM.decode(keyPair2048.privateKey).getPrivateKey();
+ RSAPublicKey publicKey2048 = PEM.decode(keyPair2048.publicKey).getPublicKey();
+
+ assertEquals(privateKey2048.getModulus().bitLength(), 2048);
+ assertEquals(privateKey2048.getAlgorithm(), "RSASSA-PSS");
+ assertEquals(privateKey2048.getFormat(), "PKCS#8");
+
+ assertEquals(publicKey2048.getModulus().bitLength(), 2048);
+ assertEquals(publicKey2048.getAlgorithm(), "RSASSA-PSS");
+ assertEquals(publicKey2048.getFormat(), "X.509");
+
+ assertPrefix(keyPair2048.privateKey, PKCS_8_PRIVATE_KEY_PREFIX);
+ assertSuffix(keyPair2048.privateKey, PKCS_8_PRIVATE_KEY_SUFFIX);
+ assertPrefix(keyPair2048.publicKey, X509_PUBLIC_KEY_PREFIX);
+ assertSuffix(keyPair2048.publicKey, X509_PUBLIC_KEY_SUFFIX);
+
+ // Now go backwards from the key to a PEM and assert they come out the same.
+ String actualPrivateKey2048 = PEM.encode(privateKey2048);
+ String actualPublicKey2048 = PEM.encode(publicKey2048);
+ assertEquals(actualPrivateKey2048, keyPair2048.privateKey);
+ assertEquals(actualPublicKey2048, keyPair2048.publicKey);
+
+ // 3072-bit key
+ KeyPair keyPair3072 = JWTUtils.generate3072_RSAPSSKeyPair();
+ RSAPrivateKey privateKey3072 = PEM.decode(keyPair3072.privateKey).getPrivateKey();
+ RSAPublicKey publicKey3072 = PEM.decode(keyPair3072.publicKey).getPublicKey();
+
+ assertEquals(privateKey3072.getModulus().bitLength(), 3072);
+ assertEquals(privateKey3072.getAlgorithm(), "RSASSA-PSS");
+ assertEquals(privateKey3072.getFormat(), "PKCS#8");
+
+ assertEquals(publicKey3072.getModulus().bitLength(), 3072);
+ assertEquals(publicKey3072.getAlgorithm(), "RSASSA-PSS");
+ assertEquals(publicKey3072.getFormat(), "X.509");
+
+ assertPrefix(keyPair3072.privateKey, PKCS_8_PRIVATE_KEY_PREFIX);
+ assertSuffix(keyPair3072.privateKey, PKCS_8_PRIVATE_KEY_SUFFIX);
+ assertPrefix(keyPair3072.publicKey, X509_PUBLIC_KEY_PREFIX);
+ assertSuffix(keyPair3072.publicKey, X509_PUBLIC_KEY_SUFFIX);
+
+ // Now go backwards from the key to a PEM and assert they come out the same.
+ String actualPrivateKey3072 = PEM.encode(privateKey3072);
+ String actualPublicKey3072 = PEM.encode(publicKey3072);
+ assertEquals(actualPrivateKey3072, keyPair3072.privateKey);
+ assertEquals(actualPublicKey3072, keyPair3072.publicKey);
+
+ // 4096-bit key
+ KeyPair keyPair4096 = JWTUtils.generate4096_RSAPSSKeyPair();
+ RSAPrivateKey privateKey4096 = PEM.decode(keyPair4096.privateKey).getPrivateKey();
+ RSAPublicKey publicKey4096 = PEM.decode(keyPair4096.publicKey).getPublicKey();
+
+ assertEquals(privateKey4096.getModulus().bitLength(), 4096);
+ assertEquals(privateKey4096.getAlgorithm(), "RSASSA-PSS");
+ assertEquals(privateKey4096.getFormat(), "PKCS#8");
+
+ assertEquals(publicKey4096.getModulus().bitLength(), 4096);
+ assertEquals(publicKey4096.getAlgorithm(), "RSASSA-PSS");
+ assertEquals(publicKey4096.getFormat(), "X.509");
+
+ assertPrefix(keyPair4096.privateKey, PKCS_8_PRIVATE_KEY_PREFIX);
+ assertSuffix(keyPair4096.privateKey, PKCS_8_PRIVATE_KEY_SUFFIX);
+ assertPrefix(keyPair4096.publicKey, X509_PUBLIC_KEY_PREFIX);
+ assertSuffix(keyPair4096.publicKey, X509_PUBLIC_KEY_SUFFIX);
+
+ // Now go backwards from the key to a PEM and assert they come out the same.
+ String actualPrivateKey4096 = PEM.encode(privateKey4096);
+ String actualPublicKey4096 = PEM.encode(publicKey4096);
+ assertEquals(actualPrivateKey4096, keyPair4096.privateKey);
+ assertEquals(actualPublicKey4096, keyPair4096.publicKey);
+ }
+
@Test
public void generateRSAKey() {
- // 2048 bit key
+ // 2048-bit key
KeyPair keyPair2048 = JWTUtils.generate2048_RSAKeyPair();
RSAPrivateKey privateKey2048 = PEM.decode(keyPair2048.privateKey).getPrivateKey();
RSAPublicKey publicKey2048 = PEM.decode(keyPair2048.publicKey).getPublicKey();
@@ -166,7 +273,7 @@ public void generateRSAKey() {
assertEquals(actualPrivateKey2048, keyPair2048.privateKey);
assertEquals(actualPublicKey2048, keyPair2048.publicKey);
- // 3072 bit key
+ // 3072-bit key
KeyPair keyPair3072 = JWTUtils.generate3072_RSAKeyPair();
RSAPrivateKey privateKey3072 = PEM.decode(keyPair3072.privateKey).getPrivateKey();
RSAPublicKey publicKey3072 = PEM.decode(keyPair3072.publicKey).getPublicKey();
@@ -190,7 +297,7 @@ public void generateRSAKey() {
assertEquals(actualPrivateKey3072, keyPair3072.privateKey);
assertEquals(actualPublicKey3072, keyPair3072.publicKey);
- // 4096 bit key
+ // 4096-bit key
KeyPair keyPair4096 = JWTUtils.generate4096_RSAKeyPair();
RSAPrivateKey privateKey4096 = PEM.decode(keyPair4096.privateKey).getPrivateKey();
RSAPublicKey publicKey4096 = PEM.decode(keyPair4096.publicKey).getPublicKey();
diff --git a/src/test/java/io/fusionauth/jwt/OpenIdConnectTest.java b/src/test/java/io/fusionauth/jwt/OpenIdConnectTest.java
index 9c754439..172b48f1 100644
--- a/src/test/java/io/fusionauth/jwt/OpenIdConnectTest.java
+++ b/src/test/java/io/fusionauth/jwt/OpenIdConnectTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016-2019, FusionAuth, All Rights Reserved
+ * Copyright (c) 2016-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
package io.fusionauth.jwt;
+import io.fusionauth.BaseTest;
import io.fusionauth.jwt.domain.Algorithm;
import org.testng.annotations.Test;
@@ -27,7 +28,7 @@
/**
* @author Daniel DeGroff
*/
-public class OpenIdConnectTest {
+public class OpenIdConnectTest extends BaseTest {
@Test
public void test_at_hash() {
assertEquals(at_hash("dNZX1hEZ9wBCzNL40Upu646bdzQA", Algorithm.HS256), "wfgvmE9VxjAudsl9lc6TqA");
@@ -42,9 +43,31 @@ public void test_at_hash() {
assertEquals(at_hash("dNZX1hEZ9wBCzNL40Upu646bdzQA", Algorithm.RS384), "phZaPQJosyg-qi-OIYyQ3xJB9wsHYEEz");
assertEquals(at_hash("dNZX1hEZ9wBCzNL40Upu646bdzQA", Algorithm.RS512), "8xltSlOGYrWy8W9yNvRlEth1i_bXW-JROWPLvCv5zog");
+ assertEquals(at_hash("dNZX1hEZ9wBCzNL40Upu646bdzQA", Algorithm.Ed25519), "8xltSlOGYrWy8W9yNvRlEth1i_bXW-JROWPLvCv5zog");
+ requiresShake256(() ->
+ assertEquals(at_hash("dNZX1hEZ9wBCzNL40Upu646bdzQA", Algorithm.Ed448), "ACuRpk9jl5IEa3yqpBCNNOCpBEI7qjud6mc80cs6vWX2fcqpsk8RozYBKTUuSS6SqJhw302xFZeM"));
+
// Controls
assertEquals(at_hash("1940a308-d492-3660-a9f8-46723cc582e9", Algorithm.RS256), "JrZY9MtYVEIIJUx-DDBmww");
assertEquals(at_hash("jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y", Algorithm.RS256), "77QmUPtjPfzWtF2AnpK9RQ");
+ // https://bitbucket.org/openid/connect/issues/1125
+ requiresShake256(() ->
+ assertEquals(at_hash("YmJiZTAwYmYtMzgyOC00NzhkLTkyOTItNjJjNDM3MGYzOWIy9sFhvH8K_x8UIHj1osisS57f5DduL", Algorithm.Ed448), "sB_U72jyb0WgtX8TsVoqJnm6CD295W9gfSDRxkilB3LAL7REi9JYutRW_s1yE4lD8cOfMZf83gi4"));
+ }
+
+ private void requiresShake256(Runnable runnable) {
+ // The JCA does not ship with SHAKE256 which will be used to calculate the hash for Ed448.
+ // - Expect failure unless FIPS has been enabled.
+ try {
+ runnable.run();
+ if (!FipsEnabled) {
+ fail("Expected this to fail unless FIPS was enabled.");
+ }
+ } catch (Exception e) {
+ if (FipsEnabled) {
+ throw e;
+ }
+ }
}
@Test
@@ -61,6 +84,13 @@ public void test_c_hash() {
assertEquals(c_hash("dNZX1hEZ9wBCzNL40Upu646bdzQA", Algorithm.RS384), "phZaPQJosyg-qi-OIYyQ3xJB9wsHYEEz");
assertEquals(c_hash("dNZX1hEZ9wBCzNL40Upu646bdzQA", Algorithm.RS512), "8xltSlOGYrWy8W9yNvRlEth1i_bXW-JROWPLvCv5zog");
+ assertEquals(c_hash("dNZX1hEZ9wBCzNL40Upu646bdzQA", Algorithm.Ed25519), "8xltSlOGYrWy8W9yNvRlEth1i_bXW-JROWPLvCv5zog");
+
+ // The JCA does not ship with SHAKE256 which will be used to calculate the hash for Ed448.
+ // - Expect this to fail unless FIPS has been enabled.
+ requiresShake256(() ->
+ assertEquals(c_hash("dNZX1hEZ9wBCzNL40Upu646bdzQA", Algorithm.Ed448), "ACuRpk9jl5IEa3yqpBCNNOCpBEI7qjud6mc80cs6vWX2fcqpsk8RozYBKTUuSS6SqJhw302xFZeM"));
+
// Controls
assertEquals(c_hash("16fd899f-5f0c-3114-875e-2547b629cd05", Algorithm.HS256), "S5UOXRNNyYsI6Z0G3xxdpw");
assertEquals(c_hash("Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk", Algorithm.HS256), "LDktKdoQak3Pk0cnXxCltA");
diff --git a/src/test/java/io/fusionauth/jwt/RequiresAlgorithm.java b/src/test/java/io/fusionauth/jwt/RequiresAlgorithm.java
deleted file mode 100644
index 3a55904c..00000000
--- a/src/test/java/io/fusionauth/jwt/RequiresAlgorithm.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (c) 2020, FusionAuth, All Rights Reserved
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
- * either express or implied. See the License for the specific
- * language governing permissions and limitations under the License.
- */
-
-package io.fusionauth.jwt;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-import static java.lang.annotation.ElementType.METHOD;
-
-/**
- * Test marker annotation to indicate the test should be only be run when a particular algorithm is available.
- *
- * @author Daniel DeGroff
- */
-@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
-@Target({METHOD})
-public @interface RequiresAlgorithm {
- String value();
-}
\ No newline at end of file
diff --git a/src/test/java/io/fusionauth/jwt/TestNGAnnotationTransformer.java b/src/test/java/io/fusionauth/jwt/TestNGAnnotationTransformer.java
deleted file mode 100644
index 1d5a38bf..00000000
--- a/src/test/java/io/fusionauth/jwt/TestNGAnnotationTransformer.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (c) 2020, FusionAuth, All Rights Reserved
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
- * either express or implied. See the License for the specific
- * language governing permissions and limitations under the License.
- */
-
-package io.fusionauth.jwt;
-
-import org.testng.IAnnotationTransformer;
-import org.testng.annotations.ITestAnnotation;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
-import java.security.Signature;
-
-/**
- * Test NG transformer used to disable tests at runtime.
- *
- * @author Daniel DeGroff
- */
-@SuppressWarnings("unused")
-public class TestNGAnnotationTransformer implements IAnnotationTransformer {
- private static boolean RSAProbabilisticSignatureSchemaAvailable;
-
- static {
- try {
- Signature.getInstance("RSASSA-PSS");
- RSAProbabilisticSignatureSchemaAvailable = true;
- } catch (Exception ignore) {
- }
- }
-
- @Override
- public void transform(ITestAnnotation annotation, Class testClass, Constructor testConstructor, Method testMethod) {
- RequiresAlgorithm requiresAlgorithm = testMethod.getAnnotation(RequiresAlgorithm.class);
- if (requiresAlgorithm != null) {
- // Only run these tests if the RSASSA PSS algorithm is available
- if (requiresAlgorithm.value().equals("RSASSA-PSS")) {
- annotation.setEnabled(RSAProbabilisticSignatureSchemaAvailable);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/test/java/io/fusionauth/jwt/VerifierTest.java b/src/test/java/io/fusionauth/jwt/VerifierTest.java
index a3d9d4ab..d9c15209 100644
--- a/src/test/java/io/fusionauth/jwt/VerifierTest.java
+++ b/src/test/java/io/fusionauth/jwt/VerifierTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016-2019, FusionAuth, All Rights Reserved
+ * Copyright (c) 2016-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -38,6 +38,16 @@
public class VerifierTest {
static List
+ * Key lengths can differ, and when encoding larger integers in DER encode sequences, or parsing them in and out of
+ * JWK formats, we want to be certain we are not making incorrect assumptions. During development, you may wish to
+ * run some of these with 5-10k invocation counts to ensure these types of anomalies are un-covered and addressed.
+ *
+ * It may be reasonable to reduce the invocation counts if tests take too long to run - once we know that the tests
+ * will pass with a high number of invocations. However, the time is not yet that significant, and there is value to
+ * ensuring that the same result can be expected regardless of the number of times we run the same test.
+ *
* @author Daniel DeGroff
*/
public class PEMEncoderTest extends BaseTest {
- @Test
+ @Test(invocationCount = 250)
+ public void eddsa() throws Exception {
+ String privateKeyPEM = new String(Files.readAllBytes(Paths.get("src/test/resources/ed_dsa_ed25519_private_key.pem"))).trim();
+ String publicKeyPEM = new String(Files.readAllBytes(Paths.get("src/test/resources/ed_dsa_ed25519_public_key.pem"))).trim();
+
+ PEM pem = PEM.decode(privateKeyPEM);
+ String extractedPublicKeyPEM = PEM.encode(pem.publicKey);
+ assertEquals(publicKeyPEM, extractedPublicKeyPEM);
+
+ // The key is already in the correct format, and we don't recombine them. So the result should be equal to the private key.
+ String pkcs8PEM = PEM.encode(pem.getPrivateKey(), pem.getPublicKey());
+ assertEquals(privateKeyPEM, pkcs8PEM);
+
+ // Key generation and PEM Encoding
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("Ed25519");
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+ String encodedPublicKey = PEM.encode(keyPair.getPublic());
+ assertNotNull(encodedPublicKey);
+ assertTrue(encodedPublicKey.startsWith(PEM.X509_PUBLIC_KEY_PREFIX));
+ assertTrue(encodedPublicKey.endsWith(PEM.X509_PUBLIC_KEY_SUFFIX));
+
+ String encodedPrivateKey = PEM.encode(keyPair.getPrivate());
+ assertNotNull(encodedPrivateKey);
+ assertTrue(encodedPrivateKey.startsWith(PEM.PKCS_8_PRIVATE_KEY_PREFIX));
+ assertTrue(encodedPrivateKey.endsWith(PEM.PKCS_8_PRIVATE_KEY_SUFFIX));
+
+ // The public key can always be derived from the private key, so expect them both to be returned.
+ PEM pem2 = PEM.decode(encodedPrivateKey);
+ assertNotNull(pem2.getPrivateKey());
+ assertEquals(pem2.getPrivateKey().getFormat(), "PKCS#8");
+ assertEquals(pem2.getPrivateKey().getAlgorithm(), FipsEnabled ? "Ed25519" : "EdDSA");
+ assertNotNull(pem2.getPublicKey());
+ assertEquals(pem2.getPublicKey().getFormat(), "X.509");
+ assertEquals(pem2.getPublicKey().getAlgorithm(), FipsEnabled ? "Ed25519" : "EdDSA");
+
+ // Try again, but provide both keys to encode into the PEM. The PEM won't include the public key, but it can always be derived.
+ String encodedPrivateKey2 = PEM.encode(keyPair.getPrivate(), keyPair.getPublic());
+ assertNotNull(encodedPrivateKey2);
+ PEM pem3 = PEM.decode(encodedPrivateKey2);
+ // They will be the same, we didn't repackage it.
+ assertEquals(encodedPrivateKey, encodedPrivateKey2);
+ assertNotNull(pem3.getPrivateKey());
+ assertEquals(pem3.getPrivateKey().getFormat(), "PKCS#8");
+ assertEquals(pem3.getPrivateKey().getAlgorithm(), FipsEnabled ? "Ed25519" : "EdDSA");
+ assertNotNull(pem3.getPublicKey());
+ assertEquals(pem3.getPublicKey().getFormat(), "X.509");
+ assertEquals(pem3.getPublicKey().getAlgorithm(), FipsEnabled ? "Ed25519" : "EdDSA");
+ }
+
+ @Test(invocationCount = 250)
public void ec() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
keyPairGenerator.initialize(256);
@@ -68,7 +128,7 @@ public void ec() throws Exception {
assertNotNull(pem2.getPublicKey());
}
- @Test
+ @Test(invocationCount = 250)
public void ec_backAndForth() throws Exception {
// Start with openSSL PKCS#8 private key and X.509 public key
String expectedPrivate = new String(Files.readAllBytes(Paths.get("src/test/resources/ec_private_prime256v1_p_256_openssl_pkcs8.pem"))).trim();
@@ -96,7 +156,7 @@ public void ec_backAndForth() throws Exception {
assertEquals(encodedPublicKey, expectedPublic);
}
- @Test
+ @Test(invocationCount = 100)
public void rsa() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
@@ -121,7 +181,7 @@ public void rsa() throws Exception {
assertNotNull(pem.getPublicKey());
}
- @Test
+ @Test(invocationCount = 250)
public void rsa_backAndForth_pkcs_1() throws Exception {
// Start externally created PKCS#1 private key and X.509 public key
String expectedPrivate_pkcs_1 = new String(Files.readAllBytes(Paths.get("src/test/resources/rsa_private_key_2048_pkcs_1_control.pem"))).trim();
@@ -153,7 +213,7 @@ public void rsa_backAndForth_pkcs_1() throws Exception {
assertEquals(encodedPublicKey, expectedPublic);
}
- @Test
+ @Test(invocationCount = 250)
public void rsa_backAndForth_pkcs_8() throws Exception {
// Start externally created PKCS#1 private key and X.509 public key
String expectedPrivate = new String(Files.readAllBytes(Paths.get("src/test/resources/rsa_private_key_2048_pkcs_8_control.pem"))).trim();
diff --git a/src/test/java/io/fusionauth/security/KeyUtilsTests.java b/src/test/java/io/fusionauth/security/KeyUtilsTests.java
index ed3dd6e5..cb77f754 100644
--- a/src/test/java/io/fusionauth/security/KeyUtilsTests.java
+++ b/src/test/java/io/fusionauth/security/KeyUtilsTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, FusionAuth, All Rights Reserved
+ * Copyright (c) 2020-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
package io.fusionauth.security;
+import io.fusionauth.jwt.JWTUtils;
import io.fusionauth.pem.domain.PEM;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -28,6 +29,16 @@
import static org.testng.Assert.assertEquals;
/**
+ * Note that the higher invocationCount parameters are helpful to indentify incorrect assumptions in key parsing.
+ *
+ * Key lengths can differ, and when encoding larger integers in DER encode sequences, or parsing them in and out of
+ * JWK formats, we want to be certain we are not making incorrect assumptions. During development, you may wish to
+ * run some of these with 5-10k invocation counts to ensure these types of anomalies are un-covered and addressed.
+ *
+ * It may be reasonable to reduce the invocation counts if tests take too long to run - once we know that the tests
+ * will pass with a high number of invocations. However, the time is not yet that significant, and there is value to
+ * ensuring that the same result can be expected regardless of the number of times we run the same test.
+ *
* @author Daniel DeGroff
*/
public class KeyUtilsTests {
@@ -60,7 +71,7 @@ public void problematicKey() {
// Running 500 times to ensure we get consistency. EC keys can vary in length, but the "reported" size returned
// from the .getKeyLength() should be consistent. Out of 500 tests (if we had an error in the logic) we may get 1-5
- // failures where the key is not an exact size and we have to figure out which key size it should be reported as.
+ // failures where the key is not an exact size, and we have to figure out which key size it should be reported as.
// - For testing locally, you can ramp up this invocation count to 100k or something like that to prove that we have
// consistency over time.
@Test(dataProvider = "ecKeyLengths", invocationCount = 500)
@@ -109,4 +120,32 @@ public void rsa_getKeyLength(String algorithm, int keySize, int privateKeySize,
assertEquals(KeyUtils.getKeyLength(keyPair.getPrivate()), privateKeySize);
assertEquals(KeyUtils.getKeyLength(keyPair.getPublic()), publicKeySize);
}
+
+ @Test
+ public void eddsa_25519_keyLength() throws Exception {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("Ed25519");
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+ assertEquals(KeyUtils.getKeyLength(keyPair.getPrivate()), 32);
+ assertEquals(KeyUtils.getKeyLength(keyPair.getPublic()), 32);
+
+ io.fusionauth.jwt.domain.KeyPair keyPair2 = JWTUtils.generate_ed25519_EdDSAKeyPair();
+ PEM pem = PEM.decode(keyPair2.privateKey);
+ assertEquals(KeyUtils.getKeyLength(pem.privateKey), 32);
+ assertEquals(KeyUtils.getKeyLength(pem.publicKey), 32);
+ }
+
+ @Test
+ public void eddsa_448_keyLength() throws Exception {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("Ed448");
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+ assertEquals(KeyUtils.getKeyLength(keyPair.getPrivate()), 57);
+ assertEquals(KeyUtils.getKeyLength(keyPair.getPublic()), 57);
+
+ io.fusionauth.jwt.domain.KeyPair keyPair2 = JWTUtils.generate_ed448_EdDSAKeyPair();
+ PEM pem = PEM.decode(keyPair2.privateKey);
+ assertEquals(KeyUtils.getKeyLength(pem.privateKey), 57);
+ assertEquals(KeyUtils.getKeyLength(pem.publicKey), 57);
+ }
}
diff --git a/src/test/resources/ed_dsa_ed25519_certificate.pem b/src/test/resources/ed_dsa_ed25519_certificate.pem
new file mode 100644
index 00000000..827458d9
--- /dev/null
+++ b/src/test/resources/ed_dsa_ed25519_certificate.pem
@@ -0,0 +1,9 @@
+-----BEGIN CERTIFICATE-----
+MIIBRjCB+aADAgECAhRkaWbKP+7fMvU0YeWFaLNP4fTOrDAFBgMrZXAwGTEXMBUG
+A1UEAwwOWW91ckNvbW1vbk5hbWUwHhcNMjUxMTE5MTgzNzExWhcNMzUxMTE3MTgz
+NzExWjAZMRcwFQYDVQQDDA5Zb3VyQ29tbW9uTmFtZTAqMAUGAytlcAMhAJt4YOrK
+ZL0N9aJoVxx//7seQcJXkpbnzlxHzpVFjpd9o1MwUTAdBgNVHQ4EFgQU0EsYYr79
+uEtDvXm5BC7S2cP/l3wwHwYDVR0jBBgwFoAU0EsYYr79uEtDvXm5BC7S2cP/l3ww
+DwYDVR0TAQH/BAUwAwEB/zAFBgMrZXADQQCng75PlxDw8C/6YsU0xmR1CCIrysno
+dQUc/Y+SidnkIdUOuX02nkUwmUacmiZqoiPazywA1mS9TiHyh9tIBYIO
+-----END CERTIFICATE-----
diff --git a/src/test/resources/ed_dsa_ed25519_private_key.pem b/src/test/resources/ed_dsa_ed25519_private_key.pem
new file mode 100644
index 00000000..26f7b5b9
--- /dev/null
+++ b/src/test/resources/ed_dsa_ed25519_private_key.pem
@@ -0,0 +1,3 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIBkw8lkoSlf7XAVpXgpY/SPasbkgqqMT6xr1efkWUniV
+-----END PRIVATE KEY-----
diff --git a/src/test/resources/ed_dsa_ed25519_public_key.pem b/src/test/resources/ed_dsa_ed25519_public_key.pem
new file mode 100644
index 00000000..42596616
--- /dev/null
+++ b/src/test/resources/ed_dsa_ed25519_public_key.pem
@@ -0,0 +1,3 @@
+-----BEGIN PUBLIC KEY-----
+MCowBQYDK2VwAyEAm3hg6spkvQ31omhXHH//ux5BwleSlufOXEfOlUWOl30=
+-----END PUBLIC KEY-----
diff --git a/src/test/resources/ed_dsa_ed448_certificate.pem b/src/test/resources/ed_dsa_ed448_certificate.pem
new file mode 100644
index 00000000..a1095f58
--- /dev/null
+++ b/src/test/resources/ed_dsa_ed448_certificate.pem
@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE-----
+MIIBkjCCARKgAwIBAgIUN0sbt0cpXo+mn5o2kU9Kp9F2e+kwBQYDK2VxMBkxFzAV
+BgNVBAMMDllvdXJDb21tb25OYW1lMB4XDTI1MTExOTE4Mzc1NVoXDTM1MTExNzE4
+Mzc1NVowGTEXMBUGA1UEAwwOWW91ckNvbW1vbk5hbWUwQzAFBgMrZXEDOgBrxOwI
+OB/g8RZoBmAoNYziyKnx7CVA0YG+b2D77k5k5rhIwXnZbDMybdEsy+bBHtApB5b5
+dcJT+gCjUzBRMB0GA1UdDgQWBBSQ6b0X7ezulZj6ELR0xIO+Jb+WBTAfBgNVHSME
+GDAWgBSQ6b0X7ezulZj6ELR0xIO+Jb+WBTAPBgNVHRMBAf8EBTADAQH/MAUGAytl
+cQNzACin0AxBvCvWbsKKeToIXdmnvD+1OgAeuysO77Y/dyiNInkFxINrxtbNrbx+
+EDuLyGO5oHgal6usgJ/83RapwP2+Zfed+a4ZQ/HynSKNNzOo2LEWDC0mpu3/4ASa
+TIhpiM800u/QzXilSexuTUG8tZwYAA==
+-----END CERTIFICATE-----
diff --git a/src/test/resources/ed_dsa_ed448_private_key.pem b/src/test/resources/ed_dsa_ed448_private_key.pem
new file mode 100644
index 00000000..0ea1d105
--- /dev/null
+++ b/src/test/resources/ed_dsa_ed448_private_key.pem
@@ -0,0 +1,4 @@
+-----BEGIN PRIVATE KEY-----
+MEcCAQAwBQYDK2VxBDsEOdZsoRQRXAqB+koKbi+CGMHnY0sgofzfee7rgir3VLFf
+Zxqw7sJKX4RAi6Uec20saAyUgl2VRBJyJA==
+-----END PRIVATE KEY-----
diff --git a/src/test/resources/ed_dsa_ed448_public_key.pem b/src/test/resources/ed_dsa_ed448_public_key.pem
new file mode 100644
index 00000000..cf1721ca
--- /dev/null
+++ b/src/test/resources/ed_dsa_ed448_public_key.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MEMwBQYDK2VxAzoAgWl4sYkZhkljqw4C0GDCjDgU44Q3+Sxnd9XXZKcP+kcIoEMm
+coSFW7aMWJXcWKa5BB6/eyuTE1IA
+-----END PUBLIC KEY-----
diff --git a/src/test/resources/ed_dsa_private_key.pem b/src/test/resources/ed_dsa_private_key.pem
new file mode 100644
index 00000000..c5c49669
--- /dev/null
+++ b/src/test/resources/ed_dsa_private_key.pem
@@ -0,0 +1,3 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIIJtJBnTuKbIy5YjoNiH95ky3DcA3kRB0I2i7DkVM6Cf
+-----END PRIVATE KEY-----
\ No newline at end of file
diff --git a/src/test/resources/ed_dsa_public_key.pem b/src/test/resources/ed_dsa_public_key.pem
new file mode 100644
index 00000000..eb48cdc4
--- /dev/null
+++ b/src/test/resources/ed_dsa_public_key.pem
@@ -0,0 +1,3 @@
+-----BEGIN PUBLIC KEY-----
+MCowBQYDK2VwAyEA7fySb/9h7hVH8j1paD5IoLfXj4prjfNLwOPUYKvsTOc=
+-----END PUBLIC KEY-----
\ No newline at end of file
diff --git a/src/test/resources/eddsa_ed448_private_key.pem b/src/test/resources/eddsa_ed448_private_key.pem
new file mode 100644
index 00000000..d4188c91
--- /dev/null
+++ b/src/test/resources/eddsa_ed448_private_key.pem
@@ -0,0 +1,4 @@
+-----BEGIN PRIVATE KEY-----
+MEcCAQAwBQYDK2VxBDsEOfPaKOwyUy3zj6GNVbd15TM/dDqip4H1JI7sfEMT3UmM
+NzDIkqAEdojZa3KxNUgguoZDqmdwtfCYlQ==
+-----END PRIVATE KEY-----
diff --git a/src/test/resources/eddsa_ed448_private_key_pkcs8.pem b/src/test/resources/eddsa_ed448_private_key_pkcs8.pem
new file mode 100644
index 00000000..bbec7fe9
--- /dev/null
+++ b/src/test/resources/eddsa_ed448_private_key_pkcs8.pem
@@ -0,0 +1,4 @@
+-----BEGIN PRIVATE KEY-----
+MEcCAQAwBQYDK2VxBDsEOYlqvUFWVa3g3qywhvJF5Rsly4CS/K+2vbM3+fYvJWes
+GTr+9owAeV/hUJI1BmYIwvoFgyJF3dz7JQ==
+-----END PRIVATE KEY-----
diff --git a/src/test/resources/eddsa_ed448_public_key.pem b/src/test/resources/eddsa_ed448_public_key.pem
new file mode 100644
index 00000000..a5348547
--- /dev/null
+++ b/src/test/resources/eddsa_ed448_public_key.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MEMwBQYDK2VxAzoA/pL1a9MsvdXgVpFpfJVEefmtYqdh7wHupmVvQg5nYPtfbDc2
+DZIxtAEGw4irOf4VolCmc8n+JjcA
+-----END PUBLIC KEY-----
diff --git a/src/test/resources/jwk/ed_dsa_ed25519_certificate.json b/src/test/resources/jwk/ed_dsa_ed25519_certificate.json
new file mode 100644
index 00000000..6d6feeb0
--- /dev/null
+++ b/src/test/resources/jwk/ed_dsa_ed25519_certificate.json
@@ -0,0 +1,12 @@
+{
+ "alg": "Ed25519",
+ "crv": "Ed25519",
+ "kty": "OKP",
+ "use": "sig",
+ "x": "m3hg6spkvQ31omhXHH__ux5BwleSlufOXEfOlUWOl30",
+ "x5c": [
+ "MIIBRjCB+aADAgECAhRkaWbKP+7fMvU0YeWFaLNP4fTOrDAFBgMrZXAwGTEXMBUGA1UEAwwOWW91ckNvbW1vbk5hbWUwHhcNMjUxMTE5MTgzNzExWhcNMzUxMTE3MTgzNzExWjAZMRcwFQYDVQQDDA5Zb3VyQ29tbW9uTmFtZTAqMAUGAytlcAMhAJt4YOrKZL0N9aJoVxx//7seQcJXkpbnzlxHzpVFjpd9o1MwUTAdBgNVHQ4EFgQU0EsYYr79uEtDvXm5BC7S2cP/l3wwHwYDVR0jBBgwFoAU0EsYYr79uEtDvXm5BC7S2cP/l3wwDwYDVR0TAQH/BAUwAwEB/zAFBgMrZXADQQCng75PlxDw8C/6YsU0xmR1CCIrysnodQUc/Y+SidnkIdUOuX02nkUwmUacmiZqoiPazywA1mS9TiHyh9tIBYIO"
+ ],
+ "x5t": "JPLrwBHnDr6gxvCHibgDjW6yJCk",
+ "x5t#S256": "aSQLmuU9GKdPVWLRAnUB9dD3Kgm_RlPuFGDR7VoL_yE"
+}
diff --git a/src/test/resources/jwk/ed_dsa_ed25519_private_key.json b/src/test/resources/jwk/ed_dsa_ed25519_private_key.json
new file mode 100644
index 00000000..676268d4
--- /dev/null
+++ b/src/test/resources/jwk/ed_dsa_ed25519_private_key.json
@@ -0,0 +1,8 @@
+{
+ "alg": "Ed25519",
+ "crv": "Ed25519",
+ "d": "GTDyWShKV_tcBWleClj9I9qxuSCqoxPrGvV5-RZSeJU",
+ "kty": "OKP",
+ "use": "sig",
+ "x": "m3hg6spkvQ31omhXHH__ux5BwleSlufOXEfOlUWOl30"
+}
diff --git a/src/test/resources/jwk/ed_dsa_ed25519_public_key.json b/src/test/resources/jwk/ed_dsa_ed25519_public_key.json
new file mode 100644
index 00000000..e828294c
--- /dev/null
+++ b/src/test/resources/jwk/ed_dsa_ed25519_public_key.json
@@ -0,0 +1,7 @@
+{
+ "alg": "Ed25519",
+ "crv": "Ed25519",
+ "kty": "OKP",
+ "use": "sig",
+ "x": "m3hg6spkvQ31omhXHH__ux5BwleSlufOXEfOlUWOl30"
+}
diff --git a/src/test/resources/jwk/ed_dsa_ed448_certificate.json b/src/test/resources/jwk/ed_dsa_ed448_certificate.json
new file mode 100644
index 00000000..fa623751
--- /dev/null
+++ b/src/test/resources/jwk/ed_dsa_ed448_certificate.json
@@ -0,0 +1,12 @@
+{
+ "alg": "Ed448",
+ "crv": "Ed448",
+ "kty": "OKP",
+ "use": "sig",
+ "x": "a8TsCDgf4PEWaAZgKDWM4sip8ewlQNGBvm9g--5OZOa4SMF52WwzMm3RLMvmwR7QKQeW-XXCU_oA",
+ "x5c": [
+ "MIIBkjCCARKgAwIBAgIUN0sbt0cpXo+mn5o2kU9Kp9F2e+kwBQYDK2VxMBkxFzAVBgNVBAMMDllvdXJDb21tb25OYW1lMB4XDTI1MTExOTE4Mzc1NVoXDTM1MTExNzE4Mzc1NVowGTEXMBUGA1UEAwwOWW91ckNvbW1vbk5hbWUwQzAFBgMrZXEDOgBrxOwIOB/g8RZoBmAoNYziyKnx7CVA0YG+b2D77k5k5rhIwXnZbDMybdEsy+bBHtApB5b5dcJT+gCjUzBRMB0GA1UdDgQWBBSQ6b0X7ezulZj6ELR0xIO+Jb+WBTAfBgNVHSMEGDAWgBSQ6b0X7ezulZj6ELR0xIO+Jb+WBTAPBgNVHRMBAf8EBTADAQH/MAUGAytlcQNzACin0AxBvCvWbsKKeToIXdmnvD+1OgAeuysO77Y/dyiNInkFxINrxtbNrbx+EDuLyGO5oHgal6usgJ/83RapwP2+Zfed+a4ZQ/HynSKNNzOo2LEWDC0mpu3/4ASaTIhpiM800u/QzXilSexuTUG8tZwYAA=="
+ ],
+ "x5t": "s6yEmdTmvYlE14xE62q9JgEa3sA",
+ "x5t#S256": "eZJkos1xtzK7wCH9ZcRX9e6LFpbSOKaATVphIFjj8v0"
+}
diff --git a/src/test/resources/jwk/ed_dsa_ed448_private_key.json b/src/test/resources/jwk/ed_dsa_ed448_private_key.json
new file mode 100644
index 00000000..0cf6408a
--- /dev/null
+++ b/src/test/resources/jwk/ed_dsa_ed448_private_key.json
@@ -0,0 +1,8 @@
+{
+ "alg": "Ed448",
+ "crv": "Ed448",
+ "d": "1myhFBFcCoH6SgpuL4IYwedjSyCh_N957uuCKvdUsV9nGrDuwkpfhECLpR5zbSxoDJSCXZVEEnIk",
+ "kty": "OKP",
+ "use": "sig",
+ "x": "gWl4sYkZhkljqw4C0GDCjDgU44Q3-Sxnd9XXZKcP-kcIoEMmcoSFW7aMWJXcWKa5BB6_eyuTE1IA"
+}
diff --git a/src/test/resources/jwk/ed_dsa_ed448_public_key.json b/src/test/resources/jwk/ed_dsa_ed448_public_key.json
new file mode 100644
index 00000000..81a09889
--- /dev/null
+++ b/src/test/resources/jwk/ed_dsa_ed448_public_key.json
@@ -0,0 +1,7 @@
+{
+ "alg": "Ed448",
+ "crv": "Ed448",
+ "kty": "OKP",
+ "use": "sig",
+ "x": "gWl4sYkZhkljqw4C0GDCjDgU44Q3-Sxnd9XXZKcP-kcIoEMmcoSFW7aMWJXcWKa5BB6_eyuTE1IA"
+}
diff --git a/src/test/resources/jwk/ed_dsa_private_key.json b/src/test/resources/jwk/ed_dsa_private_key.json
new file mode 100644
index 00000000..8428040b
--- /dev/null
+++ b/src/test/resources/jwk/ed_dsa_private_key.json
@@ -0,0 +1,7 @@
+{
+ "alg": "Ed25519",
+ "crv": "Ed25519",
+ "kty": "OKP",
+ "use": "sig",
+ "d": ""
+}
diff --git a/src/test/resources/jwk/ed_dsa_public_key.json b/src/test/resources/jwk/ed_dsa_public_key.json
new file mode 100644
index 00000000..d6bea8a4
--- /dev/null
+++ b/src/test/resources/jwk/ed_dsa_public_key.json
@@ -0,0 +1,7 @@
+{
+ "alg": "Ed25519",
+ "crv": "Ed25519",
+ "kty": "OKP",
+ "use": "sig",
+ "x": "Z0zsq2DU48BL841rio_Xt6BIPmhpPfJHFe5h_2-S_O0"
+}
diff --git a/src/test/resources/jwk/rsa_pss_private_key_2048.json b/src/test/resources/jwk/rsa_pss_private_key_2048.json
new file mode 100644
index 00000000..69c1c87f
--- /dev/null
+++ b/src/test/resources/jwk/rsa_pss_private_key_2048.json
@@ -0,0 +1,12 @@
+{
+ "d": "EJH10HKG3hGJ8nI1GisExYjxyIexlSHrv3t_4A_OMex_thEOQvIzB9RgN1RQ5sJnhAfOouCTq3PQSOhn3K9TYNJsW_mXy_yN2_I-5dlDHFz2XQBuknaze-Ev8LMQdZBgI579chhBFAXA-D0CbgxhostbVaVxWPKzARjVJfBVUKTaEh7Ig_qJaJSczY5DisKQsvpU3ePbpu1gOBSs18x4vCcTjSXslrlYMPI6nNHAY9Y9xUc2lkxC-SqXSGxb5j6ZRk9q4Lg8ht7h3Gb0hOhc_Fgi6aaoRBrAUtFVmBKPgFlMCHiXj858-smcXiSVqHhoVsIA8NKfEEhL-9ZA9VrxGQ",
+ "kty": "RSA",
+ "dp": "WTfZxUylsWpdtkieb6b8bI2cfajNdkCSk81xMN4Sf4hft0KFQ82aHPrx1coqfTmry-NHTlXsdw6XkD4RTLD-i5L0A4zIGjAFKmUcIeRrtITZHnBFNzZt-TfIa9DPbEqHGssyUM7Hxb81EM0IWTvcagbQoosqbEwXuiP5dSusOws",
+ "dq": "O--Q0ga1G8h4Dq-5NrulpJFpavSti1SlQUx8_dtuy6okzAwcruzrrjLg9G-QbO1K-PGVic3zSQtwONg3B1EOy0DalPDSekYvSJU7s9wKRnydh-plyJo_h2dj41Hg-1iG4h9w5EfWGhRA5Z0Twx1Zqorc7L1AU3822MzZk32i_Ws",
+ "e": "AQAB",
+ "n": "vlGU-GNj5XVHJ8Hi8b0Dhjk3m-1uULwdIrEZMckvuWnderIKDMMpdORGg79nmMd7fPayrvA7ZxOjnEBIAzGfovDje9NN3esWAZ6uQpZAUdZO3wPW2qPBTgFtdzTnLEY-z5itxCL1J7KyeQuD82Jrda5C-9FLkf40CMlGj_5Z9BSF4OXlsY8LwLlNd-3jQLK3tzCFQwUncmXenprsZmw37fyEqN6mfoXVp22i0LbtHdh7-JbLg7AWaovKkqikh79HKG7ptcAmwpd4uc4zbH5yowBepscfqsYKlvwheYBudJumxq-H_-8ZippSt3__fcCK64IqwW0DkbsU61SlzntkvQ",
+ "p": "-V_zCqvziDSo-lCxk8WnXFYA4yg-91IEFmeJoLgpdpIZhdlrsK1yPnEqYbfgjH9nurl9-zsyT6CaajVoM3jHuQEN-Eznf8cLG78KzyAE0HUEi4zf0XwC7BfoMaA9HEnjV9ctij9w-g9s6NfugwsDPdJTRv_IAWGeyVnXgX3kRoc",
+ "q": "w1_6uSjS_ptRIzrWr84KNvg-zfkAzWNdPoD3DZDzJAi3IW-kDezOXiFtLZQcORD_WobtNfglEO57imb1aaxrxwB-lyKJokRYjlz7KZ8TtHPsPrJ2yZ37XFb73uqS7kGnsO5nKRLc17rFAXBCn05OW8MG4jGlu0K2nnkSU8yxB5s",
+ "qi": "GQpitPntsdYUdVK3tYk-Utl7vDJA0HH7rxwAqx6gwxVXLDZf9SEYocJtulNWl8HIzUmoHu0KdMsosq721WbU4Jt35QiOpfa-zRNMcKV_7C_MUMvKTOWg9LzuJ2H2IJYxnglacScv5SYH6ms2LRs52-NUm4yAqJzSjUa9Zi-jgos",
+ "use": "sig"
+}
diff --git a/src/test/resources/jwk/rsa_pss_public_key_2048.json b/src/test/resources/jwk/rsa_pss_public_key_2048.json
new file mode 100644
index 00000000..5ca5080f
--- /dev/null
+++ b/src/test/resources/jwk/rsa_pss_public_key_2048.json
@@ -0,0 +1,6 @@
+{
+ "e": "AQAB",
+ "kty": "RSA",
+ "n": "vlGU-GNj5XVHJ8Hi8b0Dhjk3m-1uULwdIrEZMckvuWnderIKDMMpdORGg79nmMd7fPayrvA7ZxOjnEBIAzGfovDje9NN3esWAZ6uQpZAUdZO3wPW2qPBTgFtdzTnLEY-z5itxCL1J7KyeQuD82Jrda5C-9FLkf40CMlGj_5Z9BSF4OXlsY8LwLlNd-3jQLK3tzCFQwUncmXenprsZmw37fyEqN6mfoXVp22i0LbtHdh7-JbLg7AWaovKkqikh79HKG7ptcAmwpd4uc4zbH5yowBepscfqsYKlvwheYBudJumxq-H_-8ZippSt3__fcCK64IqwW0DkbsU61SlzntkvQ",
+ "use": "sig"
+}
diff --git a/src/test/resources/jwk/rsa_pss_public_key_2048_certificate.json b/src/test/resources/jwk/rsa_pss_public_key_2048_certificate.json
new file mode 100644
index 00000000..7ee939e8
--- /dev/null
+++ b/src/test/resources/jwk/rsa_pss_public_key_2048_certificate.json
@@ -0,0 +1,12 @@
+{
+ "alg": "PS256",
+ "e": "AQAB",
+ "kty": "RSA",
+ "n": "vlGU-GNj5XVHJ8Hi8b0Dhjk3m-1uULwdIrEZMckvuWnderIKDMMpdORGg79nmMd7fPayrvA7ZxOjnEBIAzGfovDje9NN3esWAZ6uQpZAUdZO3wPW2qPBTgFtdzTnLEY-z5itxCL1J7KyeQuD82Jrda5C-9FLkf40CMlGj_5Z9BSF4OXlsY8LwLlNd-3jQLK3tzCFQwUncmXenprsZmw37fyEqN6mfoXVp22i0LbtHdh7-JbLg7AWaovKkqikh79HKG7ptcAmwpd4uc4zbH5yowBepscfqsYKlvwheYBudJumxq-H_-8ZippSt3__fcCK64IqwW0DkbsU61SlzntkvQ",
+ "use": "sig",
+ "x5c": [
+ "MIIDeTCCAi2gAwIBAgIUT4pGD7o7p19ffPEjhBb/+7okKwowQQYJKoZIhvcNAQEKMDSgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIDAgEgMBkxFzAVBgNVBAMMDllvdXJDb21tb25OYW1lMB4XDTI1MTExOTIyMDQ0MFoXDTM1MTExNzIyMDQ0MFowGTEXMBUGA1UEAwwOWW91ckNvbW1vbk5hbWUwggEgMAsGCSqGSIb3DQEBCgOCAQ8AMIIBCgKCAQEAvlGU+GNj5XVHJ8Hi8b0Dhjk3m+1uULwdIrEZMckvuWnderIKDMMpdORGg79nmMd7fPayrvA7ZxOjnEBIAzGfovDje9NN3esWAZ6uQpZAUdZO3wPW2qPBTgFtdzTnLEY+z5itxCL1J7KyeQuD82Jrda5C+9FLkf40CMlGj/5Z9BSF4OXlsY8LwLlNd+3jQLK3tzCFQwUncmXenprsZmw37fyEqN6mfoXVp22i0LbtHdh7+JbLg7AWaovKkqikh79HKG7ptcAmwpd4uc4zbH5yowBepscfqsYKlvwheYBudJumxq+H/+8ZippSt3//fcCK64IqwW0DkbsU61SlzntkvQIDAQABo1MwUTAdBgNVHQ4EFgQUaYHpMLSO63cRr8sgmoLc+5M6XTUwHwYDVR0jBBgwFoAUaYHpMLSO63cRr8sgmoLc+5M6XTUwDwYDVR0TAQH/BAUwAwEB/zBBBgkqhkiG9w0BAQowNKAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAQUAogMCASADggEBAAUo7hZ29UOnb7sCxjrvCQ3tCMcIR+aT2vu0owDibJ9zsFRvRoIJgXqFdOtpJFPfx2U0zSVAy3eqCyf5cOJ0fDyApt9SLAd6pF45MTKRMNwH3iY13wRYIdN2Rcdd1Z3gR8HtrMYFf1RU/AwgQKPTvCKmRUbya4ZCbUGckoTdfHfYZZ7o+D7i4Eov+4MV9o/QOpz6jF9Q+j7S0+kNa/hUbJxs8C5TpHz9B9z1U28Z59Xezt2m2fPLzQ2XfHRAjTFpM5rNJUS8q3ZKHskVrKBuM6jZ9ML0CfCZozU9093inmaflGnwKeVmGgZ1DSlz2YQ92lTN2xSqfgRJN5nqs8OuM6I="
+ ],
+ "x5t": "baN9dfF8TqyfmLsfyjy-kKGIB0Y",
+ "x5t#S256": "t6-s3DRQhe_g-12YNvHrrWnLane49ikW62ir5NIqRQ8"
+}
diff --git a/src/test/resources/rsa_pss_private_key_2048.pem b/src/test/resources/rsa_pss_private_key_2048.pem
new file mode 100644
index 00000000..a0f82096
--- /dev/null
+++ b/src/test/resources/rsa_pss_private_key_2048.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEugIBADALBgkqhkiG9w0BAQoEggSmMIIEogIBAAKCAQEAvlGU+GNj5XVHJ8Hi
+8b0Dhjk3m+1uULwdIrEZMckvuWnderIKDMMpdORGg79nmMd7fPayrvA7ZxOjnEBI
+AzGfovDje9NN3esWAZ6uQpZAUdZO3wPW2qPBTgFtdzTnLEY+z5itxCL1J7KyeQuD
+82Jrda5C+9FLkf40CMlGj/5Z9BSF4OXlsY8LwLlNd+3jQLK3tzCFQwUncmXenprs
+Zmw37fyEqN6mfoXVp22i0LbtHdh7+JbLg7AWaovKkqikh79HKG7ptcAmwpd4uc4z
+bH5yowBepscfqsYKlvwheYBudJumxq+H/+8ZippSt3//fcCK64IqwW0DkbsU61Sl
+zntkvQIDAQABAoIBABCR9dByht4RifJyNRorBMWI8ciHsZUh6797f+APzjHsf7YR
+DkLyMwfUYDdUUObCZ4QHzqLgk6tz0EjoZ9yvU2DSbFv5l8v8jdvyPuXZQxxc9l0A
+bpJ2s3vhL/CzEHWQYCOe/XIYQRQFwPg9Am4MYaLLW1WlcVjyswEY1SXwVVCk2hIe
+yIP6iWiUnM2OQ4rCkLL6VN3j26btYDgUrNfMeLwnE40l7Ja5WDDyOpzRwGPWPcVH
+NpZMQvkql0hsW+Y+mUZPauC4PIbe4dxm9IToXPxYIummqEQawFLRVZgSj4BZTAh4
+l4/OfPrJnF4klah4aFbCAPDSnxBIS/vWQPVa8RkCgYEA+V/zCqvziDSo+lCxk8Wn
+XFYA4yg+91IEFmeJoLgpdpIZhdlrsK1yPnEqYbfgjH9nurl9+zsyT6CaajVoM3jH
+uQEN+Eznf8cLG78KzyAE0HUEi4zf0XwC7BfoMaA9HEnjV9ctij9w+g9s6NfugwsD
+PdJTRv/IAWGeyVnXgX3kRocCgYEAw1/6uSjS/ptRIzrWr84KNvg+zfkAzWNdPoD3
+DZDzJAi3IW+kDezOXiFtLZQcORD/WobtNfglEO57imb1aaxrxwB+lyKJokRYjlz7
+KZ8TtHPsPrJ2yZ37XFb73uqS7kGnsO5nKRLc17rFAXBCn05OW8MG4jGlu0K2nnkS
+U8yxB5sCgYBZN9nFTKWxal22SJ5vpvxsjZx9qM12QJKTzXEw3hJ/iF+3QoVDzZoc
++vHVyip9OavL40dOVex3DpeQPhFMsP6LkvQDjMgaMAUqZRwh5Gu0hNkecEU3Nm35
+N8hr0M9sSocayzJQzsfFvzUQzQhZO9xqBtCiiypsTBe6I/l1K6w7CwKBgDvvkNIG
+tRvIeA6vuTa7paSRaWr0rYtUpUFMfP3bbsuqJMwMHK7s664y4PRvkGztSvjxlYnN
+80kLcDjYNwdRDstA2pTw0npGL0iVO7PcCkZ8nYfqZciaP4dnY+NR4PtYhuIfcORH
+1hoUQOWdE8MdWaqK3Oy9QFN/NtjM2ZN9ov1rAoGAGQpitPntsdYUdVK3tYk+Utl7
+vDJA0HH7rxwAqx6gwxVXLDZf9SEYocJtulNWl8HIzUmoHu0KdMsosq721WbU4Jt3
+5QiOpfa+zRNMcKV/7C/MUMvKTOWg9LzuJ2H2IJYxnglacScv5SYH6ms2LRs52+NU
+m4yAqJzSjUa9Zi+jgos=
+-----END PRIVATE KEY-----
diff --git a/src/test/resources/rsa_pss_private_key_3072.pem b/src/test/resources/rsa_pss_private_key_3072.pem
new file mode 100644
index 00000000..b7ec7261
--- /dev/null
+++ b/src/test/resources/rsa_pss_private_key_3072.pem
@@ -0,0 +1,40 @@
+-----BEGIN PRIVATE KEY-----
+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
+KYe+hikMOyJ0VfocTLM=
+-----END PRIVATE KEY-----
diff --git a/src/test/resources/rsa_pss_private_key_4096.pem b/src/test/resources/rsa_pss_private_key_4096.pem
new file mode 100644
index 00000000..e952ee22
--- /dev/null
+++ b/src/test/resources/rsa_pss_private_key_4096.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+J41j6FuHwFBfzUyaSj1ePotS
+-----END PRIVATE KEY-----
diff --git a/src/test/resources/rsa_pss_public_key_2048.pem b/src/test/resources/rsa_pss_public_key_2048.pem
new file mode 100644
index 00000000..1b5d3213
--- /dev/null
+++ b/src/test/resources/rsa_pss_public_key_2048.pem
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIDALBgkqhkiG9w0BAQoDggEPADCCAQoCggEBAL5RlPhjY+V1RyfB4vG9A4Y5
+N5vtblC8HSKxGTHJL7lp3XqyCgzDKXTkRoO/Z5jHe3z2sq7wO2cTo5xASAMxn6Lw
+43vTTd3rFgGerkKWQFHWTt8D1tqjwU4BbXc05yxGPs+YrcQi9SeysnkLg/Nia3Wu
+QvvRS5H+NAjJRo/+WfQUheDl5bGPC8C5TXft40Cyt7cwhUMFJ3Jl3p6a7GZsN+38
+hKjepn6F1adtotC27R3Ye/iWy4OwFmqLypKopIe/Ryhu6bXAJsKXeLnOM2x+cqMA
+XqbHH6rGCpb8IXmAbnSbpsavh//vGYqaUrd//33AiuuCKsFtA5G7FOtUpc57ZL0C
+AwEAAQ==
+-----END PUBLIC KEY-----
diff --git a/src/test/resources/rsa_pss_public_key_2048_certificate.pem b/src/test/resources/rsa_pss_public_key_2048_certificate.pem
new file mode 100644
index 00000000..1ea932dd
--- /dev/null
+++ b/src/test/resources/rsa_pss_public_key_2048_certificate.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDeTCCAi2gAwIBAgIUT4pGD7o7p19ffPEjhBb/+7okKwowQQYJKoZIhvcNAQEK
+MDSgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEF
+AKIDAgEgMBkxFzAVBgNVBAMMDllvdXJDb21tb25OYW1lMB4XDTI1MTExOTIyMDQ0
+MFoXDTM1MTExNzIyMDQ0MFowGTEXMBUGA1UEAwwOWW91ckNvbW1vbk5hbWUwggEg
+MAsGCSqGSIb3DQEBCgOCAQ8AMIIBCgKCAQEAvlGU+GNj5XVHJ8Hi8b0Dhjk3m+1u
+ULwdIrEZMckvuWnderIKDMMpdORGg79nmMd7fPayrvA7ZxOjnEBIAzGfovDje9NN
+3esWAZ6uQpZAUdZO3wPW2qPBTgFtdzTnLEY+z5itxCL1J7KyeQuD82Jrda5C+9FL
+kf40CMlGj/5Z9BSF4OXlsY8LwLlNd+3jQLK3tzCFQwUncmXenprsZmw37fyEqN6m
+foXVp22i0LbtHdh7+JbLg7AWaovKkqikh79HKG7ptcAmwpd4uc4zbH5yowBepscf
+qsYKlvwheYBudJumxq+H/+8ZippSt3//fcCK64IqwW0DkbsU61SlzntkvQIDAQAB
+o1MwUTAdBgNVHQ4EFgQUaYHpMLSO63cRr8sgmoLc+5M6XTUwHwYDVR0jBBgwFoAU
+aYHpMLSO63cRr8sgmoLc+5M6XTUwDwYDVR0TAQH/BAUwAwEB/zBBBgkqhkiG9w0B
+AQowNKAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQC
+AQUAogMCASADggEBAAUo7hZ29UOnb7sCxjrvCQ3tCMcIR+aT2vu0owDibJ9zsFRv
+RoIJgXqFdOtpJFPfx2U0zSVAy3eqCyf5cOJ0fDyApt9SLAd6pF45MTKRMNwH3iY1
+3wRYIdN2Rcdd1Z3gR8HtrMYFf1RU/AwgQKPTvCKmRUbya4ZCbUGckoTdfHfYZZ7o
++D7i4Eov+4MV9o/QOpz6jF9Q+j7S0+kNa/hUbJxs8C5TpHz9B9z1U28Z59Xezt2m
+2fPLzQ2XfHRAjTFpM5rNJUS8q3ZKHskVrKBuM6jZ9ML0CfCZozU9093inmaflGnw
+KeVmGgZ1DSlz2YQ92lTN2xSqfgRJN5nqs8OuM6I=
+-----END CERTIFICATE-----
diff --git a/src/test/resources/rsa_pss_public_key_3072.pem b/src/test/resources/rsa_pss_public_key_3072.pem
new file mode 100644
index 00000000..13eed94d
--- /dev/null
+++ b/src/test/resources/rsa_pss_public_key_3072.pem
@@ -0,0 +1,11 @@
+-----BEGIN PUBLIC KEY-----
+MIIBoDALBgkqhkiG9w0BAQoDggGPADCCAYoCggGBANq8uekMevpx9mRE1IYJeuJB
+gFpfcNJhdhA2ZTGUcbh0zJD9KnQ0ojapZZz9kNT6VEvBe0oMBJTSET6yNCIObB8m
+OU4i32Bjs2S5b2PfOFS83eeYtLZoA/YyIEEZ8rE/fuA+7M15y0AzPsOFRYRHN5s+
+PPyX/sr0QJW5ox2XgiKv2Noq/Fvr1IUjZpx3iz1taTSVRMUdnK02MlkbNi+sZvjG
+RVzkbc6IvZ6YlbpvKmzg91ahNtwU46c6CYR4yI1Z/YRyQ73MUtxkg7MgYYr4liji
+2oujj9Tsem2l31kht5qFWFtlHfeQz3sCBeJEeNQdPf2DjXbZik+sF9HW9MTwLqAn
+len2U+Ewc2A/YweXyK1DRtYBkVnsF8r29Gm0fE1sn52NW2w/arO5Mirc0n//Oug7
+VEyL3Id5Qbjy2zopMwMjR0lLL+aCm1agvACzfWJxNiyeA1zH+7vBrUSq+5YWmDXh
+E8eV4qKOfhtQD+xMXb9UvP7wGd/6tyZhNc7ZTjI6nwIDAQAB
+-----END PUBLIC KEY-----
diff --git a/src/test/resources/rsa_pss_public_key_4096.pem b/src/test/resources/rsa_pss_public_key_4096.pem
new file mode 100644
index 00000000..0c1fc3f2
--- /dev/null
+++ b/src/test/resources/rsa_pss_public_key_4096.pem
@@ -0,0 +1,14 @@
+-----BEGIN PUBLIC KEY-----
+MIICIDALBgkqhkiG9w0BAQoDggIPADCCAgoCggIBALS2HskLJFZ3ejwAZFL7pYQG
+3t6E+1ULOBfEX0sT1YzJGZmaoUxHGjlOlB5bJaOyDNoZb+XOWsCND0kfTa0nlXA5
+mZBnneOujabFdsQC8d02qK3LAHR9LvPqhob4nvOnYFbXq9Fz0EQvdHPMzqCM2hFl
+58jcKvRHPxjW8EKamAPZToh5Ay+9nf/fk02hGPkJQnqfZgTWLkrBMMv43hG7MZc7
+sRQs7TsL2BhzW1VaRoeamxn6xXbu+4dizt22C4G/tTjGOzgIZ6LVn2cPGMvU1tVB
+67f4+N3u89MUDCNy14URKR2kdFtjD6zZa313xbojXn0mWhkASaxhvGi0+FoZVdC8
+e2/zteL9+CvNbu5GcEBUc8oezHqD4gk+LuB9AjK0eIjleXC6foe4zDkAVrh961sf
+35tPHa0GdSQeIjOJu9ws+qskkv8dU4nhDLjvpe74q6zLm0B5CX4ZcHtbB3X1cwg6
+x8YZDbouHg35Ivd18IGK8qxy8KGKB83S9gtWGYLrLVpo/IVqJXf6NLe2AeMmhDyC
+7/alDejT+WhE2cvwbiVXKps7wGm8SdtH/5CFCsZo6DEmaGHV2ZCXPwHWyqC31LmN
+jrLs+EPs2Cl/j6W8bUFIECh5RZderSLXyDF657Y+hfy02m6hGT6yyVbTapT8CTlB
+ixCGAS27GdkqNoJw6TlLAgMBAAE=
+-----END PUBLIC KEY-----