Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework keys to remove PemReader, user BC instead #568

Merged
merged 1 commit into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 31 additions & 80 deletions sigstore-java/src/main/java/dev/sigstore/encryption/Keys.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,39 @@
*/
package dev.sigstore.encryption;

import static org.bouncycastle.jce.ECPointUtil.*;
import static org.bouncycastle.jce.ECPointUtil.decodePoint;

import com.google.common.annotations.VisibleForTesting;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.*;
import java.util.Locale;
import java.util.logging.Logger;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.Security;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.List;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.ECKeyParameters;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.params.RSAKeyParameters;
import org.bouncycastle.crypto.util.PublicKeyFactory;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.util.encoders.DecoderException;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;

/** For internal use. Key related utility functions. */
public class Keys {

private static final Logger log = Logger.getLogger(Keys.class.getName());
private static final List<String> SUPPORTED_KEY_TYPES =
List.of("ECDSA", "EC", "RSA", "Ed25519", "EdDSA");

static {
Security.addProvider(new BouncyCastleProvider());
Expand All @@ -60,48 +63,26 @@ public class Keys {
*/
public static PublicKey parsePublicKey(byte[] keyBytes)
throws InvalidKeySpecException, IOException, NoSuchAlgorithmException {
PemReader pemReader =
new PemReader(
new InputStreamReader(new ByteArrayInputStream(keyBytes), StandardCharsets.UTF_8));
PemObject section = null;
try {
section = pemReader.readPemObject();
if (pemReader.readPemObject() != null) {
try (PEMParser pemParser =
new PEMParser(
new InputStreamReader(new ByteArrayInputStream(keyBytes), StandardCharsets.UTF_8))) {
var keyObj = pemParser.readObject(); // throws DecoderException
if (keyObj == null) {
throw new InvalidKeySpecException(
"sigstore public keys must be only a single PEM encoded public key");
}
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
if (keyObj instanceof SubjectPublicKeyInfo) {
PublicKey pk = converter.getPublicKey((SubjectPublicKeyInfo) keyObj);
if (!SUPPORTED_KEY_TYPES.contains(pk.getAlgorithm())) {
throw new NoSuchAlgorithmException("Unsupported key type: " + pk.getAlgorithm());
}
return pk;
}
throw new InvalidKeySpecException("Could not parse PEM section into public key");
} catch (DecoderException e) {
throw new InvalidKeySpecException("Invalid key, could not parse PEM section");
}
// special handling for PKCS1 (rsa) public key
// TODO: The length checking is not necessary after https://github.com/bcgit/bc-java/issues/1370
// has been merged. Remove it when bc-java is updated with the merge.
if ((section == null) || (section.getContent() == null) || (section.getContent().length == 0)) {
throw new InvalidKeySpecException("Invalid key, empty PEM section");
}
if (section.getType().equals("RSA PUBLIC KEY")) {
return parsePkcs1RsaPublicKey(section.getContent());
}

// otherwise, we are dealing with PKIX X509 encoded keys
byte[] content = section.getContent();
EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(content);
AsymmetricKeyParameter keyParameters = null;

// Ensure PEM content can be parsed correctly
try {
keyParameters = PublicKeyFactory.createKey(content);
} catch (IllegalStateException e) {
throw new InvalidKeySpecException("Invalid key, could not parse PEM content");
}
if (keyParameters == null) {
throw new InvalidKeySpecException("Invalid key, could not parse PEM content");
}

// get algorithm inspecting the created class
String keyAlgorithm = extractKeyAlgorithm(keyParameters);
KeyFactory keyFactory = KeyFactory.getInstance(keyAlgorithm);
return keyFactory.generatePublic(publicKeySpec);
}

/**
Expand Down Expand Up @@ -184,34 +165,4 @@ public static PublicKey constructTufPublicKey(byte[] contents, String scheme)
throw new RuntimeException(scheme + " not currently supported");
}
}

// https://stackoverflow.com/questions/42911637/get-publickey-from-key-bytes-not-knowing-the-key-algorithm
private static String extractKeyAlgorithm(AsymmetricKeyParameter keyParameters)
throws NoSuchAlgorithmException {
if (keyParameters instanceof RSAKeyParameters) {
return "RSA";
} else if (keyParameters instanceof Ed25519PublicKeyParameters) {
return "EdDSA";
} else if (keyParameters instanceof ECKeyParameters) {
return "EC";
} else {
String error =
String.format(
Locale.ROOT,
"The key provided was of type: %s. We only support RSA, EdDSA, and EC ",
keyParameters);
log.warning(error);
throw new NoSuchAlgorithmException(error);
}
}

@VisibleForTesting
protected static int getJavaVersion() {
return getJavaVersion(System.getProperty("java.version"));
}

@VisibleForTesting
protected static int getJavaVersion(String version) {
return Integer.parseInt(version.substring(0, version.indexOf(".")));
}
}
35 changes: 14 additions & 21 deletions sigstore-java/src/test/java/dev/sigstore/encryption/KeysTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ class KeysTest {
static final String ECDSA_SHA2_NISTP256 =
"dev/sigstore/samples/keys/test-ecdsa-sha2-nistp256.pub";

@Test
void parsePublicKey_invalid() {
var key =
"-----BEGIN Ã-----\nMGMGB1gFB00gFM0EEEEEEEzEEEEEEEEEEEEEEEEEEEEEEEEEEEEEFB00gFM0EEEEEEEzEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEFGB1g070v129B1700372=\n-----END ïI";
Assertions.assertThrows(
IOException.class, () -> Keys.parsePublicKey(key.getBytes(StandardCharsets.UTF_8)));
}

@Test
void parsePublicKey_rsa() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
PublicKey result =
Expand All @@ -61,7 +69,7 @@ void parsePublicKey_rsaPkcs1()
void parsePublicKey_ec() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
PublicKey result =
Keys.parsePublicKey(Resources.toByteArray(Resources.getResource(EC_PUB_PATH)));
assertEquals("EC", result.getAlgorithm());
assertEquals("ECDSA", result.getAlgorithm());
}

@Test
Expand All @@ -70,7 +78,7 @@ void parsePublicKey_ed25519_withBouncyCastle()
throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
PublicKey result =
Keys.parsePublicKey(Resources.toByteArray(Resources.getResource(ED25519_PUB_PATH)));
// BouncyCastle names the algorithm differently than the JDK
// BouncyCastle names the algorithm differently than the JDK (Ed25519 vs EdDSA)
assertEquals("Ed25519", result.getAlgorithm());
}

Expand All @@ -95,7 +103,7 @@ void parseTufPublicKeyPemEncoded_sha2_nistp256()
throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
PublicKey result =
Keys.parsePublicKey(Resources.toByteArray(Resources.getResource(ECDSA_SHA2_NISTP256)));
assertEquals("EC", result.getAlgorithm());
assertEquals("ECDSA", result.getAlgorithm());
}

@Test
Expand Down Expand Up @@ -126,7 +134,7 @@ void parseTufPublicKey_ecdsaBad()
@Test
@EnabledForJreRange(min = JRE.JAVA_15)
void parseTufPublicKey_ed25519_java15Plus()
throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException {
throws NoSuchAlgorithmException, InvalidKeySpecException {
// {@code step crypto keypair ed25519.pub /dev/null --kty OKP --curve Ed25519}
// copy just the key part out of ed25519.pub removing PEM header and footer
// {@code echo $(copied content) | base64 -d | hexdump -v -e '/1 "%02x" '}
Expand Down Expand Up @@ -210,23 +218,8 @@ void parsePkixPublicKey_ecdsa() throws NoSuchAlgorithmException, InvalidKeySpecE
}

@Test
void parsePublicKey_failOnNullSection()
throws IOException, NoSuchAlgorithmException, NoSuchProviderException {
// This unit test is used to test the fix for a bug discovered by oss-fuzz
// The bug happens when a malformed byte array is passed to the method
// https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=57247
void parsePublicKey_failOnBadPEM() throws Exception {
byte[] byteArray = "-----BEGIN A-----\nBBBBB-----END A".getBytes(StandardCharsets.UTF_8);
Assertions.assertThrows(InvalidKeySpecException.class, () -> Keys.parsePublicKey(byteArray));
}

@Test
void testGetJavaVersion() {
assertEquals(1, Keys.getJavaVersion("1.6.0_23"));
assertEquals(1, Keys.getJavaVersion("1.7.0"));
assertEquals(1, Keys.getJavaVersion("1.6.0_23"));
assertEquals(9, Keys.getJavaVersion("9.0.1"));
assertEquals(11, Keys.getJavaVersion("11.0.4"));
assertEquals(12, Keys.getJavaVersion("12.0.1"));
assertEquals(15, Keys.getJavaVersion("15.0.1"));
Assertions.assertThrows(IOException.class, () -> Keys.parsePublicKey(byteArray));
}
}
Loading