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

Cleanup keys parsing, caller specifies type #851

Merged
merged 1 commit into from
Nov 20, 2024
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
23 changes: 18 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,31 @@
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();
Parser parser =
data.pickValue(
new Parser[] {
Keys::parseRsaPkcs1, Keys::parseRsa, Keys::parseEcdsa, Keys::parseEd25519,
});
byte[] keyContents = data.consumeRemainingAsBytes();

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 | NullPointerException 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
Loading