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 1 commit
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] [--single-cert]
[--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)

--single-cert Use a single signing certificate and key to sign
multiple artifacts (default: False)

Sigstore instance options:
--staging Use sigstore's staging instances, instead of the
default production instances. This option will be
Expand Down
11 changes: 9 additions & 2 deletions sigstore/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,12 @@ def _parser() -> argparse.ArgumentParser:
default=_boolify_env("SIGSTORE_OVERWRITE"),
help="Overwrite preexisting signature and certificate outputs, if present",
)
output_options.add_argument(
"--single-cert",
action="store_true",
default=_boolify_env("SIGSTORE_SINGLE_CERT"),
help="Use a single signing certificate and key to sign multiple artifacts",
)

instance_options = sign.add_argument_group("Sigstore instance options")
_add_shared_instance_options(instance_options)
Expand Down Expand Up @@ -614,10 +620,10 @@ def _sign(args: argparse.Namespace) -> None:
# Select the signer to use.
if args.staging:
logger.debug("sign: staging instances requested")
signer = Signer.staging()
signer = Signer.staging(single_certificate=args.single_cert)
args.oidc_issuer = STAGING_OAUTH_ISSUER_URL
elif args.fulcio_url == DEFAULT_FULCIO_URL and args.rekor_url == DEFAULT_REKOR_URL:
signer = Signer.production()
signer = Signer.production(single_certificate=args.single_cert)
else:
# Assume "production" keys if none are given as arguments
updater = TrustUpdater.production()
Expand All @@ -636,6 +642,7 @@ def _sign(args: argparse.Namespace) -> None:
signer = Signer(
fulcio=FulcioClient(args.fulcio_url),
rekor=RekorClient(args.rekor_url, rekor_keyring, ct_keyring),
single_certificate=args.single_cert,
)

# The order of precedence is as follows:
Expand Down
90 changes: 59 additions & 31 deletions sigstore/sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class Signer:
The primary API for signing operations.
"""

def __init__(self, *, fulcio: FulcioClient, rekor: RekorClient):
def __init__(self, *, fulcio: FulcioClient, rekor: RekorClient, single_certificate: bool=False):
"""
Create a new `Signer`.

Expand All @@ -91,27 +91,79 @@ def __init__(self, *, fulcio: FulcioClient, rekor: RekorClient):

`rekor` is a `RekorClient` capable of connecting to a Rekor instance
and creating transparency log entries.

`single_certificate` indicates if a single private key and signing certificate
should be generated to sign multiple artifacts.
"""
self._fulcio = fulcio
self._rekor = rekor
self._single_certificate = single_certificate
self._signing_certificate = None
self._private_key = None

@classmethod
def production(cls) -> Signer:
def production(cls, single_certificate=False) -> Signer:
"""
Return a `Signer` instance configured against Sigstore's production-level services.
"""
updater = TrustUpdater.production()
rekor = RekorClient.production(updater)
return cls(fulcio=FulcioClient.production(), rekor=rekor)
return cls(fulcio=FulcioClient.production(), rekor=rekor, single_certificate=single_certificate)

@classmethod
def staging(cls) -> Signer:
def staging(cls, single_certificate=False) -> Signer:
"""
Return a `Signer` instance configured against Sigstore's staging-level services.
"""
updater = TrustUpdater.staging()
rekor = RekorClient.staging(updater)
return cls(fulcio=FulcioClient.staging(), rekor=rekor)
return cls(fulcio=FulcioClient.staging(), rekor=rekor, single_certificate=single_certificate)

@property
def private_key(self) -> ec.EllipticCurvePrivateKey:
"""Get or generate a signing key."""
if not self._single_certificate or self._private_key is None:
self._private_key = ec.generate_private_key(ec.SECP384R1())

return self._private_key

@property
def signing_cert(self) -> FulcioCertificateSigningResponse:
"""Get or request a signing certificate for Fulcio."""
def get_signing_cert(identity_token: str) -> FulcioCertificateSigningResponse:
if not self._single_certificate or self._signing_certificate is None:
logger.debug("Retrieving signed certificate...")

oidc_identity = Identity(identity_token)
logger.debug(f"cert-identity: {oidc_identity.proof}")
logger.debug(f"cert-oidc-issuer: {oidc_identity.issuer}")

# Build an X.509 Certificiate Signing Request
builder = (
x509.CertificateSigningRequestBuilder()
.subject_name(
x509.Name(
[
x509.NameAttribute(NameOID.EMAIL_ADDRESS, oidc_identity.proof),
]
)
)
.add_extension(
x509.BasicConstraints(ca=False, path_length=None),
critical=True,
)
)
certificate_request = builder.sign(self.private_key, hashes.SHA256())

certificate_response = self._fulcio.signing_cert.post(
certificate_request, identity_token
)

self._signing_certificate = certificate_response
return self._signing_certificate

return get_signing_cert


def sign(
self,
Expand All @@ -122,34 +174,10 @@ def sign(
input_digest = sha256_streaming(input_)

logger.debug("Generating ephemeral keys...")
private_key = ec.generate_private_key(ec.SECP384R1())
private_key = self.private_key

logger.debug("Retrieving signed certificate...")

oidc_identity = Identity(identity_token)
logger.debug(f"cert-identity: {oidc_identity.proof}")
logger.debug(f"cert-oidc-issuer: {oidc_identity.issuer}")

# Build an X.509 Certificiate Signing Request
builder = (
x509.CertificateSigningRequestBuilder()
.subject_name(
x509.Name(
[
x509.NameAttribute(NameOID.EMAIL_ADDRESS, oidc_identity.proof),
]
)
)
.add_extension(
x509.BasicConstraints(ca=False, path_length=None),
critical=True,
)
)
certificate_request = builder.sign(private_key, hashes.SHA256())

certificate_response = self._fulcio.signing_cert.post(
certificate_request, identity_token
)
certificate_response = self.signing_cert(identity_token)

# TODO(alex): Retrieve the public key via TUF
#
Expand Down