Skip to content

Commit

Permalink
Merge pull request #546 from sigstore/signer-select-id
Browse files Browse the repository at this point in the history
Allow signers to specify allow list of oidc ids
  • Loading branch information
loosebazooka authored Oct 31, 2023
2 parents f6a9131 + 184a17c commit e2bd2a9
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public OidcToken getIDToken() throws OidcException {
var jws = JsonWebSignature.parse(new GsonFactory(), idToken);
return ImmutableOidcToken.builder()
.idToken(idToken)
.issuer(jws.getPayload().getIssuer())
.subjectAlternativeName(jws.getPayload().getSubject())
.build();
} catch (IOException e) {
Expand Down
98 changes: 72 additions & 26 deletions sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import dev.sigstore.rekor.client.HashedRekordRequest;
import dev.sigstore.rekor.client.RekorClient;
import dev.sigstore.rekor.client.RekorParseException;
import dev.sigstore.rekor.client.RekorResponse;
import dev.sigstore.rekor.client.RekorVerificationException;
import dev.sigstore.rekor.client.RekorVerifier;
import dev.sigstore.tuf.SigstoreTufClient;
Expand All @@ -51,6 +52,7 @@
import java.security.spec.InvalidKeySpecException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
Expand All @@ -77,6 +79,7 @@ public class KeylessSigner implements AutoCloseable {
private final RekorClient rekorClient;
private final RekorVerifier rekorVerifier;
private final OidcClients oidcClients;
private final List<OidcIdentity> oidcIdentities;
private final Signer signer;
private final Duration minSigningCertificateLifetime;

Expand All @@ -99,13 +102,15 @@ private KeylessSigner(
RekorClient rekorClient,
RekorVerifier rekorVerifier,
OidcClients oidcClients,
List<OidcIdentity> oidcIdentities,
Signer signer,
Duration minSigningCertificateLifetime) {
this.fulcioClient = fulcioClient;
this.fulcioVerifier = fulcioVerifier;
this.rekorClient = rekorClient;
this.rekorVerifier = rekorVerifier;
this.oidcClients = oidcClients;
this.oidcIdentities = oidcIdentities;
this.signer = signer;
this.minSigningCertificateLifetime = minSigningCertificateLifetime;
}
Expand All @@ -129,6 +134,7 @@ public static Builder builder() {
public static class Builder {
private SigstoreTufClient sigstoreTufClient;
private OidcClients oidcClients;
private List<OidcIdentity> oidcIdentities = Collections.emptyList();
private Signer signer;
private Duration minSigningCertificateLifetime = DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME;

Expand All @@ -144,6 +150,16 @@ public Builder oidcClients(OidcClients oidcClients) {
return this;
}

/**
* An allow list OIDC identities to be used during signing. If the OidcClients are misconfigured
* or pick up unexpected credentials, this should prevent signing from proceeding
*/
@CanIgnoreReturnValue
public Builder allowedOidcIdentities(List<OidcIdentity> oidcIdentities) {
this.oidcIdentities = ImmutableList.copyOf(oidcIdentities);
return this;
}

@CanIgnoreReturnValue
public Builder signer(Signer signer) {
this.signer = signer;
Expand Down Expand Up @@ -185,6 +201,7 @@ public KeylessSigner build()
rekorClient,
rekorVerifier,
oidcClients,
oidcIdentities,
signer,
minSigningCertificateLifetime);
}
Expand Down Expand Up @@ -225,11 +242,7 @@ public Builder sigstoreStagingDefaults() throws IOException, NoSuchAlgorithmExce
* @return a list of keyless singing results.
*/
@CheckReturnValue
public List<KeylessSignature> sign(List<byte[]> artifactDigests)
throws OidcException, NoSuchAlgorithmException, SignatureException, InvalidKeyException,
UnsupportedAlgorithmException, CertificateException, IOException,
FulcioVerificationException, RekorVerificationException, InterruptedException,
RekorParseException, InvalidKeySpecException {
public List<KeylessSignature> sign(List<byte[]> artifactDigests) throws KeylessSignerException {

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

for (var artifactDigest : artifactDigests) {
var signature = signer.signDigest(artifactDigest);
byte[] signature;
try {
signature = signer.signDigest(artifactDigest);
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException ex) {
throw new KeylessSignerException("Failed to sign artifact", ex);
}

// Technically speaking, it is unlikely the certificate will expire between signing artifacts
// However, files might be large, and it might take time to talk to Rekor
// so we check the certificate expiration here.
renewSigningCertificate();
try {
renewSigningCertificate();
} catch (FulcioVerificationException
| UnsupportedAlgorithmException
| OidcException
| IOException
| InterruptedException
| InvalidKeyException
| NoSuchAlgorithmException
| SignatureException
| CertificateException ex) {
throw new KeylessSignerException("Failed to obtain signing certificate", ex);
}

CertPath signingCert;
byte[] signingCertPemBytes;
lock.readLock().lock();
Expand All @@ -260,8 +291,19 @@ public List<KeylessSignature> sign(List<byte[]> artifactDigests)
var rekorRequest =
HashedRekordRequest.newHashedRekordRequest(
artifactDigest, signingCertPemBytes, signature);
var rekorResponse = rekorClient.putEntry(rekorRequest);
rekorVerifier.verifyEntry(rekorResponse.getEntry());

RekorResponse rekorResponse;
try {
rekorResponse = rekorClient.putEntry(rekorRequest);
} catch (RekorParseException | IOException ex) {
throw new KeylessSignerException("Failed to put entry in rekor", ex);
}

try {
rekorVerifier.verifyEntry(rekorResponse.getEntry());
} catch (RekorVerificationException ex) {
throw new KeylessSignerException("Failed to validate rekor response after signing", ex);
}

result.add(
KeylessSignature.builder()
Expand All @@ -277,7 +319,7 @@ public List<KeylessSignature> sign(List<byte[]> artifactDigests)
private void renewSigningCertificate()
throws InterruptedException, CertificateException, IOException, UnsupportedAlgorithmException,
NoSuchAlgorithmException, InvalidKeyException, SignatureException,
FulcioVerificationException, OidcException {
FulcioVerificationException, OidcException, KeylessSignerException {
// Check if the certificate is still valid
lock.readLock().lock();
try {
Expand All @@ -300,6 +342,18 @@ private void renewSigningCertificate()
signingCert = null;
signingCertPemBytes = null;
OidcToken tokenInfo = oidcClients.getIDToken();

// check if we have an allow list and if so, ensure the provided token is in there
if (!oidcIdentities.isEmpty()) {
var obtainedToken = OidcIdentity.from(tokenInfo);
if (!oidcIdentities.contains(OidcIdentity.from(tokenInfo))) {
throw new KeylessSignerException(
"Obtained Oidc Token "
+ obtainedToken
+ " does not match any identities in allow list");
}
}

CertPath signingCert =
fulcioClient.signingCertificate(
CertificateRequest.newCertificateRequest(
Expand All @@ -324,11 +378,7 @@ private void renewSigningCertificate()
* @return a keyless singing results.
*/
@CheckReturnValue
public KeylessSignature sign(byte[] artifactDigest)
throws FulcioVerificationException, RekorVerificationException, UnsupportedAlgorithmException,
CertificateException, NoSuchAlgorithmException, SignatureException, IOException,
OidcException, InvalidKeyException, InterruptedException, RekorParseException,
InvalidKeySpecException {
public KeylessSignature sign(byte[] artifactDigest) throws KeylessSignerException {
return sign(List.of(artifactDigest)).get(0);
}

Expand All @@ -339,18 +389,18 @@ public KeylessSignature sign(byte[] artifactDigest)
* @return a map of artifacts and their keyless singing results.
*/
@CheckReturnValue
public Map<Path, KeylessSignature> signFiles(List<Path> artifacts)
throws FulcioVerificationException, RekorVerificationException, UnsupportedAlgorithmException,
CertificateException, NoSuchAlgorithmException, SignatureException, IOException,
OidcException, InvalidKeyException, InterruptedException, RekorParseException,
InvalidKeySpecException {
public Map<Path, KeylessSignature> signFiles(List<Path> artifacts) throws KeylessSignerException {
if (artifacts.size() == 0) {
throw new IllegalArgumentException("Require one or more paths");
}
var digests = new ArrayList<byte[]>(artifacts.size());
for (var artifact : artifacts) {
var artifactByteSource = com.google.common.io.Files.asByteSource(artifact.toFile());
digests.add(artifactByteSource.hash(Hashing.sha256()).asBytes());
try {
digests.add(artifactByteSource.hash(Hashing.sha256()).asBytes());
} catch (IOException ex) {
throw new KeylessSignerException("Failed to hash artifact " + artifact);
}
}
var signingResult = sign(digests);
var result = ImmutableMap.<Path, KeylessSignature>builder();
Expand All @@ -367,11 +417,7 @@ public Map<Path, KeylessSignature> signFiles(List<Path> artifacts)
* @return a keyless singing results.
*/
@CheckReturnValue
public KeylessSignature signFile(Path artifact)
throws FulcioVerificationException, RekorVerificationException, UnsupportedAlgorithmException,
CertificateException, NoSuchAlgorithmException, SignatureException, IOException,
OidcException, InvalidKeyException, InterruptedException, RekorParseException,
InvalidKeySpecException {
public KeylessSignature signFile(Path artifact) throws KeylessSignerException {
return signFiles(List.of(artifact)).get(artifact);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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;

public class KeylessSignerException extends Exception {
public KeylessSignerException(String message) {
super(message);
}

public KeylessSignerException(String message, Throwable cause) {
super(message, cause);
}

public KeylessSignerException(Throwable cause) {
super(cause);
}
}
40 changes: 40 additions & 0 deletions sigstore-java/src/main/java/dev/sigstore/OidcIdentity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2023 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;

import dev.sigstore.oidc.client.OidcToken;
import org.immutables.value.Value.Immutable;

@Immutable
public interface OidcIdentity {

static OidcIdentity of(String identity, String issuer) {
return ImmutableOidcIdentity.builder().identity(identity).issuer(issuer).build();
}

static OidcIdentity from(OidcToken oidcToken) {
return ImmutableOidcIdentity.builder()
.identity(oidcToken.getSubjectAlternativeName())
.issuer(oidcToken.getIssuer())
.build();
}

/** The user or machineId. */
String getIdentity();

/** The oidc issuing authority */
String getIssuer();
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ public OidcToken getIDToken() throws OidcException {
var jws = JsonWebSignature.parse(new GsonFactory(), idToken);
return ImmutableOidcToken.builder()
.idToken(idToken)
.issuer(jws.getPayload().getIssuer())
.subjectAlternativeName(jws.getPayload().getSubject())
.build();
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public interface OidcToken {
/** The subject or email claim from the token to include in the SAN on the certificate. */
String getSubjectAlternativeName();

/** The issuer of the id token. */
String getIssuer();

/** The full oidc token obtained from the provider. */
String getIdToken();
}
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ public OidcToken getIDToken() throws OidcException {
return ImmutableOidcToken.builder()
.subjectAlternativeName(emailFromIDToken)
.idToken(idTokenString)
.issuer(issuer)
.build();
}

Expand Down
53 changes: 53 additions & 0 deletions sigstore-java/src/test/java/dev/sigstore/KeylessSignerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,23 @@
*/
package dev.sigstore;

import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.json.webtoken.JsonWebSignature;
import com.google.common.hash.Hashing;
import dev.sigstore.oidc.client.GithubActionsOidcClient;
import dev.sigstore.testing.matchers.ByteArrayListMatcher;
import dev.sigstore.testkit.annotations.EnabledIfOidcExists;
import dev.sigstore.testkit.annotations.OidcProviderType;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import org.bouncycastle.util.encoders.Hex;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -92,4 +100,49 @@ public void sign_files() throws Exception {
public void sign_digest() throws Exception {
Assertions.assertEquals(signingResults.get(0), signer.sign(artifactHashes.get(0)));
}

@Test
@EnabledIfOidcExists(provider = OidcProviderType.GITHUB)
// this test will only pass on the github.com/sigstore/sigstore-java repository
public void sign_failGithubOidcCheck() throws Exception {
var signer =
KeylessSigner.builder()
.sigstorePublicDefaults()
.allowedOidcIdentities(List.of(OidcIdentity.of("[email protected]", "goose.com")))
.build();
var ex =
Assertions.assertThrows(
KeylessSignerException.class,
() ->
signer.sign(
Hex.decode(
"10f26b52447ec6427c178cadb522ce649922ee67f6d59709e45700aa5df68b30")));
MatcherAssert.assertThat(ex.getMessage(), CoreMatchers.startsWith("Obtained Oidc Token"));
MatcherAssert.assertThat(
ex.getMessage(), CoreMatchers.endsWith("does not match any identities in allow list"));
}

@Test
@EnabledIfOidcExists(provider = OidcProviderType.GITHUB)
// this test will only pass on the github.com/sigstore/sigstore-java repository
public void sign_passGithubOidcCheck() throws Exception {
// silly way to get the right oidc identity to make sure our simple matcher works
var jws =
JsonWebSignature.parse(
new GsonFactory(), GithubActionsOidcClient.builder().build().getIDToken().getIdToken());
var expectedGithubSubject = jws.getPayload().getSubject();
var signer =
KeylessSigner.builder()
.sigstorePublicDefaults()
.allowedOidcIdentities(
List.of(
OidcIdentity.of(
expectedGithubSubject, "https://token.actions.githubusercontent.com"),
OidcIdentity.of("[email protected]", "https://accounts.other.com")))
.build();
Assertions.assertDoesNotThrow(
() ->
signer.sign(
Hex.decode("10f26b52447ec6427c178cadb522ce649922ee67f6d59709e45700aa5df68b30")));
}
}

0 comments on commit e2bd2a9

Please sign in to comment.