Skip to content

Commit

Permalink
Cleanup keys parsing, caller specifies type
Browse files Browse the repository at this point in the history
Signed-off-by: Appu Goundan <[email protected]>
  • Loading branch information
loosebazooka committed Nov 18, 2024
1 parent 7402c14 commit 0934b5b
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 202 deletions.
24 changes: 19 additions & 5 deletions fuzzing/src/main/java/fuzzing/KeysParsingFuzzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,32 @@
package fuzzing;

import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import dev.sigstore.encryption.Keys;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;

public class KeysParsingFuzzer {

@FunctionalInterface
interface Parser {
@CanIgnoreReturnValue
PublicKey parse(byte[] contents) throws InvalidKeySpecException;
}

public static void fuzzerTestOneInput(FuzzedDataProvider data) {
try {
byte[] byteArray = data.consumeRemainingAsBytes();
byte[] keyContents = data.consumeRemainingAsBytes();
Parser parser = data.pickValue(new Parser[]{
Keys::parseRsaPkcs1,
Keys::parseRsa,
Keys::parseEcdsa,
Keys::parseEd25519,
});

parser.parse(keyContents);

Keys.parsePublicKey(byteArray);
} catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) {
} catch (InvalidKeySpecException e) {
// known exceptions
}
}
Expand Down
109 changes: 52 additions & 57 deletions sigstore-java/src/main/java/dev/sigstore/encryption/Keys.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,95 +15,90 @@
*/
package dev.sigstore.encryption;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.Security;
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.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.util.encoders.DecoderException;

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

private static final List<String> SUPPORTED_KEY_TYPES =
List.of("ECDSA", "EC", "RSA", "Ed25519", "EdDSA");

static {
Security.addProvider(new BouncyCastleProvider());
}

/**
* Takes a PEM formatted public key in bytes and constructs a {@code PublicKey} with it.
* Takes a PKIX DER formatted ECDSA public key in bytes and constructs a {@code PublicKey} with
* it.
*
* <p>This method supports the follow public key algorithms: RSA, EdDSA, EC.
* @param contents the public key bytes
* @return a PublicKey object
* @throws InvalidKeySpecException if the public key material is invalid
*/
public static PublicKey parseEcdsa(byte[] contents) throws InvalidKeySpecException {
return parse(contents, "ECDSA");
}

/**
* Takes a PKIX DER formatted Ed25519 public key in bytes and constructs a {@code PublicKey} with
* it.
*
* @throws InvalidKeySpecException if the PEM does not contain just one public key.
* @throws NoSuchAlgorithmException if the public key is using an unsupported algorithm.
* @param contents the public key bytes
* @return a PublicKey object
* @throws InvalidKeySpecException if the public key material is invalid
*/
public static PublicKey parsePublicKey(byte[] keyBytes)
throws InvalidKeySpecException, IOException, NoSuchAlgorithmException {
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();
converter.setProvider(BouncyCastleProvider.PROVIDER_NAME);
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");
}
public static PublicKey parseEd25519(byte[] contents) throws InvalidKeySpecException {
return parse(contents, "Ed25519");
}

/**
* Takes a PKIX DER formatted public key in bytes and constructs a {@code PublicKey} with it.
* Takes a PKIX DER formatted RSA public key in bytes and constructs a {@code PublicKey} with it.
*
* <p>This method is known to work with keys algorithms: RSA, EdDSA, EC.
* @param contents the public key bytes
* @return a PublicKey object
* @throws InvalidKeySpecException if the public key material is invalid
*/
public static PublicKey parseRsa(byte[] contents) throws InvalidKeySpecException {
return parse(contents, "RSA");
}

/**
* Takes a PKCS1 DER formatted RSA public key in bytes and constructs a {@code PublicKey} with it.
*
* @param contents the public key bytes
* @param algorithm the key algorithm
* @return a PublicKey object
* @throws NoSuchAlgorithmException if we don't support the scheme provided
* @throws InvalidKeySpecException if the public key material is invalid
*/
public static PublicKey parsePkixPublicKey(byte[] contents, String algorithm)
throws NoSuchAlgorithmException, InvalidKeySpecException {
X509EncodedKeySpec spec = new X509EncodedKeySpec(contents);
KeyFactory factory = KeyFactory.getInstance(algorithm);
return factory.generatePublic(spec);
public static PublicKey parseRsaPkcs1(byte[] contents) throws InvalidKeySpecException {
try {
ASN1Sequence sequence = ASN1Sequence.getInstance(contents);
ASN1Integer modulus = ASN1Integer.getInstance(sequence.getObjectAt(0));
ASN1Integer exponent = ASN1Integer.getInstance(sequence.getObjectAt(1));
RSAPublicKeySpec keySpec =
new RSAPublicKeySpec(modulus.getPositiveValue(), exponent.getPositiveValue());
KeyFactory factory = KeyFactory.getInstance("RSA", "BC");
return factory.generatePublic(keySpec);
} catch (IllegalArgumentException e) {
throw new InvalidKeySpecException("Failed to parse pkcs1 rsa key", e);
} catch (NoSuchProviderException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}

public static PublicKey parsePkcs1RsaPublicKey(byte[] contents)
throws NoSuchAlgorithmException, InvalidKeySpecException {
ASN1Sequence sequence = ASN1Sequence.getInstance(contents);
ASN1Integer modulus = ASN1Integer.getInstance(sequence.getObjectAt(0));
ASN1Integer exponent = ASN1Integer.getInstance(sequence.getObjectAt(1));
RSAPublicKeySpec keySpec =
new RSAPublicKeySpec(modulus.getPositiveValue(), exponent.getPositiveValue());
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(keySpec);
private static PublicKey parse(byte[] contents, String type) throws InvalidKeySpecException {
try {
var keySpec = new X509EncodedKeySpec(contents);
var factory = KeyFactory.getInstance(type, BouncyCastleProvider.PROVIDER_NAME);
return factory.generatePublic(keySpec);
} catch (NoSuchProviderException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ public abstract class PublicKey {
public java.security.PublicKey toJavaPublicKey()
throws NoSuchAlgorithmException, InvalidKeySpecException {
if (getKeyDetails().equals("PKIX_ECDSA_P256_SHA_256")) {
return Keys.parsePkixPublicKey(getRawBytes(), "EC");
return Keys.parseEcdsa(getRawBytes());
}
if (getKeyDetails().startsWith("PKIX_RSA")) {
return Keys.parseRsa(getRawBytes());
}
if (getKeyDetails().equals("PKCS1_RSA_PKCS1V5")) {
return Keys.parsePkcs1RsaPublicKey(getRawBytes());
return Keys.parseRsaPkcs1(getRawBytes());
}
throw new InvalidKeySpecException("Unsupported key algorithm: " + getKeyDetails());
}
Expand Down
108 changes: 30 additions & 78 deletions sigstore-java/src/test/java/dev/sigstore/encryption/KeysTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,116 +15,68 @@
*/
package dev.sigstore.encryption;

import static org.junit.jupiter.api.Assertions.*;

import com.google.common.io.Resources;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import org.bouncycastle.util.encoders.Base64;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

class KeysTest {

static final String RSA_PUB_PATH = "dev/sigstore/samples/keys/test-rsa.pub";
static final String RSA_PUB_PKCS1_PATH = "dev/sigstore/samples/keys/test-rsa-pkcs1.pub";
static final String EC_PUB_PATH = "dev/sigstore/samples/keys/test-ec.pub";
static final String ED25519_PUB_PATH = "dev/sigstore/samples/keys/test-ed25519.pub";
static final String DSA_PUB_PATH = "dev/sigstore/samples/keys/test-dsa.pub";

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 =
Keys.parsePublicKey(Resources.toByteArray(Resources.getResource(RSA_PUB_PATH)));
assertEquals("RSA", result.getAlgorithm());
}

@Test
void parsePublicKey_rsaPkcs1()
throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
PublicKey result =
Keys.parsePublicKey(Resources.toByteArray(Resources.getResource(RSA_PUB_PKCS1_PATH)));
assertEquals("RSA", result.getAlgorithm());
}

@Test
void parsePublicKey_ec() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
PublicKey result =
Keys.parsePublicKey(Resources.toByteArray(Resources.getResource(EC_PUB_PATH)));
assertEquals("ECDSA", result.getAlgorithm());
}

@Test
void parsePublicKey_ed25519()
throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
PublicKey result =
Keys.parsePublicKey(Resources.toByteArray(Resources.getResource(ED25519_PUB_PATH)));
// BouncyCastle names the algorithm differently than the JDK (Ed25519 vs EdDSA) but we
// force the converter to use BouncyCastle always.
assertEquals("Ed25519", result.getAlgorithm());
void parseRsa() throws InvalidKeySpecException {
var base64Key =
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAghWkDAnX9F5QZZ9NxIWg9vcjULtD/kkbQwlcSm22e06FrgOdiFy1fKN/Ng32qEk1ZIKyi0HFzZxzPIcvg7eaFTRb7+AQiG6eMDmUzPGr67Jp0Di2ncH9+uOZmv4PVKovvQLq7qnEwbDk0HttxUscLQ2e36Lfv/2lpGW7apVmHVMoC5kwZ3KTiAk/DUtDhD4VQjU2rBy6OneO6pm6vdNzG4Jktjc0uUKFCRRUzydGEh05PgC9vSQu/EOiU+7aQPV1ZDUGpjg9tOM0SgaTOU3YSUfGiXZNHoiS2HwLyQPaxiHR2NPVH75bwnUFBHhdMxT1rhU+yLhXaweDQW6GQ0ti8wIDAQAB";
Assertions.assertEquals("RSA", Keys.parseRsa(Base64.decode(base64Key)).getAlgorithm());
}

@Test
void parsePublicKey_dsaShouldFail() {
void parseRsa_bad() {
var base64Key =
"MIIBIjANBgkqhkiG8w0BAQEFAAOCAQ8AMIIBCgKCAQEAghWkDAnX9F5QZZ9NxIWg9vcjULtD/kkbQwlcSm22e06FrgOdiFy1fKN/Ng32qEk1ZIKyi0HFzZxzPIcvg7eaFTRb7+AQiG6eMDmUzPGr67Jp0Di2ncH9+uOZmv4PVKovvQLq7qnEwbDk0HttxUscLQ2e36Lfv/2lpGW7apVmHVMoC5kwZ3KTiAk/DUtDhD4VQjU2rBy6OneO6pm6vdNzG4Jktjc0uUKFCRRUzydGEh05PgC9vSQu/EOiU+7aQPV1ZDUGpjg9tOM0SgaTOU3YSUfGiXZNHoiS2HwLyQPaxiHR2NPVH75bwnUFBHhdMxT1rhU+yLhXaweDQW6GQ0ti8wIDAQAB";
Assertions.assertThrows(
NoSuchAlgorithmException.class,
() -> Keys.parsePublicKey(Resources.toByteArray(Resources.getResource(DSA_PUB_PATH))));
InvalidKeySpecException.class, () -> Keys.parseRsa(Base64.decode(base64Key)));
}

@Test
void parseTufPublicKeyPemEncoded_sha2_nistp256()
throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
PublicKey result =
Keys.parsePublicKey(Resources.toByteArray(Resources.getResource(ECDSA_SHA2_NISTP256)));
assertEquals("ECDSA", result.getAlgorithm());
}

@Test
void parsePkixPublicKey_rsa() throws NoSuchAlgorithmException, InvalidKeySpecException {
void parseRsaPkcs1() throws InvalidKeySpecException {
var base64Key =
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAghWkDAnX9F5QZZ9NxIWg9vcjULtD/kkbQwlcSm22e06FrgOdiFy1fKN/Ng32qEk1ZIKyi0HFzZxzPIcvg7eaFTRb7+AQiG6eMDmUzPGr67Jp0Di2ncH9+uOZmv4PVKovvQLq7qnEwbDk0HttxUscLQ2e36Lfv/2lpGW7apVmHVMoC5kwZ3KTiAk/DUtDhD4VQjU2rBy6OneO6pm6vdNzG4Jktjc0uUKFCRRUzydGEh05PgC9vSQu/EOiU+7aQPV1ZDUGpjg9tOM0SgaTOU3YSUfGiXZNHoiS2HwLyQPaxiHR2NPVH75bwnUFBHhdMxT1rhU+yLhXaweDQW6GQ0ti8wIDAQAB";
Assertions.assertNotNull(Keys.parsePkixPublicKey(Base64.decode(base64Key), "RSA"));
"MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZGz/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT53cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXXw4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZevopmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lIxNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0xigwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYUSeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7gjoCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ==";
Assertions.assertEquals("RSA", Keys.parseRsaPkcs1(Base64.decode(base64Key)).getAlgorithm());
}

@Test
void parsePkixPublicKey_rsaKeyButWrongAlgorithm() {
void parseRsaPkcs1_bad() throws InvalidKeySpecException {
var base64Key =
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAghWkDAnX9F5QZZ9NxIWg9vcjULtD/kkbQwlcSm22e06FrgOdiFy1fKN/Ng32qEk1ZIKyi0HFzZxzPIcvg7eaFTRb7+AQiG6eMDmUzPGr67Jp0Di2ncH9+uOZmv4PVKovvQLq7qnEwbDk0HttxUscLQ2e36Lfv/2lpGW7apVmHVMoC5kwZ3KTiAk/DUtDhD4VQjU2rBy6OneO6pm6vdNzG4Jktjc0uUKFCRRUzydGEh05PgC9vSQu/EOiU+7aQPV1ZDUGpjg9tOM0SgaTOU3YSUfGiXZNHoiS2HwLyQPaxiHR2NPVH75bwnUFBHhdMxT1rhU+yLhXaweDQW6GQ0ti8wIDAQAB";
"MIIBIjANBgkqikiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAghWkDAnX9F5QZZ9NxIWg9vcjULtD/kkbQwlcSm22e06FrgOdiFy1fKN/Ng32qEk1ZIKyi0HFzZxzPIcvg7eaFTRb7+AQiG6eMDmUzPGr67Jp0Di2ncH9+uOZmv4PVKovvQLq7qnEwbDk0HttxUscLQ2e36Lfv/2lpGW7apVmHVMoC5kwZ3KTiAk/DUtDhD4VQjU2rBy6OneO6pm6vdNzG4Jktjc0uUKFCRRUzydGEh05PgC9vSQu/EOiU+7aQPV1ZDUGpjg9tOM0SgaTOU3YSUfGiXZNHoiS2HwLyQPaxiHR2NPVH75bwnUFBHhdMxT1rhU+yLhXaweDQW6GQ0ti8wIDAQAB";
Assertions.assertThrows(
InvalidKeySpecException.class,
() -> Keys.parsePkixPublicKey(Base64.decode(base64Key), "EC"));
InvalidKeySpecException.class, () -> Keys.parseRsaPkcs1(Base64.decode(base64Key)));
}

@Test
void parsePkixPublicKey_eddsa() throws NoSuchAlgorithmException, InvalidKeySpecException {
void parseEd25519() throws InvalidKeySpecException {
var base64Key = "MCowBQYDK2VwAyEAixzZOnx34hveTZ69J5iBCkmerK5Oh7EzJqTh3YY55jI=";
Assertions.assertNotNull(Keys.parsePkixPublicKey(Base64.decode(base64Key), "EdDSA"));
Assertions.assertEquals("Ed25519", Keys.parseEd25519(Base64.decode(base64Key)).getAlgorithm());
}

@Test
void parsePkixPublicKey_ecdsa() throws NoSuchAlgorithmException, InvalidKeySpecException {
void parseEd25519_bad() {
var base64Key = "MCowBQYDK2VxAyEAixzZOnx34hveTZ69J5iBCkmerK5Oh7EzJqTh3YY55jI=";
Assertions.assertThrows(
InvalidKeySpecException.class, () -> Keys.parseEd25519(Base64.decode(base64Key)));
}

@Test
void parseEcdsa() throws InvalidKeySpecException {
var base64Key =
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVqBnvab9XEVlTLW4iGKBIdrL6Sxf0x5vclZyXtR6hl79/o+RSgyr1ZQKLLCUC20imDWUgFMmfLu4UUiKNcI2uQ==";
Assertions.assertNotNull(Keys.parsePkixPublicKey(Base64.decode(base64Key), "EC"));
Assertions.assertNotNull(Keys.parseEcdsa(Base64.decode(base64Key)));
}

@Test
void parsePublicKey_failOnBadPEM() throws Exception {
byte[] byteArray = "-----BEGIN A-----\nBBBBB-----END A".getBytes(StandardCharsets.UTF_8);
Assertions.assertThrows(IOException.class, () -> Keys.parsePublicKey(byteArray));
void parseEcdsa_bad() {
var base64Key =
"MFkwEwYHKoZIzj0CAQYIKoZJzj0DAQcDQgAEVqBnvab9XEVlTLW4iGKBIdrL6Sxf0x5vclZyXtR6hl79/o+RSgyr1ZQKLLCUC20imDWUgFMmfLu4UUiKNcI2uQ==";
Assertions.assertThrows(
InvalidKeySpecException.class, () -> Keys.parseEcdsa(Base64.decode(base64Key)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
import com.google.common.io.Resources;
import dev.sigstore.encryption.Keys;
import dev.sigstore.encryption.certificates.Certificates;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
import org.bouncycastle.util.encoders.Base64;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -55,11 +57,16 @@ public void setUp() throws Exception {
Resources.getResource(
"dev/sigstore/samples/certificatetransparency/cert-ct-embedded.pem")));

PublicKey key =
Keys.parsePublicKey(
Resources.toByteArray(
// a little hacky pem parser, but lightweight
String keyData =
Resources.toString(
Resources.getResource(
"dev/sigstore/samples/certificatetransparency/ct-server-key-public.pem")));
"dev/sigstore/samples/certificatetransparency/ct-server-key-public.pem"),
StandardCharsets.UTF_8)
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
PublicKey key = Keys.parseEcdsa(Base64.decode(keyData));

final CTLogInfo log = new CTLogInfo(key, "Test Log", "foo");
CTLogStore store =
Expand Down
Loading

0 comments on commit 0934b5b

Please sign in to comment.