Skip to content

Commit

Permalink
Update protobuf-spec to 0.3.0
Browse files Browse the repository at this point in the history
Generated sigstore bundles are now 0.3

Bundle parser can read 0.1, 0.2 and 0.3 bundles

Signed-off-by: Appu Goundan <[email protected]>
  • Loading branch information
loosebazooka committed Feb 22, 2024
1 parent fb4df60 commit 6314f67
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 84 deletions.
2 changes: 1 addition & 1 deletion sigstore-java/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ dependencies {

implementation("io.github.erdtman:java-json-canonicalization:1.1")

implementation("dev.sigstore:protobuf-specs:0.2.1") {
implementation("dev.sigstore:protobuf-specs:0.3.0") {
because("It generates Sigstore Bundle file")
}
implementation(platform("com.google.protobuf:protobuf-bom:3.25.2"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ interface VerificationOptions {
* rekor entry in a {@link KeylessSignature}. Verifier may still connect to Rekor to obtain an
* entry if no {@link KeylessSignature#getEntry()} is empty.
*/
boolean alwaysUseRemoteRekorEntry();
@Default
default boolean alwaysUseRemoteRekorEntry() {
return false;
}

List<CertificateIdentity> getCertificateIdentities();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@
*/
package dev.sigstore.bundle;

import com.google.common.collect.Iterables;
import com.google.protobuf.ByteString;
import com.google.protobuf.util.JsonFormat;
import dev.sigstore.KeylessSignature;
import dev.sigstore.encryption.certificates.Certificates;
import dev.sigstore.proto.ProtoMutators;
import dev.sigstore.proto.bundle.v1.Bundle;
import dev.sigstore.proto.bundle.v1.VerificationMaterial;
import dev.sigstore.proto.common.v1.HashAlgorithm;
import dev.sigstore.proto.common.v1.HashOutput;
import dev.sigstore.proto.common.v1.LogId;
import dev.sigstore.proto.common.v1.MessageSignature;
import dev.sigstore.proto.common.v1.X509Certificate;
import dev.sigstore.proto.common.v1.X509CertificateChain;
import dev.sigstore.proto.rekor.v1.Checkpoint;
import dev.sigstore.proto.rekor.v1.InclusionPromise;
import dev.sigstore.proto.rekor.v1.InclusionProof;
Expand All @@ -39,13 +39,11 @@
import java.io.IOException;
import java.io.Reader;
import java.security.cert.CertPath;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.bouncycastle.util.encoders.Hex;

Expand All @@ -59,6 +57,12 @@
class BundleFactoryInternal {
static final JsonFormat.Printer JSON_PRINTER = JsonFormat.printer();

private static final String BUNDLE_V_0_1 = "application/vnd.dev.sigstore.bundle+json;version=0.1";
private static final String BUNDLE_V_0_2 = "application/vnd.dev.sigstore.bundle+json;version=0.2";
private static final String BUNDLE_V_0_3 = "application/vnd.dev.sigstore.bundle+json;version=0.3";
private static final List<String> SUPPORTED_MEDIA_TYPES =
List.of(BUNDLE_V_0_1, BUNDLE_V_0_2, BUNDLE_V_0_3);

/**
* Generates Sigstore Bundle Builder from {@link KeylessSignature}. This might be useful in case
* you want to add additional information to the bundle.
Expand All @@ -72,7 +76,7 @@ static Bundle.Builder createBundleBuilder(KeylessSignature signingResult) {
"keyless signature must have artifact digest when serializing to bundle");
}
return Bundle.newBuilder()
.setMediaType("application/vnd.dev.sigstore.bundle+json;version=0.2")
.setMediaType(BUNDLE_V_0_3)
.setVerificationMaterial(buildVerificationMaterial(signingResult))
.setMessageSignature(
MessageSignature.newBuilder()
Expand All @@ -85,29 +89,18 @@ static Bundle.Builder createBundleBuilder(KeylessSignature signingResult) {

private static VerificationMaterial.Builder buildVerificationMaterial(
KeylessSignature signingResult) {
var builder =
VerificationMaterial.newBuilder()
.setX509CertificateChain(
X509CertificateChain.newBuilder()
.addAllCertificates(
signingResult.getCertPath().getCertificates().stream()
.map(
c -> {
byte[] encoded;
try {
encoded = c.getEncoded();
} catch (CertificateEncodingException e) {
throw new IllegalArgumentException(
"Cannot encode certificate " + c, e);
}
return X509Certificate.newBuilder()
.setRawBytes(ByteString.copyFrom(encoded))
.build();
})
.collect(Collectors.toList())));
if (signingResult.getEntry().isPresent()) {
builder.addTlogEntries(buildTlogEntries(signingResult.getEntry().get()));
X509Certificate cert;
var javaCert = Iterables.getLast(signingResult.getCertPath().getCertificates());
try {
cert = ProtoMutators.fromCert((java.security.cert.X509Certificate) javaCert);
} catch (CertificateEncodingException ce) {
throw new IllegalArgumentException("Cannot encode certificate " + javaCert, ce);
}
var builder = VerificationMaterial.newBuilder().setCertificate(cert);
if (signingResult.getEntry().isEmpty()) {
throw new IllegalArgumentException("A log entry must be present in the signing result");
}
builder.addTlogEntries(buildTlogEntries(signingResult.getEntry().get()));
return builder;
}

Expand Down Expand Up @@ -135,10 +128,13 @@ private static TransparencyLogEntry.Builder buildTlogEntries(RekorEntry entry) {
private static void addInclusionProof(
TransparencyLogEntry.Builder transparencyLogEntry, RekorEntry entry) {
RekorEntry.InclusionProof inclusionProof =
entry.getVerification().getInclusionProof().orElse(null);
if (inclusionProof == null) {
return;
}
entry
.getVerification()
.getInclusionProof()
.orElseThrow(
() ->
new IllegalArgumentException(
"An inclusion proof must be present in the log entry in the signing result"));
transparencyLogEntry.setInclusionProof(
InclusionProof.newBuilder()
.setLogIndex(inclusionProof.getLogIndex())
Expand All @@ -156,51 +152,47 @@ static KeylessSignature readBundle(Reader jsonReader) throws BundleParseExceptio
try {
JsonFormat.parser().merge(jsonReader, bundleBuilder);
} catch (IOException ioe) {
throw new BundleParseException("Could not read bundle json", ioe);
throw new BundleParseException("Could not process bundle json", ioe);
}
Bundle bundle = bundleBuilder.build();

// TODO: only allow v0.2 bundles at some point, we will only be producing v0.2 bundles
// TODO: in our GA release.
// var supportedMediaType = "application/vnd.dev.sigstore.bundle+json;version=0.2";
// if (!supportedMediaType.equals(bundle.getMediaType())) {
// throw new BundleParseException(
// "Unsupported media type '"
// + bundle.getMediaType()
// + "', only '"
// + supportedMediaType
// + "' is supported");
// }
Bundle bundle = bundleBuilder.build();
if (!SUPPORTED_MEDIA_TYPES.contains(bundle.getMediaType())) {
throw new BundleParseException("Unsupported bundle media type: " + bundle.getMediaType());
}

if (bundle.getVerificationMaterial().getTlogEntriesCount() == 0) {
throw new BundleParseException("Could not find any tlog entries in bundle json");
}
var bundleEntry = bundle.getVerificationMaterial().getTlogEntries(0);
RekorEntry.InclusionProof inclusionProof = null;
if (!bundleEntry.hasInclusionProof()) {
throw new BundleParseException("Could not find an inclusion proof");
if (!bundle.getMediaType().equals(BUNDLE_V_0_1)) {
throw new BundleParseException("Could not find an inclusion proof");
}
} else {
var bundleInclusionProof = bundleEntry.getInclusionProof();

inclusionProof =
ImmutableInclusionProof.builder()
.logIndex(bundleInclusionProof.getLogIndex())
.rootHash(Hex.toHexString(bundleInclusionProof.getRootHash().toByteArray()))
.treeSize(bundleInclusionProof.getTreeSize())
.checkpoint(bundleInclusionProof.getCheckpoint().getEnvelope())
.addAllHashes(
bundleInclusionProof.getHashesList().stream()
.map(ByteString::toByteArray)
.map(Hex::toHexString)
.collect(Collectors.toList()))
.build();
}
var bundleInclusionProof = bundleEntry.getInclusionProof();

ImmutableInclusionProof inclusionProof =
ImmutableInclusionProof.builder()
.logIndex(bundleInclusionProof.getLogIndex())
.rootHash(Hex.toHexString(bundleInclusionProof.getRootHash().toByteArray()))
.treeSize(bundleInclusionProof.getTreeSize())
.checkpoint(bundleInclusionProof.getCheckpoint().getEnvelope())
.addAllHashes(
bundleInclusionProof.getHashesList().stream()
.map(ByteString::toByteArray)
.map(Hex::toHexString)
.collect(Collectors.toList()))
.build();

var verification =
ImmutableVerification.builder()
.signedEntryTimestamp(
Base64.getEncoder()
.encodeToString(
bundleEntry.getInclusionPromise().getSignedEntryTimestamp().toByteArray()))
.inclusionProof(inclusionProof)
.inclusionProof(Optional.ofNullable(inclusionProof))
.build();

var rekorEntry =
Expand All @@ -214,6 +206,10 @@ static KeylessSignature readBundle(Reader jsonReader) throws BundleParseExceptio
.verification(verification)
.build();

if (bundle.hasDsseEnvelope()) {
throw new BundleParseException("DSSE envelope signatures are not supported by this client");
}

var digest = new byte[] {};
if (bundle.getMessageSignature().hasMessageDigest()) {
var hashAlgorithm = bundle.getMessageSignature().getMessageDigest().getAlgorithm();
Expand All @@ -228,27 +224,24 @@ static KeylessSignature readBundle(Reader jsonReader) throws BundleParseExceptio
digest = bundle.getMessageSignature().getMessageDigest().getDigest().toByteArray();
}

CertPath certPath;
try {
return KeylessSignature.builder()
.digest(digest)
.certPath(
toCertPath(
bundle.getVerificationMaterial().getX509CertificateChain().getCertificatesList()))
.signature(bundle.getMessageSignature().getSignature().toByteArray())
.entry(rekorEntry)
.build();
if (bundle.getVerificationMaterial().hasCertificate()) {
certPath =
ProtoMutators.toCertPath(List.of(bundle.getVerificationMaterial().getCertificate()));
} else {
certPath =
ProtoMutators.toCertPath(
bundle.getVerificationMaterial().getX509CertificateChain().getCertificatesList());
}
} catch (CertificateException ce) {
throw new BundleParseException("Could not parse bundle certificate chain", ce);
}
}

private static CertPath toCertPath(List<X509Certificate> certificates)
throws CertificateException {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
List<Certificate> converted = new ArrayList<>(certificates.size());
for (var cert : certificates) {
converted.add(Certificates.fromDer(cert.getRawBytes().toByteArray()));
}
return cf.generateCertPath(converted);
return KeylessSignature.builder()
.digest(digest)
.certPath(certPath)
.signature(bundle.getMessageSignature().getSignature().toByteArray())
.entry(rekorEntry)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
*/
package dev.sigstore.proto;

import com.google.protobuf.ByteString;
import com.google.protobuf.Timestamp;
import dev.sigstore.encryption.certificates.Certificates;
import dev.sigstore.proto.common.v1.X509Certificate;
import java.security.cert.CertPath;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.time.Instant;
Expand All @@ -41,4 +43,11 @@ public static CertPath toCertPath(List<X509Certificate> certificates)
public static Instant toInstant(Timestamp timestamp) {
return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos());
}

public static X509Certificate fromCert(java.security.cert.X509Certificate certificate)
throws CertificateEncodingException {
byte[] encoded;
encoded = certificate.getEncoded();
return X509Certificate.newBuilder().setRawBytes(ByteString.copyFrom(encoded)).build();
}
}
39 changes: 37 additions & 2 deletions sigstore-java/src/test/java/dev/sigstore/KeylessVerifierTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,46 @@ public void testVerify_mismatchedSet() throws Exception {
var verificationReq =
KeylessVerificationRequest.builder()
.keylessSignature(BundleFactory.readBundle(new StringReader(bundleFile)))
.verificationOptions(
VerificationOptions.builder().alwaysUseRemoteRekorEntry(false).build())
.build();
Assertions.assertThrows(
KeylessVerificationException.class,
() -> verifier.verify(Path.of(artifact), verificationReq));
}

@Test
public void testVerify_canVerifyV01Bundle() throws Exception {
verifyBundle(
"dev/sigstore/samples/bundles/artifact.txt",
"dev/sigstore/samples/bundles/bundle.v1.sigstore");
}

@Test
public void testVerify_canVerifyV02Bundle() throws Exception {
verifyBundle(
"dev/sigstore/samples/bundles/artifact.txt",
"dev/sigstore/samples/bundles/bundle.v2.sigstore");
}

@Test
public void testVerify_canVerifyV03Bundle() throws Exception {
verifyBundle(
"dev/sigstore/samples/bundles/artifact.txt",
"dev/sigstore/samples/bundles/bundle.v3.sigstore");
}

public void verifyBundle(String artifactResourcePath, String bundleResourcePath)
throws Exception {
var artifact = Resources.getResource(artifactResourcePath).getPath();
var bundleFile =
Resources.toString(Resources.getResource(bundleResourcePath), StandardCharsets.UTF_8);

var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build();
var verificationReq =
KeylessVerificationRequest.builder()
.keylessSignature(BundleFactory.readBundle(new StringReader(bundleFile)))
.verificationOptions(VerificationOptions.builder().build())
.build();

verifier.verify(Path.of(artifact), verificationReq);
}
}
Loading

0 comments on commit 6314f67

Please sign in to comment.