Skip to content

feat: Add in-memory signing API returning the Sigstore bundle as bytes#642

Open
DevamShah wants to merge 1 commit into
sigstore:mainfrom
DevamShah:sign-to-bytes-api
Open

feat: Add in-memory signing API returning the Sigstore bundle as bytes#642
DevamShah wants to merge 1 commit into
sigstore:mainfrom
DevamShah:sign-to-bytes-api

Conversation

@DevamShah

Copy link
Copy Markdown

Summary

Adds Config.sign_to_bytes() (and a module-level signing.sign_to_bytes() helper) so callers can obtain the Sigstore bundle in memory as bytes instead of being forced to write it to disk, enabling serverless and pipeline signing flows. The existing disk-writing sign() remains the default and is unchanged for callers. This is the library half of #582; the --stdout CLI flag also requested in that issue is a natural follow-up that builds directly on this API, so this PR is tagged Part of: #582 rather than closing it.

Problem

Today the only way to capture a signature through the library API is Config.sign(model_path, signature_path), which serializes the bundle and writes it to a filesystem path. In serverless functions (read-only or ephemeral filesystems), CI/CD steps, and streaming pipelines, the caller wants the bundle bytes directly -- to push to an object store, attach to an HTTP response, or hand to another process -- without round-tripping through a temp file. The workaround (write to a temp file, read it back, delete it) adds filesystem dependencies, cleanup burden, and a small race/leak surface for what is sensitive signing output. This is the gap raised in #582 and requested by the maintainer.

Change

Following the existing API conventions:

  • model_signing/_signing/signing.py: added an abstract to_bytes(self) -> bytes method to the Signature base class, mirroring the existing write/read contract.
  • model_signing/_signing/sign_sigstore.py and sign_sigstore_pb.py: implemented to_bytes() as self.bundle.to_json().encode("utf-8"), and refactored write() to delegate to it (path.write_bytes(self.to_bytes())) so the on-disk and in-memory representations are guaranteed identical and produced by a single code path. These are the only two concrete Signature subclasses; the elliptic-key and PKCS#11 signers both return sign_sigstore_pb.Signature, so no subclass is left without an implementation.
  • model_signing/signing.py: extracted the hash -> payload -> sign sequence shared by both output modes into a private _sign() helper, then added Config.sign_to_bytes(model_path) -> bytes and a module-level sign_to_bytes(model_path) convenience function that mirrors the existing sign() function. sign() now reuses _sign() and is behaviorally unchanged.
  • README.md: added a usage snippet for sign_to_bytes() in the Model Signing API section, alongside the existing sign() examples.
  • tests/api_test.py: added TestKeySigning::test_sign_to_bytes. It asserts the returned value is bytes, that nothing is written to disk, that the bytes parse as a valid Sigstore bundle with the expected signed resources, and that the in-memory DSSE payload is identical to what sign() writes to disk.
  • CHANGELOG.md: added an entry under Unreleased / Added.

The change is purely additive and backwards compatible -- no existing signature, default, or output is altered.

Security rationale

The bundle returned by to_bytes() is byte-for-byte the same Sigstore bundle that write() persists -- both now flow through the single to_bytes() serializer, so there is no divergence between the in-memory and on-disk signed artifact, and no second serialization path to audit. Keeping signing output in memory removes an unnecessary plaintext-on-disk step for sensitive provenance material, which is desirable on shared or ephemeral build hosts and reduces residual-file cleanup obligations (cf. CWE-459 Incomplete Cleanup, CWE-212 Improper Removal of Sensitive Information, CWE-200). This strengthens ML supply-chain integrity -- the project's core purpose, aligned with SLSA provenance and OWASP ML Security Top 10 ML06 (AI Supply Chain Attacks) -- by making it practical to sign in environments that previously could not write to disk, rather than encouraging the insecure write-read-delete workaround. No new key material handling, network behavior, or trust decision is introduced; the signer, payload, and bundle format are unchanged.

Testing

Validated against a clean clone of main with the package installed editable in a fresh venv:

  • ruff format --check src/ -- pass.
  • ruff check src/ -- All checks passed!.
  • pytest tests/api_test.py -m "not integration" -- 4 passed, 2 deselected, including the new test_sign_to_bytes.
  • Full non-integration suite (with the pkcs11 extra) -- 196 passed; the only 2 failures (scripts/tests/verify_test.py v0.3.1 / v1.0.0 fixtures) reproduce identically on unmodified main and are unrelated to this change.

The new test uses local elliptic-key signing, so it runs offline with no Sigstore/OIDC network dependency. Because the elliptic-key signer returns sign_sigstore_pb.Signature, this offline test also exercises the new to_bytes() implementation directly.

Part of: #582

Checklist
  • All commits are signed-off, using DCO
  • All new code has docstrings and type annotations
  • All new code is covered by tests. Aim for at least 90% coverage. CI is configured to highlight lines not covered by tests.
  • Public facing changes are paired with documentation changes
  • Release note has been added to CHANGELOG.md if needed

The library only exposed `Config.sign(model_path, signature_path)`, which
forces signing output to a filesystem path. Serverless functions, CI steps,
and streaming pipelines often run on read-only or ephemeral filesystems and
need the Sigstore bundle in memory, leaving a write-read-delete temp-file
workaround as the only option.

Add an abstract `Signature.to_bytes()` and implement it on both Sigstore
signature types as the UTF-8 encoded bundle JSON, then route `write()`
through it so the on-disk and in-memory artifacts share a single
serialization path. Expose `Config.sign_to_bytes()` and a module-level
`sign_to_bytes()` helper that mirror the existing `sign()` API. The hash,
payload, and sign sequence is factored into a private `_sign()` helper
reused by both output modes; `sign()` is behaviorally unchanged. Document
the new API in the README and add a CHANGELOG entry.

Add a key-based API test asserting the returned bytes are a valid Sigstore
bundle whose signed payload matches what `sign()` writes to disk.

Part of: sigstore#582

Signed-off-by: Devam Shah <devamshah91@gmail.com>
@DevamShah DevamShah requested review from a team as code owners June 23, 2026 01:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant