Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rekor client method to upload a record #23

Merged
merged 1 commit into from
May 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
79 changes: 79 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,79 @@
/*
* 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 java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the (lackof) camecase a typo here or unavoidable code gen artifact?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hrmm... yeah I think so. It should be an internal value, so users wont see it. There's probably a way to make it more "java-like".


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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for custom key signed artifacts (not using fulcio), is there a leaf cert to submit, or do you just use the singer's public key? It's all still a little muddled for me so asking out of curiosity,

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So presumably there's two ways to do this.

  1. with a certificate
  2. with a public key

this code just doesn't support (2) yet. But that would be a new factory method

support for public key hashedrekord: #25

* 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 {

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;
}
}
66 changes: 66 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,66 @@
/*
* 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo? I think Jason was working on doing this for his maven testing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what the right solution is. I think sigstore-scaffolding is fine, but it's super heavy weight.

Issue: #27

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/"));
}
}
114 changes: 114 additions & 0 deletions src/test/java/dev/sigstore/testing/CertGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* 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.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.Calendar;
import java.util.Date;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.*;
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah so would this be used for the custom key flow too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you use a custom key, you probably don't want a certificate -- you just use a pem encoded public key?

public static Certificate newCert(PublicKey publicKey)
throws OperatorCreationException, CertificateException, IOException,
NoSuchAlgorithmException {

// generate a keypair for signing this certificate
KeyPairGenerator keypairGen = KeyPairGenerator.getInstance("EC");
keypairGen.initialize(256);
KeyPair certSigningKeyPair = keypairGen.generateKeyPair();

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

// create a short lived cert
Date startDate = new Date(System.currentTimeMillis());
var calendar = Calendar.getInstance();
calendar.setTime(startDate);
calendar.add(Calendar.MINUTE, 20);
var endDate = calendar.getTime();

// arbitrary serial number
BigInteger serial = new BigInteger(Long.toString(System.currentTimeMillis()));

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

// add all extensions like a real fulcio cert
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());
certificate.addExtension(
Extension.subjectAlternativeName,
true,
new GeneralNames(new GeneralName(GeneralName.rfc822Name, "test@test.com")).getEncoded());
// identity provider
certificate.addExtension(
new ASN1ObjectIdentifier("1.3.6.1.4.1.57264.1.1"),
false,
"https://fakeaccounts.test.com".getBytes(StandardCharsets.UTF_8));

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

// covert cert to a Java native x509 cert
JcaX509CertificateConverter converter = new JcaX509CertificateConverter();
converter.setProvider(new BouncyCastleProvider());

return converter.getCertificate(holder);
}
}