k2gl / sigstore-sign
Sign artifacts and attestations with Sigstore from PHP — sign, log to Rekor, timestamp, and assemble a bundle
Requires
- php: >=8.1
- ext-json: *
- k2gl/dsse: ^1.3
- k2gl/rekor-client: ^1.1
- k2gl/sigstore-bundle: ^1.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1|^2.0
Requires (Dev)
- k2gl/composer-attest: ^1.0
- k2gl/phpunit-fluent-assertions: ^12
- laravel/pint: ~1.20.0
- nyholm/psr7: ^1.8
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10|^11|^12
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
The Sigstore signing flow, end to end, in PHP: sign an artifact or an attestation,
log the entry to a Rekor v2 transparency log, timestamp the signature, and
assemble the .sigstore.json bundle — the emit counterpart to
k2gl/sigstore-verify.
It ties the family together: k2gl/dsse signs,
k2gl/rekor-client logs,
k2gl/sigstore-bundle is emitted. It does both
keyful signing (you bring the key and its certificate or a public-key hint) and
keyless signing (an ephemeral key certified by Fulcio against a CI OIDC identity).
Requirements
- PHP 8.1+
- A PSR-18 HTTP client and PSR-17 factory (for Rekor and the timestamp authority)
k2gl/dsse,k2gl/rekor-client,k2gl/sigstore-bundle
Installation
composer require k2gl/sigstore-sign
Usage
use K2gl\Dsse\EcdsaP256Signer; use K2gl\RekorClient\{RekorClient, KeyDetails}; use K2gl\SigstoreSign\{SigstoreSigner, SigningKey, TsaClient}; $rekor = new RekorClient($psr18, $psr17, $psr17, 'https://rekor.sigstore.dev'); $tsa = new TsaClient($psr18, $psr17, $psr17, 'https://timestamp.sigstore.dev'); $signer = new SigstoreSigner($rekor, $tsa); // The key: a DSSE signer for the private half, plus the public half — here a // public-key hint (or use SigningKey::certificate() with a Fulcio leaf). $key = SigningKey::publicKey( signer: EcdsaP256Signer::fromPem(file_get_contents('signing-key.pem'), null), publicKeyDer: $publicKeyDer, keyDetails: KeyDetails::PKIX_ECDSA_P256_SHA_256, hint: $hexSha256OfThePublicKey, ); // Sign an artifact → a message-signature bundle. $bundleJson = $signer->signArtifact(file_get_contents('release.tar.gz'), $key)->toJson(); // …or sign an attestation payload (e.g. an in-toto Statement) → a DSSE bundle. $bundleJson = $signer->signAttestation($statementJson, 'application/vnd.in-toto+json', $key)->toJson();
Keyless signing (Fulcio + CI OIDC)
In CI, sign without a long-lived key: read the ambient OIDC identity, let Fulcio certify an ephemeral key against it, and sign with that.
use K2gl\SigstoreSign\{AmbientCredentials, FulcioClient, FulcioSigningKey, SigstoreSigner}; // The CI identity token (GitHub Actions needs `id-token: write`). $oidcToken = AmbientCredentials::githubActions($psr18, $psr17); // …or AmbientCredentials::gitlabCi() for a GitLab id_token. $fulcio = new FulcioClient($psr18, $psr17, $psr17, 'https://fulcio.sigstore.dev'); $key = FulcioSigningKey::create($fulcio, $oidcToken); // ephemeral key + Fulcio certificate $bundleJson = (new SigstoreSigner($rekor, $tsa))->signArtifact($artifact, $key)->toJson();
FulcioSigningKey generates the ephemeral P-256 key, proves possession of it to Fulcio by
signing the token's sub, and returns a SigningKey bound to the issued certificate — the
same type keyful signing uses, so the rest of the flow is identical.
Where to sign: the signing config
Sigstore publishes the endpoints to sign against as a TUF target, and a client should read
them rather than hard-code URLs — Rekor v2 log URLs rotate, and the default public config
still points at Rekor v1. SigningConfig parses that target and applies the spec's
selection rules (validity window, supported API version, and the ALL / ANY / EXACT
selector, with EXACT drawing from distinct operators):
use K2gl\RekorClient\RekorApiVersion; use K2gl\RekorClient\RekorClient; use K2gl\SigstoreSign\SigningConfig; // fetched through k2gl/tuf, next to the trusted root $target = $updater->getTargetInfo('signing_config.v0.2.json'); $config = SigningConfig::fromJson($updater->downloadTarget($target)); $log = $config->rekorLog(); $rekor = new RekorClient( $psr18, $psr17, $psr17, baseUrl: $log->url, apiVersion: RekorApiVersion::from($log->majorApiVersion), ); $fulcio = new FulcioClient($psr18, $psr17, $psr17, $config->certificateAuthority()->url); $tsa = new TsaClient($psr18, $psr17, $psr17, $config->timestampAuthorities()[0]->url);
Selection is against the moment you pass (at:, defaulting to now) and the versions you
support (supportedApiVersions:), so a client that speaks only Rekor v1 asks for it and
gets the v1 log even from a config that lists both.
Why the timestamp authority matters
A Rekor v2 entry has no integrated time, so a bundle needs a trusted RFC 3161 timestamp to
have a verifiable signing time. Pass a TsaClient (Sigstore's public-good TSA is
timestamp.sigstore.dev) when signing against Rekor v2 — without it, the bundle logs and
assembles but will not verify for lack of a time source. A Rekor v1 entry carries its own
integrated time, so signing against a v1 log needs no TSA.
What gets signed
- Artifact — the signature is over the artifact's SHA-256 digest (the message-signature convention). Use an attestation for Ed25519 keys, which the message-signature path does not cover on the verify side.
- Attestation — the payload is wrapped in a DSSE envelope and the signature is over the PAE; the Rekor entry binds the PAE digest, as Rekor v2 records DSSE attestations.
Errors
Everything thrown implements K2gl\SigstoreSign\Exception\SigstoreSignException:
SigningException (the signing flow), TimestampException (the timestamp authority), and
FulcioException (the OIDC credential or the Fulcio certificate step).