Skip to content

Commit dfe3807

Browse files
authored
Merge pull request #492 from sigstore/tuf-in-keyless-workflows
Use tuf to init signer and verifier
2 parents 4a8c23b + 34dcb33 commit dfe3807

File tree

5 files changed

+790
-0
lines changed

5 files changed

+790
-0
lines changed
Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
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;
17+
18+
import com.google.api.client.util.Preconditions;
19+
import com.google.common.collect.ImmutableList;
20+
import com.google.common.collect.ImmutableMap;
21+
import com.google.common.hash.Hashing;
22+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
23+
import com.google.errorprone.annotations.CheckReturnValue;
24+
import com.google.errorprone.annotations.concurrent.GuardedBy;
25+
import dev.sigstore.encryption.certificates.Certificates;
26+
import dev.sigstore.encryption.signers.Signer;
27+
import dev.sigstore.encryption.signers.Signers;
28+
import dev.sigstore.fulcio.client.CertificateRequest;
29+
import dev.sigstore.fulcio.client.FulcioClient2;
30+
import dev.sigstore.fulcio.client.FulcioVerificationException;
31+
import dev.sigstore.fulcio.client.FulcioVerifier2;
32+
import dev.sigstore.fulcio.client.SigningCertificate;
33+
import dev.sigstore.fulcio.client.UnsupportedAlgorithmException;
34+
import dev.sigstore.oidc.client.OidcClients;
35+
import dev.sigstore.oidc.client.OidcException;
36+
import dev.sigstore.oidc.client.OidcToken;
37+
import dev.sigstore.rekor.client.HashedRekordRequest;
38+
import dev.sigstore.rekor.client.RekorClient2;
39+
import dev.sigstore.rekor.client.RekorParseException;
40+
import dev.sigstore.rekor.client.RekorVerificationException;
41+
import dev.sigstore.rekor.client.RekorVerifier2;
42+
import dev.sigstore.tuf.SigstoreTufClient;
43+
import java.io.IOException;
44+
import java.nio.charset.StandardCharsets;
45+
import java.nio.file.Path;
46+
import java.security.InvalidAlgorithmParameterException;
47+
import java.security.InvalidKeyException;
48+
import java.security.NoSuchAlgorithmException;
49+
import java.security.SignatureException;
50+
import java.security.cert.CertificateException;
51+
import java.security.spec.InvalidKeySpecException;
52+
import java.time.Duration;
53+
import java.util.ArrayList;
54+
import java.util.List;
55+
import java.util.Map;
56+
import java.util.concurrent.locks.ReentrantReadWriteLock;
57+
import org.checkerframework.checker.nullness.qual.Nullable;
58+
59+
/**
60+
* A full sigstore keyless signing flow.
61+
*
62+
* <p>Note: the implementation is thread-safe assuming the clients (Fulcio, OIDC, Rekor) are
63+
* thread-safe
64+
*/
65+
public class KeylessSigner2 implements AutoCloseable {
66+
/**
67+
* The instance of the {@link KeylessSigner2} will try to reuse a previously acquired certificate
68+
* if the expiration time on the certificate is more than {@code minSigningCertificateLifetime}
69+
* time away. Otherwise, it will make a new request (OIDC, Fulcio) to obtain a new updated
70+
* certificate to use for signing. This is a default value for the remaining lifetime of the
71+
* signing certificate that is considered good enough.
72+
*/
73+
public static final Duration DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME = Duration.ofMinutes(5);
74+
75+
private final FulcioClient2 fulcioClient;
76+
private final FulcioVerifier2 fulcioVerifier;
77+
private final RekorClient2 rekorClient;
78+
private final RekorVerifier2 rekorVerifier;
79+
private final OidcClients oidcClients;
80+
private final Signer signer;
81+
private final Duration minSigningCertificateLifetime;
82+
83+
/** The code signing certificate from Fulcio. */
84+
@GuardedBy("lock")
85+
private @Nullable SigningCertificate signingCert;
86+
87+
/**
88+
* Representation {@link #signingCert} in PEM bytes format. This is used to avoid serializing the
89+
* certificate for each use.
90+
*/
91+
@GuardedBy("lock")
92+
private byte @Nullable [] signingCertPemBytes;
93+
94+
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
95+
96+
private KeylessSigner2(
97+
FulcioClient2 fulcioClient,
98+
FulcioVerifier2 fulcioVerifier,
99+
RekorClient2 rekorClient,
100+
RekorVerifier2 rekorVerifier,
101+
OidcClients oidcClients,
102+
Signer signer,
103+
Duration minSigningCertificateLifetime) {
104+
this.fulcioClient = fulcioClient;
105+
this.fulcioVerifier = fulcioVerifier;
106+
this.rekorClient = rekorClient;
107+
this.rekorVerifier = rekorVerifier;
108+
this.oidcClients = oidcClients;
109+
this.signer = signer;
110+
this.minSigningCertificateLifetime = minSigningCertificateLifetime;
111+
}
112+
113+
@Override
114+
public void close() {
115+
lock.writeLock().lock();
116+
try {
117+
signingCert = null;
118+
signingCertPemBytes = null;
119+
} finally {
120+
lock.writeLock().unlock();
121+
}
122+
}
123+
124+
@CheckReturnValue
125+
public static Builder builder() {
126+
return new Builder();
127+
}
128+
129+
public static class Builder {
130+
private SigstoreTufClient sigstoreTufClient;
131+
private OidcClients oidcClients;
132+
private Signer signer;
133+
private Duration minSigningCertificateLifetime = DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME;
134+
135+
@CanIgnoreReturnValue
136+
public Builder sigstoreTufClient(SigstoreTufClient sigstoreTufClient) {
137+
this.sigstoreTufClient = sigstoreTufClient;
138+
return this;
139+
}
140+
141+
@CanIgnoreReturnValue
142+
public Builder oidcClients(OidcClients oidcClients) {
143+
this.oidcClients = oidcClients;
144+
return this;
145+
}
146+
147+
@CanIgnoreReturnValue
148+
public Builder signer(Signer signer) {
149+
this.signer = signer;
150+
return this;
151+
}
152+
153+
/**
154+
* The instance of the {@link KeylessSigner2} will try to reuse a previously acquired
155+
* certificate if the expiration time on the certificate is more than {@code
156+
* minSigningCertificateLifetime} time away. Otherwise, it will make a new request (OIDC,
157+
* Fulcio) to obtain a new updated certificate to use for signing. Default {@code
158+
* minSigningCertificateLifetime} is {@link #DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME}".
159+
*
160+
* @param minSigningCertificateLifetime the minimum lifetime of the signing certificate before
161+
* renewal
162+
* @return this builder
163+
* @see <a href="https://docs.sigstore.dev/fulcio/overview/">Fulcio certificate validity</a>
164+
*/
165+
@CanIgnoreReturnValue
166+
public Builder minSigningCertificateLifetime(Duration minSigningCertificateLifetime) {
167+
this.minSigningCertificateLifetime = minSigningCertificateLifetime;
168+
return this;
169+
}
170+
171+
@CheckReturnValue
172+
public KeylessSigner2 build()
173+
throws CertificateException, IOException, NoSuchAlgorithmException, InvalidKeySpecException,
174+
InvalidKeyException, InvalidAlgorithmParameterException {
175+
Preconditions.checkNotNull(sigstoreTufClient, "sigstoreTufClient");
176+
sigstoreTufClient.update();
177+
var trustedRoot = sigstoreTufClient.getSigstoreTrustedRoot();
178+
var fulcioClient = FulcioClient2.builder().setCertificateAuthority(trustedRoot).build();
179+
var fulcioVerifier = FulcioVerifier2.newFulcioVerifier(trustedRoot);
180+
var rekorClient = RekorClient2.builder().setTransparencyLog(trustedRoot).build();
181+
var rekorVerifier = RekorVerifier2.newRekorVerifier(trustedRoot);
182+
return new KeylessSigner2(
183+
fulcioClient,
184+
fulcioVerifier,
185+
rekorClient,
186+
rekorVerifier,
187+
oidcClients,
188+
signer,
189+
minSigningCertificateLifetime);
190+
}
191+
192+
/**
193+
* Initialize a builder with the sigstore public good instance tuf root and oidc targets with
194+
* ecdsa signing.
195+
*/
196+
@CanIgnoreReturnValue
197+
public Builder sigstorePublicDefaults() throws IOException, NoSuchAlgorithmException {
198+
sigstoreTufClient = SigstoreTufClient.builder().usePublicGoodInstance().build();
199+
oidcClients(OidcClients.DEFAULTS);
200+
signer(Signers.newEcdsaSigner());
201+
minSigningCertificateLifetime(DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME);
202+
return this;
203+
}
204+
205+
/**
206+
* Initialize a builder with the sigstore staging instance tuf root and oidc targets with ecdsa
207+
* signing.
208+
*/
209+
@CanIgnoreReturnValue
210+
public Builder sigstoreStagingDefaults() throws IOException, NoSuchAlgorithmException {
211+
sigstoreTufClient = SigstoreTufClient.builder().useStagingInstance().build();
212+
oidcClients(OidcClients.STAGING_DEFAULTS);
213+
signer(Signers.newEcdsaSigner());
214+
minSigningCertificateLifetime(DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME);
215+
return this;
216+
}
217+
}
218+
219+
/**
220+
* Sign one or more artifact digests using the keyless signing workflow. The oidc/fulcio dance to
221+
* obtain a signing certificate will only occur once. The same ephemeral private key will be used
222+
* to sign all artifacts. This method will renew certificates as they expire.
223+
*
224+
* @param artifactDigests sha256 digests of the artifacts to sign.
225+
* @return a list of keyless singing results.
226+
*/
227+
@CheckReturnValue
228+
public List<KeylessSignature> sign(List<byte[]> artifactDigests)
229+
throws OidcException, NoSuchAlgorithmException, SignatureException, InvalidKeyException,
230+
UnsupportedAlgorithmException, CertificateException, IOException,
231+
FulcioVerificationException, RekorVerificationException, InterruptedException,
232+
RekorParseException, InvalidKeySpecException {
233+
234+
if (artifactDigests.size() == 0) {
235+
throw new IllegalArgumentException("Require one or more digests");
236+
}
237+
238+
var result = ImmutableList.<KeylessSignature>builder();
239+
240+
for (var artifactDigest : artifactDigests) {
241+
var signature = signer.signDigest(artifactDigest);
242+
243+
// Technically speaking, it is unlikely the certificate will expire between signing artifacts
244+
// However, files might be large, and it might take time to talk to Rekor
245+
// so we check the certificate expiration here.
246+
renewSigningCertificate();
247+
SigningCertificate signingCert;
248+
byte[] signingCertPemBytes;
249+
lock.readLock().lock();
250+
try {
251+
signingCert = this.signingCert;
252+
signingCertPemBytes = this.signingCertPemBytes;
253+
if (signingCert == null) {
254+
throw new IllegalStateException("Signing certificate is null");
255+
}
256+
} finally {
257+
lock.readLock().unlock();
258+
}
259+
260+
var rekorRequest =
261+
HashedRekordRequest.newHashedRekordRequest(
262+
artifactDigest, signingCertPemBytes, signature);
263+
var rekorResponse = rekorClient.putEntry(rekorRequest);
264+
rekorVerifier.verifyEntry(rekorResponse.getEntry());
265+
266+
result.add(
267+
ImmutableKeylessSignature.builder()
268+
.digest(artifactDigest)
269+
.certPath(signingCert.getCertPath())
270+
.signature(signature)
271+
.entry(rekorResponse.getEntry())
272+
.build());
273+
}
274+
return result.build();
275+
}
276+
277+
private void renewSigningCertificate()
278+
throws InterruptedException, CertificateException, IOException, UnsupportedAlgorithmException,
279+
NoSuchAlgorithmException, InvalidKeyException, SignatureException,
280+
FulcioVerificationException, OidcException {
281+
// Check if the certificate is still valid
282+
lock.readLock().lock();
283+
try {
284+
if (signingCert != null) {
285+
@SuppressWarnings("JavaUtilDate")
286+
long lifetimeLeft =
287+
signingCert.getLeafCertificate().getNotAfter().getTime() - System.currentTimeMillis();
288+
if (lifetimeLeft > minSigningCertificateLifetime.toMillis()) {
289+
// The current certificate is fine, reuse it
290+
return;
291+
}
292+
}
293+
} finally {
294+
lock.readLock().unlock();
295+
}
296+
297+
// Renew Fulcio certificate
298+
lock.writeLock().lock();
299+
try {
300+
signingCert = null;
301+
signingCertPemBytes = null;
302+
OidcToken tokenInfo = oidcClients.getIDToken();
303+
SigningCertificate signingCert =
304+
fulcioClient.signingCertificate(
305+
CertificateRequest.newCertificateRequest(
306+
signer.getPublicKey(),
307+
tokenInfo.getIdToken(),
308+
signer.sign(
309+
tokenInfo.getSubjectAlternativeName().getBytes(StandardCharsets.UTF_8))));
310+
fulcioVerifier.verifyCertChain(signingCert);
311+
// TODO: this signing workflow mandates SCTs, but fulcio itself doesn't, figure out a way to
312+
// allow that to be known
313+
fulcioVerifier.verifySct(signingCert);
314+
this.signingCert = signingCert;
315+
signingCertPemBytes = Certificates.toPemBytes(signingCert.getLeafCertificate());
316+
} finally {
317+
lock.writeLock().unlock();
318+
}
319+
}
320+
321+
/**
322+
* Convenience wrapper around {@link #sign(List)} to sign a single digest
323+
*
324+
* @param artifactDigest sha256 digest of the artifact to sign.
325+
* @return a keyless singing results.
326+
*/
327+
@CheckReturnValue
328+
public KeylessSignature sign(byte[] artifactDigest)
329+
throws FulcioVerificationException, RekorVerificationException, UnsupportedAlgorithmException,
330+
CertificateException, NoSuchAlgorithmException, SignatureException, IOException,
331+
OidcException, InvalidKeyException, InterruptedException, RekorParseException,
332+
InvalidKeySpecException {
333+
return sign(List.of(artifactDigest)).get(0);
334+
}
335+
336+
/**
337+
* Convenience wrapper around {@link #sign(List)} to accept files instead of digests
338+
*
339+
* @param artifacts list of the artifacts to sign.
340+
* @return a map of artifacts and their keyless singing results.
341+
*/
342+
@CheckReturnValue
343+
public Map<Path, KeylessSignature> signFiles(List<Path> artifacts)
344+
throws FulcioVerificationException, RekorVerificationException, UnsupportedAlgorithmException,
345+
CertificateException, NoSuchAlgorithmException, SignatureException, IOException,
346+
OidcException, InvalidKeyException, InterruptedException, RekorParseException,
347+
InvalidKeySpecException {
348+
if (artifacts.size() == 0) {
349+
throw new IllegalArgumentException("Require one or more paths");
350+
}
351+
var digests = new ArrayList<byte[]>(artifacts.size());
352+
for (var artifact : artifacts) {
353+
var artifactByteSource = com.google.common.io.Files.asByteSource(artifact.toFile());
354+
digests.add(artifactByteSource.hash(Hashing.sha256()).asBytes());
355+
}
356+
var signingResult = sign(digests);
357+
var result = ImmutableMap.<Path, KeylessSignature>builder();
358+
for (int i = 0; i < artifacts.size(); i++) {
359+
result.put(artifacts.get(i), signingResult.get(i));
360+
}
361+
return result.build();
362+
}
363+
364+
/**
365+
* Convenience wrapper around {@link #sign(List)} to accept a file instead of digests
366+
*
367+
* @param artifact the artifacts to sign.
368+
* @return a keyless singing results.
369+
*/
370+
@CheckReturnValue
371+
public KeylessSignature signFile(Path artifact)
372+
throws FulcioVerificationException, RekorVerificationException, UnsupportedAlgorithmException,
373+
CertificateException, NoSuchAlgorithmException, SignatureException, IOException,
374+
OidcException, InvalidKeyException, InterruptedException, RekorParseException,
375+
InvalidKeySpecException {
376+
return signFiles(List.of(artifact)).get(artifact);
377+
}
378+
}

0 commit comments

Comments
 (0)