Skip to content

Commit 4b2a03a

Browse files
merge updates
Signed-off-by: Ramon Petgrave <[email protected]>
1 parent 5baeb8f commit 4b2a03a

File tree

3 files changed

+129
-68
lines changed

3 files changed

+129
-68
lines changed

sigstore/_internal/rekor/__init__.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,54 @@
1717
"""
1818

1919
import base64
20+
from abc import ABC, abstractmethod
2021

2122
import rekor_types
2223
from cryptography.x509 import Certificate
2324

25+
from sigstore._internal.rekor.v2_types.dev.sigstore.rekor import v2
2426
from sigstore._utils import base64_encode_pem_cert
27+
from sigstore.dsse import Envelope
2528
from sigstore.hashes import Hashed
29+
from sigstore.models import LogEntry
2630

2731
__all__ = [
2832
"_hashedrekord_from_parts",
2933
]
3034

3135

36+
class RekorLogSubmitter(ABC):
37+
@abstractmethod
38+
def create_entry(
39+
self,
40+
request: rekor_types.Hashedrekord | rekor_types.Dsse | v2.CreateEntryRequest,
41+
) -> LogEntry:
42+
"""
43+
Submit the request to Rekor.
44+
"""
45+
pass
46+
47+
@classmethod
48+
@abstractmethod
49+
def _build_hashed_rekord_request(
50+
self, hashed_input: Hashed, signature: bytes, certificate: Certificate
51+
) -> rekor_types.Hashedrekord | v2.CreateEntryRequest:
52+
"""
53+
Construct a hashed rekord request to submit to Rekor.
54+
"""
55+
pass
56+
57+
@classmethod
58+
@abstractmethod
59+
def _build_dsse_request(
60+
self, envelope: Envelope, certificate: Certificate
61+
) -> rekor_types.Dsse | v2.CreateEntryRequest:
62+
"""
63+
Construct a dsse request to submit to Rekor.
64+
"""
65+
pass
66+
67+
3268
# TODO: This should probably live somewhere better.
3369
def _hashedrekord_from_parts(
3470
cert: Certificate, sig: bytes, hashed: Hashed

sigstore/_internal/rekor/client_v2.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from cryptography.x509 import Certificate
2828

2929
from sigstore._internal import USER_AGENT
30+
from sigstore._internal.rekor import RekorLogSubmitter
3031
from sigstore._internal.rekor.v2_types.dev.sigstore.common import v1 as common_v1
3132
from sigstore._internal.rekor.v2_types.dev.sigstore.rekor import v2
3233
from sigstore._internal.rekor.v2_types.io import intoto as v2_intoto
@@ -41,12 +42,12 @@
4142

4243
# TODO: Link to merged documenation.
4344
# See https://github.com/sigstore/rekor-tiles/pull/255/files#diff-eb568acf84d583e4d3734b07773e96912277776bad39c560392aa33ea2cf2210R196
44-
CREATE_ENTRIES_TIMEOUT_SECONDS = 10
45+
CREATE_ENTRIES_TIMEOUT_SECONDS = 20
4546

4647
DEFAULT_KEY_DETAILS = common_v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_256
4748

4849

49-
class RekorV2Client:
50+
class RekorV2Client(RekorLogSubmitter):
5051
"""The internal Rekor client for the v2 API"""
5152

5253
# TODO: implement get_tile, get_entry_bundle, get_checkpoint.
@@ -71,15 +72,22 @@ def __del__(self) -> None:
7172
"""
7273
self.session.close()
7374

74-
def create_entry(self, request: v2.CreateEntryRequest) -> LogEntry:
75+
# TODO: when we remove the original Rekor client, remove the type ignore here
76+
def create_entry(self, request: v2.CreateEntryRequest) -> LogEntry: # type: ignore[override]
7577
"""
7678
Submit a new entry for inclusion in the Rekor log.
7779
"""
7880
# TODO: There may be a bug in betterproto, where the V_0_0_2 is changed to V002,
7981
# Or it is an issue with the proto `json_value`.
8082
# See https://github.com/sigstore/rekor-tiles/blob/bd5893730de581629a5f475923c663f776793496/api/proto/rekor_service.proto#L66.
8183
payload = request.to_dict()
82-
_logger.debug(f"proposed: {json.dumps(payload)}")
84+
if "hashedRekordRequestV002" in payload:
85+
payload["hashedRekordRequestV0_0_2"] = payload.pop(
86+
"hashedRekordRequestV002"
87+
)
88+
if "dsseRequestV002" in payload:
89+
payload["dsseRequestV0_0_2"] = payload.pop("dsseRequestV002")
90+
_logger.debug(f"request: {json.dumps(payload)}")
8391
resp = self.session.post(
8492
f"{self.url}/log/entries",
8593
json=payload,
@@ -96,20 +104,23 @@ def create_entry(self, request: v2.CreateEntryRequest) -> LogEntry:
96104
return LogEntry._from_dict_rekor(integrated_entry)
97105

98106
@classmethod
99-
def _build_hashed_rekord_create_entry_request(
107+
def _build_hashed_rekord_request(
100108
cls,
101-
artifact_hashed_input: Hashed,
102-
artifact_signature: bytes,
103-
signing_certificate: Certificate,
109+
hashed_input: Hashed,
110+
signature: bytes,
111+
certificate: Certificate,
104112
) -> v2.CreateEntryRequest:
113+
"""
114+
Construct a hashed rekord request to submit to Rekor.
115+
"""
105116
return v2.CreateEntryRequest(
106117
hashed_rekord_request_v0_0_2=v2.HashedRekordRequestV002(
107-
digest=artifact_hashed_input.digest,
118+
digest=hashed_input.digest,
108119
signature=v2.Signature(
109-
content=artifact_signature,
120+
content=signature,
110121
verifier=v2.Verifier(
111122
x509_certificate=common_v1.X509Certificate(
112-
raw_bytes=signing_certificate.public_bytes(
123+
raw_bytes=certificate.public_bytes(
113124
encoding=serialization.Encoding.DER
114125
)
115126
),
@@ -120,9 +131,12 @@ def _build_hashed_rekord_create_entry_request(
120131
)
121132

122133
@classmethod
123-
def _build_dsse_create_entry_request(
124-
cls, envelope: Envelope, signing_certificate: Certificate
134+
def _build_dsse_request(
135+
cls, envelope: Envelope, certificate: Certificate
125136
) -> v2.CreateEntryRequest:
137+
"""
138+
Construct a dsse request to submit to Rekor.
139+
"""
126140
return v2.CreateEntryRequest(
127141
dsse_request_v0_0_2=v2.DsseRequestV002(
128142
envelope=v2_intoto.Envelope(
@@ -139,7 +153,7 @@ def _build_dsse_create_entry_request(
139153
verifiers=[
140154
v2.Verifier(
141155
x509_certificate=common_v1.X509Certificate(
142-
raw_bytes=signing_certificate.public_bytes(
156+
raw_bytes=certificate.public_bytes(
143157
encoding=serialization.Encoding.DER
144158
)
145159
),

test/unit/internal/rekor/test_client_v2.py

Lines changed: 65 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import hashlib
2+
import os
23
import secrets
34

45
import pytest
6+
from id import (
7+
detect_credential,
8+
)
59

610
from sigstore import dsse
11+
from sigstore._internal.rekor.client import STAGING_REKOR_URL
712
from sigstore._internal.rekor.client_v2 import (
813
DEFAULT_KEY_DETAILS,
9-
DEFAULT_REKOR_URL,
10-
STAGING_REKOR_URL,
1114
Certificate,
1215
Hashed,
1316
LogEntry,
@@ -17,10 +20,11 @@
1720
v2,
1821
v2_intoto,
1922
)
20-
21-
# from sigstore.models import rekor_v1
23+
from sigstore._internal.trust import ClientTrustConfig
2224
from sigstore._utils import sha256_digest
23-
from sigstore.sign import ec
25+
from sigstore.models import rekor_v1
26+
from sigstore.oidc import _DEFAULT_AUDIENCE, IdentityToken
27+
from sigstore.sign import SigningContext, ec
2428

2529
ALPHA_REKOR_V2_URL = "https://log2025-alpha1.rekor.sigstage.dev"
2630
LOCAL_REKOR_V2_URL = "http://localhost:3000"
@@ -29,12 +33,13 @@
2933
# TODO: add staging and production URLs when available,
3034
# and local after using scaffolding/setup-sigstore-env action
3135
@pytest.fixture(
36+
scope="session",
3237
params=[
3338
ALPHA_REKOR_V2_URL,
3439
pytest.param(STAGING_REKOR_URL, marks=pytest.mark.xfail),
35-
pytest.param(DEFAULT_REKOR_URL, marks=pytest.mark.xfail),
36-
pytest.param(LOCAL_REKOR_V2_URL, marks=pytest.mark.xfail),
37-
]
40+
# pytest.param(DEFAULT_REKOR_URL, marks=pytest.mark.xfail),
41+
# pytest.param(LOCAL_REKOR_V2_URL, marks=pytest.mark.xfail),
42+
],
3843
)
3944
def client(request) -> RekorV2Client:
4045
"""
@@ -44,37 +49,46 @@ def client(request) -> RekorV2Client:
4449
return RekorV2Client(base_url=request.param)
4550

4651

47-
@pytest.fixture()
48-
def sample_signer(staging):
52+
@pytest.fixture(scope="session")
53+
def sample_cert_and_private_key() -> tuple[Certificate, ec.EllipticCurvePrivateKey]:
4954
"""
50-
Returns a `Signer`.
55+
Returns a sample Certificate and ec.EllipticCurvePrivateKey.
5156
"""
52-
sign_ctx_cls, _, id_token = staging
53-
with sign_ctx_cls().signer(id_token) as signer:
54-
return signer
57+
# Detect env variable for local interactive tests.
58+
token = os.getenv("SIGSTORE_IDENTITY_TOKEN_staging")
59+
if not token:
60+
# If the variable is not defined, try getting an ambient token.
61+
token = detect_credential(_DEFAULT_AUDIENCE)
5562

63+
with SigningContext.from_trust_config(ClientTrustConfig.staging()).signer(
64+
IdentityToken(token)
65+
) as signer:
66+
return signer._signing_cert(), signer._private_key
5667

57-
@pytest.fixture()
68+
69+
@pytest.fixture(scope="session")
5870
def sample_hashed_rekord_request_materials(
59-
sample_signer,
71+
sample_cert_and_private_key,
6072
) -> tuple[Hashed, bytes, Certificate]:
6173
"""
6274
Creates materials needed for `RekorV2Client._build_hashed_rekord_create_entry_request`.
6375
"""
64-
cert = sample_signer._signing_cert()
76+
cert, private_key = sample_cert_and_private_key
6577
hashed_input = sha256_digest(secrets.token_bytes(32))
66-
signature = sample_signer._private_key.sign(
78+
signature = private_key.sign(
6779
hashed_input.digest, ec.ECDSA(hashed_input._as_prehashed())
6880
)
6981
return hashed_input, signature, cert
7082

7183

72-
@pytest.fixture()
73-
def sample_dsse_request_materials(sample_signer) -> tuple[dsse.Envelope, Certificate]:
84+
@pytest.fixture(scope="session")
85+
def sample_dsse_request_materials(
86+
sample_cert_and_private_key,
87+
) -> tuple[dsse.Envelope, Certificate]:
7488
"""
7589
Creates materials needed for `RekorV2Client._build_dsse_create_entry_request`.
7690
"""
77-
cert = sample_signer._signing_cert()
91+
cert, private_key = sample_cert_and_private_key
7892
stmt = (
7993
dsse.StatementBuilder()
8094
.subjects(
@@ -92,49 +106,34 @@ def sample_dsse_request_materials(sample_signer) -> tuple[dsse.Envelope, Certifi
92106
}
93107
)
94108
).build()
95-
envelope = dsse._sign(key=sample_signer._private_key, stmt=stmt)
109+
envelope = dsse._sign(key=private_key, stmt=stmt)
96110
return envelope, cert
97111

98112

99-
@pytest.fixture()
113+
@pytest.fixture(scope="session")
100114
def sample_hashed_rekord_create_entry_request(
101115
sample_hashed_rekord_request_materials,
102116
) -> v2.CreateEntryRequest:
103117
"""
104118
Returns a sample `CreateEntryRequest` for for hashedrekor.
105119
"""
106120
hashed_input, signature, cert = sample_hashed_rekord_request_materials
107-
return RekorV2Client._build_hashed_rekord_create_entry_request(
108-
artifact_hashed_input=hashed_input,
109-
artifact_signature=signature,
110-
signing_certificate=cert,
121+
return RekorV2Client._build_hashed_rekord_request(
122+
hashed_input=hashed_input,
123+
signature=signature,
124+
certificate=cert,
111125
)
112126

113127

114-
@pytest.fixture()
128+
@pytest.fixture(scope="session")
115129
def sample_dsse_create_entry_request(
116130
sample_dsse_request_materials,
117131
) -> v2.CreateEntryRequest:
118132
"""
119133
Returns a sample `CreateEntryRequest` for for dsse.
120134
"""
121135
envelope, cert = sample_dsse_request_materials
122-
return RekorV2Client._build_dsse_create_entry_request(
123-
envelope=envelope, signing_certificate=cert
124-
)
125-
126-
127-
@pytest.fixture(
128-
params=[
129-
sample_hashed_rekord_create_entry_request,
130-
sample_dsse_create_entry_request,
131-
]
132-
)
133-
def sample_create_entry_request(request) -> v2.CreateEntryRequest:
134-
"""
135-
Returns a sample `CreateEntryRequest`, for each of the the params in the supplied fixture.
136-
"""
137-
return request.getfixturevalue(request.param.__name__)
136+
return RekorV2Client._build_dsse_request(envelope=envelope, certificate=cert)
138137

139138

140139
@pytest.mark.ambient_oidc
@@ -159,10 +158,10 @@ def test_build_hashed_rekord_create_entry_request(
159158
),
160159
)
161160
)
162-
actual_request = RekorV2Client._build_hashed_rekord_create_entry_request(
163-
artifact_hashed_input=hashed_input,
164-
artifact_signature=signature,
165-
signing_certificate=cert,
161+
actual_request = RekorV2Client._build_hashed_rekord_request(
162+
hashed_input=hashed_input,
163+
signature=signature,
164+
certificate=cert,
166165
)
167166
assert expected_request == actual_request
168167

@@ -196,18 +195,30 @@ def test_build_dsse_create_entry_request(sample_dsse_request_materials):
196195
],
197196
)
198197
)
199-
actual_request = RekorV2Client._build_dsse_create_entry_request(
200-
envelope=envelope, signing_certificate=cert
198+
actual_request = RekorV2Client._build_dsse_request(
199+
envelope=envelope, certificate=cert
201200
)
202201
assert expected_request == actual_request
203202

204203

204+
@pytest.mark.parametrize(
205+
"sample_create_entry_request",
206+
[
207+
sample_hashed_rekord_create_entry_request.__name__,
208+
sample_dsse_create_entry_request.__name__,
209+
],
210+
)
205211
@pytest.mark.ambient_oidc
206-
def test_create_entry(sample_create_entry_request, client):
212+
def test_create_entry(
213+
request: pytest.FixtureRequest,
214+
sample_create_entry_request: str,
215+
client: RekorV2Client,
216+
):
207217
"""
208218
Sends a request to RekorV2 and ensure's the response is parseable to a `LogEntry` and a `TransparencyLogEntry`.
209219
"""
210-
log_entry = client.create_entry(sample_create_entry_request)
220+
log_entry = client.create_entry(
221+
request.getfixturevalue(sample_create_entry_request)
222+
)
211223
assert isinstance(log_entry, LogEntry)
212-
# TODO: Pending https://github.com/sigstore/sigstore-python/pull/1370
213-
# assert isinstance(log_entry._to_rekor(), rekor_v1.TransparencyLogEntry)
224+
assert isinstance(log_entry._to_rekor(), rekor_v1.TransparencyLogEntry)

0 commit comments

Comments
 (0)