Skip to content

Sign and verify with rekorv2 #1431

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

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 13 additions & 3 deletions sigstore/_internal/trust.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@
)

from sigstore._internal.fulcio.client import FulcioClient
from sigstore._internal.rekor import RekorLogSubmitter
from sigstore._internal.rekor.client import RekorClient
from sigstore._internal.rekor.client_v2 import RekorV2Client
from sigstore._internal.timestamp import TimestampAuthorityClient
from sigstore._internal.tuf import DEFAULT_TUF_URL, STAGING_TUF_URL, TrustUpdater
from sigstore._utils import (
Expand All @@ -73,7 +75,7 @@
from sigstore.errors import Error, MetadataError, TUFError, VerificationError

# Versions supported by this client
REKOR_VERSIONS = [1]
REKOR_VERSIONS = [1, 2]
TSA_VERSIONS = [1]
FULCIO_VERSIONS = [1]
OIDC_VERSIONS = [1]
Expand Down Expand Up @@ -420,11 +422,19 @@ def _get_valid_services(

return result[:count]

def get_tlogs(self) -> list[RekorClient]:
def get_tlogs(self) -> list[RekorLogSubmitter]:
"""
Returns the rekor transparency log clients to sign with.
"""
return [RekorClient(tlog.url) for tlog in self._tlogs]
result: list[RekorLogSubmitter] = []
for tlog in self._tlogs:
if tlog.major_api_version == 1:
result.append(RekorClient(tlog.url))
elif tlog.major_api_version == 2:
result.append(RekorV2Client(tlog.url))
else:
raise AssertionError(f"Unexpected Rekor v{tlog.major_api_version}")
return result

def get_fulcio(self) -> FulcioClient:
"""
Expand Down
179 changes: 141 additions & 38 deletions sigstore/verify/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@

import rekor_types
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509 import ExtendedKeyUsage, KeyUsage
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
from cryptography.x509 import Certificate, ExtendedKeyUsage, KeyUsage
from cryptography.x509.oid import ExtendedKeyUsageOID
from OpenSSL.crypto import (
X509,
Expand All @@ -38,6 +40,8 @@
from pydantic import ValidationError
from rfc3161_client import TimeStampResponse, VerifierBuilder
from rfc3161_client import VerificationError as Rfc3161VerificationError
from sigstore_protobuf_specs.dev.sigstore.common import v1
from sigstore_protobuf_specs.dev.sigstore.rekor import v2

from sigstore import dsse
from sigstore._internal.rekor import _hashedrekord_from_parts
Expand Down Expand Up @@ -371,6 +375,20 @@ def _verify_common_signing_cert(
f"invalid signing cert: expired at time of signing, time via {vts}"
)

@staticmethod
def _get_key_details(certificate: Certificate) -> v1.PublicKeyDetails:
"""Determine PublicKeyDetails from a certificate"""
public_key = certificate.public_key()
if isinstance(public_key, EllipticCurvePublicKey):
if public_key.curve.name == "secp256r1":
return cast(
v1.PublicKeyDetails,
v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256,
)
# TODO support other keys
raise ValueError(f"Unsupported EC curve: {public_key.curve.name}")
raise ValueError(f"Unsupported public key type: {type(public_key)}")

def verify_dsse(
self, bundle: Bundle, policy: VerificationPolicy
) -> tuple[str, bytes]:
Expand Down Expand Up @@ -418,34 +436,77 @@ def verify_dsse(
# Instead, we manually pick apart the entry body below and verify
# the parts we can (namely the payload hash and signature list).
entry = bundle.log_entry
try:
entry_body = rekor_types.Dsse.model_validate_json(
base64.b64decode(entry.body)
)
except ValidationError as exc:
raise VerificationError(f"invalid DSSE log entry: {exc}")

payload_hash = sha256_digest(envelope._inner.payload).digest.hex()
if (
entry_body.spec.root.payload_hash.algorithm # type: ignore[union-attr]
!= rekor_types.dsse.Algorithm.SHA256
entry._kind_version.kind == "dsse"
and entry._kind_version.version == "0.0.2"
):
raise VerificationError("expected SHA256 payload hash in DSSE log entry")
if payload_hash != entry_body.spec.root.payload_hash.value: # type: ignore[union-attr]
raise VerificationError("log entry payload hash does not match bundle")

# NOTE: Like `dsse._verify`: multiple signatures would be frivolous here,
# but we handle them just in case the signer has somehow produced multiple
# signatures for their envelope with the same signing key.
signatures = [
rekor_types.dsse.Signature(
signature=base64.b64encode(signature.sig).decode(),
verifier=base64_encode_pem_cert(bundle.signing_certificate),
)
for signature in envelope._inner.signatures
]
if signatures != entry_body.spec.root.signatures:
raise VerificationError("log entry signatures do not match bundle")
try:
v2_body = v2.Entry().from_json(base64.b64decode(entry.body))
except ValidationError as exc:
raise VerificationError(f"invalid DSSE log entry: {exc}")

if v2_body.spec.dsse_v002 is None:
raise VerificationError(
"invalid DSSE log entry: missing dsse_v002 field"
)

if (
v2_body.spec.dsse_v002.payload_hash.algorithm
!= v1.HashAlgorithm.SHA2_256
):
raise VerificationError("expected SHA256 hash in DSSE entry")

digest = sha256_digest(envelope._inner.payload).digest
if v2_body.spec.dsse_v002.payload_hash.digest != digest:
raise VerificationError("DSSE entry payload hash does not match bundle")

v2_signatures = [
v2.Signature(
content=signature.sig,
verifier=v2.Verifier(
x509_certificate=v1.X509Certificate(
bundle.signing_certificate.public_bytes(
encoding=serialization.Encoding.DER
)
),
key_details=self._get_key_details(bundle.signing_certificate),
),
)
for signature in envelope._inner.signatures
]
if v2_signatures != v2_body.spec.dsse_v002.signatures:
raise VerificationError("log entry signatures do not match bundle")
else:
try:
entry_body = rekor_types.Dsse.model_validate_json(
base64.b64decode(entry.body)
)
except ValidationError as exc:
raise VerificationError(f"invalid DSSE log entry: {exc}")

payload_hash = sha256_digest(envelope._inner.payload).digest.hex()
if (
entry_body.spec.root.payload_hash.algorithm # type: ignore[union-attr]
!= rekor_types.dsse.Algorithm.SHA256
):
raise VerificationError(
"expected SHA256 payload hash in DSSE log entry"
)
if payload_hash != entry_body.spec.root.payload_hash.value: # type: ignore[union-attr]
raise VerificationError("log entry payload hash does not match bundle")

# NOTE: Like `dsse._verify`: multiple signatures would be frivolous here,
# but we handle them just in case the signer has somehow produced multiple
# signatures for their envelope with the same signing key.
signatures = [
rekor_types.dsse.Signature(
signature=base64.b64encode(signature.sig).decode(),
verifier=base64_encode_pem_cert(bundle.signing_certificate),
)
for signature in envelope._inner.signatures
]
if signatures != entry_body.spec.root.signatures:
raise VerificationError("log entry signatures do not match bundle")

return (envelope._inner.payload_type, envelope._inner.payload)

Expand Down Expand Up @@ -491,15 +552,57 @@ def verify_artifact(
# the other bundle materials (and input being verified).
entry = bundle.log_entry

expected_body = _hashedrekord_from_parts(
bundle.signing_certificate,
bundle._inner.message_signature.signature, # type: ignore[union-attr]
hashed_input,
)
actual_body = rekor_types.Hashedrekord.model_validate_json(
base64.b64decode(entry.body)
)
if expected_body != actual_body:
raise VerificationError(
"transparency log entry is inconsistent with other materials"
if (
entry._kind_version.kind == "hashedrekord"
and entry._kind_version.version == "0.0.2"
):
if bundle._inner.message_signature is None:
raise VerificationError(
"invalid hashedrekord log entry: missing message signature"
)

v2_expected_body = v2.Entry(
kind=entry._kind_version.kind,
api_version=entry._kind_version.version,
spec=v2.Spec(
hashed_rekord_v002=v2.HashedRekordLogEntryV002(
data=v1.HashOutput(
algorithm=bundle._inner.message_signature.message_digest.algorithm,
digest=bundle._inner.message_signature.message_digest.digest,
),
signature=v2.Signature(
content=bundle._inner.message_signature.signature,
verifier=v2.Verifier(
x509_certificate=v1.X509Certificate(
bundle.signing_certificate.public_bytes(
encoding=serialization.Encoding.DER
)
),
key_details=self._get_key_details(
bundle.signing_certificate
),
),
),
)
),
)
v2_actual_body = v2.Entry().from_json(base64.b64decode(entry.body))
if v2_expected_body != v2_actual_body:
raise VerificationError(
"transparency log entry is inconsistent with other materials"
)

else:
expected_body = _hashedrekord_from_parts(
bundle.signing_certificate,
bundle._inner.message_signature.signature, # type: ignore[union-attr]
hashed_input,
)
actual_body = rekor_types.Hashedrekord.model_validate_json(
base64.b64decode(entry.body)
)

if expected_body != actual_body:
raise VerificationError(
"transparency log entry is inconsistent with other materials"
)
53 changes: 53 additions & 0 deletions test/assets/signing_config/signingconfig-only-v1-rekor.v2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"mediaType": "application/vnd.dev.sigstore.signingconfig.v0.2+json",
"caUrls": [
{
"url": "https://fulcio.example.com",
"majorApiVersion": 1,
"validFor": {
"start": "2023-04-14T21:38:40Z"
}
},
{
"url": "https://fulcio-old.example.com",
"majorApiVersion": 1,
"validFor": {
"start": "2022-04-14T21:38:40Z",
"end": "2023-04-14T21:38:40Z"
}
}
],
"oidcUrls": [
{
"url": "https://oauth2.example.com/auth",
"majorApiVersion": 1,
"validFor": {
"start": "2025-04-16T00:00:00Z"
}
}
],
"rekorTlogUrls": [
{
"url": "https://rekor.example.com",
"majorApiVersion": 1,
"validFor": {
"start": "2021-01-12T11:53:27Z"
}
}
],
"tsaUrls": [
{
"url": "https://timestamp.example.com/api/v1/timestamp",
"majorApiVersion": 1,
"validFor": {
"start": "2025-04-09T00:00:00Z"
}
}
],
"rekorTlogConfig": {
"selector": "ANY"
},
"tsaConfig": {
"selector": "ANY"
}
}
5 changes: 5 additions & 0 deletions test/assets/staging-rekor-v2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
DO NOT MODIFY ME!

this is "staging-rekor-v2.txt", a sample input for sigstore-python's unit tests.

DO NOT MODIFY ME!
1 change: 1 addition & 0 deletions test/assets/staging-rekor-v2.txt.sigstore.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial": {"certificate": {"rawBytes": "MIICyzCCAlCgAwIBAgIUJc/6ox+xb+Cmb5UVrFhdu5jiMzIwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjUwNjA5MTE1NzM1WhcNMjUwNjA5MTIwNzM1WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvoYb1h6sjlOR276rCjnPc/PgZtTahLzmf32f08PZ/2eWr4q979itVw1PG8IhcK3E2ZiihegXEgh4mPkkMn78BKOCAW8wggFrMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUsoZlvpIKgR6WlgezvkD6xzHypcMwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwGQYDVR0RAQH/BA8wDYELamt1QGdvdG8uZmkwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMC4GCisGAQQBg78wAQgEIAweaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYAKzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshnoAAAGXVI2aFgAABAMARzBFAiBDHRpKGTpiU3Nx28XgewlvzbMt/ug6ipN8Xj9tryWbwQIhAP/3Cngo4St1nAggkflowySL0fPYg/QDcJKE6XceON3WMAoGCCqGSM49BAMDA2kAMGYCMQCfyQmcNbg2g5PD9Jrb9yOS+vEwwThoY2YDoptDzhJvOxNYLek6DRwCAjZ4SqeTwmQCMQDD3lXotLGsn/CJxGlEiVaF2+z3SKb+bLGGKQATHPkZ/XHvLI2cAdVhcTYeEn36shE="}, "tlogEntries": [{"logIndex": "645", "logId": {"keyId": "8w1amZ2S5mJIQkQmPxdMuOrL/oJkvFg9MnQXmeOCXck="}, "kindVersion": {"kind": "hashedrekord", "version": "0.0.2"}, "inclusionProof": {"logIndex": "645", "rootHash": "kNum4JmdViJPfZLMRB3xPi6flATj2JzJSiF+1pQDzNQ=", "treeSize": "646", "hashes": ["eTqr8nE8VGEREKQ2MDQeD+zKHTJERE6iNw0tG1G+WbQ=", "wzbEsO0X3AWHadlgJZx7yhJdRVEZ2dEY21okXQ6UIi4=", "QMesRTEZdIgthOEinYE/9J7wGv+VmArDZTICj9POmhY=", "UNUMG62rMwoqCqFKknh4R5Ubkf5Z6dj+Pk0m/1xu8uo="], "checkpoint": {"envelope": "log2025-alpha1.rekor.sigstage.dev\n646\nkNum4JmdViJPfZLMRB3xPi6flATj2JzJSiF+1pQDzNQ=\n\n\u2014 log2025-alpha1.rekor.sigstage.dev 8w1amQA0XB55lIjvC/rvbpawQn9lp2R5TSkvqoNJuxcH9Ii05Ddi66xN8z5ZE6GsK2MkvgNZuqnZ5RtHbq2kpt/B8AE=\n"}}, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJoYXNoZWRSZWtvcmRWMDAyIjp7ImRhdGEiOnsiYWxnb3JpdGhtIjoiU0hBMl8yNTYiLCJkaWdlc3QiOiJGZlp5UmhGWklidDhIZURuNmVrblhJQVczQ1ZLREFDWWlKUkxmdE5rU3FvPSJ9LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJQVo2VDhBVVpTQ0JaYUtKa3NMbFNpbE5xRUVPdDRaeUdNR2VwVXBLcDdWR0FpRUFzL1gwa01KVG5FT3V6L0RMV3hUTDR3QlZOa2lXSVVERjM2RUVENzAzOTZBPSIsInZlcmlmaWVyIjp7ImtleURldGFpbHMiOiJQS0lYX0VDRFNBX1AyNTZfU0hBXzI1NiIsIng1MDlDZXJ0aWZpY2F0ZSI6eyJyYXdCeXRlcyI6Ik1JSUN5ekNDQWxDZ0F3SUJBZ0lVSmMvNm94K3hiK0NtYjVVVnJGaGR1NWppTXpJd0NnWUlLb1pJemowRUF3TXdOekVWTUJNR0ExVUVDaE1NYzJsbmMzUnZjbVV1WkdWMk1SNHdIQVlEVlFRREV4VnphV2R6ZEc5eVpTMXBiblJsY20xbFpHbGhkR1V3SGhjTk1qVXdOakE1TVRFMU56TTFXaGNOTWpVd05qQTVNVEl3TnpNMVdqQUFNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUV2b1liMWg2c2psT1IyNzZyQ2puUGMvUGdadFRhaEx6bWYzMmYwOFBaLzJlV3I0cTk3OWl0VncxUEc4SWhjSzNFMlppaWhlZ1hFZ2g0bVBra01uNzhCS09DQVc4d2dnRnJNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBekFkQmdOVkhRNEVGZ1FVc29abHZwSUtnUjZXbGdlenZrRDZ4ekh5cGNNd0h3WURWUjBqQkJnd0ZvQVVjWVl3cGhSOFltLzU5OWIwQlJwL1gvL3JiNnd3R1FZRFZSMFJBUUgvQkE4d0RZRUxhbXQxUUdkdmRHOHVabWt3TEFZS0t3WUJCQUdEdnpBQkFRUWVhSFIwY0hNNkx5OW5hWFJvZFdJdVkyOXRMMnh2WjJsdUwyOWhkWFJvTUM0R0Npc0dBUVFCZzc4d0FRZ0VJQXdlYUhSMGNITTZMeTluYVhSb2RXSXVZMjl0TDJ4dloybHVMMjloZFhSb01JR0tCZ29yQmdFRUFkWjVBZ1FDQkh3RWVnQjRBSFlBS3pDODNHaUl5ZUxoMkNZcFhuUWZTRGt4bGdMeW5EUExYa05BL3JLc2hub0FBQUdYVkkyYUZnQUFCQU1BUnpCRkFpQkRIUnBLR1RwaVUzTngyOFhnZXdsdnpiTXQvdWc2aXBOOFhqOXRyeVdid1FJaEFQLzNDbmdvNFN0MW5BZ2drZmxvd3lTTDBmUFlnL1FEY0pLRTZYY2VPTjNXTUFvR0NDcUdTTTQ5QkFNREEya0FNR1lDTVFDZnlRbWNOYmcyZzVQRDlKcmI5eU9TK3ZFd3dUaG9ZMllEb3B0RHpoSnZPeE5ZTGVrNkRSd0NBalo0U3FlVHdtUUNNUUREM2xYb3RMR3NuL0NKeEdsRWlWYUYyK3ozU0tiK2JMR0dLUUFUSFBrWi9YSHZMSTJjQWRWaGNUWWVFbjM2c2hFPSJ9fX19fX0="}], "timestampVerificationData": {"rfc3161Timestamps": [{"signedTimestamp": "MIIE6zADAgEAMIIE4gYJKoZIhvcNAQcCoIIE0zCCBM8CAQMxDTALBglghkgBZQMEAgEwgcMGCyqGSIb3DQEJEAEEoIGzBIGwMIGtAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgOjYPmS6Qixa9OQqdXWQMPN66194GUnV3liEVd7cbW8oCFQDuYcF6Hx3Wi2sgxpmG+IG2KlvUKRgPMjAyNTA2MDkxMTU3MzhaMAMCAQECCQCbf5cNt4JRDqAypDAwLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2GgggITMIICDzCCAZagAwIBAgIUCjWhBmHV4kFzxomWp/J98n4DfKcwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEx1v5F3HpD9egHuknpBFlRz7QBRDJu4aeVzt9zJLRY0lvmx1lF7WBM2c9AN8ZGPQsmDqHlJN2R/7+RxLkvlLzkc19IOx38t7mGGEcB7agUDdCF/Ky3RTLSK0Xo/0AgHQdo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFKj8ZPYo3i7mO3NPVIxSxOGc3VOlMB8GA1UdIwQYMBaAFDsgRlletTJNRzDObmPuc3RH8gR9MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2cAMGQCMESvVS6GGtF33+J19TfwENWJXjRv4i0/HQFwLUSkX6TfV7g0nG8VnqNHJLvEpAtOjQIwUD3uywTXorQP1DgbV09rF9Yen+CEqs/iEpieJWPst280SSOZ5Na+dyPVk9/8SFk6MYIB3DCCAdgCAQEwUTA5MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIDAeBgNVBAMTF3NpZ3N0b3JlLXRzYS1zZWxmc2lnbmVkAhQKNaEGYdXiQXPGiZan8n3yfgN8pzALBglghkgBZQMEAgGggfwwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNTA2MDkxMTU3MzhaMC8GCSqGSIb3DQEJBDEiBCA6qJ7IlNaN4uuHegN2O+NsWY5kB6sw8E/Q3H3arU8jmDCBjgYLKoZIhvcNAQkQAi8xfzB9MHsweQQgBvT/4Ef+s1mZtzOw16MjUBz8GOTAM2aoRdd1NudLJ0QwVTA9pDswOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUCjWhBmHV4kFzxomWp/J98n4DfKcwCgYIKoZIzj0EAwIEaDBmAjEA9vHFXY/Ia5L2g8F7ipZpiJOgDoAau7L+UkE5c1cCM2FYDZN1QQzWjXGj1CwQMOcuAjEAtBIxQiiecOzOkFo1Bj0n9xkIjyErSBT+P3P6OWgwdivDosxQCTMF7iNeI7wgFQxw"}]}}, "messageSignature": {"messageDigest": {"algorithm": "SHA2_256", "digest": "FfZyRhFZIbt8HeDn6eknXIAW3CVKDACYiJRLftNkSqo="}, "signature": "MEUCIAZ6T8AUZSCBZaKJksLlSilNqEEOt4ZyGMGepUpKp7VGAiEAs/X0kMJTnEOuz/DLWxTL4wBVNkiWIUDF36EED70396A="}}
Loading
Loading