Skip to content

Commit

Permalink
Add sct and cert validations
Browse files Browse the repository at this point in the history
- 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 <appu@google.com>
Co-authored-by: Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
  • Loading branch information
loosebazooka and vlsi committed May 10, 2022
1 parent 4aa2b19 commit de94559
Show file tree
Hide file tree
Showing 18 changed files with 567 additions and 92 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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")
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/dev/sigstore/fulcio/client/CertificateRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -49,4 +51,16 @@ public PublicKey getPublicKey() {
public byte[] getSignedEmailAddress() {
return signedEmailAddress;
}

public String toJsonPayload() {
HashMap<String, Object> key = new HashMap<>();
key.put("content", getPublicKey().getEncoded());
key.put("algorithm", getPublicKey().getAlgorithm());

HashMap<String, Object> data = new HashMap<>();
data.put("publicKey", key);
data.put("signedEmailAddress", getSignedEmailAddress());

return new GsonSupplier().get().toJson(data);
}
}
33 changes: 0 additions & 33 deletions src/main/java/dev/sigstore/fulcio/client/CertificateRequests.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -40,22 +37,26 @@ 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 {
private long timeout = DEFAULT_TIMEOUT;
private URI serverUrl = URI.create(PUBLIC_FULCIO_SERVER);
private String userAgent = DEFAULT_USER_AGENT;
private boolean useSSLVerification = true;
private boolean requireSct = true;

private Builder() {}

Expand Down Expand Up @@ -85,28 +86,32 @@ 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 =
httpTransport
.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);
Expand All @@ -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<X509Certificate> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
149 changes: 149 additions & 0 deletions src/main/java/dev/sigstore/fulcio/client/FulcioValidator.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}

0 comments on commit de94559

Please sign in to comment.