diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParser.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParser.java index fdd433bd09b..6b3f061831a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParser.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParser.java @@ -18,6 +18,7 @@ package org.springframework.boot.buildpack.platform.docker.ssl; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.security.GeneralSecurityException; @@ -33,6 +34,10 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.springframework.boot.buildpack.platform.docker.ssl.PrivateKeyParser.DerElement.TagType; +import org.springframework.boot.buildpack.platform.docker.ssl.PrivateKeyParser.DerElement.ValueType; +import org.springframework.util.Assert; + /** * Parser for PKCS private key files in PEM format. * @@ -88,7 +93,38 @@ final class PrivateKeyParser { } private static PKCS8EncodedKeySpec createKeySpecForEc(byte[] bytes) { - return createKeySpecForAlgorithm(bytes, EC_ALGORITHM, EC_PARAMETERS); + DerElement ecPrivateKey = DerElement.of(bytes); + Assert.state(ecPrivateKey.isType(ValueType.ENCODED, TagType.SEQUENCE), + "Key spec should be an ASN.1 encoded sequence"); + DerElement version = DerElement.of(ecPrivateKey.getContents()); + Assert.state(version != null && version.isType(ValueType.PRIMITIVE, TagType.INTEGER), + "Key spec should start with version"); + Assert.state(version.getContents().remaining() == 1 && version.getContents().get() == 1, + "Key spec version must be 1"); + DerElement privateKey = DerElement.of(ecPrivateKey.getContents()); + Assert.state(privateKey != null && privateKey.isType(ValueType.PRIMITIVE, TagType.OCTET_STRING), + "Key spec should contain private key"); + DerElement parameters = DerElement.of(ecPrivateKey.getContents()); + return createKeySpecForAlgorithm(bytes, EC_ALGORITHM, getEcParameters(parameters)); + } + + private static int[] getEcParameters(DerElement parameters) { + if (parameters == null) { + return EC_PARAMETERS; + } + Assert.state(parameters.isType(ValueType.ENCODED), "Key spec should contain encoded parameters"); + DerElement contents = DerElement.of(parameters.getContents()); + Assert.state(contents.isType(ValueType.PRIMITIVE, TagType.OBJECT_IDENTIFIER), + "Key spec parameters should contain object identifier"); + return getEcParameters(contents.getContents()); + } + + private static int[] getEcParameters(ByteBuffer bytes) { + int[] result = new int[bytes.remaining()]; + for (int i = 0; i < result.length; i++) { + result[i] = bytes.get() & 0xFF; + } + return result; } private static PKCS8EncodedKeySpec createKeySpecForAlgorithm(byte[] bytes, int[] algorithm, int[] parameters) { @@ -251,4 +287,102 @@ final class PrivateKeyParser { } + /** + * An ASN.1 DER encoded element. + */ + static final class DerElement { + + private final ValueType valueType; + + private final long tagType; + + private ByteBuffer contents; + + private DerElement(ByteBuffer bytes) { + byte b = bytes.get(); + this.valueType = ((b & 0x20) == 0) ? ValueType.PRIMITIVE : ValueType.ENCODED; + this.tagType = decodeTagType(b, bytes); + int length = decodeLength(bytes); + bytes.limit(bytes.position() + length); + this.contents = bytes.slice(); + bytes.limit(bytes.capacity()); + bytes.position(bytes.position() + length); + } + + private long decodeTagType(byte b, ByteBuffer bytes) { + long tagType = (b & 0x1F); + if (tagType != 0x1F) { + return tagType; + } + tagType = 0; + b = bytes.get(); + while ((b & 0x80) != 0) { + tagType <<= 7; + tagType = tagType | (b & 0x7F); + b = bytes.get(); + } + return tagType; + } + + private int decodeLength(ByteBuffer bytes) { + byte b = bytes.get(); + if ((b & 0x80) == 0) { + return b & 0x7F; + } + int numberOfLengthBytes = (b & 0x7F); + Assert.state(numberOfLengthBytes != 0, "Infinite length encoding is not supported"); + Assert.state(numberOfLengthBytes != 0x7F, "Reserved length encoding is not supported"); + Assert.state(numberOfLengthBytes <= 4, "Length overflow"); + int length = 0; + for (int i = 0; i < numberOfLengthBytes; i++) { + length <<= 8; + length |= (bytes.get() & 0xFF); + } + return length; + } + + boolean isType(ValueType valueType) { + return this.valueType == valueType; + } + + boolean isType(ValueType valueType, TagType tagType) { + return this.valueType == valueType && this.tagType == tagType.getNumber(); + } + + ByteBuffer getContents() { + return this.contents; + } + + static DerElement of(byte[] bytes) { + return of(ByteBuffer.wrap(bytes)); + } + + static DerElement of(ByteBuffer bytes) { + return (bytes.remaining() > 0) ? new DerElement(bytes) : null; + } + + enum ValueType { + + PRIMITIVE, ENCODED + + } + + enum TagType { + + INTEGER(0x02), OCTET_STRING(0x04), OBJECT_IDENTIFIER(0x06), SEQUENCE(0x10); + + private final int number; + + TagType(int number) { + this.number = number; + } + + int getNumber() { + return this.number; + } + + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/PemFileWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/PemFileWriter.java index 509f59b25f0..6921dd5c3c3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/PemFileWriter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/PemFileWriter.java @@ -90,12 +90,16 @@ public class PemFileWriter { -----END RSA PRIVATE KEY----- """.formatted(EXAMPLE_SECRET_QUALIFIER); - public static final String PRIVATE_EC_KEY = """ - %s-----BEGIN EC PRIVATE KEY----- - MHcCAQEEIIwZkO8Zjbggzi8wwrk5rzSPzUX31gqTRhBYw4AL6w44oAoGCCqGSM49 - AwEHoUQDQgAE8y28khug747bA68M90IAMCPHAYyen+RsN6i84LORpNDUhv00QZWd - hOhjWFCQjnewR98Y8pEb1fnORll4LhHPlQ== - -----END EC PRIVATE KEY-----""".formatted(EXAMPLE_SECRET_QUALIFIER); + public static final String PRIVATE_EC_KEY = EXAMPLE_SECRET_QUALIFIER + "-----BEGIN EC PRIVATE KEY-----\n" + + "MIGkAgEBBDB21WGGOb1DokKW0MUHO7RQ6jZSUYXfO2iyfCbjmSJhyK8fSuq1V0N2\n" + + "Bj7X+XYhS6ygBwYFK4EEACKhZANiAATsRaYri/tDMvrrB2NJlxWFOZ4YBLYdSM+a\n" + + "FlGh1FuLjOHW9cx8w0iRHd1Hxn4sxqsa62KzGoCj63lGoaJgi67YNCF0lBa/zCLy\n" + + "ktaMsQePDOR8UR0Cfi2J9bh+IjxXd+o=\n" + "-----END EC PRIVATE KEY-----"; + + public static final String PRIVATE_EC_KEY_PRIME_256_V1 = EXAMPLE_SECRET_QUALIFIER + + "-----BEGIN EC PRIVATE KEY-----\n" + "MHcCAQEEIIwZkO8Zjbggzi8wwrk5rzSPzUX31gqTRhBYw4AL6w44oAoGCCqGSM49\n" + + "AwEHoUQDQgAE8y28khug747bA68M90IAMCPHAYyen+RsN6i84LORpNDUhv00QZWd\n" + + "hOhjWFCQjnewR98Y8pEb1fnORll4LhHPlQ==\n" + "-----END EC PRIVATE KEY-----"; public static final String PRIVATE_DSA_KEY = EXAMPLE_SECRET_QUALIFIER + "-----BEGIN PRIVATE KEY-----\n" + "MIICXAIBADCCAjUGByqGSM44BAEwggIoAoIBAQCPeTXZuarpv6vtiHrPSVG28y7F\n" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParserTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParserTests.java index f98e1806461..f9a615fe0f1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParserTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParserTests.java @@ -22,6 +22,7 @@ import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; import java.security.PrivateKey; +import java.security.interfaces.ECPrivateKey; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -89,17 +90,27 @@ class PrivateKeyParserTests { Path path = this.fileWriter.writeFile("key.pem", PemFileWriter.PRIVATE_RSA_KEY); PrivateKey privateKey = PrivateKeyParser.parse(path); assertThat(privateKey).isNotNull(); - // keys in PKCS#1 format are converted to PKCS#8 for parsing assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); } @Test - void parsePkcs1EcKeyFile() throws IOException { + void parsePemEcKeyFile() throws IOException { Path path = this.fileWriter.writeFile("key.pem", PemFileWriter.PRIVATE_EC_KEY); - PrivateKey privateKey = PrivateKeyParser.parse(path); + ECPrivateKey privateKey = (ECPrivateKey) PrivateKeyParser.parse(path); assertThat(privateKey).isNotNull(); - // keys in PKCS#1 format are converted to PKCS#8 for parsing assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); + assertThat(privateKey.getAlgorithm()).isEqualTo("EC"); + assertThat(privateKey.getParams().toString()).contains("1.3.132.0.34").doesNotContain("prime256v1"); + } + + @Test + void parsePemEcKeyFilePrime256v1() throws IOException { + Path path = this.fileWriter.writeFile("key.pem", PemFileWriter.PRIVATE_EC_KEY_PRIME_256_V1); + ECPrivateKey privateKey = (ECPrivateKey) PrivateKeyParser.parse(path); + assertThat(privateKey).isNotNull(); + assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); + assertThat(privateKey.getAlgorithm()).isEqualTo("EC"); + assertThat(privateKey.getParams().toString()).contains("prime256v1").doesNotContain("1.3.132.0.34"); } @Test diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java index 7f3e6c33044..9044953c661 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java @@ -18,6 +18,7 @@ package org.springframework.boot.ssl.pem; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.ByteBuffer; import java.security.AlgorithmParameters; import java.security.GeneralSecurityException; import java.security.KeyFactory; @@ -38,6 +39,8 @@ import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; +import org.springframework.boot.ssl.pem.PemPrivateKeyParser.DerElement.TagType; +import org.springframework.boot.ssl.pem.PemPrivateKeyParser.DerElement.ValueType; import org.springframework.util.Assert; /** @@ -104,7 +107,38 @@ final class PemPrivateKeyParser { } private static PKCS8EncodedKeySpec createKeySpecForEc(byte[] bytes, String password) { - return createKeySpecForAlgorithm(bytes, EC_ALGORITHM, EC_PARAMETERS); + DerElement ecPrivateKey = DerElement.of(bytes); + Assert.state(ecPrivateKey.isType(ValueType.ENCODED, TagType.SEQUENCE), + "Key spec should be an ASN.1 encoded sequence"); + DerElement version = DerElement.of(ecPrivateKey.getContents()); + Assert.state(version != null && version.isType(ValueType.PRIMITIVE, TagType.INTEGER), + "Key spec should start with version"); + Assert.state(version.getContents().remaining() == 1 && version.getContents().get() == 1, + "Key spec version must be 1"); + DerElement privateKey = DerElement.of(ecPrivateKey.getContents()); + Assert.state(privateKey != null && privateKey.isType(ValueType.PRIMITIVE, TagType.OCTET_STRING), + "Key spec should contain private key"); + DerElement parameters = DerElement.of(ecPrivateKey.getContents()); + return createKeySpecForAlgorithm(bytes, EC_ALGORITHM, getEcParameters(parameters)); + } + + private static int[] getEcParameters(DerElement parameters) { + if (parameters == null) { + return EC_PARAMETERS; + } + Assert.state(parameters.isType(ValueType.ENCODED), "Key spec should contain encoded parameters"); + DerElement contents = DerElement.of(parameters.getContents()); + Assert.state(contents.isType(ValueType.PRIMITIVE, TagType.OBJECT_IDENTIFIER), + "Key spec parameters should contain object identifier"); + return getEcParameters(contents.getContents()); + } + + private static int[] getEcParameters(ByteBuffer bytes) { + int[] result = new int[bytes.remaining()]; + for (int i = 0; i < result.length; i++) { + result[i] = bytes.get() & 0xFF; + } + return result; } private static PKCS8EncodedKeySpec createKeySpecForAlgorithm(byte[] bytes, int[] algorithm, int[] parameters) { @@ -114,8 +148,7 @@ final class PemPrivateKeyParser { DerEncoder algorithmIdentifier = new DerEncoder(); algorithmIdentifier.objectIdentifier(algorithm); algorithmIdentifier.objectIdentifier(parameters); - byte[] byteArray = algorithmIdentifier.toByteArray(); - encoder.sequence(byteArray); + encoder.sequence(algorithmIdentifier.toByteArray()); encoder.octetString(bytes); return new PKCS8EncodedKeySpec(encoder.toSequence()); } @@ -288,6 +321,107 @@ final class PemPrivateKeyParser { } + /** + * An ASN.1 DER encoded element. + */ + static final class DerElement { + + private final ValueType valueType; + + private final long tagType; + + private ByteBuffer contents; + + private DerElement(ByteBuffer bytes) { + byte b = bytes.get(); + this.valueType = ((b & 0x20) == 0) ? ValueType.PRIMITIVE : ValueType.ENCODED; + this.tagType = decodeTagType(b, bytes); + int length = decodeLength(bytes); + bytes.limit(bytes.position() + length); + this.contents = bytes.slice(); + bytes.limit(bytes.capacity()); + bytes.position(bytes.position() + length); + } + + private long decodeTagType(byte b, ByteBuffer bytes) { + long tagType = (b & 0x1F); + if (tagType != 0x1F) { + return tagType; + } + tagType = 0; + b = bytes.get(); + while ((b & 0x80) != 0) { + tagType <<= 7; + tagType = tagType | (b & 0x7F); + b = bytes.get(); + } + return tagType; + } + + private int decodeLength(ByteBuffer bytes) { + byte b = bytes.get(); + if ((b & 0x80) == 0) { + return b & 0x7F; + } + int numberOfLengthBytes = (b & 0x7F); + Assert.state(numberOfLengthBytes != 0, "Infinite length encoding is not supported"); + Assert.state(numberOfLengthBytes != 0x7F, "Reserved length encoding is not supported"); + Assert.state(numberOfLengthBytes <= 4, "Length overflow"); + int length = 0; + for (int i = 0; i < numberOfLengthBytes; i++) { + length <<= 8; + length |= (bytes.get() & 0xFF); + } + return length; + } + + boolean isType(ValueType valueType) { + return this.valueType == valueType; + } + + boolean isType(ValueType valueType, TagType tagType) { + return this.valueType == valueType && this.tagType == tagType.getNumber(); + } + + ByteBuffer getContents() { + return this.contents; + } + + static DerElement of(byte[] bytes) { + return of(ByteBuffer.wrap(bytes)); + } + + static DerElement of(ByteBuffer bytes) { + return (bytes.remaining() > 0) ? new DerElement(bytes) : null; + } + + enum ValueType { + + PRIMITIVE, ENCODED + + } + + enum TagType { + + INTEGER(0x02), OCTET_STRING(0x04), OBJECT_IDENTIFIER(0x06), SEQUENCE(0x10); + + private final int number; + + TagType(int number) { + this.number = number; + } + + int getNumber() { + return this.number; + } + + } + + } + + /** + * Decryptor for PKCS8 encoded private keys. + */ static class Pkcs8PrivateKeyDecryptor { public static final String PBES2_ALGORITHM = "PBES2"; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java index 82203b75dcf..e3d3659e1b0 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java @@ -19,6 +19,7 @@ package org.springframework.boot.ssl.pem; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; +import java.security.interfaces.ECPrivateKey; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -34,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; * * @author Scott Frederick * @author Moritz Halbritter + * @author Phillip Webb */ class PemPrivateKeyParserTests { @@ -72,11 +74,21 @@ class PemPrivateKeyParserTests { } @Test - void parsePkcs8KeyFileWithEcdsa() throws Exception { - PrivateKey privateKey = PemPrivateKeyParser.parse(read("test-ec-key.pem")); + void parsePemKeyFileWithEcdsa() throws Exception { + ECPrivateKey privateKey = (ECPrivateKey) PemPrivateKeyParser.parse(read("test-ec-key.pem")); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo("EC"); + assertThat(privateKey.getParams().toString()).contains("1.3.132.0.34").doesNotContain("prime256v1"); + } + + @Test + void parsePemKeyFileWithEcdsaPrime256v1() throws Exception { + ECPrivateKey privateKey = (ECPrivateKey) PemPrivateKeyParser.parse(read("test-ec-key-prime256v1.pem")); + assertThat(privateKey).isNotNull(); + assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); + assertThat(privateKey.getAlgorithm()).isEqualTo("EC"); + assertThat(privateKey.getParams().toString()).contains("prime256v1").doesNotContain("1.3.132.0.34"); } @Test diff --git a/spring-boot-project/spring-boot/src/test/resources/test-ec-key-prime256v1.pem b/spring-boot-project/spring-boot/src/test/resources/test-ec-key-prime256v1.pem new file mode 100644 index 00000000000..6256a64396b --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/test-ec-key-prime256v1.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEINa5DtcVrTiC4pMDX+rnMbhIlxm26Rn5FYz2+uT5DmBcoAoGCCqGSM49 +AwEHoUQDQgAEj0M8x6W5fF5y5tdHvFCp0ws8gCOcETQk52uXSL46G3Uukng6lscf +yNobOV+NjmBCqJRZd0bKEvbIiMvpo6B0Fw== +-----END EC PRIVATE KEY----- diff --git a/spring-boot-project/spring-boot/src/test/resources/test-ec-key.pem b/spring-boot-project/spring-boot/src/test/resources/test-ec-key.pem index b3a1ce0bd8e..56cdfc004c2 100644 --- a/spring-boot-project/spring-boot/src/test/resources/test-ec-key.pem +++ b/spring-boot-project/spring-boot/src/test/resources/test-ec-key.pem @@ -1,5 +1,6 @@ -----BEGIN EC PRIVATE KEY----- -MHcCAQEEIBEZhSR+d8kwL5L/K0f/eNBm4RfzyyA1jfg+dV1/8WvqoAoGCCqGSM49 -AwEHoUQDQgAEBbfdBTSUWuui7O2R+W9mDPjAHjgdBJsjrjnvkjnq8f/k4U/OqvjK -qnHEZwYgdaF2WqYdqBYMns0n+tSMgBoonQ== +MIGkAgEBBDB21WGGOb1DokKW0MUHO7RQ6jZSUYXfO2iyfCbjmSJhyK8fSuq1V0N2 +Bj7X+XYhS6ygBwYFK4EEACKhZANiAATsRaYri/tDMvrrB2NJlxWFOZ4YBLYdSM+a +FlGh1FuLjOHW9cx8w0iRHd1Hxn4sxqsa62KzGoCj63lGoaJgi67YNCF0lBa/zCLy +ktaMsQePDOR8UR0Cfi2J9bh+IjxXd+o= -----END EC PRIVATE KEY----- diff --git a/src/checkstyle/import-control.xml b/src/checkstyle/import-control.xml index d0e94c77536..78d5bbabeab 100644 --- a/src/checkstyle/import-control.xml +++ b/src/checkstyle/import-control.xml @@ -101,6 +101,7 @@ +