From adc9c2a718d6d644b5e68403fc476740fe4f12ec Mon Sep 17 00:00:00 2001 From: Appu Goundan Date: Thu, 7 Apr 2022 18:40:01 -0400 Subject: [PATCH] Add sct and cert validations - Rename CertificateResponse to SigningCertificate - SCTs are optional - Add more testing around parsing - Add example fulcio/ctfe public keys and sct/cert examples for testing - Exceptions are still kinda just passed along Signed-off-by: Appu Goundan Co-authored-by: Vladimir Sitnikov --- README.md | 2 +- build.gradle.kts | 3 + .../fulcio/client/CertificateRequest.java | 14 ++ .../fulcio/client/CertificateRequests.java | 33 ---- .../client/{Client.java => FulcioClient.java} | 60 +++---- ...se.java => FulcioValidationException.java} | 23 +-- .../fulcio/client/FulcioValidator.java | 149 ++++++++++++++++++ .../fulcio/client/SigningCertificate.java | 126 +++++++++++++++ ...{ClientTest.java => FulcioClientTest.java} | 64 +++++++- .../fulcio/client/FulcioValidatorTest.java | 95 +++++++++++ .../fulcio/client/SigningCertificateTest.java | 55 +++++++ .../dev/sigstore/testing/FulcioWrapper.java | 32 ++-- .../dev/sigstore/samples/certs/cert.der | Bin 0 -> 528 bytes .../dev/sigstore/samples/certs/cert.pem | 26 +++ .../samples/fulcio-response/valid/cert.pem | 26 +++ .../samples/fulcio-response/valid/ctfe.pub | 4 + .../fulcio-response/valid/fulcio.crt.pem | 13 ++ .../samples/fulcio-response/valid/sct.base64 | 1 + 18 files changed, 620 insertions(+), 106 deletions(-) delete mode 100644 src/main/java/dev/sigstore/fulcio/client/CertificateRequests.java rename src/main/java/dev/sigstore/fulcio/client/{Client.java => FulcioClient.java} (70%) rename src/main/java/dev/sigstore/fulcio/client/{CertificateResponse.java => FulcioValidationException.java} (58%) create mode 100644 src/main/java/dev/sigstore/fulcio/client/FulcioValidator.java create mode 100644 src/main/java/dev/sigstore/fulcio/client/SigningCertificate.java rename src/test/java/dev/sigstore/fulcio/client/{ClientTest.java => FulcioClientTest.java} (52%) create mode 100644 src/test/java/dev/sigstore/fulcio/client/FulcioValidatorTest.java create mode 100644 src/test/java/dev/sigstore/fulcio/client/SigningCertificateTest.java create mode 100644 src/test/resources/dev/sigstore/samples/certs/cert.der create mode 100644 src/test/resources/dev/sigstore/samples/certs/cert.pem create mode 100644 src/test/resources/dev/sigstore/samples/fulcio-response/valid/cert.pem create mode 100644 src/test/resources/dev/sigstore/samples/fulcio-response/valid/ctfe.pub create mode 100644 src/test/resources/dev/sigstore/samples/fulcio-response/valid/fulcio.crt.pem create mode 100644 src/test/resources/dev/sigstore/samples/fulcio-response/valid/sct.base64 diff --git a/README.md b/README.md index 333312ca..b5da9767 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ byte[] signed = signature.sign(); CertificateRequest cReq = new CertificateRequest(keys.getPublic(), signed); // ask fulcio for a signing cert chain for our public key -CertificateResponse cResp = fulcioClient.SigningCert(cReq, token); +SigningCertificate signingCert = fulcioClient.SigningCert(cReq, token); // sign something with our private key, throw it away and save the cert with the artifact ``` diff --git a/build.gradle.kts b/build.gradle.kts index 0187fc51..fdd105d1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,6 +23,9 @@ dependencies { implementation("com.google.api-client:google-api-client-gson:1.31.5") implementation("com.google.code.gson:gson:2.8.9") + implementation("org.conscrypt:conscrypt-openjdk-uber:2.5.2") { + because("contains library code for all platforms") + } testImplementation("junit:junit:4.12") testImplementation("com.nimbusds:oauth2-oidc-sdk:6.21.2") diff --git a/src/main/java/dev/sigstore/fulcio/client/CertificateRequest.java b/src/main/java/dev/sigstore/fulcio/client/CertificateRequest.java index 150993a4..e73bf1fb 100644 --- a/src/main/java/dev/sigstore/fulcio/client/CertificateRequest.java +++ b/src/main/java/dev/sigstore/fulcio/client/CertificateRequest.java @@ -15,8 +15,10 @@ */ package dev.sigstore.fulcio.client; +import dev.sigstore.json.GsonSupplier; import java.security.PublicKey; import java.util.Collections; +import java.util.HashMap; import java.util.List; public class CertificateRequest { @@ -49,4 +51,16 @@ public PublicKey getPublicKey() { public byte[] getSignedEmailAddress() { return signedEmailAddress; } + + public String toJsonPayload() { + HashMap key = new HashMap<>(); + key.put("content", getPublicKey().getEncoded()); + key.put("algorithm", getPublicKey().getAlgorithm()); + + HashMap data = new HashMap<>(); + data.put("publicKey", key); + data.put("signedEmailAddress", getSignedEmailAddress()); + + return new GsonSupplier().get().toJson(data); + } } diff --git a/src/main/java/dev/sigstore/fulcio/client/CertificateRequests.java b/src/main/java/dev/sigstore/fulcio/client/CertificateRequests.java deleted file mode 100644 index 42824e7d..00000000 --- a/src/main/java/dev/sigstore/fulcio/client/CertificateRequests.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2022 The Sigstore Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dev.sigstore.fulcio.client; - -import dev.sigstore.json.GsonSupplier; -import java.util.HashMap; - -public class CertificateRequests { - public static String toJsonPayload(CertificateRequest cr) { - HashMap key = new HashMap<>(); - key.put("content", cr.getPublicKey().getEncoded()); - key.put("algorithm", cr.getPublicKey().getAlgorithm()); - - HashMap data = new HashMap<>(); - data.put("publicKey", key); - data.put("signedEmailAddress", cr.getSignedEmailAddress()); - - return new GsonSupplier().get().toJson(data); - } -} diff --git a/src/main/java/dev/sigstore/fulcio/client/Client.java b/src/main/java/dev/sigstore/fulcio/client/FulcioClient.java similarity index 70% rename from src/main/java/dev/sigstore/fulcio/client/Client.java rename to src/main/java/dev/sigstore/fulcio/client/FulcioClient.java index 5c2386eb..c03bcb85 100644 --- a/src/main/java/dev/sigstore/fulcio/client/Client.java +++ b/src/main/java/dev/sigstore/fulcio/client/FulcioClient.java @@ -17,21 +17,18 @@ import com.google.api.client.http.*; import com.google.api.client.http.apache.v2.ApacheHttpTransport; -import com.google.api.client.util.PemReader; -import java.io.ByteArrayInputStream; +import com.google.common.io.CharStreams; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Base64; import java.util.concurrent.TimeUnit; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.impl.client.HttpClientBuilder; +import org.conscrypt.ct.SerializationException; -public class Client { +public class FulcioClient { public static final String PUBLIC_FULCIO_SERVER = "https://fulcio.sigstore.dev"; public static final String SIGNING_CERT_PATH = "/api/v1/signingCert"; public static final String DEFAULT_USER_AGENT = "fulcioJavaClient/0.0.1"; @@ -40,15 +37,18 @@ public class Client { private final HttpTransport httpTransport; private final URI serverUrl; private final String userAgent; + private final boolean requireSct; - public static Builder Builder() { + public static Builder builder() { return new Builder(); } - private Client(HttpTransport httpTransport, URI serverUrl, String userAgent) { + private FulcioClient( + HttpTransport httpTransport, URI serverUrl, String userAgent, boolean requireSct) { this.httpTransport = httpTransport; this.serverUrl = serverUrl; this.userAgent = userAgent; + this.requireSct = requireSct; } public static class Builder { @@ -56,6 +56,7 @@ public static class Builder { private URI serverUrl = URI.create(PUBLIC_FULCIO_SERVER); private String userAgent = DEFAULT_USER_AGENT; private boolean useSSLVerification = true; + private boolean requireSct = true; private Builder() {} @@ -85,19 +86,24 @@ public Builder setUseSSLVerification(boolean enable) { return this; } - public Client build() { + public Builder requireSct(boolean requireSct) { + this.requireSct = requireSct; + return this; + } + + public FulcioClient build() { HttpClientBuilder hcb = ApacheHttpTransport.newDefaultHttpClientBuilder(); hcb.setConnectionTimeToLive(timeout, TimeUnit.SECONDS); if (!useSSLVerification) { hcb = hcb.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE); } HttpTransport httpTransport = new ApacheHttpTransport(hcb.build()); - return new Client(httpTransport, serverUrl, userAgent); + return new FulcioClient(httpTransport, serverUrl, userAgent, requireSct); } } - public CertificateResponse SigningCert(CertificateRequest cr, String bearerToken) - throws IOException, CertificateException { + public SigningCertificate SigningCert(CertificateRequest cr, String bearerToken) + throws IOException, CertificateException, SerializationException { URI fulcioEndpoint = serverUrl.resolve(SIGNING_CERT_PATH); HttpRequest req = @@ -105,8 +111,7 @@ public CertificateResponse SigningCert(CertificateRequest cr, String bearerToken .createRequestFactory() .buildPostRequest( new GenericUrl(fulcioEndpoint), - ByteArrayContent.fromString( - "application/json", CertificateRequests.toJsonPayload(cr))); + ByteArrayContent.fromString("application/json", cr.toJsonPayload())); req.getHeaders().setAccept("application/pem-certificate-chain"); req.getHeaders().setAuthorization("Bearer " + bearerToken); @@ -119,29 +124,14 @@ public CertificateResponse SigningCert(CertificateRequest cr, String bearerToken } String sctHeader = resp.getHeaders().getFirstHeaderStringValue("SCT"); - if (sctHeader == null) { + if (sctHeader == null && requireSct) { throw new IOException("no signed certificate timestamps were found in response from Fulcio"); } - byte[] sct = Base64.getDecoder().decode(sctHeader); - - System.out.println(new String(sct)); - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - ArrayList certList = new ArrayList<>(); - PemReader pemReader = new PemReader(new InputStreamReader(resp.getContent())); - while (true) { - PemReader.Section section = pemReader.readNextSection(); - if (section == null) { - break; - } - - byte[] certBytes = section.getBase64DecodedBytes(); - certList.add((X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certBytes))); + try (InputStream content = resp.getContent()) { + return SigningCertificate.newSigningCertificate( + CharStreams.toString(new InputStreamReader(content, resp.getContentCharset())), + sctHeader); } - if (certList.isEmpty()) { - throw new IOException("no certificates were found in response from Fulcio"); - } - - return new CertificateResponse(cf.generateCertPath(certList), sct); } } diff --git a/src/main/java/dev/sigstore/fulcio/client/CertificateResponse.java b/src/main/java/dev/sigstore/fulcio/client/FulcioValidationException.java similarity index 58% rename from src/main/java/dev/sigstore/fulcio/client/CertificateResponse.java rename to src/main/java/dev/sigstore/fulcio/client/FulcioValidationException.java index 79f38e18..7573d6ad 100644 --- a/src/main/java/dev/sigstore/fulcio/client/CertificateResponse.java +++ b/src/main/java/dev/sigstore/fulcio/client/FulcioValidationException.java @@ -15,25 +15,16 @@ */ package dev.sigstore.fulcio.client; -import java.security.cert.CertPath; -import javax.annotation.Nullable; - -public class CertificateResponse { - private final CertPath certPath; - - // TODO: This could be saved potentially as a more concrete type - @Nullable private final byte[] sct; - - public CertificateResponse(CertPath certPath, @Nullable byte[] sct) { - this.certPath = certPath; - this.sct = sct; +public class FulcioValidationException extends Exception { + public FulcioValidationException(String message) { + super(message); } - public CertPath getCertPath() { - return certPath; + public FulcioValidationException(String message, Throwable cause) { + super(message, cause); } - public byte[] getSct() { - return sct; + public FulcioValidationException(Throwable cause) { + super(cause); } } diff --git a/src/main/java/dev/sigstore/fulcio/client/FulcioValidator.java b/src/main/java/dev/sigstore/fulcio/client/FulcioValidator.java new file mode 100644 index 00000000..b8033ede --- /dev/null +++ b/src/main/java/dev/sigstore/fulcio/client/FulcioValidator.java @@ -0,0 +1,149 @@ +/* + * Copyright 2022 The Sigstore Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.sigstore.fulcio.client; + +import com.google.api.client.util.PemReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.cert.*; +import java.security.spec.EncodedKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Collections; +import java.util.Date; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.conscrypt.ct.CTLogInfo; +import org.conscrypt.ct.CertificateEntry; +import org.conscrypt.ct.SignedCertificateTimestamp; +import org.conscrypt.ct.VerifiedSCT; + +public class FulcioValidator { + @Nullable private final CTLogInfo ctLogInfo; + private final TrustAnchor fulcioRoot; + + public static FulcioValidator newFulcioValidator( + byte[] fulcioRoot, @Nullable byte[] ctfePublicKey) + throws InvalidKeySpecException, NoSuchAlgorithmException, CertificateException, IOException, + InvalidAlgorithmParameterException { + + CTLogInfo ctLogInfo = null; + if (ctfePublicKey != null) { + // TODO: ctfePublicKey can be EC or RSA or EDDSA/ED25519 + // (https://github.com/sigstore/sigstore-java/issues/4) + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + PemReader pemReader = + new PemReader( + new InputStreamReader( + new ByteArrayInputStream(ctfePublicKey), StandardCharsets.UTF_8)); + PemReader.Section section = pemReader.readNextSection(); + if (pemReader.readNextSection() != null) { + throw new InvalidKeySpecException( + "ctfe public key must be only a single PEM encoded public key"); + } + EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(section.getBase64DecodedBytes()); + PublicKey ctfePublicKeyObj = keyFactory.generatePublic(publicKeySpec); + ctLogInfo = new CTLogInfo(ctfePublicKeyObj, "fulcio ct log", "unused-url"); + } + + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + X509Certificate fulcioRootObj = + (X509Certificate) + certificateFactory.generateCertificate(new ByteArrayInputStream(fulcioRoot)); + + TrustAnchor fulcioRootTrustAnchor = new TrustAnchor(fulcioRootObj, null); + // this should throw an InvalidAlgorithmException a bit earlier that would otherwise be + // encountered + // in validateCertPath + new PKIXParameters(Collections.singleton(fulcioRootTrustAnchor)); + + return new FulcioValidator(ctLogInfo, fulcioRootTrustAnchor); + } + + private FulcioValidator(@Nullable CTLogInfo ctLogInfo, TrustAnchor fulcioRoot) { + this.ctLogInfo = ctLogInfo; + this.fulcioRoot = fulcioRoot; + } + + public void validateSct(SigningCertificate sc) throws FulcioValidationException { + + SignedCertificateTimestamp sct = + sc.getSct() + .orElseThrow(() -> new FulcioValidationException("No SCT was found to validate")); + if (ctLogInfo == null) { + throw new FulcioValidationException("No ct-log public key was provided to validator"); + } + + // leaf certificate are guaranteed to be X509Certificates if they were built via + // a client request. + if (!(sc.getLeafCertificate() instanceof X509Certificate)) { + throw new RuntimeException( + "Encountered non X509 Certificate when validating SCT. Leaf certificate is " + + sc.getLeafCertificate().getClass()); + } + CertificateEntry ce; + + try { + ce = CertificateEntry.createForX509Certificate((X509Certificate) sc.getLeafCertificate()); + } catch (CertificateEncodingException cee) { + throw new FulcioValidationException("Leaf certificate could not be parsed", cee); + } + + VerifiedSCT.Status status = ctLogInfo.verifySingleSCT(sct, ce); + if (status != VerifiedSCT.Status.VALID) { + throw new FulcioValidationException("SCT could not be verified because " + status.toString()); + } + } + + public void validateCertChain(SigningCertificate sc) throws FulcioValidationException { + CertPathValidator cpv; + try { + cpv = CertPathValidator.getInstance("PKIX"); + } catch (NoSuchAlgorithmException e) { + // + throw new RuntimeException( + "No PKIX CertPathValidator, we probably shouldn't be here, but this seems to be a system library error not a program control flow issue", + e); + } + + PKIXParameters pkixParams; + try { + pkixParams = new PKIXParameters(Collections.singleton(fulcioRoot)); + } catch (InvalidAlgorithmParameterException e) { + throw new RuntimeException( + "Can't create PKIX parameters for fulcioRoot. This should have been checked when generating a validator instance", + e); + } + pkixParams.setRevocationEnabled(false); + + // these certs are only valid for 15 minutes, so find a time in the validity period + Date dateInValidityPeriod = + new Date(((X509Certificate) sc.getLeafCertificate()).getNotBefore().getTime()); + pkixParams.setDate(dateInValidityPeriod); + + try { + // a result is returned here, but I don't know what to do with it yet + cpv.validate(sc.getCertPath(), pkixParams); + } catch (CertPathValidatorException | InvalidAlgorithmParameterException ve) { + throw new FulcioValidationException(ve); + } + } +} diff --git a/src/main/java/dev/sigstore/fulcio/client/SigningCertificate.java b/src/main/java/dev/sigstore/fulcio/client/SigningCertificate.java new file mode 100644 index 00000000..d10a445f --- /dev/null +++ b/src/main/java/dev/sigstore/fulcio/client/SigningCertificate.java @@ -0,0 +1,126 @@ +/* + * Copyright 2022 The Sigstore Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.sigstore.fulcio.client; + +import com.google.api.client.util.PemReader; +import com.google.common.annotations.VisibleForTesting; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import dev.sigstore.json.GsonSupplier; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.security.cert.*; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Optional; +import javax.annotation.Nullable; +import org.conscrypt.ct.DigitallySigned; +import org.conscrypt.ct.SerializationException; +import org.conscrypt.ct.SignedCertificateTimestamp; + +/** Response from Fulcio that includes a certPath and an SCT */ +public class SigningCertificate { + + private final CertPath certPath; + @Nullable private final SignedCertificateTimestamp sct; + + static SigningCertificate newSigningCertificate(String certs, @Nullable String sctHeader) + throws CertificateException, IOException, SerializationException { + CertPath certPath = decodeCerts(certs); + if (sctHeader != null) { + SignedCertificateTimestamp sct = decodeSCT(sctHeader); + return new SigningCertificate(certPath, sct); + } + return new SigningCertificate(certPath, null); + } + + @VisibleForTesting + static CertPath decodeCerts(String content) throws CertificateException, IOException { + PemReader pemReader = new PemReader(new StringReader(content)); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + ArrayList certList = new ArrayList<>(); + while (true) { + PemReader.Section section = pemReader.readNextSection(); + if (section == null) { + break; + } + + byte[] certBytes = section.getBase64DecodedBytes(); + certList.add((X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certBytes))); + } + if (certList.isEmpty()) { + throw new CertificateParsingException( + "no valid PEM certificates were found in response from Fulcio"); + } + + return cf.generateCertPath(certList); + } + + @VisibleForTesting + static SignedCertificateTimestamp decodeSCT(String sctHeader) throws SerializationException { + byte[] sct = Base64.getDecoder().decode(sctHeader); + Gson gson = new GsonSupplier().get(); + return gson.fromJson( + new InputStreamReader(new ByteArrayInputStream(sct), StandardCharsets.UTF_8), + SctJson.class) + .toSct(); + } + + private static class SctJson { + private int sct_version; + private byte[] id; + private long timestamp; + private byte[] extensions; + private byte[] signature; + + public SignedCertificateTimestamp toSct() throws JsonParseException, SerializationException { + if (sct_version != 0) { + throw new JsonParseException( + "Invalid SCT version:" + sct_version + ", only 0 (V1) is allowed"); + } + if (extensions.length != 0) { + throw new JsonParseException( + "SCT has extensions that cannot be handled by client:" + new String(extensions)); + } + + DigitallySigned digiSig = DigitallySigned.decode(signature); + return new SignedCertificateTimestamp( + SignedCertificateTimestamp.Version.V1, + id, + timestamp, + extensions, + digiSig, + SignedCertificateTimestamp.Origin.OCSP_RESPONSE); + } + } + + private SigningCertificate(CertPath certPath, @Nullable SignedCertificateTimestamp sct) { + this.certPath = certPath; + this.sct = sct; + } + + public CertPath getCertPath() { + return certPath; + } + + public Certificate getLeafCertificate() { + return certPath.getCertificates().get(0); + } + + Optional getSct() { + return Optional.ofNullable(sct); + } +} diff --git a/src/test/java/dev/sigstore/fulcio/client/ClientTest.java b/src/test/java/dev/sigstore/fulcio/client/FulcioClientTest.java similarity index 52% rename from src/test/java/dev/sigstore/fulcio/client/ClientTest.java rename to src/test/java/dev/sigstore/fulcio/client/FulcioClientTest.java index b28c863e..600dcebe 100644 --- a/src/test/java/dev/sigstore/fulcio/client/ClientTest.java +++ b/src/test/java/dev/sigstore/fulcio/client/FulcioClientTest.java @@ -29,7 +29,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; -public class ClientTest { +public class FulcioClientTest { @Rule public TemporaryFolder testRoot = new TemporaryFolder(); @Test @@ -42,9 +42,11 @@ public void testSigningCert() throws Exception { try (FakeCTLogServer ctLogServer = FakeCTLogServer.startNewServer()) { // start fulcio client with config from oidc server - try (FulcioWrapper fulcioServer = - FulcioWrapper.startNewServer(fulcioConfig, ctLogServer.getURI().toString())) { - Client c = Client.Builder().setServerUrl(fulcioServer.getURI()).build(); + FulcioWrapper fulcioServer = null; + try { + fulcioServer = + FulcioWrapper.startNewServer(fulcioConfig, ctLogServer.getURI().toString()); + FulcioClient c = FulcioClient.builder().setServerUrl(fulcioServer.getURI()).build(); // create a "subject" and sign it with the oidc server key (signed JWT) String subject = FakeOIDCServer.USER; @@ -65,11 +67,59 @@ public void testSigningCert() throws Exception { CertificateRequest cReq = new CertificateRequest(keys.getPublic(), signed); // ask fulcio for a signing cert - CertificateResponse cResp = c.SigningCert(cReq, token); + SigningCertificate sc = c.SigningCert(cReq, token); // some pretty basic assertions - Assert.assertTrue(cResp.getCertPath().getCertificates().size() > 0); - Assert.assertTrue(cResp.getSct().length > 0); + Assert.assertTrue(sc.getCertPath().getCertificates().size() > 0); + Assert.assertNotNull(sc.getSct()); + } finally { + if (fulcioServer != null) { + fulcioServer.shutdown(); + } + } + } + } + } + + @Test + public void testSigningCert_NoSct() throws Exception { + try (FakeOIDCServer oidcServer = FakeOIDCServer.startNewServer()) { + File fulcioConfig = testRoot.newFile("fulcio-config.json"); + Files.write(oidcServer.getFulcioConfig().getBytes(StandardCharsets.UTF_8), fulcioConfig); + // start fulcio client with config from oidc server + FulcioWrapper fulcioServer = null; + try { + fulcioServer = FulcioWrapper.startNewServer(fulcioConfig, null); + FulcioClient c = + FulcioClient.builder().setServerUrl(fulcioServer.getURI()).requireSct(false).build(); + + // create a "subject" and sign it with the oidc server key (signed JWT) + String subject = FakeOIDCServer.USER; + String token = oidcServer.sign(subject); + + // create an ECDSA p-256 keypair, this is our key that we want to generate certs for + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); + keyGen.initialize(256); + KeyPair keys = keyGen.generateKeyPair(); + + // sign the "subject" with our key, this signer already generates asn1 notation + Signature signature = Signature.getInstance("SHA256withECDSA"); + signature.initSign(keys.getPrivate()); + signature.update(subject.getBytes(StandardCharsets.UTF_8)); + byte[] signed = signature.sign(); + + // create a certificate request with our public key and our signed "subject" + CertificateRequest cReq = new CertificateRequest(keys.getPublic(), signed); + + // ask fulcio for a signing cert + SigningCertificate sc = c.SigningCert(cReq, token); + + // some pretty basic assertions + Assert.assertTrue(sc.getCertPath().getCertificates().size() > 0); + Assert.assertFalse(sc.getSct().isPresent()); + } finally { + if (fulcioServer != null) { + fulcioServer.shutdown(); } } } diff --git a/src/test/java/dev/sigstore/fulcio/client/FulcioValidatorTest.java b/src/test/java/dev/sigstore/fulcio/client/FulcioValidatorTest.java new file mode 100644 index 00000000..de812e9c --- /dev/null +++ b/src/test/java/dev/sigstore/fulcio/client/FulcioValidatorTest.java @@ -0,0 +1,95 @@ +/* + * Copyright 2022 The Sigstore Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.sigstore.fulcio.client; + +import com.google.common.io.Resources; +import java.io.IOException; +import java.nio.charset.Charset; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.spec.InvalidKeySpecException; +import org.conscrypt.ct.SerializationException; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class FulcioValidatorTest { + private String sctBase64; + private String certs; + private byte[] fulcioRoot; + private byte[] ctfePub; + + @Before + public void loadResources() throws IOException { + sctBase64 = + Resources.toString( + Resources.getResource("dev/sigstore/samples/fulcio-response/valid/sct.base64"), + Charset.defaultCharset()); + certs = + Resources.toString( + Resources.getResource("dev/sigstore/samples/fulcio-response/valid/cert.pem"), + Charset.defaultCharset()); + + fulcioRoot = + Resources.toByteArray( + Resources.getResource("dev/sigstore/samples/fulcio-response/valid/fulcio.crt.pem")); + ctfePub = + Resources.toByteArray( + Resources.getResource("dev/sigstore/samples/fulcio-response/valid/ctfe.pub")); + } + + @Test + public void validSigningCertAndSct() + throws IOException, SerializationException, CertificateException, InvalidKeySpecException, + NoSuchAlgorithmException, InvalidAlgorithmParameterException, FulcioValidationException { + var signingCertificate = SigningCertificate.newSigningCertificate(certs, sctBase64); + var fulcioValidator = FulcioValidator.newFulcioValidator(fulcioRoot, ctfePub); + + fulcioValidator.validateCertChain(signingCertificate); + fulcioValidator.validateSct(signingCertificate); + } + + @Test + public void testValidateSct_nullCtLogKey() + throws IOException, SerializationException, CertificateException, InvalidKeySpecException, + NoSuchAlgorithmException, InvalidAlgorithmParameterException { + var signingCertificate = SigningCertificate.newSigningCertificate(certs, sctBase64); + var fulcioValidator = FulcioValidator.newFulcioValidator(fulcioRoot, null); + + try { + fulcioValidator.validateSct(signingCertificate); + Assert.fail(); + } catch (FulcioValidationException fve) { + Assert.assertEquals("No ct-log public key was provided to validator", fve.getMessage()); + } + } + + @Test + public void testValidateSct_noSct() + throws SerializationException, CertificateException, IOException, + InvalidAlgorithmParameterException, InvalidKeySpecException, NoSuchAlgorithmException { + var signingCertificate = SigningCertificate.newSigningCertificate(certs, null); + var fulcioValidator = FulcioValidator.newFulcioValidator(fulcioRoot, ctfePub); + + try { + fulcioValidator.validateSct(signingCertificate); + Assert.fail(); + } catch (FulcioValidationException fve) { + Assert.assertEquals("No SCT was found to validate", fve.getMessage()); + } + } +} diff --git a/src/test/java/dev/sigstore/fulcio/client/SigningCertificateTest.java b/src/test/java/dev/sigstore/fulcio/client/SigningCertificateTest.java new file mode 100644 index 00000000..4f6cb080 --- /dev/null +++ b/src/test/java/dev/sigstore/fulcio/client/SigningCertificateTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2022 The Sigstore Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.sigstore.fulcio.client; + +import com.google.common.io.Resources; +import java.io.IOException; +import java.nio.charset.Charset; +import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; +import org.conscrypt.ct.SerializationException; +import org.junit.Assert; +import org.junit.Test; + +public class SigningCertificateTest { + @Test + public void TestDecode() throws SerializationException, IOException, CertificateException { + String sctBase64 = + Resources.toString( + Resources.getResource("dev/sigstore/samples/fulcio-response/valid/sct.base64"), + Charset.defaultCharset()); + String certs = + Resources.toString( + Resources.getResource("dev/sigstore/samples/fulcio-response/valid/cert.pem"), + Charset.defaultCharset()); + + SigningCertificate.newSigningCertificate(certs, sctBase64); + } + + @Test + public void TestDecode_DerCert() throws CertificateException, IOException { + String certs = + Resources.toString( + Resources.getResource("dev/sigstore/samples/certs/cert.der"), Charset.defaultCharset()); + try { + SigningCertificate.decodeCerts(certs); + Assert.fail("DER certificate was unexpectedly successfully parsed"); + } catch (CertificateParsingException cpe) { + Assert.assertEquals( + "no valid PEM certificates were found in response from Fulcio", cpe.getMessage()); + } + } +} diff --git a/src/test/java/dev/sigstore/testing/FulcioWrapper.java b/src/test/java/dev/sigstore/testing/FulcioWrapper.java index f27e9244..96cea9da 100644 --- a/src/test/java/dev/sigstore/testing/FulcioWrapper.java +++ b/src/test/java/dev/sigstore/testing/FulcioWrapper.java @@ -23,18 +23,20 @@ * A test fixture to start fulcio from an executable on the system path. This requires fulcio to be * installed, do something like go install github.com/sigstore/fulcio */ -public class FulcioWrapper implements AutoCloseable { +public class FulcioWrapper { - private final Process p; + private final Process fulcioProcess; private FulcioWrapper(Process p) { - this.p = p; + this.fulcioProcess = p; } - public static FulcioWrapper startNewServer(File config, String ctLogUrl) throws IOException { - ProcessBuilder pb = new ProcessBuilder(); - String fulcioEnv = System.getenv("FULCIO_BINARY"); - String fulcioCmd = fulcioEnv == null ? "fulcio" : fulcioEnv; + public static FulcioWrapper startNewServer(File config, String ctLogUrl) + throws IOException, InterruptedException { + var pb = new ProcessBuilder(); + var fulcioEnv = System.getenv("FULCIO_BINARY"); + var fulcioCmd = fulcioEnv == null ? "fulcio" : fulcioEnv; + var ctLogOpt = ctLogUrl == null ? "--ct-log-url=" : "--ct-log-url=" + ctLogUrl; pb.command( fulcioCmd, "serve", @@ -42,19 +44,21 @@ public static FulcioWrapper startNewServer(File config, String ctLogUrl) throws "5555", "--ca", "ephemeralca", - "--ct-log-url", - ctLogUrl, + ctLogOpt, "--config-path", config.getAbsolutePath()); pb.redirectErrorStream(true); pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); - Process p = pb.start(); - return new FulcioWrapper(p); + var fp = pb.start(); + Thread.sleep(1000); // wait for the server to come up + return new FulcioWrapper(fp); } - @Override - public void close() { - p.destroy(); + public void shutdown() throws Exception { + // forcible kill fulcio and all it's subprocesses + fulcioProcess.destroyForcibly(); + fulcioProcess.waitFor(); + Thread.sleep(1000); // give the server a chance to shutdown } public URI getURI() { diff --git a/src/test/resources/dev/sigstore/samples/certs/cert.der b/src/test/resources/dev/sigstore/samples/certs/cert.der new file mode 100644 index 0000000000000000000000000000000000000000..e89ea7f2fc3a75ceb38b2d5461c2f9c98325e3a3 GIT binary patch literal 528 zcmXqLV&XAqVw}8ynTe5!Nra*M$^_O3Ylf?GGu)Nxe4pGok*H+A#m1r4=5fxJg_+qv z%TUxnn2kAHBuc8+>9x!O!YrqF| zlPo_Y<9`+wW+wIqu+wE#S$IHx(q?01Wn~BYi3g-e7^H{^=q6bZkB>!+MWjn=jqC5Z zS1OEGpR10KM1WrD-dx&P-T1S?Pk-(WAd0`UOFz z2^$DD@+TG)lscs6=cnhS>Lusr8fda{0WD@>Y~F9c$jBm{QBqP+Y^ATCn4FwnnpaY+ zholrK+?mrE3{sd3TvQ9qgu)J-eWNH{8r^=*G4BM=yyI2RmOifj@oA%Qp+RO