Skip to content

Commit a81ad5b

Browse files
committed
configure fulcio (v2 for now) with trustroot
Signed-off-by: Appu Goundan <[email protected]>
1 parent d9ec7fe commit a81ad5b

File tree

6 files changed

+624
-1
lines changed

6 files changed

+624
-1
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2022 The Sigstore Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package dev.sigstore.fulcio.client;
17+
18+
import static dev.sigstore.fulcio.v2.SigningCertificate.CertificateCase.SIGNED_CERTIFICATE_DETACHED_SCT;
19+
20+
import com.google.protobuf.ByteString;
21+
import dev.sigstore.fulcio.v2.CAGrpc;
22+
import dev.sigstore.fulcio.v2.CreateSigningCertificateRequest;
23+
import dev.sigstore.fulcio.v2.Credentials;
24+
import dev.sigstore.fulcio.v2.PublicKey;
25+
import dev.sigstore.fulcio.v2.PublicKeyRequest;
26+
import dev.sigstore.http.GrpcChannels;
27+
import dev.sigstore.http.HttpParams;
28+
import dev.sigstore.http.ImmutableHttpParams;
29+
import dev.sigstore.trustroot.CertificateAuthority;
30+
import java.security.cert.CertificateException;
31+
import java.util.Base64;
32+
import java.util.concurrent.TimeUnit;
33+
34+
/** A client to communicate with a fulcio service instance over gRPC. */
35+
public class FulcioClient2 {
36+
37+
private final HttpParams httpParams;
38+
private final CertificateAuthority certificateAuthority;
39+
40+
public static Builder builder() {
41+
return new Builder();
42+
}
43+
44+
private FulcioClient2(HttpParams httpParams, CertificateAuthority certificateAuthority) {
45+
this.certificateAuthority = certificateAuthority;
46+
this.httpParams = httpParams;
47+
}
48+
49+
public static class Builder {
50+
private CertificateAuthority certificateAuthority;
51+
private HttpParams httpParams = ImmutableHttpParams.builder().build();
52+
53+
private Builder() {}
54+
55+
/** Configure the http properties, see {@link HttpParams}. */
56+
public Builder setHttpParams(HttpParams httpParams) {
57+
this.httpParams = httpParams;
58+
return this;
59+
}
60+
61+
/** The remote fulcio instance. */
62+
public Builder setCertificateAuthority(CertificateAuthority certificateAuthority) {
63+
this.certificateAuthority = certificateAuthority;
64+
return this;
65+
}
66+
67+
public FulcioClient2 build() {
68+
return new FulcioClient2(httpParams, certificateAuthority);
69+
}
70+
}
71+
72+
/**
73+
* Request a signing certificate from fulcio.
74+
*
75+
* @param request certificate request parameters
76+
* @return a {@link SigningCertificate} from fulcio
77+
*/
78+
public SigningCertificate signingCertificate(CertificateRequest request)
79+
throws InterruptedException, CertificateException {
80+
if (!certificateAuthority.isCurrent()) {
81+
throw new RuntimeException(
82+
"Certificate Authority '" + certificateAuthority.getUri() + "' is not current");
83+
}
84+
// TODO: 1. If we want to reduce the cost of creating channels/connections, we could try
85+
// to make a new connection once per batch of fulcio requests, but we're not really
86+
// at that point yet.
87+
// TODO: 2. getUri().getAuthority() is potentially prone to error if we don't get a good URI
88+
var channel =
89+
GrpcChannels.newManagedChannel(certificateAuthority.getUri().getAuthority(), httpParams);
90+
91+
try {
92+
var client = CAGrpc.newBlockingStub(channel);
93+
var credentials = Credentials.newBuilder().setOidcIdentityToken(request.getIdToken()).build();
94+
95+
String pemEncodedPublicKey =
96+
"-----BEGIN PUBLIC KEY-----\n"
97+
+ Base64.getEncoder().encodeToString(request.getPublicKey().getEncoded())
98+
+ "\n-----END PUBLIC KEY-----";
99+
var publicKeyRequest =
100+
PublicKeyRequest.newBuilder()
101+
.setPublicKey(
102+
PublicKey.newBuilder()
103+
.setAlgorithm(request.getPublicKeyAlgorithm())
104+
.setContent(pemEncodedPublicKey)
105+
.build())
106+
.setProofOfPossession(ByteString.copyFrom(request.getProofOfPossession()))
107+
.build();
108+
var req =
109+
CreateSigningCertificateRequest.newBuilder()
110+
.setCredentials(credentials)
111+
.setPublicKeyRequest(publicKeyRequest)
112+
.build();
113+
114+
var certs =
115+
client
116+
.withDeadlineAfter(httpParams.getTimeout(), TimeUnit.SECONDS)
117+
.createSigningCertificate(req);
118+
119+
if (certs.getCertificateCase() == SIGNED_CERTIFICATE_DETACHED_SCT) {
120+
throw new CertificateException("Detached SCTs are not supported");
121+
}
122+
return SigningCertificate.newSigningCertificate(certs.getSignedCertificateEmbeddedSct());
123+
} finally {
124+
channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
125+
}
126+
}
127+
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/*
2+
* Copyright 2022 The Sigstore Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package dev.sigstore.fulcio.client;
17+
18+
import dev.sigstore.encryption.certificates.Certificates;
19+
import dev.sigstore.encryption.certificates.transparency.CTLogInfo;
20+
import dev.sigstore.encryption.certificates.transparency.CTVerificationResult;
21+
import dev.sigstore.encryption.certificates.transparency.CTVerifier;
22+
import dev.sigstore.trustroot.CertificateAuthorities;
23+
import dev.sigstore.trustroot.SigstoreTrustedRoot;
24+
import dev.sigstore.trustroot.TransparencyLogs;
25+
import java.io.IOException;
26+
import java.security.InvalidAlgorithmParameterException;
27+
import java.security.NoSuchAlgorithmException;
28+
import java.security.cert.CertPathValidator;
29+
import java.security.cert.CertPathValidatorException;
30+
import java.security.cert.CertificateEncodingException;
31+
import java.security.cert.CertificateException;
32+
import java.security.cert.PKIXParameters;
33+
import java.security.spec.InvalidKeySpecException;
34+
import java.time.Instant;
35+
import java.util.ArrayList;
36+
import java.util.Arrays;
37+
import java.util.Collections;
38+
import java.util.Date;
39+
import java.util.LinkedHashMap;
40+
import java.util.List;
41+
import java.util.Map;
42+
import java.util.stream.Collectors;
43+
44+
/** Verifier for fulcio {@link SigningCertificate}. */
45+
public class FulcioVerifier2 {
46+
private final CertificateAuthorities cas;
47+
private final TransparencyLogs ctLogs;
48+
private final CTVerifier ctVerifier;
49+
50+
public static FulcioVerifier2 newFulcioVerifier(SigstoreTrustedRoot trustRoot)
51+
throws InvalidAlgorithmParameterException, CertificateException, InvalidKeySpecException,
52+
NoSuchAlgorithmException {
53+
return newFulcioVerifier(trustRoot.getCAs(), trustRoot.getCTLogs());
54+
}
55+
56+
public static FulcioVerifier2 newFulcioVerifier(
57+
CertificateAuthorities cas, TransparencyLogs ctLogs)
58+
throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
59+
CertificateException {
60+
List<CTLogInfo> logs = new ArrayList<>();
61+
for (var ctLog : ctLogs.all()) {
62+
logs.add(
63+
new CTLogInfo(
64+
ctLog.getPublicKey().toJavaPublicKey(), "CT Log", ctLog.getBaseUrl().toString()));
65+
}
66+
var verifier =
67+
new CTVerifier(
68+
logId ->
69+
logs.stream()
70+
.filter(ctLogInfo -> Arrays.equals(ctLogInfo.getID(), logId))
71+
.findFirst()
72+
.orElse(null));
73+
74+
// check to see if we can use all fulcio roots (this is a bit eager)
75+
for (var ca : cas.all()) {
76+
ca.asTrustAnchor();
77+
}
78+
79+
return new FulcioVerifier2(cas, ctLogs, verifier);
80+
}
81+
82+
private FulcioVerifier2(
83+
CertificateAuthorities cas, TransparencyLogs ctLogs, CTVerifier ctVerifier) {
84+
this.cas = cas;
85+
this.ctLogs = ctLogs;
86+
this.ctVerifier = ctVerifier;
87+
}
88+
89+
/**
90+
* Verify that an SCT associated with a Singing Certificate is valid and signed by the configured
91+
* CT-log public key.
92+
*
93+
* @param signingCertificate containing the SCT metadata to verify
94+
* @throws FulcioVerificationException if verification fails for any reason
95+
*/
96+
public void verifySct(SigningCertificate signingCertificate) throws FulcioVerificationException {
97+
if (ctLogs.size() == 0) {
98+
throw new FulcioVerificationException("No ct logs were provided to verifier");
99+
}
100+
101+
if (signingCertificate.getDetachedSct().isPresent()) {
102+
throw new FulcioVerificationException(
103+
"Detached SCTs are not supported for validating certificates");
104+
} else if (signingCertificate.getEmbeddedSct().isPresent()) {
105+
verifyEmbeddedScts(signingCertificate);
106+
} else {
107+
throw new FulcioVerificationException("No valid SCTs were found during verification");
108+
}
109+
}
110+
111+
private void verifyEmbeddedScts(SigningCertificate signingCertificate)
112+
throws FulcioVerificationException {
113+
var certs = signingCertificate.getCertificates();
114+
CTVerificationResult result;
115+
try {
116+
// even though we're sending the whole chain, this method only checks SCTs on the leaf cert
117+
result = ctVerifier.verifySignedCertificateTimestamps(certs, null, null);
118+
} catch (CertificateEncodingException cee) {
119+
throw new FulcioVerificationException(
120+
"Certificates could not be parsed during SCT verification");
121+
}
122+
123+
// these are technically valid, but we have the additional constraint of sigstore's trustroot
124+
// providing a validity period for logs, so make sure all SCTs were signed by a log during
125+
// that log's validity period
126+
for (var validSct : result.getValidSCTs()) {
127+
var sct = validSct.sct;
128+
129+
var logId = sct.getLogID();
130+
var entryTime = Instant.ofEpochMilli(sct.getTimestamp());
131+
132+
var ctLog = ctLogs.find(logId, entryTime);
133+
if (ctLog.isPresent()) {
134+
// TODO: currently we only require one valid SCT, but maybe this should be configurable?
135+
// found at least one valid sct with a matching valid log
136+
return;
137+
}
138+
}
139+
throw new FulcioVerificationException(
140+
"No valid SCTs were found, all("
141+
+ (result.getValidSCTs().size() + result.getInvalidSCTs().size())
142+
+ ") SCTs were invalid");
143+
}
144+
145+
/**
146+
* Verify that a cert chain is valid and chains up to the trust anchor (fulcio public key)
147+
* configured in this validator.
148+
*
149+
* @param signingCertificate containing the certificate chain
150+
* @throws FulcioVerificationException if verification fails for any reason
151+
*/
152+
public void verifyCertChain(SigningCertificate signingCertificate)
153+
throws FulcioVerificationException, IOException {
154+
CertPathValidator cpv;
155+
try {
156+
cpv = CertPathValidator.getInstance("PKIX");
157+
} catch (NoSuchAlgorithmException e) {
158+
//
159+
throw new RuntimeException(
160+
"No PKIX CertPathValidator, we probably shouldn't be here, but this seems to be a system library error not a program control flow issue",
161+
e);
162+
}
163+
164+
var leaf = signingCertificate.getLeafCertificate();
165+
var validCAs = cas.find(leaf.getNotBefore().toInstant());
166+
167+
if (validCAs.size() == 0) {
168+
throw new FulcioVerificationException(
169+
"No valid Certificate Authorities found when validating certificate");
170+
}
171+
172+
Map<String, String> caVerificationFailure = new LinkedHashMap<>();
173+
174+
System.out.println(Certificates.toPemString(signingCertificate.getCertPath()));
175+
176+
for (var ca : validCAs) {
177+
PKIXParameters pkixParams;
178+
try {
179+
pkixParams = new PKIXParameters(Collections.singleton(ca.asTrustAnchor()));
180+
} catch (InvalidAlgorithmParameterException | CertificateException e) {
181+
throw new RuntimeException(
182+
"Can't create PKIX parameters for fulcioRoot. This should have been checked when generating a verifier instance",
183+
e);
184+
}
185+
pkixParams.setRevocationEnabled(false);
186+
187+
// these certs are only valid for 15 minutes, so find a time in the validity period
188+
@SuppressWarnings("JavaUtilDate")
189+
Date dateInValidityPeriod =
190+
new Date(signingCertificate.getLeafCertificate().getNotBefore().getTime());
191+
pkixParams.setDate(dateInValidityPeriod);
192+
193+
try {
194+
// build a cert chain with the root-chain in question and the provided leaf
195+
var rebuiltCert =
196+
Certificates.appendCertPath(ca.getCertPath(), signingCertificate.getLeafCertificate());
197+
// a result is returned here, but we ignore it
198+
cpv.validate(rebuiltCert, pkixParams);
199+
} catch (CertPathValidatorException
200+
| InvalidAlgorithmParameterException
201+
| CertificateException ve) {
202+
caVerificationFailure.put(ca.getUri().toString(), ve.getMessage());
203+
// verification failed
204+
continue;
205+
}
206+
// verification passed so just end this method
207+
return;
208+
}
209+
String errors =
210+
caVerificationFailure.entrySet().stream()
211+
.map(entry -> entry.getKey() + " (" + entry.getValue() + ")")
212+
.collect(Collectors.joining("\n"));
213+
throw new FulcioVerificationException("Certificate was not verifiable against CAs\n" + errors);
214+
}
215+
}

sigstore-java/src/main/java/dev/sigstore/fulcio/client/SigningCertificate.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,19 @@ static SignedCertificateTimestamp decodeSCT(String sctJson) throws Serialization
126126
return GSON.get().fromJson(sctJson, SctJson.class).toSct();
127127
}
128128

129-
/** Returns true if the signing certificate constains an SCT embedded in the X509 extensions. */
129+
/** Returns true if the signing certificate contains scts embedded in X509 extensions. */
130130
boolean hasEmbeddedSct() {
131131
return getLeafCertificate().getExtensionValue(SCT_X509_OID) != null;
132132
}
133133

134+
/**
135+
* Returns scts if present, or empty if not. The returned byte array may contain any number of
136+
* embedded scts.
137+
*/
138+
Optional<byte[]> getEmbeddedSct() {
139+
return Optional.ofNullable(getLeafCertificate().getExtensionValue(SCT_X509_OID));
140+
}
141+
134142
private static class SctJson {
135143
private int sct_version;
136144
private byte[] id;

0 commit comments

Comments
 (0)