-
Notifications
You must be signed in to change notification settings - Fork 75
Add functionality to sign DSSE envelopes with arbitrary payloads #1054
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
Changes from all commits
8285c67
b5047c0
beaa5f3
53708cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
|
|
@@ -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 | ||
|
|
@@ -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. | ||
|
|
@@ -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) | ||
|
|
||
| def to_json(self) -> str: | ||
| """ | ||
| Return a JSON string with this DSSE envelope's contents. | ||
|
|
@@ -256,6 +289,19 @@ def _sign(key: ec.EllipticCurvePrivateKey, stmt: Statement) -> Envelope: | |
| ) | ||
|
|
||
|
|
||
| def _sign_payload( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than a new |
||
| 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. | ||
|
|
||
| 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. | ||
|
|
@@ -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, | ||
|
|
@@ -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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Merging
_sign_payloadwith_signshould eliminate the need for this new helper 🙂