Skip to content

Commit a16aed9

Browse files
authored
rekor: use sigstore_rekor_types for models (#788)
* rekor: use sigstore_rekor_types for models Signed-off-by: William Woodruff <william@trailofbits.com> * pyproject: sigstore-rekor-types 0.0.2 Signed-off-by: William Woodruff <william@trailofbits.com> * pyproject: bump sigstore-rekor-types Signed-off-by: William Woodruff <william@trailofbits.com> * sign: fix API usage Signed-off-by: William Woodruff <william@trailofbits.com> * rekor/client: debugging Signed-off-by: William Woodruff <william@trailofbits.com> * Revert "rekor/client: debugging" This reverts commit cabd3a3. * rekor/client: use field aliases Signed-off-by: William Woodruff <william@trailofbits.com> * sigstore: bump sigstore_rekor_types, refactor Signed-off-by: William Woodruff <william@trailofbits.com> * pyproject: bump sigstore-rekor-types Signed-off-by: William Woodruff <william@trailofbits.com> * pyproject: the bumping continues Signed-off-by: William Woodruff <william@trailofbits.com> * sign: fix type Annoying. Signed-off-by: William Woodruff <william@trailofbits.com> * sign: another typo Signed-off-by: William Woodruff <william@trailofbits.com> * bump again, rename Signed-off-by: William Woodruff <william@trailofbits.com> * remove more hardcoded models Signed-off-by: William Woodruff <william@trailofbits.com> * verify/models: reflow comments Signed-off-by: William Woodruff <william@trailofbits.com> --------- Signed-off-by: William Woodruff <william@trailofbits.com>
1 parent 5586a32 commit a16aed9

File tree

5 files changed

+57
-88
lines changed

5 files changed

+57
-88
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ dependencies = [
3535
"requests",
3636
"securesystemslib",
3737
"sigstore-protobuf-specs ~= 0.2.0",
38+
"sigstore-rekor-types >= 0.0.11",
3839
"tuf >= 2.1,< 4.0",
3940
]
4041
requires-python = ">=3.8"

sigstore/_internal/rekor/client.py

Lines changed: 7 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,18 @@
1818

1919
from __future__ import annotations
2020

21-
import base64
2221
import logging
2322
from abc import ABC
2423
from dataclasses import dataclass
2524
from typing import Any, Dict, NewType, Optional
2625
from urllib.parse import urljoin
2726

2827
import requests
29-
from cryptography.x509 import Certificate
28+
import sigstore_rekor_types
3029

3130
from sigstore._internal.ctfe import CTKeyring
3231
from sigstore._internal.keyring import Keyring
3332
from sigstore._internal.tuf import TrustUpdater
34-
from sigstore._utils import B64Str, base64_encode_pem_cert
3533
from sigstore.transparency import LogEntry
3634

3735
logger = logging.getLogger(__name__)
@@ -139,29 +137,15 @@ def get(
139137

140138
def post(
141139
self,
142-
b64_artifact_signature: B64Str,
143-
sha256_artifact_hash: str,
144-
b64_cert: B64Str,
140+
proposed_entry: sigstore_rekor_types.Hashedrekord,
145141
) -> LogEntry:
146142
"""
147143
Submit a new entry for inclusion in the Rekor log.
148144
"""
149-
# TODO(ww): Dedupe this payload construction with the retrieve endpoint below.
150-
data = {
151-
"kind": "hashedrekord",
152-
"apiVersion": "0.0.1",
153-
"spec": {
154-
"signature": {
155-
"content": b64_artifact_signature,
156-
"publicKey": {"content": b64_cert},
157-
},
158-
"data": {
159-
"hash": {"algorithm": "sha256", "value": sha256_artifact_hash}
160-
},
161-
},
162-
}
163145

164-
resp: requests.Response = self.session.post(self.url, json=data)
146+
resp: requests.Response = self.session.post(
147+
self.url, json=proposed_entry.model_dump(mode="json", by_alias=True)
148+
)
165149
try:
166150
resp.raise_for_status()
167151
except requests.HTTPError as http_error:
@@ -186,9 +170,7 @@ class RekorEntriesRetrieve(_Endpoint):
186170

187171
def post(
188172
self,
189-
signature: bytes,
190-
artifact_hash: str,
191-
certificate: Certificate,
173+
expected_entry: sigstore_rekor_types.Hashedrekord,
192174
) -> Optional[LogEntry]:
193175
"""
194176
Retrieves an extant Rekor entry, identified by its artifact signature,
@@ -197,28 +179,7 @@ def post(
197179
Returns None if Rekor has no entry corresponding to the signing
198180
materials.
199181
"""
200-
data = {
201-
"entries": [
202-
{
203-
"kind": "hashedrekord",
204-
"apiVersion": "0.0.1",
205-
"spec": {
206-
"signature": {
207-
"content": B64Str(base64.b64encode(signature).decode()),
208-
"publicKey": {
209-
"content": B64Str(base64_encode_pem_cert(certificate)),
210-
},
211-
},
212-
"data": {
213-
"hash": {
214-
"algorithm": "sha256",
215-
"value": artifact_hash,
216-
}
217-
},
218-
},
219-
}
220-
]
221-
}
182+
data = {"entries": [expected_entry.model_dump(mode="json", by_alias=True)]}
222183

223184
resp: requests.Response = self.session.post(self.url, json=data)
224185
try:

sigstore/sign.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from typing import IO, Iterator, Optional
4747

4848
import cryptography.x509 as x509
49+
import sigstore_rekor_types
4950
from cryptography.hazmat.primitives import hashes, serialization
5051
from cryptography.hazmat.primitives.asymmetric import ec
5152
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
@@ -210,11 +211,25 @@ def sign(
210211
)
211212

212213
# Create the transparency log entry
213-
entry = self._signing_ctx._rekor.log.entries.post(
214-
b64_artifact_signature=B64Str(b64_artifact_signature),
215-
sha256_artifact_hash=input_digest.hex(),
216-
b64_cert=B64Str(b64_cert.decode()),
214+
proposed_entry = sigstore_rekor_types.Hashedrekord(
215+
kind="hashedrekord",
216+
api_version="0.0.1",
217+
spec=sigstore_rekor_types.HashedrekordV001Schema(
218+
signature=sigstore_rekor_types.Signature1(
219+
content=b64_artifact_signature,
220+
public_key=sigstore_rekor_types.PublicKey1(
221+
content=b64_cert.decode()
222+
),
223+
),
224+
data=sigstore_rekor_types.Data(
225+
hash=sigstore_rekor_types.Hash(
226+
algorithm=sigstore_rekor_types.Algorithm.SHA256,
227+
value=input_digest.hex(),
228+
)
229+
),
230+
),
217231
)
232+
entry = self._signing_ctx._rekor.log.entries.post(proposed_entry)
218233

219234
logger.debug(f"Transparency log entry created with index: {entry.log_index}")
220235

sigstore/verify/models.py

Lines changed: 29 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from textwrap import dedent
2626
from typing import IO
2727

28+
import sigstore_rekor_types
2829
from cryptography.hazmat.primitives.serialization import Encoding
2930
from cryptography.x509 import (
3031
Certificate,
@@ -403,6 +404,28 @@ def rekor_entry(self, client: RekorClient) -> LogEntry:
403404
f"has_inclusion_promise={has_inclusion_promise}"
404405
)
405406

407+
# This "expected" entry is used both to retrieve the Rekor entry
408+
# (if we don't have one) *and* to cross-check whatever response
409+
# we receive. See below.
410+
expected_entry = sigstore_rekor_types.Hashedrekord(
411+
kind="hashedrekord",
412+
api_version="0.0.1",
413+
spec=sigstore_rekor_types.HashedrekordV001Schema(
414+
signature=sigstore_rekor_types.Signature1(
415+
content=base64.b64encode(self.signature).decode(),
416+
public_key=sigstore_rekor_types.PublicKey1(
417+
content=base64_encode_pem_cert(self.certificate)
418+
),
419+
),
420+
data=sigstore_rekor_types.Data(
421+
hash=sigstore_rekor_types.Hash(
422+
algorithm=sigstore_rekor_types.Algorithm.SHA256,
423+
value=self.input_digest.hex(),
424+
),
425+
),
426+
),
427+
)
428+
406429
entry: LogEntry | None = None
407430
if offline:
408431
logger.debug("offline mode; using offline log entry")
@@ -415,53 +438,22 @@ def rekor_entry(self, client: RekorClient) -> LogEntry:
415438
# entry doesn't have one, then we perform a lookup.
416439
if not has_inclusion_proof:
417440
logger.debug("retrieving transparency log entry")
418-
entry = client.log.entries.retrieve.post(
419-
self.signature,
420-
self.input_digest.hex(),
421-
self.certificate,
422-
)
441+
entry = client.log.entries.retrieve.post(expected_entry)
423442
else:
424443
entry = self._rekor_entry
425444

426445
# No matter what we do above, we must end up with a Rekor entry.
427446
if entry is None:
428447
raise RekorEntryMissing
429448

430-
# To verify that an entry matches our other signing materials,
431-
# we transform our signature, artifact hash, and certificate
432-
# into a "hashedrekord" style payload and compare it against the
433-
# entry's own body.
434-
#
435-
# This is done by:
436-
#
437-
# * Serializing the certificate as PEM, and then base64-encoding it;
438-
# * base64-encoding the signature;
439-
# * Packing the resulting cert, signature, and hash into the
440-
# hashedrekord body format;
441-
# * Comparing that body against the entry's own body, which
442-
# is extracted from its base64(json(...)) encoding.
443-
444449
logger.debug("Rekor entry: ensuring contents match signing materials")
445450

446-
expected_body = {
447-
"kind": "hashedrekord",
448-
"apiVersion": "0.0.1",
449-
"spec": {
450-
"signature": {
451-
"content": B64Str(base64.b64encode(self.signature).decode()),
452-
"publicKey": {
453-
"content": B64Str(base64_encode_pem_cert(self.certificate))
454-
},
455-
},
456-
"data": {
457-
"hash": {"algorithm": "sha256", "value": self.input_digest.hex()}
458-
},
459-
},
460-
}
461-
451+
# To catch a potentially dishonest or compromised Rekor instance, we compare
452+
# the expected entry (generated above) with the JSON structure returned
453+
# by Rekor. If the two don't match, then we have an invalid entry
454+
# and can't proceed.
462455
actual_body = json.loads(base64.b64decode(entry.body))
463-
464-
if expected_body != actual_body:
456+
if actual_body != expected_entry.model_dump(mode="json", by_alias=True):
465457
raise InvalidRekorEntry
466458

467459
return entry

test/unit/verify/test_models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def test_rekor_entry_missing(self, signing_materials):
5757
a_materials._rekor_entry = None
5858
client = pretend.stub(
5959
log=pretend.stub(
60-
entries=pretend.stub(retrieve=pretend.stub(post=lambda a, b, c: None))
60+
entries=pretend.stub(retrieve=pretend.stub(post=lambda a: None))
6161
)
6262
)
6363

0 commit comments

Comments
 (0)