-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #657 from sigstore/checkpoints
Handle parsing checkpoints from a rekor entry
- Loading branch information
Showing
12 changed files
with
346 additions
and
0 deletions.
There are no files selected for viewing
105 changes: 105 additions & 0 deletions
105
sigstore-java/src/main/java/dev/sigstore/rekor/client/Checkpoints.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/* | ||
* Copyright 2024 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 com.google.common.base.Splitter; | ||
import dev.sigstore.rekor.client.RekorEntry.Checkpoint; | ||
import dev.sigstore.rekor.client.RekorEntry.CheckpointSignature; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.Base64; | ||
import java.util.List; | ||
import java.util.regex.Pattern; | ||
import java.util.stream.Collectors; | ||
|
||
/** | ||
* Checkpoint helper class to parse from a string in the format described in | ||
* https://github.com/transparency-dev/formats/blob/12bf59947efb7ae227c12f218b4740fb17a87e50/log/README.md | ||
*/ | ||
class Checkpoints { | ||
private static final Pattern SIGNATURE_BLOCK = Pattern.compile("\\u2014 (\\S+) (\\S+)"); | ||
|
||
static Checkpoint from(String encoded) throws RekorParseException { | ||
var split = Splitter.on("\n\n").splitToList(encoded); | ||
if (split.size() != 2) { | ||
throw new RekorParseException( | ||
"Checkpoint must contain one blank line, delineating the header from the signature block"); | ||
} | ||
var header = split.get(0); | ||
var data = split.get(1); | ||
|
||
// note that the string actually contains \n literally, not newlines | ||
var headers = Splitter.on("\n").splitToList(header); | ||
if (headers.size() < 3) { | ||
throw new RekorParseException("Checkpoint header must contain at least 3 lines"); | ||
} | ||
|
||
var origin = headers.get(0); | ||
long size; | ||
try { | ||
size = Long.parseLong(headers.get(1)); | ||
} catch (NumberFormatException nfe) { | ||
throw new RekorParseException( | ||
"Checkpoint header attribute size must be a number, but was: " + headers.get(1)); | ||
} | ||
var base64Hash = headers.get(2); | ||
// we don't care about any other headers after this | ||
|
||
if (data.length() == 0) { | ||
throw new RekorParseException("Checkpoint body must contain at least one signature"); | ||
} | ||
if (!data.endsWith("\n")) { | ||
throw new RekorParseException("Checkpoint signature section must end with newline"); | ||
} | ||
|
||
List<CheckpointSignature> signatures = new ArrayList<>(); | ||
for (String sig : data.lines().collect(Collectors.toList())) { | ||
signatures.add(sigFrom(sig)); | ||
} | ||
|
||
return ImmutableCheckpoint.builder() | ||
.origin(origin) | ||
.size(size) | ||
.base64Hash(base64Hash) | ||
.addAllSignatures(signatures) | ||
.build(); | ||
} | ||
|
||
static CheckpointSignature sigFrom(String signatureLine) throws RekorParseException { | ||
var m = SIGNATURE_BLOCK.matcher(signatureLine); | ||
if (!m.find()) { | ||
throw new RekorParseException( | ||
"Checkpoint signature '" | ||
+ signatureLine | ||
+ "' was not in the format '— <id> <base64 keyhint+signature>'"); | ||
} | ||
var identity = m.group(1); | ||
var keySig = Base64.getDecoder().decode(m.group(2)); | ||
if (keySig.length < 5) { | ||
throw new RekorParseException( | ||
"Checkpoint signature <keyhint + signature> was " | ||
+ keySig.length | ||
+ " bytes long, but must be at least 5 bytes long"); | ||
} | ||
var keyHint = Arrays.copyOfRange(keySig, 0, 4); | ||
var signature = Arrays.copyOfRange(keySig, 4, keySig.length); | ||
return ImmutableCheckpointSignature.builder() | ||
.identity(identity) | ||
.keyHint(keyHint) | ||
.signature(signature) | ||
.build(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
151 changes: 151 additions & 0 deletions
151
sigstore-java/src/test/java/dev/sigstore/rekor/client/CheckpointsTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
/* | ||
* Copyright 2024 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 com.google.common.io.Resources; | ||
import java.io.IOException; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.Arrays; | ||
import java.util.Base64; | ||
import org.junit.jupiter.api.Assertions; | ||
import org.junit.jupiter.api.Test; | ||
|
||
public class CheckpointsTest { | ||
|
||
public static final String REKOR_PUB_KEYID = "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="; | ||
|
||
public String getResource(String filename) throws IOException { | ||
return Resources.toString( | ||
Resources.getResource("dev/sigstore/samples/checkpoints/" + filename), | ||
StandardCharsets.UTF_8); | ||
} | ||
|
||
@Test | ||
public void from_valid() throws Exception { | ||
var checkpoint = Checkpoints.from(getResource("valid.txt")); | ||
Assertions.assertEquals("rekor.sigstore.dev - 2605736670972794746", checkpoint.getOrigin()); | ||
Assertions.assertEquals(37795272, checkpoint.getSize()); | ||
Assertions.assertEquals( | ||
"60ll7idWI1jYRZzxc+jKflYoW+4jWxgZaGR15ASsWt4=", checkpoint.getBase64Hash()); | ||
|
||
var keyBytesHintExpected = | ||
Arrays.copyOfRange(Base64.getDecoder().decode(REKOR_PUB_KEYID), 0, 4); | ||
var sig = checkpoint.getSignatures().get(0); | ||
Assertions.assertEquals(1, checkpoint.getSignatures().size()); | ||
Assertions.assertEquals("rekor.sigstore.dev", sig.getIdentity()); | ||
Assertions.assertArrayEquals(keyBytesHintExpected, sig.getKeyHint()); | ||
Assertions.assertEquals( | ||
"MEYCIQCVZQfYdI9rogwhEGAVwhemHcyP3EzvRZHRVUAO8YiX+gIhAKB+9RSNH9fmN7CWqkBYjw24kiJwqlMbri+jpQzl+lKB", | ||
Base64.getEncoder().encodeToString(sig.getSignature())); | ||
} | ||
|
||
@Test | ||
public void from_validMultiSig() throws Exception { | ||
var checkpoint = Checkpoints.from(getResource("valid_multi_sig.txt")); | ||
Assertions.assertEquals("rekor.sigstore.dev - 2605736670972794746", checkpoint.getOrigin()); | ||
Assertions.assertEquals(37795272, checkpoint.getSize()); | ||
Assertions.assertEquals( | ||
"60ll7idWI1jYRZzxc+jKflYoW+4jWxgZaGR15ASsWt4=", checkpoint.getBase64Hash()); | ||
|
||
Assertions.assertEquals(2, checkpoint.getSignatures().size()); | ||
var keyBytesHintExpected = | ||
Arrays.copyOfRange(Base64.getDecoder().decode(REKOR_PUB_KEYID), 0, 4); | ||
|
||
var sig1 = checkpoint.getSignatures().get(0); | ||
Assertions.assertEquals("rekor.sigstore.dev", sig1.getIdentity()); | ||
Assertions.assertArrayEquals(keyBytesHintExpected, sig1.getKeyHint()); | ||
Assertions.assertEquals( | ||
"MEYCIQCVZQfYdI9rogwhEGAVwhemHcyP3EzvRZHRVUAO8YiX+gIhAKB+9RSNH9fmN7CWqkBYjw24kiJwqlMbri+jpQzl+lKB", | ||
Base64.getEncoder().encodeToString(sig1.getSignature())); | ||
|
||
var sig2 = checkpoint.getSignatures().get(1); | ||
Assertions.assertEquals("bob.loblaw.dev", sig2.getIdentity()); | ||
Assertions.assertArrayEquals(keyBytesHintExpected, sig2.getKeyHint()); | ||
Assertions.assertEquals( | ||
"MEYCIQCVZQfYdI9rogwhEGAVwhGmHcyP3EzvRZHRVUAO8YiX+gIhAKB+9RSNH9fmN7CWqkBYjw24kiJwqlMbri+jpQzl+lKB", | ||
Base64.getEncoder().encodeToString(sig2.getSignature())); | ||
} | ||
|
||
@Test | ||
public void from_noSeparator() throws Exception { | ||
var ex = | ||
Assertions.assertThrows( | ||
RekorParseException.class, | ||
() -> Checkpoints.from(getResource("error_header_body_separator.txt"))); | ||
Assertions.assertEquals( | ||
"Checkpoint must contain one blank line, delineating the header from the signature block", | ||
ex.getMessage()); | ||
} | ||
|
||
@Test | ||
public void from_notEnoughHeaders() throws Exception { | ||
var ex = | ||
Assertions.assertThrows( | ||
RekorParseException.class, | ||
() -> Checkpoints.from(getResource("error_header_count.txt"))); | ||
Assertions.assertEquals("Checkpoint header must contain at least 3 lines", ex.getMessage()); | ||
} | ||
|
||
@Test | ||
public void from_notANumber() throws Exception { | ||
var ex = | ||
Assertions.assertThrows( | ||
RekorParseException.class, | ||
() -> Checkpoints.from(getResource("error_not_a_number.txt"))); | ||
Assertions.assertEquals( | ||
"Checkpoint header attribute size must be a number, but was: abcdefg", ex.getMessage()); | ||
} | ||
|
||
@Test | ||
public void from_noSignatures() throws Exception { | ||
var ex = | ||
Assertions.assertThrows( | ||
RekorParseException.class, | ||
() -> Checkpoints.from(getResource("error_no_signatures.txt"))); | ||
Assertions.assertEquals("Checkpoint body must contain at least one signature", ex.getMessage()); | ||
} | ||
|
||
@Test | ||
public void from_noNewlineAfterSignatures() throws Exception { | ||
var ex = | ||
Assertions.assertThrows( | ||
RekorParseException.class, | ||
() -> Checkpoints.from(getResource("error_no_newline_after_signature.txt"))); | ||
Assertions.assertEquals("Checkpoint signature section must end with newline", ex.getMessage()); | ||
} | ||
|
||
@Test | ||
public void from_signatureFormatInvalid() throws Exception { | ||
var ex = | ||
Assertions.assertThrows( | ||
RekorParseException.class, | ||
() -> Checkpoints.from(getResource("error_signature_format_invalid.txt"))); | ||
Assertions.assertEquals( | ||
"Checkpoint signature 'rekor.sigstore.dev wNI9ajBGAiEAlWUH2HSPa6IMIRBgFcIXph3Mj9xM70WR0VVADvGIl/oCIQCgfvUUjR/X5jewlqpAWI8NuJIicKpTG64vo6UM5fpSgQ==' was not in the format '— <id> <base64 keyhint+signature>'", | ||
ex.getMessage()); | ||
} | ||
|
||
@Test | ||
public void from_signatureLengthInsufficient() throws Exception { | ||
var ex = | ||
Assertions.assertThrows( | ||
RekorParseException.class, | ||
() -> Checkpoints.from(getResource("error_signature_length_insufficient.txt"))); | ||
Assertions.assertEquals( | ||
"Checkpoint signature <keyhint + signature> was 4 bytes long, but must be at least 5 bytes long", | ||
ex.getMessage()); | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
...-java/src/test/resources/dev/sigstore/samples/checkpoints/error_header_body_separator.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
rekor.sigstore.dev - 2605736670972794746 | ||
37795272 | ||
60ll7idWI1jYRZzxc+jKflYoW+4jWxgZaGR15ASsWt4= | ||
Timestamp: 1697034484441201852 | ||
— rekor.sigstore.dev wNI9ajBGAiEAlWUH2HSPa6IMIRBgFcIXph3Mj9xM70WR0VVADvGIl/oCIQCgfvUUjR/X5jewlqpAWI8NuJIicKpTG64vo6UM5fpSgQ== |
4 changes: 4 additions & 0 deletions
4
sigstore-java/src/test/resources/dev/sigstore/samples/checkpoints/error_header_count.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
rekor.sigstore.dev - 2605736670972794746 | ||
37795272 | ||
|
||
— rekor.sigstore.dev wNI9ajBGAiEAlWUH2HSPa6IMIRBgFcIXph3Mj9xM70WR0VVADvGIl/oCIQCgfvUUjR/X5jewlqpAWI8NuJIicKpTG64vo6UM5fpSgQ== |
6 changes: 6 additions & 0 deletions
6
.../src/test/resources/dev/sigstore/samples/checkpoints/error_no_newline_after_signature.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
rekor.sigstore.dev - 2605736670972794746 | ||
37795272 | ||
60ll7idWI1jYRZzxc+jKflYoW+4jWxgZaGR15ASsWt4= | ||
Timestamp: 1697034484441201852 | ||
|
||
— rekor.sigstore.dev wNI9ajBGAiEAlWUH2HSPa6IMIRBgFcIXph3Mj9xM70WR0VVADvGIl/oCIQCgfvUUjR/X5jewlqpAWI8NuJIicKpTG64vo6UM5fpSgQ== |
5 changes: 5 additions & 0 deletions
5
sigstore-java/src/test/resources/dev/sigstore/samples/checkpoints/error_no_signatures.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
rekor.sigstore.dev - 2605736670972794746 | ||
37795272 | ||
60ll7idWI1jYRZzxc+jKflYoW+4jWxgZaGR15ASsWt4= | ||
Timestamp: 1697034484441201852 | ||
|
6 changes: 6 additions & 0 deletions
6
sigstore-java/src/test/resources/dev/sigstore/samples/checkpoints/error_not_a_number.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
rekor.sigstore.dev - 2605736670972794746 | ||
abcdefg | ||
60ll7idWI1jYRZzxc+jKflYoW+4jWxgZaGR15ASsWt4= | ||
Timestamp: 1697034484441201852 | ||
|
||
— rekor.sigstore.dev wNI9ajBGAiEAlWUH2HSPa6IMIRBgFcIXph3Mj9xM70WR0VVADvGIl/oCIQCgfvUUjR/X5jewlqpAWI8NuJIicKpTG64vo6UM5fpSgQ== |
6 changes: 6 additions & 0 deletions
6
...va/src/test/resources/dev/sigstore/samples/checkpoints/error_signature_format_invalid.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
rekor.sigstore.dev - 2605736670972794746 | ||
37795272 | ||
60ll7idWI1jYRZzxc+jKflYoW+4jWxgZaGR15ASsWt4= | ||
Timestamp: 1697034484441201852 | ||
|
||
rekor.sigstore.dev wNI9ajBGAiEAlWUH2HSPa6IMIRBgFcIXph3Mj9xM70WR0VVADvGIl/oCIQCgfvUUjR/X5jewlqpAWI8NuJIicKpTG64vo6UM5fpSgQ== |
6 changes: 6 additions & 0 deletions
6
...c/test/resources/dev/sigstore/samples/checkpoints/error_signature_length_insufficient.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
rekor.sigstore.dev - 2605736670972794746 | ||
37795272 | ||
60ll7idWI1jYRZzxc+jKflYoW+4jWxgZaGR15ASsWt4= | ||
Timestamp: 1697034484441201852 | ||
|
||
— rekor.sigstore.dev wNI9aj |
6 changes: 6 additions & 0 deletions
6
sigstore-java/src/test/resources/dev/sigstore/samples/checkpoints/valid.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
rekor.sigstore.dev - 2605736670972794746 | ||
37795272 | ||
60ll7idWI1jYRZzxc+jKflYoW+4jWxgZaGR15ASsWt4= | ||
Timestamp: 1697034484441201852 | ||
|
||
— rekor.sigstore.dev wNI9ajBGAiEAlWUH2HSPa6IMIRBgFcIXph3Mj9xM70WR0VVADvGIl/oCIQCgfvUUjR/X5jewlqpAWI8NuJIicKpTG64vo6UM5fpSgQ== |
7 changes: 7 additions & 0 deletions
7
sigstore-java/src/test/resources/dev/sigstore/samples/checkpoints/valid_multi_sig.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
rekor.sigstore.dev - 2605736670972794746 | ||
37795272 | ||
60ll7idWI1jYRZzxc+jKflYoW+4jWxgZaGR15ASsWt4= | ||
Timestamp: 1697034484441201852 | ||
|
||
— rekor.sigstore.dev wNI9ajBGAiEAlWUH2HSPa6IMIRBgFcIXph3Mj9xM70WR0VVADvGIl/oCIQCgfvUUjR/X5jewlqpAWI8NuJIicKpTG64vo6UM5fpSgQ== | ||
— bob.loblaw.dev wNI9ajBGAiEAlWUH2HSPa6IMIRBgFcIRph3Mj9xM70WR0VVADvGIl/oCIQCgfvUUjR/X5jewlqpAWI8NuJIicKpTG64vo6UM5fpSgQ== |