Skip to content

Commit e2bd2a9

Browse files
authored
Merge pull request #546 from sigstore/signer-select-id
Allow signers to specify allow list of oidc ids
2 parents f6a9131 + 184a17c commit e2bd2a9

File tree

8 files changed

+201
-26
lines changed

8 files changed

+201
-26
lines changed

sigstore-cli/src/main/java/dev/sigstore/cli/TokenStringOidcClient.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public OidcToken getIDToken() throws OidcException {
4242
var jws = JsonWebSignature.parse(new GsonFactory(), idToken);
4343
return ImmutableOidcToken.builder()
4444
.idToken(idToken)
45+
.issuer(jws.getPayload().getIssuer())
4546
.subjectAlternativeName(jws.getPayload().getSubject())
4647
.build();
4748
} catch (IOException e) {

sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import dev.sigstore.rekor.client.HashedRekordRequest;
3737
import dev.sigstore.rekor.client.RekorClient;
3838
import dev.sigstore.rekor.client.RekorParseException;
39+
import dev.sigstore.rekor.client.RekorResponse;
3940
import dev.sigstore.rekor.client.RekorVerificationException;
4041
import dev.sigstore.rekor.client.RekorVerifier;
4142
import dev.sigstore.tuf.SigstoreTufClient;
@@ -51,6 +52,7 @@
5152
import java.security.spec.InvalidKeySpecException;
5253
import java.time.Duration;
5354
import java.util.ArrayList;
55+
import java.util.Collections;
5456
import java.util.List;
5557
import java.util.Map;
5658
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -77,6 +79,7 @@ public class KeylessSigner implements AutoCloseable {
7779
private final RekorClient rekorClient;
7880
private final RekorVerifier rekorVerifier;
7981
private final OidcClients oidcClients;
82+
private final List<OidcIdentity> oidcIdentities;
8083
private final Signer signer;
8184
private final Duration minSigningCertificateLifetime;
8285

@@ -99,13 +102,15 @@ private KeylessSigner(
99102
RekorClient rekorClient,
100103
RekorVerifier rekorVerifier,
101104
OidcClients oidcClients,
105+
List<OidcIdentity> oidcIdentities,
102106
Signer signer,
103107
Duration minSigningCertificateLifetime) {
104108
this.fulcioClient = fulcioClient;
105109
this.fulcioVerifier = fulcioVerifier;
106110
this.rekorClient = rekorClient;
107111
this.rekorVerifier = rekorVerifier;
108112
this.oidcClients = oidcClients;
113+
this.oidcIdentities = oidcIdentities;
109114
this.signer = signer;
110115
this.minSigningCertificateLifetime = minSigningCertificateLifetime;
111116
}
@@ -129,6 +134,7 @@ public static Builder builder() {
129134
public static class Builder {
130135
private SigstoreTufClient sigstoreTufClient;
131136
private OidcClients oidcClients;
137+
private List<OidcIdentity> oidcIdentities = Collections.emptyList();
132138
private Signer signer;
133139
private Duration minSigningCertificateLifetime = DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME;
134140

@@ -144,6 +150,16 @@ public Builder oidcClients(OidcClients oidcClients) {
144150
return this;
145151
}
146152

153+
/**
154+
* An allow list OIDC identities to be used during signing. If the OidcClients are misconfigured
155+
* or pick up unexpected credentials, this should prevent signing from proceeding
156+
*/
157+
@CanIgnoreReturnValue
158+
public Builder allowedOidcIdentities(List<OidcIdentity> oidcIdentities) {
159+
this.oidcIdentities = ImmutableList.copyOf(oidcIdentities);
160+
return this;
161+
}
162+
147163
@CanIgnoreReturnValue
148164
public Builder signer(Signer signer) {
149165
this.signer = signer;
@@ -185,6 +201,7 @@ public KeylessSigner build()
185201
rekorClient,
186202
rekorVerifier,
187203
oidcClients,
204+
oidcIdentities,
188205
signer,
189206
minSigningCertificateLifetime);
190207
}
@@ -225,11 +242,7 @@ public Builder sigstoreStagingDefaults() throws IOException, NoSuchAlgorithmExce
225242
* @return a list of keyless singing results.
226243
*/
227244
@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 {
245+
public List<KeylessSignature> sign(List<byte[]> artifactDigests) throws KeylessSignerException {
233246

234247
if (artifactDigests.size() == 0) {
235248
throw new IllegalArgumentException("Require one or more digests");
@@ -238,12 +251,30 @@ public List<KeylessSignature> sign(List<byte[]> artifactDigests)
238251
var result = ImmutableList.<KeylessSignature>builder();
239252

240253
for (var artifactDigest : artifactDigests) {
241-
var signature = signer.signDigest(artifactDigest);
254+
byte[] signature;
255+
try {
256+
signature = signer.signDigest(artifactDigest);
257+
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException ex) {
258+
throw new KeylessSignerException("Failed to sign artifact", ex);
259+
}
242260

243261
// Technically speaking, it is unlikely the certificate will expire between signing artifacts
244262
// However, files might be large, and it might take time to talk to Rekor
245263
// so we check the certificate expiration here.
246-
renewSigningCertificate();
264+
try {
265+
renewSigningCertificate();
266+
} catch (FulcioVerificationException
267+
| UnsupportedAlgorithmException
268+
| OidcException
269+
| IOException
270+
| InterruptedException
271+
| InvalidKeyException
272+
| NoSuchAlgorithmException
273+
| SignatureException
274+
| CertificateException ex) {
275+
throw new KeylessSignerException("Failed to obtain signing certificate", ex);
276+
}
277+
247278
CertPath signingCert;
248279
byte[] signingCertPemBytes;
249280
lock.readLock().lock();
@@ -260,8 +291,19 @@ public List<KeylessSignature> sign(List<byte[]> artifactDigests)
260291
var rekorRequest =
261292
HashedRekordRequest.newHashedRekordRequest(
262293
artifactDigest, signingCertPemBytes, signature);
263-
var rekorResponse = rekorClient.putEntry(rekorRequest);
264-
rekorVerifier.verifyEntry(rekorResponse.getEntry());
294+
295+
RekorResponse rekorResponse;
296+
try {
297+
rekorResponse = rekorClient.putEntry(rekorRequest);
298+
} catch (RekorParseException | IOException ex) {
299+
throw new KeylessSignerException("Failed to put entry in rekor", ex);
300+
}
301+
302+
try {
303+
rekorVerifier.verifyEntry(rekorResponse.getEntry());
304+
} catch (RekorVerificationException ex) {
305+
throw new KeylessSignerException("Failed to validate rekor response after signing", ex);
306+
}
265307

266308
result.add(
267309
KeylessSignature.builder()
@@ -277,7 +319,7 @@ public List<KeylessSignature> sign(List<byte[]> artifactDigests)
277319
private void renewSigningCertificate()
278320
throws InterruptedException, CertificateException, IOException, UnsupportedAlgorithmException,
279321
NoSuchAlgorithmException, InvalidKeyException, SignatureException,
280-
FulcioVerificationException, OidcException {
322+
FulcioVerificationException, OidcException, KeylessSignerException {
281323
// Check if the certificate is still valid
282324
lock.readLock().lock();
283325
try {
@@ -300,6 +342,18 @@ private void renewSigningCertificate()
300342
signingCert = null;
301343
signingCertPemBytes = null;
302344
OidcToken tokenInfo = oidcClients.getIDToken();
345+
346+
// check if we have an allow list and if so, ensure the provided token is in there
347+
if (!oidcIdentities.isEmpty()) {
348+
var obtainedToken = OidcIdentity.from(tokenInfo);
349+
if (!oidcIdentities.contains(OidcIdentity.from(tokenInfo))) {
350+
throw new KeylessSignerException(
351+
"Obtained Oidc Token "
352+
+ obtainedToken
353+
+ " does not match any identities in allow list");
354+
}
355+
}
356+
303357
CertPath signingCert =
304358
fulcioClient.signingCertificate(
305359
CertificateRequest.newCertificateRequest(
@@ -324,11 +378,7 @@ private void renewSigningCertificate()
324378
* @return a keyless singing results.
325379
*/
326380
@CheckReturnValue
327-
public KeylessSignature sign(byte[] artifactDigest)
328-
throws FulcioVerificationException, RekorVerificationException, UnsupportedAlgorithmException,
329-
CertificateException, NoSuchAlgorithmException, SignatureException, IOException,
330-
OidcException, InvalidKeyException, InterruptedException, RekorParseException,
331-
InvalidKeySpecException {
381+
public KeylessSignature sign(byte[] artifactDigest) throws KeylessSignerException {
332382
return sign(List.of(artifactDigest)).get(0);
333383
}
334384

@@ -339,18 +389,18 @@ public KeylessSignature sign(byte[] artifactDigest)
339389
* @return a map of artifacts and their keyless singing results.
340390
*/
341391
@CheckReturnValue
342-
public Map<Path, KeylessSignature> signFiles(List<Path> artifacts)
343-
throws FulcioVerificationException, RekorVerificationException, UnsupportedAlgorithmException,
344-
CertificateException, NoSuchAlgorithmException, SignatureException, IOException,
345-
OidcException, InvalidKeyException, InterruptedException, RekorParseException,
346-
InvalidKeySpecException {
392+
public Map<Path, KeylessSignature> signFiles(List<Path> artifacts) throws KeylessSignerException {
347393
if (artifacts.size() == 0) {
348394
throw new IllegalArgumentException("Require one or more paths");
349395
}
350396
var digests = new ArrayList<byte[]>(artifacts.size());
351397
for (var artifact : artifacts) {
352398
var artifactByteSource = com.google.common.io.Files.asByteSource(artifact.toFile());
353-
digests.add(artifactByteSource.hash(Hashing.sha256()).asBytes());
399+
try {
400+
digests.add(artifactByteSource.hash(Hashing.sha256()).asBytes());
401+
} catch (IOException ex) {
402+
throw new KeylessSignerException("Failed to hash artifact " + artifact);
403+
}
354404
}
355405
var signingResult = sign(digests);
356406
var result = ImmutableMap.<Path, KeylessSignature>builder();
@@ -367,11 +417,7 @@ public Map<Path, KeylessSignature> signFiles(List<Path> artifacts)
367417
* @return a keyless singing results.
368418
*/
369419
@CheckReturnValue
370-
public KeylessSignature signFile(Path artifact)
371-
throws FulcioVerificationException, RekorVerificationException, UnsupportedAlgorithmException,
372-
CertificateException, NoSuchAlgorithmException, SignatureException, IOException,
373-
OidcException, InvalidKeyException, InterruptedException, RekorParseException,
374-
InvalidKeySpecException {
420+
public KeylessSignature signFile(Path artifact) throws KeylessSignerException {
375421
return signFiles(List.of(artifact)).get(artifact);
376422
}
377423
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
public class KeylessSignerException extends Exception {
19+
public KeylessSignerException(String message) {
20+
super(message);
21+
}
22+
23+
public KeylessSignerException(String message, Throwable cause) {
24+
super(message, cause);
25+
}
26+
27+
public KeylessSignerException(Throwable cause) {
28+
super(cause);
29+
}
30+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2023 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 dev.sigstore.oidc.client.OidcToken;
19+
import org.immutables.value.Value.Immutable;
20+
21+
@Immutable
22+
public interface OidcIdentity {
23+
24+
static OidcIdentity of(String identity, String issuer) {
25+
return ImmutableOidcIdentity.builder().identity(identity).issuer(issuer).build();
26+
}
27+
28+
static OidcIdentity from(OidcToken oidcToken) {
29+
return ImmutableOidcIdentity.builder()
30+
.identity(oidcToken.getSubjectAlternativeName())
31+
.issuer(oidcToken.getIssuer())
32+
.build();
33+
}
34+
35+
/** The user or machineId. */
36+
String getIdentity();
37+
38+
/** The oidc issuing authority */
39+
String getIssuer();
40+
}

sigstore-java/src/main/java/dev/sigstore/oidc/client/GithubActionsOidcClient.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ public OidcToken getIDToken() throws OidcException {
109109
var jws = JsonWebSignature.parse(new GsonFactory(), idToken);
110110
return ImmutableOidcToken.builder()
111111
.idToken(idToken)
112+
.issuer(jws.getPayload().getIssuer())
112113
.subjectAlternativeName(jws.getPayload().getSubject())
113114
.build();
114115
} catch (IOException e) {

sigstore-java/src/main/java/dev/sigstore/oidc/client/OidcToken.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public interface OidcToken {
2323
/** The subject or email claim from the token to include in the SAN on the certificate. */
2424
String getSubjectAlternativeName();
2525

26+
/** The issuer of the id token. */
27+
String getIssuer();
28+
2629
/** The full oidc token obtained from the provider. */
2730
String getIdToken();
2831
}

sigstore-java/src/main/java/dev/sigstore/oidc/client/WebOidcClient.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ public OidcToken getIDToken() throws OidcException {
193193
return ImmutableOidcToken.builder()
194194
.subjectAlternativeName(emailFromIDToken)
195195
.idToken(idTokenString)
196+
.issuer(issuer)
196197
.build();
197198
}
198199

sigstore-java/src/test/java/dev/sigstore/KeylessSignerTest.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,23 @@
1515
*/
1616
package dev.sigstore;
1717

18+
import com.google.api.client.json.gson.GsonFactory;
19+
import com.google.api.client.json.webtoken.JsonWebSignature;
1820
import com.google.common.hash.Hashing;
21+
import dev.sigstore.oidc.client.GithubActionsOidcClient;
1922
import dev.sigstore.testing.matchers.ByteArrayListMatcher;
23+
import dev.sigstore.testkit.annotations.EnabledIfOidcExists;
24+
import dev.sigstore.testkit.annotations.OidcProviderType;
2025
import java.nio.charset.StandardCharsets;
2126
import java.nio.file.Files;
2227
import java.nio.file.Path;
2328
import java.util.ArrayList;
2429
import java.util.HashMap;
2530
import java.util.List;
2631
import java.util.UUID;
32+
import org.bouncycastle.util.encoders.Hex;
33+
import org.hamcrest.CoreMatchers;
34+
import org.hamcrest.MatcherAssert;
2735
import org.junit.jupiter.api.Assertions;
2836
import org.junit.jupiter.api.BeforeAll;
2937
import org.junit.jupiter.api.Test;
@@ -92,4 +100,49 @@ public void sign_files() throws Exception {
92100
public void sign_digest() throws Exception {
93101
Assertions.assertEquals(signingResults.get(0), signer.sign(artifactHashes.get(0)));
94102
}
103+
104+
@Test
105+
@EnabledIfOidcExists(provider = OidcProviderType.GITHUB)
106+
// this test will only pass on the github.com/sigstore/sigstore-java repository
107+
public void sign_failGithubOidcCheck() throws Exception {
108+
var signer =
109+
KeylessSigner.builder()
110+
.sigstorePublicDefaults()
111+
.allowedOidcIdentities(List.of(OidcIdentity.of("[email protected]", "goose.com")))
112+
.build();
113+
var ex =
114+
Assertions.assertThrows(
115+
KeylessSignerException.class,
116+
() ->
117+
signer.sign(
118+
Hex.decode(
119+
"10f26b52447ec6427c178cadb522ce649922ee67f6d59709e45700aa5df68b30")));
120+
MatcherAssert.assertThat(ex.getMessage(), CoreMatchers.startsWith("Obtained Oidc Token"));
121+
MatcherAssert.assertThat(
122+
ex.getMessage(), CoreMatchers.endsWith("does not match any identities in allow list"));
123+
}
124+
125+
@Test
126+
@EnabledIfOidcExists(provider = OidcProviderType.GITHUB)
127+
// this test will only pass on the github.com/sigstore/sigstore-java repository
128+
public void sign_passGithubOidcCheck() throws Exception {
129+
// silly way to get the right oidc identity to make sure our simple matcher works
130+
var jws =
131+
JsonWebSignature.parse(
132+
new GsonFactory(), GithubActionsOidcClient.builder().build().getIDToken().getIdToken());
133+
var expectedGithubSubject = jws.getPayload().getSubject();
134+
var signer =
135+
KeylessSigner.builder()
136+
.sigstorePublicDefaults()
137+
.allowedOidcIdentities(
138+
List.of(
139+
OidcIdentity.of(
140+
expectedGithubSubject, "https://token.actions.githubusercontent.com"),
141+
OidcIdentity.of("[email protected]", "https://accounts.other.com")))
142+
.build();
143+
Assertions.assertDoesNotThrow(
144+
() ->
145+
signer.sign(
146+
Hex.decode("10f26b52447ec6427c178cadb522ce649922ee67f6d59709e45700aa5df68b30")));
147+
}
95148
}

0 commit comments

Comments
 (0)