Skip to content

Commit 9782545

Browse files
committed
Merge branch 'main' into ww/rm-protobufs
2 parents 90b3896 + 5ea398f commit 9782545

File tree

15 files changed

+262
-132
lines changed

15 files changed

+262
-132
lines changed

.github/workflows/scorecards-analysis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,6 @@ jobs:
5252

5353
# Upload the results to GitHub's code scanning dashboard.
5454
- name: "Upload to code-scanning"
55-
uses: github/codeql-action/upload-sarif@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4
55+
uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
5656
with:
5757
sarif_file: results.sarif

install/requirements.txt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -539,9 +539,9 @@ rfc8785==0.1.4 \
539539
--hash=sha256:520d690b448ecf0703691c76e1a34a24ddcd4fc5bc41d589cb7c58ec651bcd48 \
540540
--hash=sha256:e545841329fe0eee4f6a3b44e7034343100c12b4ec566dc06ca9735681deb4da
541541
# via sigstore
542-
rich==14.0.0 \
543-
--hash=sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0 \
544-
--hash=sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725
542+
rich==14.1.0 \
543+
--hash=sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f \
544+
--hash=sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8
545545
# via sigstore
546546
securesystemslib==1.3.0 \
547547
--hash=sha256:5b53e5989289d97fa42ed7fde1b4bad80985f15dba8c774c043b395a90c908e5 \
@@ -550,7 +550,7 @@ securesystemslib==1.3.0 \
550550
sigstore==3.6.4 \
551551
--hash=sha256:76f247a86738c9e076a243e0068ac68625848868890ed38491acc159752a46ac \
552552
--hash=sha256:d5678a7f4b78b084eb2c1a9eab31af81e6daf1f949abc3b7539a96900220d0d6
553-
# via -r requirements.in
553+
# via -r install/requirements.in
554554
sigstore-protobuf-specs==0.3.2 \
555555
--hash=sha256:50c99fa6747a3a9c5c562a43602cf76df0b199af28f0e9d4319b6775630425ea \
556556
--hash=sha256:cae041b40502600b8a633f43c257695d0222a94efa1e5110a7ec7ada78c39d99
@@ -575,7 +575,6 @@ typing-extensions==4.14.0 \
575575
# pydantic
576576
# pydantic-core
577577
# pyopenssl
578-
# rich
579578
# typing-inspection
580579
typing-inspection==0.4.1 \
581580
--hash=sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51 \

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ lint = [
6363
"mypy ~= 1.1",
6464
# NOTE(ww): ruff is under active development, so we pin conservatively here
6565
# and let Dependabot periodically perform this update.
66-
"ruff < 0.12.6",
66+
"ruff < 0.12.8",
6767
"types-requests",
6868
"types-pyOpenSSL",
6969
]

sigstore/_cli.py

Lines changed: 78 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@
2020
import logging
2121
import os
2222
import sys
23+
from concurrent import futures
2324
from dataclasses import dataclass
2425
from pathlib import Path
25-
from typing import Any, NoReturn, TextIO, Union
26+
from typing import Any, NoReturn, Union
2627

2728
from cryptography.hazmat.primitives.serialization import Encoding
2829
from cryptography.x509 import load_pem_x509_certificate
@@ -54,7 +55,7 @@
5455
Issuer,
5556
detect_credential,
5657
)
57-
from sigstore.sign import SigningContext
58+
from sigstore.sign import Signer, SigningContext
5859
from sigstore.verify import (
5960
Verifier,
6061
policy,
@@ -634,6 +635,57 @@ def _get_identity_token(args: argparse.Namespace) -> None:
634635
_invalid_arguments(args, "No identity token supplied or detected!")
635636

636637

638+
def _sign_file_threaded(
639+
signer: Signer,
640+
predicate_type: str | None,
641+
predicate: dict[str, Any] | None,
642+
file: Path,
643+
outputs: SigningOutputs,
644+
) -> None:
645+
"""sign method to be called from signing thread"""
646+
_logger.debug(f"signing for {file.name}")
647+
with file.open(mode="rb") as io:
648+
# The input can be indefinitely large, so we perform a streaming
649+
# digest and sign the prehash rather than buffering it fully.
650+
digest = sha256_digest(io)
651+
try:
652+
if predicate is None:
653+
result = signer.sign_artifact(input_=digest)
654+
else:
655+
subject = Subject(name=file.name, digest={"sha256": digest.digest.hex()})
656+
statement_builder = StatementBuilder(
657+
subjects=[subject],
658+
predicate_type=predicate_type,
659+
predicate=predicate,
660+
)
661+
result = signer.sign_dsse(statement_builder.build())
662+
except ExpiredIdentity as exp_identity:
663+
_logger.error("Signature failed: identity token has expired")
664+
raise exp_identity
665+
666+
except ExpiredCertificate as exp_certificate:
667+
_logger.error("Signature failed: Fulcio signing certificate has expired")
668+
raise exp_certificate
669+
670+
_logger.info(
671+
f"Transparency log entry created at index: {result.log_entry._inner.log_index}"
672+
)
673+
674+
if outputs.signature is not None:
675+
signature = base64.b64encode(result.signature).decode()
676+
with outputs.signature.open(mode="w") as io:
677+
print(signature, file=io)
678+
679+
if outputs.certificate is not None:
680+
cert_pem = signer._signing_cert().public_bytes(Encoding.PEM).decode()
681+
with outputs.certificate.open(mode="w") as io:
682+
print(cert_pem, file=io)
683+
684+
if outputs.bundle is not None:
685+
with outputs.bundle.open(mode="w") as io:
686+
print(result.to_json(), file=io)
687+
688+
637689
def _sign_common(
638690
args: argparse.Namespace, output_map: OutputMap, predicate: dict[str, Any] | None
639691
) -> None:
@@ -664,63 +716,37 @@ def _sign_common(
664716
if not identity:
665717
_invalid_arguments(args, "No identity token supplied or detected!")
666718

667-
with signing_ctx.signer(identity) as signer:
668-
for file, outputs in output_map.items():
669-
_logger.debug(f"signing for {file.name}")
670-
with file.open(mode="rb") as io:
671-
# The input can be indefinitely large, so we perform a streaming
672-
# digest and sign the prehash rather than buffering it fully.
673-
digest = sha256_digest(io)
674-
try:
675-
if predicate is None:
676-
result = signer.sign_artifact(input_=digest)
677-
else:
678-
subject = Subject(
679-
name=file.name, digest={"sha256": digest.digest.hex()}
680-
)
681-
predicate_type = args.predicate_type
682-
statement_builder = StatementBuilder(
683-
subjects=[subject],
684-
predicate_type=predicate_type,
685-
predicate=predicate,
686-
)
687-
result = signer.sign_dsse(statement_builder.build())
688-
except ExpiredIdentity as exp_identity:
689-
print("Signature failed: identity token has expired")
690-
raise exp_identity
691-
692-
except ExpiredCertificate as exp_certificate:
693-
print("Signature failed: Fulcio signing certificate has expired")
694-
raise exp_certificate
695-
696-
print("Using ephemeral certificate:")
697-
cert = result.signing_certificate
698-
cert_pem = cert.public_bytes(Encoding.PEM).decode()
699-
print(cert_pem)
700-
701-
print(
702-
f"Transparency log entry created at index: {result.log_entry._inner.log_index}"
703-
)
719+
# Not all commands provide --predicate-type
720+
predicate_type = getattr(args, "predicate_type", None)
704721

705-
sig_output: TextIO
706-
if outputs.signature is not None:
707-
sig_output = outputs.signature.open("w")
708-
else:
709-
sig_output = sys.stdout
722+
with signing_ctx.signer(identity) as signer:
723+
print("Using ephemeral certificate:")
724+
cert_pem = signer._signing_cert().public_bytes(Encoding.PEM).decode()
725+
print(cert_pem)
726+
727+
# sign in threads: this is relevant for especially Rekor v2 as otherwise we wait
728+
# for log inclusion for each signature separately
729+
with futures.ThreadPoolExecutor() as executor:
730+
jobs = [
731+
executor.submit(
732+
_sign_file_threaded,
733+
signer,
734+
predicate_type,
735+
predicate,
736+
file,
737+
outputs,
738+
)
739+
for file, outputs in output_map.items()
740+
]
741+
for job in futures.as_completed(jobs):
742+
job.result()
710743

711-
signature = base64.b64encode(result.signature).decode()
712-
print(signature, file=sig_output)
744+
for file, outputs in output_map.items():
713745
if outputs.signature is not None:
714746
print(f"Signature written to {outputs.signature}")
715-
716747
if outputs.certificate is not None:
717-
with outputs.certificate.open(mode="w") as io:
718-
print(cert_pem, file=io)
719748
print(f"Certificate written to {outputs.certificate}")
720-
721749
if outputs.bundle is not None:
722-
with outputs.bundle.open(mode="w") as io:
723-
print(result.to_json(), file=io)
724750
print(f"Sigstore bundle written to {outputs.bundle}")
725751

726752

sigstore/_internal/rekor/client.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,20 @@ def from_response(cls, dict_: dict[str, Any]) -> RekorLogInfo:
7373

7474

7575
class _Endpoint(ABC):
76-
def __init__(self, url: str, session: requests.Session) -> None:
76+
def __init__(self, url: str, session: requests.Session | None = None) -> None:
77+
# Note that _Endpoint may not be thread be safe if the same Session is provided
78+
# to an _Endpoint in multiple threads
7779
self.url = url
80+
if session is None:
81+
session = requests.Session()
82+
session.headers.update(
83+
{
84+
"Content-Type": "application/json",
85+
"Accept": "application/json",
86+
"User-Agent": USER_AGENT,
87+
}
88+
)
89+
7890
self.session = session
7991

8092

@@ -219,20 +231,6 @@ def __init__(self, url: str) -> None:
219231
Create a new `RekorClient` from the given URL.
220232
"""
221233
self.url = f"{url}/api/v1"
222-
self.session = requests.Session()
223-
self.session.headers.update(
224-
{
225-
"Content-Type": "application/json",
226-
"Accept": "application/json",
227-
"User-Agent": USER_AGENT,
228-
}
229-
)
230-
231-
def __del__(self) -> None:
232-
"""
233-
Terminates the underlying network session.
234-
"""
235-
self.session.close()
236234

237235
@classmethod
238236
def production(cls) -> RekorClient:
@@ -255,7 +253,8 @@ def log(self) -> RekorLog:
255253
"""
256254
Returns a `RekorLog` adapter for making requests to a Rekor log.
257255
"""
258-
return RekorLog(f"{self.url}/log", session=self.session)
256+
257+
return RekorLog(f"{self.url}/log")
259258

260259
def create_entry(self, request: EntryRequestBody) -> TransparencyLogEntry:
261260
"""

sigstore/_internal/rekor/client_v2.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,6 @@ def __init__(self, base_url: str) -> None:
5555
Create a new `RekorV2Client` from the given URL.
5656
"""
5757
self.url = f"{base_url}/api/v2"
58-
self.session = requests.Session()
59-
self.session.headers.update(
60-
{
61-
"Content-Type": "application/json",
62-
"Accept": "application/json",
63-
"User-Agent": USER_AGENT,
64-
}
65-
)
66-
67-
def __del__(self) -> None:
68-
"""
69-
Terminates the underlying network session.
70-
"""
71-
self.session.close()
7258

7359
def create_entry(self, payload: EntryRequestBody) -> TransparencyLogEntry:
7460
"""
@@ -79,7 +65,19 @@ def create_entry(self, payload: EntryRequestBody) -> TransparencyLogEntry:
7965
https://github.com/sigstore/rekor-tiles/blob/main/CLIENTS.md#handling-longer-requests
8066
"""
8167
_logger.debug(f"proposed: {json.dumps(payload)}")
82-
resp = self.session.post(
68+
69+
# Use a short lived session to avoid potential issues with multi-threading:
70+
# Session thread-safety is ambiguous
71+
session = requests.Session()
72+
session.headers.update(
73+
{
74+
"Content-Type": "application/json",
75+
"Accept": "application/json",
76+
"User-Agent": USER_AGENT,
77+
}
78+
)
79+
80+
resp = session.post(
8381
f"{self.url}/log/entries",
8482
json=payload,
8583
)

sigstore/_internal/timestamp.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -68,19 +68,6 @@ def __init__(self, url: str) -> None:
6868
Create a new `TimestampAuthorityClient` from the given URL.
6969
"""
7070
self.url = url
71-
self.session = requests.Session()
72-
self.session.headers.update(
73-
{
74-
"Content-Type": "application/timestamp-query",
75-
"User-Agent": USER_AGENT,
76-
}
77-
)
78-
79-
def __del__(self) -> None:
80-
"""
81-
Terminates the underlying network session.
82-
"""
83-
self.session.close()
8471

8572
def request_timestamp(self, signature: bytes) -> TimeStampResponse:
8673
"""
@@ -104,9 +91,18 @@ def request_timestamp(self, signature: bytes) -> TimeStampResponse:
10491
msg = f"invalid request: {error}"
10592
raise TimestampError(msg)
10693

94+
# Use single use session to avoid potential Session thread safety issues
95+
session = requests.Session()
96+
session.headers.update(
97+
{
98+
"Content-Type": "application/timestamp-query",
99+
"User-Agent": USER_AGENT,
100+
}
101+
)
102+
107103
# Send it to the TSA for signing
108104
try:
109-
response = self.session.post(
105+
response = session.post(
110106
self.url,
111107
data=timestamp_request.as_bytes(),
112108
timeout=CLIENT_TIMEOUT,

sigstore/verify/verifier.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,18 @@ def _verify_signed_timestamp(
128128
for certificate_authority in cert_authorities:
129129
certificates = certificate_authority.certificates(allow_expired=True)
130130

131-
builder = VerifierBuilder()
132-
for certificate in certificates:
133-
builder.add_root_certificate(certificate)
131+
# We expect at least a signing cert and a root cert but there may be intermediates
132+
if len(certificates) < 2:
133+
_logger.debug("Unable to verify Timestamp: cert chain is incomplete")
134+
continue
135+
136+
builder = (
137+
VerifierBuilder()
138+
.tsa_certificate(certificates[0])
139+
.add_root_certificate(certificates[-1])
140+
)
141+
for certificate in certificates[1:-1]:
142+
builder = builder.add_intermediate_certificate(certificate)
134143

135144
verifier = builder.build()
136145
try:

test/assets/integration/b.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
DO NOT MODIFY ME!
2+
3+
this is "b.txt", a sample input for sigstore-python's unit tests.
4+
5+
DO NOT MODIFY ME!

test/assets/integration/c.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
DO NOT MODIFY ME!
2+
3+
this is "c.txt", a sample input for sigstore-python's unit tests.
4+
5+
DO NOT MODIFY ME!

0 commit comments

Comments
 (0)