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

Add sct and cert verification #1

Merged
merged 1 commit into from
May 11, 2022
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
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 @@ -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")
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 {
loosebazooka marked this conversation as resolved.
Show resolved Hide resolved
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's kind of weird to fork a response into an exception

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this extend RuntimeException?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we want the tool to catch this and present it to the user. I don't think it's a catastrophic failure. It could be a misconfigured private key, or the wrong fulcio or something?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Makes sense. We should chat about our approach here some time with Jason. I'll typically avoid throwing any checked exceptions unless it's type or api enables the client in some way.

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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious why we allow the ctfePublicKjey to be null if that'll just result in a validation exception when we call validateSct?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it can be null because scts aren't required to be returned from fulcio. they should be, but people could be running private instances and maybe don't care about internal certificate transparency (but maybe they should?)?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be enforced via a policy/config?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But validate will always fail as a result won't it?

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));
loosebazooka marked this conversation as resolved.
Show resolved Hide resolved

return new FulcioValidator(ctLogInfo, fulcioRootTrustAnchor);
}

private FulcioValidator(@Nullable CTLogInfo ctLogInfo, TrustAnchor fulcioRoot) {
this.ctLogInfo = ctLogInfo;
this.fulcioRoot = fulcioRoot;
loosebazooka marked this conversation as resolved.
Show resolved Hide resolved
}

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());
loosebazooka marked this conversation as resolved.
Show resolved Hide resolved
}
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));
loosebazooka marked this conversation as resolved.
Show resolved Hide resolved
} 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);
}
}
}
Loading