Skip to content

Add option to sign multiple artifacts with the same key and certificate #645

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2b40813
Add option to sign multiple artifacts with the same key and cert
mayaCostantini May 3, 2023
3a0f6b7
Fix linting
mayaCostantini May 3, 2023
ff30b09
Refactor signing API into SigningContext and Signer
mayaCostantini May 18, 2023
7f13538
Change --single-cert option to --no-cache in README.md
mayaCostantini May 18, 2023
7842675
Make _signing_cert a method instead of a property
mayaCostantini May 22, 2023
b404267
Do not store non-cached attributes
mayaCostantini May 23, 2023
2205460
Rename with_signer context manager to signer
mayaCostantini May 23, 2023
9d908c6
Merge branch 'main' into single-certificate-flow
woodruffw May 23, 2023
f075f56
Update sigstore/sign.py
woodruffw May 23, 2023
abfa525
sign: remove __del__
woodruffw May 23, 2023
68b9a9d
sigstore: simplify OIDC token handling
woodruffw May 23, 2023
5bef356
test: fixups, disable some old tests
woodruffw May 23, 2023
9fbb5d8
test: lintage
woodruffw May 23, 2023
b5d5025
Merge branch 'main' into single-certificate-flow
woodruffw May 24, 2023
839b1fb
sigstore, test: lintage, fixups
woodruffw May 24, 2023
0625ef0
test: lintage
woodruffw May 24, 2023
4ac1964
_cli, README: label `--no-cache` as advanced
woodruffw May 25, 2023
bc556bb
_cli: give the flag a scary name
woodruffw May 25, 2023
0262b0b
sigstore, test: make `nbf` claim optional
woodruffw May 25, 2023
7b1b310
Merge remote-tracking branch 'origin/main' into single-certificate-flow
woodruffw May 25, 2023
36cdad5
CHANGELOG: record changes
woodruffw May 25, 2023
568aa68
README, _cli: remove flag
woodruffw May 26, 2023
b9d4a0e
Merge branch 'main' into single-certificate-flow
woodruffw May 30, 2023
a073454
Merge branch 'main' into single-certificate-flow
woodruffw Jun 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ usage: sigstore sign [-h] [--identity-token TOKEN] [--oidc-client-id ID]
[--oidc-disable-ambient-providers] [--oidc-issuer URL]
[--no-default-files] [--signature FILE]
[--certificate FILE] [--bundle FILE]
[--output-directory DIR] [--overwrite] [--staging]
[--rekor-url URL] [--rekor-root-pubkey FILE]
[--output-directory DIR] [--overwrite] [--no-cache]
[--staging] [--rekor-url URL] [--rekor-root-pubkey FILE]
[--fulcio-url URL] [--ctfe FILE]
FILE [FILE ...]

Expand Down Expand Up @@ -179,6 +179,9 @@ Output options:
--overwrite Overwrite preexisting signature and certificate
outputs, if present (default: False)

--no-cache Generate a new signing certificate and private key for
each artifact signed (default: False)

Sigstore instance options:
--staging Use sigstore's staging instances, instead of the
default production instances. This option will be
Expand Down
91 changes: 58 additions & 33 deletions sigstore/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,13 @@

from sigstore import __version__
from sigstore._internal.ctfe import CTKeyring
from sigstore._internal.fulcio.client import DEFAULT_FULCIO_URL, FulcioClient
from sigstore._internal.fulcio.client import (
DEFAULT_FULCIO_URL,
ExpiredCertificate,
FulcioClient,
)
from sigstore._internal.keyring import Keyring
from sigstore._internal.oidc import ExpiredIdentity
from sigstore._internal.rekor.client import (
DEFAULT_REKOR_URL,
RekorClient,
Expand All @@ -44,7 +49,7 @@
Issuer,
detect_credential,
)
from sigstore.sign import Signer
from sigstore.sign import SigningContext
from sigstore.transparency import LogEntry
from sigstore.verify import (
CertificateVerificationFailure,
Expand Down Expand Up @@ -329,6 +334,13 @@ def _parser() -> argparse.ArgumentParser:
default=_boolify_env("SIGSTORE_OVERWRITE"),
help="Overwrite preexisting signature and certificate outputs, if present",
)
output_options.add_argument(
"--no-cache",
dest="no_cache",
action="store_true",
default=_boolify_env("SIGSTORE_NO_CACHE"),
help="Generate a new signing certificate and private key for each artifact signed",
)

instance_options = sign.add_argument_group("Sigstore instance options")
_add_shared_instance_options(instance_options)
Expand Down Expand Up @@ -611,13 +623,13 @@ def _sign(args: argparse.Namespace) -> None:
"bundle": bundle,
}

# Select the signer to use.
# Select the signing context to use.
if args.staging:
logger.debug("sign: staging instances requested")
signer = Signer.staging()
signing_ctx = SigningContext.staging()
args.oidc_issuer = STAGING_OAUTH_ISSUER_URL
elif args.fulcio_url == DEFAULT_FULCIO_URL and args.rekor_url == DEFAULT_REKOR_URL:
signer = Signer.production()
signing_ctx = SigningContext.production()
else:
# Assume "production" keys if none are given as arguments
updater = TrustUpdater.production()
Expand All @@ -633,7 +645,7 @@ def _sign(args: argparse.Namespace) -> None:
ct_keyring = CTKeyring(Keyring(ctfe_keys))
rekor_keyring = RekorKeyring(Keyring(rekor_keys))

signer = Signer(
signing_ctx = SigningContext(
fulcio=FulcioClient(args.fulcio_url),
rekor=RekorClient(args.rekor_url, rekor_keyring, ct_keyring),
)
Expand All @@ -648,38 +660,51 @@ def _sign(args: argparse.Namespace) -> None:
if not args.identity_token:
args._parser.error("No identity token supplied or detected!")

for file, outputs in output_map.items():
logger.debug(f"signing for {file.name}")
with file.open(mode="rb", buffering=0) as io:
result = signer.sign(
input_=io,
identity_token=args.identity_token,
cache = not args.no_cache
with signing_ctx.with_signer(
identity_token=args.identity_token, cache=cache
) as signer:
for file, outputs in output_map.items():
logger.debug(f"signing for {file.name}")
with file.open(mode="rb", buffering=0) as io:
try:
result = signer.sign(
input_=io, rekor=signing_ctx._rekor, fulcio=signing_ctx._fulcio
)
except ExpiredIdentity as exp_identity:
print("Signature failed: identity token has expired")
raise exp_identity

except ExpiredCertificate as exp_certificate:
print("Signature failed: Fulcio signing certificate has expired")
raise exp_certificate

print("Using ephemeral certificate:")
print(result.cert_pem)

print(
f"Transparency log entry created at index: {result.log_entry.log_index}"
)

print("Using ephemeral certificate:")
print(result.cert_pem)

print(f"Transparency log entry created at index: {result.log_entry.log_index}")

sig_output: TextIO
if outputs["sig"] is not None:
sig_output = outputs["sig"].open("w")
else:
sig_output = sys.stdout
sig_output: TextIO
if outputs["sig"] is not None:
sig_output = outputs["sig"].open("w")
else:
sig_output = sys.stdout

print(result.b64_signature, file=sig_output)
if outputs["sig"] is not None:
print(f"Signature written to {outputs['sig']}")
print(result.b64_signature, file=sig_output)
if outputs["sig"] is not None:
print(f"Signature written to {outputs['sig']}")

if outputs["cert"] is not None:
with outputs["cert"].open(mode="w") as io:
print(result.cert_pem, file=io)
print(f"Certificate written to {outputs['cert']}")
if outputs["cert"] is not None:
with outputs["cert"].open(mode="w") as io:
print(result.cert_pem, file=io)
print(f"Certificate written to {outputs['cert']}")

if outputs["bundle"] is not None:
with outputs["bundle"].open(mode="w") as io:
print(result._to_bundle().to_json(), file=io)
print(f"Sigstore bundle written to {outputs['bundle']}")
if outputs["bundle"] is not None:
with outputs["bundle"].open(mode="w") as io:
print(result._to_bundle().to_json(), file=io)
print(f"Sigstore bundle written to {outputs['bundle']}")


def _collect_verification_state(
Expand Down
2 changes: 2 additions & 0 deletions sigstore/_internal/fulcio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@

from .client import (
DetachedFulcioSCT,
ExpiredCertificate,
FulcioCertificateSigningResponse,
FulcioClient,
)

__all__ = [
"DetachedFulcioSCT",
"ExpiredCertificate",
"FulcioCertificateSigningResponse",
"FulcioClient",
]
4 changes: 4 additions & 0 deletions sigstore/_internal/fulcio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ def signature(self) -> bytes:
SignedCertificateTimestamp.register(DetachedFulcioSCT)


class ExpiredCertificate(Exception):
"""An error raised when the Certificate is expired."""


@dataclass(frozen=True)
class FulcioCertificateSigningResponse:
"""Certificate response"""
Expand Down
16 changes: 16 additions & 0 deletions sigstore/_internal/oidc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
OIDC functionality for sigstore-python.
"""

from datetime import datetime, timezone

import jwt
from id import IdentityError

Expand All @@ -29,6 +31,10 @@
DEFAULT_AUDIENCE = "sigstore"


class ExpiredIdentity(Exception):
"""An error raised when an identity token is expired."""


class Identity:
"""
A wrapper for an OIDC "identity", as extracted from an OIDC token.
Expand All @@ -40,6 +46,7 @@ def __init__(self, identity_token: str) -> None:
"""
identity_jwt = jwt.decode(identity_token, options={"verify_signature": False})

self.exp_timestamp = identity_jwt.get("exp")
self.issuer = identity_jwt.get("iss")
if self.issuer is None:
raise IdentityError("Identity token missing the required `iss` claim")
Expand Down Expand Up @@ -69,3 +76,12 @@ def __init__(self, identity_token: str) -> None:
self.proof = str(identity_jwt["sub"])
except KeyError:
raise IdentityError("Identity token missing `sub` claim")

def is_expired(self) -> bool:
"""Verify if the identity token for this `Identity` is expired."""
if self.exp_timestamp:
now: float = datetime.now(timezone.utc).timestamp()
token_timestamp: float = self.exp_timestamp
return now > token_timestamp
else:
raise ValueError("Identity token does not have an expiration timestamp")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's push this error state up to the constructor, and raise IdentityError (since it seems like a valid Identity needs this claim.)

Loading