Skip to content
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
46 changes: 46 additions & 0 deletions sigstore/dsse.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2022 The Sigstore Authors
# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -19,6 +20,7 @@
from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import Any, Dict, List, Literal, Optional, Union

from cryptography.exceptions import InvalidSignature
Expand Down Expand Up @@ -186,6 +188,17 @@ def build(self) -> Statement:
return Statement(stmt.model_dump_json(by_alias=True).encode())


@dataclass
class RawPayload:
"""
Represents a raw payload.

This type can be signed and wrapped into a `Envelope`.
"""
type: str
data: bytes


class Envelope:
"""
Represents a DSSE envelope.
Expand All @@ -210,6 +223,26 @@ def _from_json(cls, contents: bytes | str) -> Envelope:
inner = _Envelope().from_json(contents)
return cls(inner)

@classmethod
def _from_payload(
cls, payload: RawPayload, sigs: list[Signature]
) -> Envelope:
"""Return an unsigned DSSE envelope.

Args:
payload_type (str): The envelope's payload type
payload (bytes): The envelope's payload

Returns:
Envelope: An unsigned DSSE envelope
"""
inner = _Envelope(
payload=payload.data,
payload_type=payload.type,
signatures=sigs,
)
return cls(inner)
Comment on lines +226 to +244
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Merging _sign_payload with _sign should eliminate the need for this new helper 🙂


def to_json(self) -> str:
"""
Return a JSON string with this DSSE envelope's contents.
Expand Down Expand Up @@ -256,6 +289,19 @@ def _sign(key: ec.EllipticCurvePrivateKey, stmt: Statement) -> Envelope:
)


def _sign_payload(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rather than a new _sign_payload function, can we make _sign take Statement | RawPayload? That'll require some internal checks but will be a bit shorter 🙂

key: ec.EllipticCurvePrivateKey, payload: RawPayload) -> Envelope:
"""
Sign the given envelope's payload and set the signature field
with the generated signature.
"""
pae = _pae(payload.payload_type, payload.payload)
signature = key.sign(pae, ec.ECDSA(hashes.SHA256()))
return Envelope._from_payload(
payload_=payload,
sigs=[Signature(sig=signature)])


def _verify(key: ec.EllipticCurvePublicKey, evp: Envelope) -> bytes:
"""
Verify the given in-toto `Envelope`, returning the verified inner payload.
Expand Down
39 changes: 22 additions & 17 deletions sigstore/sign.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2022 The Sigstore Authors
# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -194,10 +195,10 @@ def _finalize_sign(

def sign_dsse(
self,
input_: dsse.Statement,
input_: dsse.Statement | dsse.RawPayload,
) -> Bundle:
"""
Sign the given in-toto statement as a DSSE envelope, and return a
Sign the given in-toto statement or DSSE envelope, and return a
`Bundle` containing the signed result.

This API is **only** for in-toto statements; to sign arbitrary artifacts,
Expand All @@ -211,22 +212,26 @@ def sign_dsse(
)

# Sign the statement, producing a DSSE envelope
content = dsse._sign(self._private_key, input_)

# Create the proposed DSSE log entry
proposed_entry = rekor_types.Dsse(
spec=rekor_types.dsse.DsseSchema(
# NOTE: mypy can't see that this kwarg is correct due to two interacting
# behaviors/bugs (one pydantic, one datamodel-codegen):
# See: <https://github.com/pydantic/pydantic/discussions/7418#discussioncomment-9024927>
# See: <https://github.com/koxudaxi/datamodel-code-generator/issues/1903>
proposed_content=rekor_types.dsse.ProposedContent( # type: ignore[call-arg]
envelope=content.to_json(),
verifiers=[b64_cert.decode()],
content: dsse.Envelope = None
proposed_entry: rekor_types.ProposedEntry = None
if type(input_) is dsse.Statement:
content = dsse._sign(self._private_key, input_)
# Create the proposed DSSE log entry
proposed_entry = rekor_types.Dsse(
spec=rekor_types.dsse.DsseSchema(
# NOTE: mypy can't see that this kwarg is correct due to two interacting
# behaviors/bugs (one pydantic, one datamodel-codegen):
# See: <https://github.com/pydantic/pydantic/discussions/7418#discussioncomment-9024927>
# See: <https://github.com/koxudaxi/datamodel-code-generator/issues/1903>
proposed_content=rekor_types.dsse.ProposedContent( # type: ignore[call-arg]
envelope=content.to_json(),
verifiers=[b64_cert.decode()],
),
),
),
)

)
elif type(input_) is dsse.RawPayload:
content = dsse._sign_payload(self._private_key, input_)
# TODO: figure out an entry that works.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

NB: this is still a hard blocker: the client specification requires bundles to have a transparency log entry, so we can't just skip it when the signed-over input isn't a DSSE.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think there are two possible resolutions here, both of which require some kind of upstream change:

  • Option 1: Rekor learns about non-JSON DSSE payloads. I don't know what the timeline for this would be 🙂
  • Option 2: The client spec clarifies/expresses a judgement about using dsse bundles with non-dsse Rekor entry types (such as hashedrekord, which would work fine here if the DSSE envelope is canonicalized).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

In case it helps: the model signing use case would contain JSON as payload. However, as things stand now rekor solely supports in-toto statements.

return self._finalize_sign(cert, content, proposed_entry)

def sign_artifact(
Expand Down
19 changes: 18 additions & 1 deletion test/unit/test_sign.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2022 The Sigstore Authors
# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -20,7 +21,7 @@
from sigstore_protobuf_specs.dev.sigstore.common.v1 import HashAlgorithm

import sigstore.oidc
from sigstore.dsse import _StatementBuilder, _Subject
from sigstore.dsse import _StatementBuilder, _Subject, RawPayload
from sigstore.errors import VerificationError
from sigstore.hashes import Hashed
from sigstore.sign import SigningContext
Expand Down Expand Up @@ -169,3 +170,19 @@ def test_sign_dsse(staging):
bundle = signer.sign_dsse(stmt)
# Ensures that all of our inner types serialize as expected.
bundle.to_json()


@pytest.mark.staging
@pytest.mark.ambient_oidc
def test_sign_dsse_envelope(staging):
sign_ctx, _, identity = staging

ctx = sign_ctx()
payload = RawPayload(
payload_type="type/custom/my_payload",
payload=b"Hello World!")

with ctx.signer(identity) as signer:
bundle = signer.sign_dsse(payload)

bundle.to_json()