Skip to content

Commit 1e836ad

Browse files
Implement Signed Entry Timestamp (SET) verification (#36)
* sigstore: Initial implementation of verify command * _verify, _cli: Check the cert for the signer email * _verify: Add proper checks * setup: Add PyOpenSSL as a dependency * _verify: Remove redundant newline * Fix lint * Fix comment formatting * Return None for consistency * Use click files instead of paths and minor fixes * treewide: embed key material, refactor to accomodate * gitignore: only ignore junk in the root * sigstore: add fulcio root * Format data correctly for Rekor endpoint * Fix Rekor API calls in verification * Fix type checking * Use the updated Fulcio root key * Use CTFE key as a file properly and fix type hints * Add more progress feedback * Use pydantic and remove custom `from_dict` helper * Add type hints to verify API * Add pydantic to setup * Implement SET verification * Check for invalid SET in verify command * Use securesystemslib instead of jcs Co-authored-by: William Woodruff <[email protected]>
1 parent 53aa2ee commit 1e836ad

File tree

5 files changed

+58
-3
lines changed

5 files changed

+58
-3
lines changed

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"pyjwt",
3838
"pyOpenSSL",
3939
"requests",
40+
"securesystemslib",
4041
],
4142
extras_require={
4243
"dev": [

sigstore/_internal/rekor/_client.py

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class RekorEntry:
2424
log_id: str
2525
log_index: int
2626
verification: dict
27+
raw_data: dict
2728

2829
@classmethod
2930
def from_response(cls, dict_) -> RekorEntry:
@@ -41,6 +42,7 @@ def from_response(cls, dict_) -> RekorEntry:
4142
log_id=entry["logID"],
4243
log_index=entry["logIndex"],
4344
verification=entry["verification"],
45+
raw_data=entry,
4446
)
4547

4648

sigstore/_internal/set.py

+45-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,52 @@
22
Utilities for verifying Signed Entry Timestamps.
33
"""
44

5+
import base64
6+
from importlib import resources
7+
from typing import cast
8+
9+
import cryptography.hazmat.primitives.asymmetric.ec as ec
10+
from cryptography.exceptions import InvalidSignature
11+
from cryptography.hazmat.primitives import hashes
12+
from cryptography.hazmat.primitives.serialization import load_pem_public_key
13+
from securesystemslib.formats import encode_canonical # type: ignore
14+
515
from sigstore._internal.rekor import RekorEntry
616

17+
REKOR_ROOT_PUBKEY = resources.read_binary("sigstore._store", "rekor.pub")
718

8-
def verify_set(entry: RekorEntry) -> None:
19+
20+
class InvalidSetError(Exception):
921
pass
22+
23+
24+
def verify_set(entry: RekorEntry) -> None:
25+
"""Verify the Signed Entry Timestamp for a given Rekor entry"""
26+
27+
# Put together the payload
28+
#
29+
# This involves removing any non-required fields (verification and attestation) and then
30+
# canonicalizing the remaining JSON in accordance with IETF's RFC 8785.
31+
raw_data = entry.raw_data.copy()
32+
del raw_data["verification"]
33+
del raw_data["attestation"]
34+
canon_data: bytes = encode_canonical(raw_data).encode()
35+
36+
# Decode the SET field
37+
signed_entry_ts: bytes = base64.b64decode(
38+
entry.verification["signedEntryTimestamp"].encode()
39+
)
40+
41+
# Load the Rekor public key
42+
rekor_key = load_pem_public_key(REKOR_ROOT_PUBKEY)
43+
rekor_key = cast(ec.EllipticCurvePublicKey, rekor_key)
44+
45+
# Validate the SET
46+
try:
47+
rekor_key.verify(
48+
signature=signed_entry_ts,
49+
data=canon_data,
50+
signature_algorithm=ec.ECDSA(hashes.SHA256()),
51+
)
52+
except InvalidSignature as inval_sig:
53+
raise InvalidSetError from inval_sig

sigstore/_store/rekor.pub

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwr
3+
kBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==
4+
-----END PUBLIC KEY-----

sigstore/_verify.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
RekorEntry,
2626
RekorInclusionProof,
2727
)
28-
from sigstore._internal.set import verify_set
28+
from sigstore._internal.set import InvalidSetError, verify_set
2929

3030

3131
# TODO(alex): Share this with `sign`
@@ -142,7 +142,11 @@ def verify(
142142
verify_merkle_inclusion(inclusion_proof)
143143

144144
# 5) Verify the Signed Entry Timestamp (SET) supplied by Rekor for this artifact
145-
verify_set(entry)
145+
try:
146+
verify_set(entry)
147+
except InvalidSetError as inval_set:
148+
output(f"Failed to validate Rekor entry's SET: {inval_set}")
149+
continue
146150

147151
valid_sig_exists = True
148152

0 commit comments

Comments
 (0)