Search by

k2gl / sigstore-sign

k2gl

Sign artifacts and attestations with Sigstore from PHP — sign, log to Rekor, timestamp, and assemble a bundle

Package info

github.com/k2gl/sigstore-sign

pkg:composer/k2gl/sigstore-sign

Statistics

Installs: 203

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

1.2.0 2026-09-04 04:10 UTC

This package is auto-updated.

Last update: 2026-09-04 04:27:12 UTC


README

CI Latest Stable Version PHPStan Level License

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).

Pull requests are always welcome

Collaborate with pull requests