ALGORITHMS = List.of(
+ OID_1_2_840_113549_1_1_1,
+ OID_1_2_840_113549_1_1_10,
+ OID_1_2_840_10040_4_1,
+ OID_1_3_101_110,
+ OID_1_3_101_111,
+ OID_1_3_101_112,
+ OID_1_3_101_113,
+ OID_1_2_840_10045_2_1);
+
+ /**
+ * SECG (Certicom) named elliptic curve (secp384r1)
+ */
+ static final ASN1ObjectIdentifier OID_1_3_132_0_34 = ASN1ObjectIdentifier.from("2b81040022", "EC");
+
+ public static ASN1ObjectIdentifier from(String hexString, String algorithmId) {
+ return new ASN1ObjectIdentifier(HexFormat.of().parseHex(hexString), algorithmId);
+ }
+
+ public static String getAlgorithmId(byte[] content) {
+ for (ASN1ObjectIdentifier oid : ALGORITHMS) {
+ if (Arrays.equals(oid.value(), content)) {
+ return oid.algorithmId();
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/der/DerEncoder.java b/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/der/DerEncoder.java
new file mode 100644
index 0000000..d22feaa
--- /dev/null
+++ b/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/der/DerEncoder.java
@@ -0,0 +1,105 @@
+package io.smallrye.certs.pem.der;
+
+import io.vertx.core.buffer.Buffer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Simple ASN.1 DER encoder.
+ * Inspired by DerOutputStream.java.
+ */
+public class DerEncoder {
+
+ private final Buffer payload = Buffer.buffer();
+
+ public void oid(ASN1ObjectIdentifier oid) {
+ int code = (oid != null) ? 0x06 : 0x05; // 5: NULL, 6: OID
+ encode(code, (oid != null) ? oid.value() : null);
+ }
+
+ public void integer(int... encodedInteger) {
+ encode(0x02, toBytes(encodedInteger));
+ }
+
+ public void octetString(byte[] bytes) {
+ encode(0x04, bytes);
+ }
+
+ public void sequence(byte[] bytes) {
+ // This is because in X.509 formats, the SEQUENCE type is used in constructed form.
+ // As the result, 6th bit is set to 1. By setting 1 in 6th bit for SEQUENCE universal tag (0x10)
+ // you get 0x30
+ encode(0x30, bytes);
+ }
+
+ public void addToSequence(byte[] bytes) {
+ payload.appendBytes(bytes);
+ }
+
+ private void write(int c) {
+ payload.appendByte((byte) c);
+ }
+
+ private void encode(int code, byte[] bytes) {
+ write(code);
+ int length = (bytes != null) ? bytes.length : 0;
+ if (length <= 127) {
+ write(length & 0xFF);
+ } else {
+ ByteArrayOutputStream lengthStream = new ByteArrayOutputStream();
+ while (length != 0) {
+ lengthStream.write(length & 0xFF);
+ length = length >> 8;
+ }
+ byte[] lengthBytes = lengthStream.toByteArray();
+ write(0x80 | lengthBytes.length);
+ for (int i = lengthBytes.length - 1; i >= 0; i--) {
+ write(lengthBytes[i]);
+ }
+ }
+ if (bytes != null) {
+ payload.appendBytes(bytes);
+ }
+ }
+
+ private static byte[] toBytes(int... elements) {
+ if (elements == null) {
+ return null;
+ }
+ byte[] result = new byte[elements.length];
+ for (int i = 0; i < elements.length; i++) {
+ result[i] = (byte) elements[i];
+ }
+ return result;
+ }
+
+ public byte[] toSequence() throws IOException {
+ DerEncoder sequenceEncoder = new DerEncoder();
+ sequenceEncoder.sequence(toBytes());
+ return sequenceEncoder.toBytes();
+ }
+
+ public byte[] toBytes() {
+ return payload.getBytes();
+ }
+
+ // /**
+ // * Creates a sequence or appends a DER object to a sequence.
+ // *
+ // * @param bytes the bytes representing the object to add. It's important that the bytes are a value DER encoded value (tag | length | value)
+ // * @throws IOException if the write fails
+ // */
+ // public void sequence(byte[] bytes) throws IOException {
+ // // This is because in X.509 formats, the SEQUENCE type is used in constructed form.
+ // // As the result, 6th bit is set to 1. By setting 1 in 6th bit for SEQUENCE universal tag (0x10)
+ // // you get 0x30
+ // if (payload.length() == 0) {
+ // encode(0x30, bytes);
+ // } else {
+ // payload.appendBytes(bytes);
+ // }
+ // }
+
+}
\ No newline at end of file
diff --git a/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/der/DerParser.java b/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/der/DerParser.java
new file mode 100644
index 0000000..9cfa354
--- /dev/null
+++ b/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/der/DerParser.java
@@ -0,0 +1,156 @@
+package io.smallrye.certs.pem.der;
+
+import io.vertx.core.buffer.Buffer;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * An ASN.1 DER encoded element.
+ *
+ * The ASN.1 DER encoding is a standard way to encode data structures in binary form:
+ * {@code Identifier octets type | Length octets | Contents octets (Value) | End-of-Contents octets (only if indefinite form)}
+ *
+ * @see Wikipedia page
+ */
+public class DerParser {
+
+ // See https://en.wikipedia.org/wiki/X.690#Encoding
+ public enum Type {
+ PRIMITIVE,
+ CONSTRUCTED;
+
+ static Type from(byte b) {
+ return ((b & 0x20) == 0) ? PRIMITIVE : CONSTRUCTED;
+ }
+
+ }
+
+ // See https://en.wikipedia.org/wiki/X.690#Identifier_octets.
+ public enum Tag {
+ // Usual Tag, attention to the ASN Sequence using 0x30 and not 0x10 because it's a constructed type.
+ INTEGER(0x02),
+ OCTET_STRING(0x04),
+ OBJECT_IDENTIFIER(0x06),
+ SEQUENCE(0x10),
+ ASN1_SEQUENCE(0x30);
+
+ private final int number;
+
+ Tag(int n) {
+ number = n;
+ }
+
+ public int number() {
+ return number;
+ }
+ }
+
+ /**
+ * The type of the element (primitive or constructed).
+ */
+ private final Type type;
+
+ /**
+ * The tag of the element (decimal value).
+ */
+ private final long tag;
+
+ /**
+ * The length of the content.
+ * Be aware this is not the size of the byte array.
+ */
+ private final int length;
+
+ /**
+ * The content of the element.
+ */
+ private final Buffer content;
+
+ /**
+ * The size of the element (tag + length + content).
+ */
+ private final int size;
+
+ /**
+ * The position in the content.
+ * Used to iterate over the content (sequence).
+ */
+ private int cursor;
+
+ public DerParser(byte[] bytes) {
+ var position = new AtomicInteger(0);
+ var buffer = Buffer.buffer(bytes);
+ byte b = buffer.getByte(position.getAndIncrement());
+ type = Type.from(b);
+ tag = decodeTag(b, buffer, position);
+ length = decodeLength(buffer, position);
+ content = buffer.slice(position.get(), position.get() + length);
+ size = position.get() + length;
+ cursor = 0; // Position in the content
+ }
+
+ private long decodeTag(byte b, Buffer bytes, AtomicInteger position) {
+ // See https://en.wikipedia.org/wiki/X.690#Identifier_octets
+ long t = (b & 0x1F);
+ if (t != 0x1F) {
+ return t;
+ }
+ t = 0;
+ b = bytes.getByte(position.getAndIncrement());
+ while ((b & 0x80) != 0) {
+ t <<= 7;
+ t = t | (b & 0x7F);
+ b = bytes.getByte(position.getAndIncrement());
+ }
+ return t;
+ }
+
+ private int decodeLength(Buffer bytes, AtomicInteger position) {
+ byte b = bytes.getByte(position.getAndIncrement());
+ if ((b & 0x80) == 0) {
+ return b & 0x7F;
+ }
+ int numberOfLengthBytes = (b & 0x7F);
+ if (numberOfLengthBytes == 0) {
+ throw new IllegalArgumentException("Indefinite form is not supported");
+ }
+ int length = 0;
+ for (int i = 0; i < numberOfLengthBytes; i++) {
+ length <<= 8;
+ length |= (bytes.getByte(position.getAndIncrement()) & 0xFF);
+ }
+ return length;
+ }
+
+ public Buffer content() {
+ return this.content;
+ }
+
+ public byte[] toByteArray() {
+ return this.content.getBytes();
+ }
+
+ public Type type() {
+ return type;
+ }
+
+ public long tag() {
+ return tag;
+ }
+
+ public int length() {
+ return length;
+ }
+
+ public DerParser next() {
+ if (tag == Tag.ASN1_SEQUENCE.number() || tag == Tag.SEQUENCE.number()) {
+ if (cursor < length) {
+ var nested = new DerParser(content.getBytes(cursor, length));
+ cursor = cursor + nested.size;
+ return nested;
+ }
+ }
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/parsers/EncryptedPKCS8Parser.java b/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/parsers/EncryptedPKCS8Parser.java
new file mode 100644
index 0000000..dc6d7fd
--- /dev/null
+++ b/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/parsers/EncryptedPKCS8Parser.java
@@ -0,0 +1,102 @@
+package io.smallrye.certs.pem.parsers;
+
+import io.vertx.core.buffer.Buffer;
+
+import javax.crypto.Cipher;
+import javax.crypto.EncryptedPrivateKeyInfo;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import java.io.IOException;
+import java.security.AlgorithmParameters;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Base64;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class EncryptedPKCS8Parser implements PKPemParser {
+
+ private static final String PKCS8_ENCRYPTED_START = "-+BEGIN\\s+ENCRYPTED\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
+
+ private static final String PKCS8_ENCRYPTED_END = "-+END\\s+ENCRYPTED\\s+PRIVATE\\s+KEY[^-]*-+";
+
+ private static final Pattern PATTERN = Pattern.compile(PKCS8_ENCRYPTED_START + BASE64_TEXT + PKCS8_ENCRYPTED_END,
+ Pattern.CASE_INSENSITIVE);
+
+ private static final List ALGORITHMS = List.of("RSA", "RSASSA-PSS", "EC", "DSA", "EdDSA", "XDH");
+
+ public EncryptedPKCS8Parser() {
+ }
+
+ @Override
+ public PrivateKey getKey(String content, String password) {
+ try {
+ Matcher matcher = PATTERN.matcher(content);
+ if (matcher.find()) {
+ var encoded = matcher.group(BASE64_TEXT_GROUP);
+ var decoded = decodeBase64(encoded);
+ return extract(decoded, password);
+ }
+ } catch (Exception e) {
+ return null;
+ }
+ // Does not match PKCS8 encrypted pattern
+ return null;
+ }
+
+ private PrivateKey extract(byte[] decoded, String password) {
+ var key = decrypt(decoded, password);
+ for (String algo : ALGORITHMS) {
+ try {
+ KeyFactory factory = KeyFactory.getInstance(algo);
+ return factory.generatePrivate(key);
+ } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
+ // Ignore
+ }
+ }
+ return null;
+ }
+
+ public static final String PBES2_ALGORITHM = "PBES2";
+
+ static PKCS8EncodedKeySpec decrypt(byte[] bytes, String password) {
+ try {
+ EncryptedPrivateKeyInfo keyInfo = new EncryptedPrivateKeyInfo(bytes);
+ AlgorithmParameters algorithmParameters = keyInfo.getAlgParameters();
+ String encryptionAlgorithm = getEncryptionAlgorithm(algorithmParameters, keyInfo.getAlgName());
+ SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(encryptionAlgorithm);
+ SecretKey key = keyFactory.generateSecret(new PBEKeySpec(password.toCharArray()));
+ Cipher cipher = Cipher.getInstance(encryptionAlgorithm);
+ cipher.init(Cipher.DECRYPT_MODE, key, algorithmParameters);
+ return keyInfo.getKeySpec(cipher);
+ } catch (IOException | GeneralSecurityException ex) {
+ throw new IllegalArgumentException("Error decrypting private key", ex);
+ }
+ }
+
+ private static String getEncryptionAlgorithm(AlgorithmParameters algParameters, String algName) {
+ if (algParameters != null && PBES2_ALGORITHM.equals(algName)) {
+ return algParameters.toString();
+ }
+ return algName;
+ }
+
+ public Buffer decryptKey(String content, String secret) {
+ var pk = getKey(content, secret);
+ if (pk == null) {
+ return null;
+ }
+ Buffer buffer = Buffer.buffer();
+ buffer.appendString("-----BEGIN PRIVATE KEY-----\n");
+ buffer.appendString(Base64.getEncoder().encodeToString(pk.getEncoded()));
+ buffer.appendString("\n-----END PRIVATE KEY-----\n\n");
+
+ return buffer;
+ }
+}
diff --git a/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/parsers/PKCS1Parser.java b/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/parsers/PKCS1Parser.java
new file mode 100644
index 0000000..cf05ec6
--- /dev/null
+++ b/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/parsers/PKCS1Parser.java
@@ -0,0 +1,57 @@
+package io.smallrye.certs.pem.parsers;
+
+import io.smallrye.certs.pem.der.DerEncoder;
+
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parse PKCS1 private key (RSA) from PEM format.
+ */
+public class PKCS1Parser implements PKPemParser {
+
+ private static final String PKCS1_RSA_START = "-+BEGIN\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
+
+ private static final String PKCS1_RSA_END = "-+END\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+";
+
+ private static final Pattern PKCS1_PATTERN = Pattern.compile(PKCS1_RSA_START + BASE64_TEXT + PKCS1_RSA_END,
+ Pattern.CASE_INSENSITIVE);
+
+ @Override
+ public PrivateKey getKey(String content, String ignored) {
+ try {
+ Matcher matcher = PKCS1_PATTERN.matcher(content);
+ if (matcher.find()) {
+ var encoded = matcher.group(BASE64_TEXT_GROUP);
+ var decoded = decodeBase64(encoded);
+ return extract(decoded);
+ }
+ } catch (Exception e) {
+ return null;
+ }
+ // Does not match PKCS1 pattern
+ return null;
+ }
+
+ private PrivateKey extract(byte[] decoded) {
+ try {
+ DerEncoder encoder = new DerEncoder();
+ encoder.integer(0x00); // Version 0
+
+ DerEncoder algorithmIdentifier = new DerEncoder();
+ algorithmIdentifier.oid(RSA_ALGORITHM);
+ algorithmIdentifier.oid(null);
+
+ encoder.sequence(algorithmIdentifier.toBytes());
+ encoder.octetString(decoded);
+ var spec = new PKCS8EncodedKeySpec(encoder.toSequence());
+ return KeyFactory.getInstance("RSA").generatePrivate(spec);
+ } catch (Exception ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+}
diff --git a/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/parsers/PKCS8Parser.java b/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/parsers/PKCS8Parser.java
new file mode 100644
index 0000000..0c2ebc9
--- /dev/null
+++ b/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/parsers/PKCS8Parser.java
@@ -0,0 +1,76 @@
+package io.smallrye.certs.pem.parsers;
+
+import io.smallrye.certs.pem.der.ASN1ObjectIdentifier;
+import io.smallrye.certs.pem.der.DerParser;
+
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class PKCS8Parser implements PKPemParser {
+
+ private static final String PKCS8_START = "-+BEGIN\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
+
+ private static final String PKCS8_END = "-+END\\s+PRIVATE\\s+KEY[^-]*-+";
+
+ private static final Pattern PATTERN = Pattern.compile(PKCS8_START + BASE64_TEXT + PKCS8_END, Pattern.CASE_INSENSITIVE);
+
+ private static final List ALGORITHMS = List.of("RSA", "RSASSA-PSS", "EC", "DSA", "EdDSA", "XDH");
+
+ public PKCS8Parser() {
+ }
+
+ @Override
+ public PrivateKey getKey(String content, String password) {
+ try {
+ Matcher matcher = PATTERN.matcher(content);
+ if (matcher.find()) {
+ var encoded = matcher.group(BASE64_TEXT_GROUP);
+ var decoded = decodeBase64(encoded);
+ return extract(decoded, password);
+ }
+ } catch (Exception e) {
+ return null;
+ }
+ // Does not match PKCS8 encrypted pattern
+ return null;
+ }
+
+ private PrivateKey extract(byte[] decoded, String ignored) {
+ DerParser parser = new DerParser(decoded);
+ if (parser.type() != DerParser.Type.CONSTRUCTED || parser.tag() != DerParser.Tag.SEQUENCE.number()) {
+ throw new IllegalArgumentException("Key spec should be an encoded sequence");
+ }
+ var version = parser.next();
+ if (version.type() != DerParser.Type.PRIMITIVE || version.tag() != DerParser.Tag.INTEGER.number()) {
+ throw new IllegalArgumentException("Key spec should contain the (integer) version");
+ }
+
+ var seq = parser.next();
+ if (seq.type() != DerParser.Type.CONSTRUCTED || seq.tag() != DerParser.Tag.SEQUENCE.number()) {
+ throw new IllegalArgumentException("Key spec should contain a sequence");
+ }
+
+ var algorithmId = seq.next();
+ if (algorithmId.type() != DerParser.Type.PRIMITIVE || algorithmId.tag() != DerParser.Tag.OBJECT_IDENTIFIER.number()) {
+ throw new IllegalArgumentException("Key spec container expects an object identifier as algorithm id");
+ }
+ String algorithm = ASN1ObjectIdentifier.getAlgorithmId(algorithmId.content().getBytes());
+ var spec = (algorithm != null) ? new PKCS8EncodedKeySpec(decoded, algorithm) : new PKCS8EncodedKeySpec(decoded);
+
+ for (String algo : ALGORITHMS) {
+ try {
+ KeyFactory factory = KeyFactory.getInstance(algo);
+ return factory.generatePrivate(spec);
+ } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
+ // Ignore
+ }
+ }
+ return null;
+ }
+}
diff --git a/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/parsers/PKPemParser.java b/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/parsers/PKPemParser.java
new file mode 100644
index 0000000..c0ba938
--- /dev/null
+++ b/private-key-pem-parser/src/main/java/io/smallrye/certs/pem/parsers/PKPemParser.java
@@ -0,0 +1,21 @@
+package io.smallrye.certs.pem.parsers;
+
+import io.smallrye.certs.pem.der.ASN1ObjectIdentifier;
+
+import java.security.PrivateKey;
+import java.util.Base64;
+
+public interface PKPemParser {
+
+ String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)";
+ int BASE64_TEXT_GROUP = 1;
+
+ ASN1ObjectIdentifier RSA_ALGORITHM = ASN1ObjectIdentifier.OID_1_2_840_113549_1_1_1;
+
+ PrivateKey getKey(String content, String password);
+
+ default byte[] decodeBase64(String content) {
+ byte[] contentBytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes();
+ return Base64.getDecoder().decode(contentBytes);
+ }
+}
diff --git a/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/der/ASN1ObjectIdentifierTest.java b/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/der/ASN1ObjectIdentifierTest.java
new file mode 100644
index 0000000..ccdfae1
--- /dev/null
+++ b/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/der/ASN1ObjectIdentifierTest.java
@@ -0,0 +1,90 @@
+package io.smallrye.certs.pem.der;
+
+import org.junit.jupiter.api.Test;
+import java.util.HexFormat;
+import static org.junit.jupiter.api.Assertions.*;
+
+class ASN1ObjectIdentifierTest {
+
+ @Test
+ void testOID_1_2_840_10040_4_1() {
+ ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.OID_1_2_840_10040_4_1;
+ byte[] expected = HexFormat.of().parseHex("2a8648ce380401");
+ assertArrayEquals(expected, oid.value(), "OID 1.2.840.10040.4.1 encoding mismatch");
+ }
+
+ @Test
+ void testOID_1_2_840_113549_1_1_1() {
+ ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.OID_1_2_840_113549_1_1_1;
+ byte[] expected = HexFormat.of().parseHex("2A864886F70D010101");
+ assertArrayEquals(expected, oid.value(), "OID 1.2.840.113549.1.1.1 encoding mismatch");
+ }
+
+ @Test
+ void testOID_1_2_840_113549_1_1_10() {
+ ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.OID_1_2_840_113549_1_1_10;
+ byte[] expected = HexFormat.of().parseHex("2a864886f70d01010a");
+ assertArrayEquals(expected, oid.value(), "OID 1.2.840.113549.1.1.10 encoding mismatch");
+ }
+
+ @Test
+ void testOID_1_3_101_110() {
+ ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.OID_1_3_101_110;
+ byte[] expected = HexFormat.of().parseHex("2b656e");
+ assertArrayEquals(expected, oid.value(), "OID 1.3.101.110 encoding mismatch");
+ }
+
+ @Test
+ void testOID_1_3_101_111() {
+ ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.OID_1_3_101_111;
+ byte[] expected = HexFormat.of().parseHex("2b656f");
+ assertArrayEquals(expected, oid.value(), "OID 1.3.101.111 encoding mismatch");
+ }
+
+ @Test
+ void testOID_1_3_101_112() {
+ ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.OID_1_3_101_112;
+ byte[] expected = HexFormat.of().parseHex("2b6570");
+ assertArrayEquals(expected, oid.value(), "OID 1.3.101.112 encoding mismatch");
+ }
+
+ @Test
+ void testOID_1_3_101_113() {
+ ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.OID_1_3_101_113;
+ byte[] expected = HexFormat.of().parseHex("2b6571");
+ assertArrayEquals(expected, oid.value(), "OID 1.3.101.113 encoding mismatch");
+ }
+
+ @Test
+ void testOID_1_2_840_10045_2_1() {
+ ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.OID_1_2_840_10045_2_1;
+ byte[] expected = HexFormat.of().parseHex("2a8648ce3d0201");
+ assertArrayEquals(expected, oid.value(), "OID 1.2.840.10045.2.1 encoding mismatch");
+ }
+
+ @Test
+ void testOID_1_3_132_0_34() {
+ ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.OID_1_3_132_0_34;
+ byte[] expected = HexFormat.of().parseHex("2b81040022");
+ assertArrayEquals(expected, oid.value(), "OID 1.3.132.0.34 encoding mismatch");
+ }
+
+ @Test
+ void testCustomOIDCreation() {
+ ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.from("2a8648", "foo");
+ byte[] expected = HexFormat.of().parseHex("2a8648");
+ assertArrayEquals(expected, oid.value(), "Custom OID encoding mismatch");
+ assertEquals("foo", oid.algorithmId(), "Custom OID algorithm ID mismatch");
+ }
+
+ @Test
+ void testGetAlgorithmId() {
+ byte[] content = HexFormat.of().parseHex("2A864886F70D010101");
+ String algorithmId = ASN1ObjectIdentifier.getAlgorithmId(content);
+ assertEquals("RSA", algorithmId, "Algorithm ID mismatch");
+
+ content = HexFormat.of().parseHex("2b6571");
+ algorithmId = ASN1ObjectIdentifier.getAlgorithmId(content);
+ assertEquals("EdDSA", algorithmId, "Algorithm ID mismatch");
+ }
+}
\ No newline at end of file
diff --git a/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/der/DerEncoderTest.java b/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/der/DerEncoderTest.java
new file mode 100644
index 0000000..eb67f55
--- /dev/null
+++ b/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/der/DerEncoderTest.java
@@ -0,0 +1,94 @@
+package io.smallrye.certs.pem.der;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.HexFormat;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class DerEncoderTest {
+
+ @Test
+ void testIntegerEncoding() {
+ DerEncoder encoder = new DerEncoder();
+ encoder.integer(0x05); // INTEGER with value 5
+
+ byte[] expected = new byte[] { 0x02, 0x01, 0x05 }; // INTEGER tag (0x02), length 1, value 5
+ assertArrayEquals(expected, encoder.toBytes());
+ }
+
+ @Test
+ void testOctetStringEncoding() {
+ DerEncoder encoder = new DerEncoder();
+ encoder.octetString(new byte[] { 0x41, 0x42 }); // OCTET STRING with value "AB"
+
+ byte[] expected = new byte[] { 0x04, 0x02, 0x41, 0x42 }; // OCTET STRING tag (0x04), length 2, value "AB"
+ assertArrayEquals(expected, encoder.toBytes());
+ }
+
+ @Test
+ void testObjectIdEncoding() {
+ DerEncoder encoder = new DerEncoder();
+ encoder.oid(new ASN1ObjectIdentifier(new byte[] { 0x2A, (byte) 0x86, 0x48 }, "foo")); // OID representing 1.2.840
+ byte[] expected = new byte[] { 0x06, 0x03, 0x2A, (byte) 0x86, 0x48 }; // OID tag (0x06), length 3, value 1.2.840
+ assertArrayEquals(expected, encoder.toBytes());
+ }
+
+ @Test
+ void testSequenceEncoding() {
+ DerEncoder encoder = new DerEncoder();
+ encoder.sequence(new byte[] { 0x02, 0x01, 0x0A, 0x04, 0x02, 0x41, 0x42 }); // SEQUENCE with INTEGER (10) and OCTET STRING ("AB")
+
+ byte[] expected = new byte[] {
+ 0x30, 0x07, // SEQUENCE tag (0x30), length 7
+ 0x02, 0x01, 0x0A, // INTEGER (10)
+ 0x04, 0x02, 0x41, 0x42 // OCTET STRING ("AB")
+ };
+ assertArrayEquals(expected, encoder.toBytes());
+ }
+
+ @Test
+ void testMultiByteLengthEncoding() {
+ DerEncoder encoder = new DerEncoder();
+ byte[] longContent = new byte[256]; // Content of 256 bytes for multi-byte length test
+ encoder.octetString(longContent);
+
+ byte[] result = encoder.toBytes();
+
+ assertEquals(0x04, result[0]); // OCTET STRING tag
+ assertEquals((byte) 0x82, result[1]); // Multi-byte length indicator (0x82 means next two bytes indicate length)
+ assertEquals(0x01, result[2]); // Length high byte (256 in two bytes)
+ assertEquals(0x00, result[3]); // Length low byte
+ assertEquals(256, result.length - 4); // Content length
+ }
+
+ @Test
+ void testSequenceContainingMultipleElements() {
+ DerEncoder sequenceEncoder = new DerEncoder();
+ DerEncoder intEncoder = new DerEncoder();
+ DerEncoder stringEncoder = new DerEncoder();
+
+ intEncoder.integer(0x05); // INTEGER with value 5
+ stringEncoder.octetString(new byte[] { 0x41, 0x42 }); // OCTET STRING with value "AB"
+
+ sequenceEncoder.sequence(intEncoder.toBytes());
+ sequenceEncoder.addToSequence(stringEncoder.toBytes());
+
+ byte[] sequence = sequenceEncoder.toBytes();
+
+ assertEquals(0x30, sequence[0]); // ASN1 SEQUENCE tag
+ assertEquals(2 + intEncoder.toBytes().length + stringEncoder.toBytes().length, sequence.length);
+ }
+
+ @Test
+ void testEmptyOidEncoding() {
+ DerEncoder encoder = new DerEncoder();
+ encoder.oid(null); // NULL OID
+
+ byte[] expected = new byte[] { 0x05, 0x00 }; // NULL tag (0x05), length 0
+ assertArrayEquals(expected, encoder.toBytes());
+ }
+
+}
diff --git a/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/der/DerParserTest.java b/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/der/DerParserTest.java
new file mode 100644
index 0000000..2407821
--- /dev/null
+++ b/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/der/DerParserTest.java
@@ -0,0 +1,221 @@
+package io.smallrye.certs.pem.der;
+
+import io.vertx.core.buffer.Buffer;
+import org.junit.jupiter.api.Test;
+
+import java.util.HexFormat;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class DerParserTest {
+
+ @Test
+ void testIntegerParsing() {
+ // ASN.1 DER encoding of an INTEGER with value 5
+ byte[] bytes = new byte[] { 0x02, 0x01, 0x05 }; // TAG: 0x02 (INTEGER), LENGTH: 0x01, VALUE: 0x05
+ DerParser parser = new DerParser(bytes);
+
+ assertEquals(DerParser.Type.PRIMITIVE, parser.type());
+ assertEquals(DerParser.Tag.INTEGER.number(), parser.tag());
+ assertEquals(5, parser.content().getByte(0));
+ }
+
+ @Test
+ void testOctetStringParsing() {
+ // ASN.1 DER encoding of an OCTET STRING with value "AB"
+ byte[] bytes = new byte[] { 0x04, 0x02, 0x41, 0x42 }; // TAG: 0x04 (OCTET STRING), LENGTH: 0x02, VALUE: "AB"
+ DerParser parser = new DerParser(bytes);
+
+ assertEquals(DerParser.Type.PRIMITIVE, parser.type());
+ assertEquals(DerParser.Tag.OCTET_STRING.number(), parser.tag());
+ assertEquals(2, parser.content().length());
+ assertEquals((byte) 0x41, parser.content().getByte(0));
+ assertEquals((byte) 0x42, parser.content().getByte(1));
+ }
+
+ @Test
+ void testObjectIdentifierParsing() {
+ // ASN.1 DER encoding of an OBJECT IDENTIFIER with value 1.2.840.113549
+ byte[] bytes = new byte[] { 0x06, 0x06, 0x2A, (byte) 0x86, 0x48, (byte) 0x86, (byte) 0xF7, 0x0D };
+ DerParser parser = new DerParser(bytes);
+
+ assertEquals(DerParser.Type.PRIMITIVE, parser.type());
+ assertEquals(DerParser.Tag.OBJECT_IDENTIFIER.number(), parser.tag());
+ assertEquals(6, parser.content().length());
+ }
+
+ @Test
+ void testSequenceParsing() {
+ // ASN.1 DER encoding of a SEQUENCE containing an INTEGER (5) and BOOLEAN (true)
+ byte[] bytes = new byte[] {
+ 0x10, 0x06, // SEQUENCE TAG (0x10) with LENGTH 6
+ 0x02, 0x01, 0x05, // INTEGER TAG (0x02) with LENGTH 1 and VALUE 5
+ 0x01, 0x01, (byte) 0xFF // BOOLEAN TAG (0x01) with LENGTH 1 and VALUE true (0xFF)
+ };
+ DerParser parser = new DerParser(bytes);
+
+ assertEquals(DerParser.Type.PRIMITIVE, parser.type());
+ assertEquals(DerParser.Tag.SEQUENCE.number(), parser.tag());
+ assertEquals(6, parser.content().length());
+ }
+
+ @Test
+ void testMultiByteLengthParsing() {
+ // ASN.1 DER encoding of an OCTET STRING with a long length (256 bytes)
+ byte[] bytes = new byte[260];
+ bytes[0] = 0x04; // TAG for OCTET STRING
+ bytes[1] = (byte) 0x82; // Length in two bytes (0x82 means multi-byte length follows)
+ bytes[2] = 0x01; // High byte of length (256 bytes)
+ bytes[3] = 0x00; // Low byte of length (256 bytes)
+
+ DerParser parser = new DerParser(bytes);
+
+ assertEquals(DerParser.Type.PRIMITIVE, parser.type());
+ assertEquals(DerParser.Tag.OCTET_STRING.number(), parser.tag());
+ assertEquals(256, parser.content().length());
+ }
+
+ @Test
+ void testAsn1SequenceParsing() {
+ // ASN.1 DER encoding of a SEQUENCE (0x30) containing:
+ // - INTEGER (value: 10)
+ // - OCTET STRING (value: "AB")
+ byte[] bytes = new byte[] {
+ 0x30, 0x07, // SEQUENCE TAG (0x30) with LENGTH 7
+ 0x02, 0x01, 0x0A, // INTEGER TAG (0x02) with LENGTH 1 and VALUE 10
+ 0x04, 0x02, 0x41, 0x42 // OCTET STRING TAG (0x04) with LENGTH 2 and VALUE "AB"
+ };
+
+ DerParser parser = new DerParser(bytes);
+
+ assertEquals(DerParser.Type.CONSTRUCTED, parser.type());
+ assertEquals(0x10, parser.tag()); // SEQUENCE tag is 0x10 in the enum mapping
+ assertEquals(7, parser.content().length());
+
+ // Now verify the inner elements
+ Buffer content = parser.content();
+
+ // INTEGER element
+ DerParser intParser = new DerParser(new byte[] { content.getByte(0), content.getByte(1), content.getByte(2) });
+ assertEquals(DerParser.Type.PRIMITIVE, intParser.type());
+ assertEquals(DerParser.Tag.INTEGER.number(), intParser.tag());
+ assertEquals(1, intParser.content().length());
+ assertEquals(10, intParser.content().getByte(0));
+
+ // OCTET STRING element
+ DerParser octetStringParser = new DerParser(
+ new byte[] { content.getByte(3), content.getByte(4), content.getByte(5), content.getByte(6) });
+ assertEquals(DerParser.Type.PRIMITIVE, octetStringParser.type());
+ assertEquals(DerParser.Tag.OCTET_STRING.number(), octetStringParser.tag());
+ assertEquals(2, octetStringParser.length());
+ assertEquals(2, octetStringParser.content().length());
+ assertEquals((byte) 0x41, octetStringParser.content().getByte(0));
+ assertEquals((byte) 0x42, octetStringParser.content().getByte(1));
+ }
+
+ @Test
+ void testAsn1SequenceParsingUsingNext() {
+ // ASN.1 DER encoding of a SEQUENCE (0x30) containing:
+ // - INTEGER (value: 10)
+ // - OCTET STRING (value: "AB")
+ byte[] bytes = new byte[] {
+ 0x30, 0x07, // SEQUENCE TAG (0x30) with LENGTH 7
+ 0x02, 0x01, 0x0A, // INTEGER TAG (0x02) with LENGTH 1 and VALUE 10
+ 0x04, 0x02, 0x41, 0x42 // OCTET STRING TAG (0x04) with LENGTH 2 and VALUE "AB"
+ };
+
+ DerParser parser = new DerParser(bytes);
+
+ assertEquals(DerParser.Type.CONSTRUCTED, parser.type());
+ assertEquals(0x10, parser.tag()); // SEQUENCE tag is 0x10 in the enum mapping
+ assertEquals(7, parser.length());
+
+ DerParser first = parser.next();
+ DerParser second = parser.next();
+ DerParser third = parser.next();
+
+ // INTEGER element
+ assertEquals(DerParser.Type.PRIMITIVE, first.type());
+ assertEquals(DerParser.Tag.INTEGER.number(), first.tag());
+ assertEquals(1, first.content().length());
+ assertEquals(10, first.content().getByte(0));
+
+ // OCTET STRING element
+ assertEquals(DerParser.Type.PRIMITIVE, second.type());
+ assertEquals(DerParser.Tag.OCTET_STRING.number(), second.tag());
+ assertEquals(2, second.length());
+ assertEquals(2, second.content().length());
+ assertEquals((byte) 0x41, second.content().getByte(0));
+ assertEquals((byte) 0x42, second.content().getByte(1));
+
+ // No more elements
+ assertNull(third);
+ assertNull(parser.next());
+ // Illegal calls
+ assertNull(first.next());
+ assertNull(second.next());
+ }
+
+ @Test
+ void testIndefiniteFormError() {
+ // Example of an invalid DER encoding (indefinite form is not supported in DER)
+ byte[] bytes = new byte[] { 0x04, (byte) 0x80 }; // OCTET STRING with indefinite length form (0x80)
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+ new DerParser(bytes);
+ });
+ assertTrue(exception.getMessage().contains("Indefinite form is not supported"));
+ }
+
+ @Test
+ void testParseNestedDERSequence() {
+ // Pre-constructed DER byte array representing the structure:
+ // SEQUENCE (outer) -> SEQUENCE (inner) -> INTEGER (42)
+ byte[] derData = {
+ 0x10, 0x05, // Outer SEQUENCE, length 5 bytes
+ 0x10, 0x03, // Inner SEQUENCE, length 4 bytes
+ 0x02, 0x01, 0x2A // INTEGER with value 42 (0x2A in hex)
+ };
+
+ // Initialize DER parser
+ DerParser parser = new DerParser(derData);
+
+ // Check that the first object is a sequence
+ assertEquals(DerParser.Type.PRIMITIVE, parser.type());
+ assertEquals(DerParser.Tag.SEQUENCE.number(), parser.tag());
+
+ var innerSeq = parser.next();
+ assertEquals(DerParser.Type.PRIMITIVE, innerSeq.type());
+ assertEquals(DerParser.Tag.SEQUENCE.number(), innerSeq.tag());
+ var num = innerSeq.next();
+ assertEquals(DerParser.Type.PRIMITIVE, num.type());
+ assertEquals(DerParser.Tag.INTEGER.number(), num.tag());
+ assertEquals(42, num.content().getByte(0));
+ }
+
+ @Test
+ void testParseNestedDERASN1Sequence() {
+ // Pre-constructed DER byte array representing the structure:
+ // SEQUENCE (outer) -> SEQUENCE (inner) -> INTEGER (42)
+ byte[] derData = {
+ 0x30, 0x05, // Outer SEQUENCE, length 5 bytes
+ 0x30, 0x03, // Inner SEQUENCE, length 4 bytes
+ 0x02, 0x01, 0x2A // INTEGER with value 42 (0x2A in hex)
+ };
+
+ // Initialize DER parser
+ DerParser parser = new DerParser(derData);
+
+ // Check that the first object is a sequence
+ assertEquals(DerParser.Type.CONSTRUCTED, parser.type());
+ assertEquals(DerParser.Tag.SEQUENCE.number(), parser.tag());
+
+ var innerSeq = parser.next();
+ assertEquals(DerParser.Type.CONSTRUCTED, innerSeq.type());
+ assertEquals(DerParser.Tag.SEQUENCE.number(), innerSeq.tag());
+ var num = innerSeq.next();
+ assertEquals(DerParser.Type.PRIMITIVE, num.type());
+ assertEquals(DerParser.Tag.INTEGER.number(), num.tag());
+ assertEquals(42, num.content().getByte(0));
+ }
+}
diff --git a/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/parsers/EncryptedPKCS8ParserTest.java b/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/parsers/EncryptedPKCS8ParserTest.java
new file mode 100644
index 0000000..36e0265
--- /dev/null
+++ b/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/parsers/EncryptedPKCS8ParserTest.java
@@ -0,0 +1,129 @@
+package io.smallrye.certs.pem.parsers;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openssl.PKCS8Generator;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
+import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.OutputEncryptor;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.StringWriter;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.Security;
+import java.util.Base64;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class EncryptedPKCS8ParserTest {
+
+ private PrivateKey originalPrivateKey;
+ private EncryptedPKCS8Parser parser;
+ private String encryptedPKCS8Key;
+ private static final String password = "correctPassword";
+
+ @BeforeEach
+ void setup() throws Exception {
+ Security.addProvider(new BouncyCastleProvider());
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048);
+ originalPrivateKey = keyGen.generateKeyPair().getPrivate();
+
+ try (StringWriter writer = new StringWriter();
+ JcaPEMWriter pemWriter = new JcaPEMWriter(writer)) {
+
+ // Define the encryptor with desired algorithm and passphrase
+ OutputEncryptor encryptor = new JceOpenSSLPKCS8EncryptorBuilder(
+ PKCS8Generator.PBE_SHA1_3DES).setPassword(password.toCharArray()).build();
+
+ // Create an encrypted PKCS8 format
+ PKCS8Generator pkcs8Generator = new JcaPKCS8Generator(originalPrivateKey, encryptor);
+
+ // Write the encrypted private key to a file in PEM format
+ pemWriter.writeObject(pkcs8Generator);
+ pemWriter.close();
+ encryptedPKCS8Key = writer.toString();
+ } catch (OperatorCreationException e) {
+ throw new RuntimeException(e);
+ }
+ parser = new EncryptedPKCS8Parser();
+ }
+
+ @Test
+ void testParseValidEncryptedKeyWithCorrectPassword() {
+ PrivateKey privateKey = parser.getKey(encryptedPKCS8Key, password);
+ assertNotNull(privateKey, "Private key should not be null with correct password");
+ }
+
+ @Test
+ void testParseValidEncryptedKeyWithIncorrectPassword() {
+ String incorrectPassword = "wrongPassword";
+ PrivateKey privateKey = parser.getKey(encryptedPKCS8Key, incorrectPassword);
+ assertNull(privateKey, "Private key should be null with incorrect password");
+ }
+
+ @Test
+ void testParseInvalidEncryptedKeyFormat() {
+ String invalidKey = """
+ -----BEGIN ENCRYPTED PRIVATE KEY-----
+ InvalidBase64Data==
+ -----END ENCRYPTED PRIVATE KEY-----
+ """;
+ PrivateKey privateKey = parser.getKey(invalidKey, password);
+ assertNull(privateKey, "Private key should be null for invalid key format");
+ }
+
+ @Test
+ void testParseNonEncryptedRandomKey() {
+ String nonEncryptedKey = """
+ -----BEGIN PRIVATE KEY-----
+ MIIBVgIBADANBgkqhkiG9w0BAQEFAASCATwwggE4AgEAAkEA...moreBase64Data...==
+ -----END PRIVATE KEY-----
+ """;
+ PrivateKey privateKey = parser.getKey(nonEncryptedKey, password);
+ assertNull(privateKey, "Private key should be null for non-encrypted key");
+ }
+
+ @Test
+ void testParseValidEncryptedPkcs8Key() {
+ PrivateKey decryptedKey = parser.getKey(encryptedPKCS8Key, password);
+ assertNotNull(decryptedKey, "Decrypted private key should not be null for a valid encrypted PKCS8 key");
+ }
+
+ @Test
+ void testParseWithIncorrectPassword() {
+ PrivateKey decryptedKey = parser.getKey(encryptedPKCS8Key, "wrongPassword");
+ assertNull(decryptedKey, "Decrypted private key should be null when an incorrect password is used");
+ }
+
+ @Test
+ void testDecryptedKeyAttributesMatchOriginal() throws Exception {
+ PrivateKey decryptedKey = parser.getKey(encryptedPKCS8Key, password);
+ assertNotNull(decryptedKey, "Decrypted private key should not be null for a valid encrypted PKCS8 key");
+
+ // Verify that the decrypted key and original key are of the same type and have the same attributes
+ assertEquals(originalPrivateKey.getAlgorithm(), decryptedKey.getAlgorithm(),
+ "Algorithm should match original private key");
+ assertArrayEquals(originalPrivateKey.getEncoded(), decryptedKey.getEncoded(),
+ "Decrypted key should match the original private key in encoding");
+ }
+
+ @Test
+ void testParseNonEncryptedKey() {
+ String nonEncryptedPem = Base64.getEncoder().encodeToString(originalPrivateKey.getEncoded());
+ String nonEncryptedKeyPem = """
+ -----BEGIN PRIVATE KEY-----
+ %s
+ -----END PRIVATE KEY-----
+ """.formatted(nonEncryptedPem);
+
+ PrivateKey decryptedKey = parser.getKey(nonEncryptedKeyPem, password);
+ assertNull(decryptedKey, "Parsed private key should be null for a non-encrypted PKCS8 PEM format");
+ }
+}
\ No newline at end of file
diff --git a/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/parsers/PKCS1ParserTest.java b/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/parsers/PKCS1ParserTest.java
new file mode 100644
index 0000000..1a85c93
--- /dev/null
+++ b/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/parsers/PKCS1ParserTest.java
@@ -0,0 +1,212 @@
+package io.smallrye.certs.pem.parsers;
+
+import org.bouncycastle.asn1.pkcs.RSAPrivateKey;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.util.io.pem.PemObject;
+import org.bouncycastle.util.io.pem.PemWriter;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.security.Security;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.RSAPrivateCrtKeySpec;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class PKCS1ParserTest {
+
+ private PKCS1Parser parser;
+ private String key;
+ private PrivateKey originalPrivateKey;
+
+ @BeforeEach
+ void setup() throws NoSuchAlgorithmException, NoSuchProviderException, IOException, InvalidKeySpecException {
+ Security.addProvider(new BouncyCastleProvider());
+
+ // Step 1: Generate RSA Key Pair
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA", "BC");
+ keyGen.initialize(2048);
+ KeyPair keyPair = keyGen.generateKeyPair();
+ originalPrivateKey = keyPair.getPrivate();
+
+ // Step 2: Extract the key parameters from the private key
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ RSAPrivateCrtKeySpec privateKeySpec = keyFactory.getKeySpec(originalPrivateKey, RSAPrivateCrtKeySpec.class);
+
+ // Step 3: Construct PKCS#1 format using RSAPrivateKey structure
+ RSAPrivateKey pkcs1PrivateKey = new RSAPrivateKey(
+ privateKeySpec.getModulus(),
+ privateKeySpec.getPublicExponent(),
+ privateKeySpec.getPrivateExponent(),
+ privateKeySpec.getPrimeP(),
+ privateKeySpec.getPrimeQ(),
+ privateKeySpec.getPrimeExponentP(),
+ privateKeySpec.getPrimeExponentQ(),
+ privateKeySpec.getCrtCoefficient());
+
+ // Step 4: Convert the RSAPrivateKey ASN.1 object to DER-encoded bytes
+ byte[] pkcs1Bytes = pkcs1PrivateKey.getEncoded();
+
+ // Step 5: Encode as PEM using PemWriter
+ StringWriter stringWriter = new StringWriter();
+ try (PemWriter pemWriter = new PemWriter(stringWriter)) {
+ pemWriter.writeObject(new PemObject("RSA PRIVATE KEY", pkcs1Bytes));
+ }
+
+ key = stringWriter.toString();
+ parser = new PKCS1Parser();
+ }
+
+ @Test
+ void testParseValidPkcs1Key() {
+ PrivateKey pk = parser.getKey(key, null);
+ assertNotNull(pk, "Parsed private key should not be null for valid PKCS1 key");
+
+ // Verify the decoded key length or check against a known portion of the DER-encoded output
+ byte[] keyBytes = pk.getEncoded();
+ assertTrue(keyBytes.length > 0, "Encoded key bytes should not be empty");
+
+ // Check the initial bytes for ASN.1 sequence (SEQUENCE tag, length, etc.) for PKCS8 format
+ assertEquals(0x30, keyBytes[0] & 0xFF,
+ "Key should start with SEQUENCE tag (0x30 in DER)");
+ }
+
+ @Test
+ void testParseInvalidPkcs1KeyFormat() {
+ String invalidKey = """
+ -----BEGIN RSA PRIVATE KEY-----
+ InvalidBase64Data==
+ -----END RSA PRIVATE KEY-----
+ """;
+ PrivateKey pk = parser.getKey(invalidKey, null);
+ assertNull(pk, "Parsed private key should be null for invalid PKCS1 key format");
+ }
+
+ @Test
+ void testParseNonMatchingKey() {
+ String nonMatchingKey = """
+ -----BEGIN PUBLIC KEY-----
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsdklj+ksdLq93Q==
+ -----END PUBLIC KEY-----
+ """;
+ PrivateKey pk = parser.getKey(nonMatchingKey, null);
+ assertNull(pk, "Parsed PKCS8EncodedKeySpec should be null for non-matching key format");
+ }
+
+ @Test
+ void testParseCorruptedPkcs1KeyData() {
+ String corruptedKey = """
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIBOgIBAAJBAK5Erl8asdk== // corrupted base64 data
+ -----END RSA PRIVATE KEY-----
+ """;
+ PrivateKey pk = parser.getKey(corruptedKey, null);
+ assertNull(pk, "Parsed private key should be null for corrupted PKCS1 key data");
+ }
+
+ @Test
+ void testParsePkcs1KeyWithExcessiveWhitespace() {
+ String keyWithWhitespace = """
+ -----BEGIN RSA PRIVATE KEY-----
+
+ MIIBOgIBAAJBAK5Erl8a+lFsA8MPsh9aL9F+NfgmHNkGr/H0X0KdD/YU==
+
+ -----END RSA PRIVATE KEY-----
+ """;
+ PrivateKey pk = parser.getKey(keyWithWhitespace, null);
+ assertNull(pk, "Parsed private key should be null for PKCS1 key with excessive whitespace");
+ }
+
+ @Test
+ void testParseNonRsaPrivateKey() {
+ String nonRsaKey = """
+ -----BEGIN PRIVATE KEY-----
+ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZC5aj6qwMF0l4V9+
+ E/Ns9js0Jx/vCEyXOg9T/MSTwRKhRANCAASFiBQ7XOkEHVFhzdL//o7aEdDmd0I5
+ KUYl3ofGdhduE5F3YoxftD0YrPrk73dbmZZKqzHpD6nG7T8PzYpGpB4L
+ -----END PRIVATE KEY-----
+ """;
+ PrivateKey pk = parser.getKey(nonRsaKey, null);
+ assertNull(pk, "Parsed private key should be null for non-RSA private key data");
+ }
+
+ @Test
+ void testParsePkcs1KeyWithIncorrectHeaderOrder() {
+ String invalidHeaderOrderKey = """
+ -----END RSA PRIVATE KEY-----
+ MIIBOgIBAAJBAK5Erl8a+lFsA8MPsh9aL9F+NfgmHNkGr/H0X0KdD/YU==
+ -----BEGIN RSA PRIVATE KEY-----
+ """;
+ PrivateKey pk = parser.getKey(invalidHeaderOrderKey, null);
+ assertNull(pk, "Parsed private key should be null for PKCS1 key with incorrect header order");
+ }
+
+ @Test
+ void testParseUnsupportedAlgorithm() {
+ String dsaPrivateKey = """
+ -----BEGIN DSA PRIVATE KEY-----
+ MIIBugIBAAKBgQDGRn7MQjFl+hLehRihs14kn5PHKeMThCbxwU82Wl5uCk6JX/YK
+ K+dzpf8ZkVPoMc1kZyMCUVYmj4nnIqbi7dBnL/NL9ixv9A5OwOwRVFYmXwvfr9dK
+ -----END DSA PRIVATE KEY-----
+ """;
+ PrivateKey pk = parser.getKey(dsaPrivateKey, null);
+ assertNull(pk, "Parsed private key should be null for unsupported algorithm");
+ }
+
+ @Test
+ void testParsePkcs1KeyWithExtraDataAroundPemHeaders() {
+ String keyWithExtraData = """
+ Some extra data before the key
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIBOgIBAAJBAK5Erl8a+lFsA8MPsh9aL9F+NfgmHNkGr/H0X0KdD/YU==
+ -----END RSA PRIVATE KEY-----
+ Some extra data after the key
+ """;
+ PrivateKey pk = parser.getKey(keyWithExtraData, null);
+ assertNull(pk, "Parsed private key should be null even with extra data around the PEM headers");
+ }
+
+ @Test
+ void testParseUnsupportedKeySize() {
+ String smallKey = """
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIBAgIBAAIBAAIBAA==
+ -----END RSA PRIVATE KEY-----
+ """;
+ PrivateKey pk = parser.getKey(smallKey, null);
+ assertNull(pk, "Parsed private key should be null for unsupported key size");
+ }
+
+ @Test
+ void testParsePkcs1KeyMatchesOriginal() throws Exception {
+ // Parse the PEM-encoded key
+ PrivateKey parsedPrivateKey = parser.getKey(key, null);
+ assertNotNull(parsedPrivateKey, "Parsed private key should not be null for valid PKCS1 key");
+
+ // Check if parsed private key matches original private key's properties
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ RSAPrivateCrtKeySpec originalSpec = keyFactory.getKeySpec(originalPrivateKey, RSAPrivateCrtKeySpec.class);
+ RSAPrivateCrtKeySpec parsedSpec = keyFactory.getKeySpec(parsedPrivateKey, RSAPrivateCrtKeySpec.class);
+
+ // Compare essential RSA parameters
+ assertEquals(originalSpec.getModulus(), parsedSpec.getModulus(), "Modulus should match");
+ assertEquals(originalSpec.getPrivateExponent(), parsedSpec.getPrivateExponent(), "Private exponent should match");
+ assertEquals(originalSpec.getPublicExponent(), parsedSpec.getPublicExponent(), "Public exponent should match");
+ assertEquals(originalSpec.getPrimeP(), parsedSpec.getPrimeP(), "Prime P should match");
+ assertEquals(originalSpec.getPrimeQ(), parsedSpec.getPrimeQ(), "Prime Q should match");
+ assertEquals(originalSpec.getPrimeExponentP(), parsedSpec.getPrimeExponentP(), "Prime Exponent P should match");
+ assertEquals(originalSpec.getPrimeExponentQ(), parsedSpec.getPrimeExponentQ(), "Prime Exponent Q should match");
+ assertEquals(originalSpec.getCrtCoefficient(), parsedSpec.getCrtCoefficient(), "CRT Coefficient should match");
+ }
+}
\ No newline at end of file
diff --git a/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/parsers/PKCS8ParserTest.java b/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/parsers/PKCS8ParserTest.java
new file mode 100644
index 0000000..c4c0e3f
--- /dev/null
+++ b/private-key-pem-parser/src/test/java/io/smallrye/certs/pem/parsers/PKCS8ParserTest.java
@@ -0,0 +1,84 @@
+package io.smallrye.certs.pem.parsers;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.Security;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Base64;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class PKCS8ParserTest {
+
+ private PKCS8Parser parser;
+ private PrivateKey originalPrivateKey;
+ private String pkcs8PemKey;
+
+ @BeforeEach
+ void setup() throws Exception {
+ // Add BouncyCastle provider for additional algorithms if needed
+ Security.addProvider(new BouncyCastleProvider());
+
+ // Initialize the parser
+ parser = new PKCS8Parser();
+
+ // Step 1: Generate an RSA key pair and store the original private key
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048);
+ KeyPair keyPair = keyGen.generateKeyPair();
+ originalPrivateKey = keyPair.getPrivate();
+
+ // Step 2: Convert the private key to PKCS#8 format and encode it as a PEM string
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(originalPrivateKey.getEncoded());
+ byte[] pkcs8EncodedKey = pkcs8KeySpec.getEncoded();
+ String base64EncodedKey = Base64.getMimeEncoder().encodeToString(pkcs8EncodedKey);
+ pkcs8PemKey = "-----BEGIN PRIVATE KEY-----\n" + base64EncodedKey + "\n-----END PRIVATE KEY-----";
+ }
+
+ @Test
+ void testParseValidPkcs8Key() {
+ // Parse the PKCS8 PEM key using PKCS8Parser
+ PrivateKey parsedKey = parser.getKey(pkcs8PemKey, null);
+ assertNotNull(parsedKey, "Parsed private key should not be null for a valid PKCS8 PEM key");
+
+ // Verify algorithm and format
+ assertEquals(originalPrivateKey.getAlgorithm(), parsedKey.getAlgorithm(),
+ "Algorithm should match the original private key");
+ assertEquals(originalPrivateKey.getFormat(), parsedKey.getFormat(), "Format should be PKCS#8");
+
+ // Verify the encoded key bytes to ensure exact match
+ assertArrayEquals(originalPrivateKey.getEncoded(), parsedKey.getEncoded(),
+ "Parsed key should match the original private key in encoding");
+ }
+
+ @Test
+ void testParseInvalidPkcs8KeyFormat() {
+ // Test that an invalid PEM format is handled gracefully and returns null
+ String invalidPemKey = "-----BEGIN PRIVATE KEY-----\nInvalidBase64Data==\n-----END PRIVATE KEY-----";
+ PrivateKey parsedKey = parser.getKey(invalidPemKey, null);
+ assertNull(parsedKey, "Parsed private key should be null for an invalid PEM format");
+ }
+
+ @Test
+ void testParseNonPkcs8Key() {
+ // Test that a non-PKCS8 key (e.g., an encrypted or unsupported format) returns null
+ String nonPkcs8PemKey = """
+ -----BEGIN ENCRYPTED PRIVATE KEY-----
+ MIIBvTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIu7HB4LKl0xgCAggA
+ ...
+ -----END ENCRYPTED PRIVATE KEY-----
+ """;
+ PrivateKey parsedKey = parser.getKey(nonPkcs8PemKey, null);
+ assertNull(parsedKey, "Parsed private key should be null for a non-PKCS8 format");
+ }
+}
\ No newline at end of file