Skip to content

Commit

Permalink
Add rekor client method to upload a record
Browse files Browse the repository at this point in the history
- Also adds a cert generator to bypass fulcio/dex in testing

Signed-off-by: Appu Goundan <appu@google.com>
  • Loading branch information
loosebazooka committed May 19, 2022
1 parent 3e699dd commit d00ebd7
Show file tree
Hide file tree
Showing 8 changed files with 326 additions and 3 deletions.
4 changes: 3 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dependencies {
because("contains library code for all platforms")
}
implementation("org.bouncycastle:bcutil-jdk18on:1.71")
implementation("org.bouncycastle:bcpkix-jdk18on:1.71")

implementation(platform("com.google.oauth-client:google-oauth-client-bom:1.33.3"))
implementation("com.google.oauth-client:google-oauth-client")
Expand All @@ -52,7 +53,6 @@ dependencies {

testImplementation("no.nav.security:mock-oauth2-server:0.4.4")
testImplementation("com.squareup.okhttp3:mockwebserver:4.9.3")
testImplementation("org.bouncycastle:bcutil-jdk15on:1.70")
testImplementation("net.sourceforge.htmlunit:htmlunit:2.61.0")

implementation("javax.validation:validation-api:2.0.1.Final")
Expand Down Expand Up @@ -84,4 +84,6 @@ jsonSchema2Pojo {
source.setFrom(files("${sourceSets.main.get().output.resourcesDir}/rekor/model"))
targetDirectoryPrefix.set(file("${project.buildDir}/generated/sources/rekor-model/"))
targetPackage.set("dev.sigstore.rekor")
generateBuilders.set(true)
annotationStyle.set("gson")
}
5 changes: 5 additions & 0 deletions src/main/java/dev/sigstore/encryption/signers/Signer.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,18 @@ public interface Signer {
/**
* Sign the content. Implementations should use an algorithm that hashes with sha256 before
* signing.
*
* @param content the full content to be signed (not a digest)
* @param charset the charset of the string {@code content}
*/
byte[] sign(String content, Charset charset)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException;

/**
* Sign the content. Implementations should use an algorithm that hashes with sha256 before
* signing.
*
* @param content the full content to be signed (not a digest)
*/
byte[] sign(byte[] content)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException;
Expand Down
84 changes: 84 additions & 0 deletions src/main/java/dev/sigstore/rekor/client/HashedRekordRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* 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.rekor.client;

import dev.sigstore.json.GsonSupplier;
import dev.sigstore.rekor.*;
import dev.sigstore.rekor.PublicKey;
import dev.sigstore.rekor.Signature;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.Certificate;
import java.util.Base64;
import java.util.HashMap;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.util.encoders.Hex;

public class HashedRekordRequest {

private final Hashedrekord hashedrekord;

private HashedRekordRequest(Hashedrekord hashedrekord) {
this.hashedrekord = hashedrekord;
}

/**
* Create a new HashedRekorRequest.
*
* @param artifactDigest the sha256 digest of the artifact (not hex/base64 encoded)
* @param leafCert the leaf certificate used to verify {@code signature}, usually obtained from
* fulcio
* @param signature the signature over the {@code artifactDigest} (not hex/base64 encoded)
*/
public static HashedRekordRequest newHashedRekordRequest(
byte[] artifactDigest, Certificate leafCert, byte[] signature) throws IOException {

System.out.println(Base64.getEncoder().encodeToString(leafCert.getPublicKey().getEncoded()));

var certWriter = new StringWriter();
try (JcaPEMWriter pemWriter = new JcaPEMWriter(certWriter)) {
pemWriter.writeObject(leafCert);
pemWriter.flush();
}

var certPem = certWriter.toString().getBytes(StandardCharsets.UTF_8);
var hashedrekord =
new Hashedrekord()
.withData(
new Data()
.withHash(
new Hash()
.withValue(new String(Hex.encode(artifactDigest)))
.withAlgorithm(Hash.Algorithm.SHA_256)))
.withSignature(
new Signature()
.withContent(Base64.getEncoder().encodeToString(signature))
.withPublicKey(
new PublicKey().withContent(Base64.getEncoder().encodeToString(certPem))));
return new HashedRekordRequest(hashedrekord);
}

public String toJsonPayload() {
var data = new HashMap<String, Object>();
data.put("kind", "hashedrekord");
data.put("apiVersion", "0.0.1");
data.put("spec", hashedrekord);

return new GsonSupplier().get().toJson(data);
}
}
31 changes: 29 additions & 2 deletions src/main/java/dev/sigstore/rekor/client/RekorClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@
*/
package dev.sigstore.rekor.client;

import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import dev.sigstore.http.HttpProvider;
import java.io.IOException;
import java.net.URI;

/** A client to communicate with a rekor service instance. */
public class RekorClient {
public static final String PUBLIC_REKOR_SERVER = "https://rekor.sigstore.dev";
public static final String REKOR_ENTRIES_PATH = "/api/v1/log/entries";

private final HttpProvider httpProvider;
private final URI serverUrl;
Expand Down Expand Up @@ -59,8 +65,29 @@ public RekorClient build() {
}

/** Put an entry on rekor. */
public void putEntry() {
throw new UnsupportedOperationException("I'm a worthless upload function");
public RekorResponse putEntry(HashedRekordRequest hashedRekordRequest) throws IOException {
URI rekorEndpoint = serverUrl.resolve(REKOR_ENTRIES_PATH);

HttpRequest req =
httpProvider
.getHttpTransport()
.createRequestFactory()
.buildPostRequest(
new GenericUrl(rekorEndpoint),
ByteArrayContent.fromString(
"application/json", hashedRekordRequest.toJsonPayload()));
req.getHeaders().set("Accept", "application/json");
req.getHeaders().set("Content-Type", "application/json");

HttpResponse resp = req.execute();
if (resp.getStatusCode() != 201) {
throw new IOException(
String.format(
"bad response from rekor @ '%s' : %s", rekorEndpoint, resp.parseAsString()));
}

URI rekorEntryUri = serverUrl.resolve(resp.getHeaders().getLocation());
return new RekorResponse(rekorEntryUri);
}

/** Obtain an entry for an artifact from rekor. */
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/dev/sigstore/rekor/client/RekorResponse.java
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.rekor.client;

import java.net.URI;

public class RekorResponse {
private final URI entryLocation;

public RekorResponse(URI entryLocation) {
this.entryLocation = entryLocation;
}

public URI getEntryLocation() {
return entryLocation;
}
}
67 changes: 67 additions & 0 deletions src/test/java/dev/sigstore/rekor/client/RekorClientTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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.rekor.client;

import dev.sigstore.encryption.signers.Signers;
import dev.sigstore.testing.CertGenerator;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import org.bouncycastle.operator.OperatorCreationException;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Test;

public class RekorClientTest {

@Test
public void putEntry_toStaging()
throws CertificateException, IOException, NoSuchAlgorithmException, OperatorCreationException,
SignatureException, InvalidKeyException, URISyntaxException {
// the data we want to sign
var data = "some data";

// get the digest
var artifactDigest =
MessageDigest.getInstance("SHA-256").digest(data.getBytes(StandardCharsets.UTF_8));

// sign the full content (these signers do the artifact hashing themselves)
var signer = Signers.newEcdsaSigner();
var signature = signer.sign(data.getBytes(StandardCharsets.UTF_8));

// create a fake signing cert (not fulcio/dex)
var cert = CertGenerator.newCert(signer.getPublicKey());

var req = HashedRekordRequest.newHashedRekordRequest(artifactDigest, cert, signature);

// this tests directly against rekor in staging, it's a bit hard to bring up a rekor instance
// without
// docker compose.
var client = RekorClient.builder().setServerUrl(new URI("https://rekor.sigstage.dev")).build();
var resp = client.putEntry(req);

// pretty basic testing
MatcherAssert.assertThat(
resp.getEntryLocation().toString(),
CoreMatchers.startsWith("https://rekor.sigstage.dev/api/v1/log/entries/"));
}
}
108 changes: 108 additions & 0 deletions src/test/java/dev/sigstore/testing/CertGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* 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.testing;

import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.Base64;
import java.util.Calendar;
import java.util.Date;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

/**
* A certificate generator, useful when trying to talk to rekor without actually using fulcio/oidc.
*/
public class CertGenerator {
public static Certificate newCert(PublicKey publicKey)
throws OperatorCreationException, CertificateException, IOException,
NoSuchAlgorithmException {
SecureRandom random = new SecureRandom();

System.out.println(Base64.getEncoder().encodeToString(publicKey.getEncoded()));

KeyPairGenerator keypairGen = KeyPairGenerator.getInstance("EC");
keypairGen.initialize(256);
KeyPair certSigningKeyPair = keypairGen.generateKeyPair();

X500Name subject =
new X500NameBuilder(BCStyle.INSTANCE)
.addRDN(BCStyle.CN, "test")
.addRDN(BCStyle.O, "test certificate")
.build();

Date startDate = new Date(System.currentTimeMillis());
var calendar = Calendar.getInstance();
calendar.setTime(startDate);
calendar.add(Calendar.MINUTE, 20);
var endDate = calendar.getTime();

BigInteger serial = new BigInteger(160, random);

X509v3CertificateBuilder certificate =
new JcaX509v3CertificateBuilder(subject, serial, startDate, endDate, subject, publicKey);

var id = new byte[20];
random.nextBytes(id);

var keyIdgen = new JcaX509ExtensionUtils();
certificate.addExtension(
Extension.subjectKeyIdentifier, false, keyIdgen.createSubjectKeyIdentifier(publicKey));
certificate.addExtension(
Extension.authorityKeyIdentifier,
false,
keyIdgen.createAuthorityKeyIdentifier(certSigningKeyPair.getPublic())); // this is nonsense
certificate.addExtension(
Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature).getEncoded());
certificate.addExtension(
Extension.extendedKeyUsage,
false,
new ExtendedKeyUsage(KeyPurposeId.id_kp_codeSigning).getEncoded());
certificate.addExtension(
Extension.basicConstraints, true, new BasicConstraints(false).getEncoded());

ContentSigner signer =
new JcaContentSignerBuilder("SHA256withECDSA").build(certSigningKeyPair.getPrivate());
X509CertificateHolder holder = certificate.build(signer);

JcaX509CertificateConverter converter = new JcaX509CertificateConverter();
converter.setProvider(new BouncyCastleProvider());

return converter.getCertificate(holder);
}
}

0 comments on commit d00ebd7

Please sign in to comment.