Modern PDF signing utility written in Rust, creates an Adobe-compatible detached OpenPGP (GPG) signature and appends it to the PDF, making it easy to sign and verify documents without dragging in heavyweight PDF signing stacks.
# pdf-sign
PDF signing utility written in Rust that supports both **OpenPGP (GPG)** and **Sigstore (keyless OIDC)** signatures, appending cryptographic signatures directly to PDFs, making it easy to sign and verify documents without heavyweight PDF signing stacks, making your PDFs authentic, tamper-proof, while being fully compatible with regular readers.
[](https://asciinema.org/a/JXR1crpqtcbMT1DIhD3dzXFB9)
## Why pdf-sign?
With `pdf-sign`, anyone can sign a PDF using their existing Google, Microsoft, or GitHub account – no cryptographic keys to generate, store, or manage. For power users and security-conscious workflows, it also supports GPG with full hardware key (YubiKey/smartcard) integration. Whether you're a huge company automating signatures, or just need to sign a contract, `pdf-sign` gets out of your way.
Many "enterprise PDF signing" solutions require a full **CMS/PKCS#7** / **X.509 PKI** toolchain (certificate chains, policy constraints, CRL/OCSP revocation, time-stamping/TSAs) plus PDF-form machinery to produce **PAdES** signatures. Those stacks are powerful, but complex to configure, audit, and automate.
`pdf-sign` intentionally stays minimal and scriptable:
* **Two signing backends**: Choose between traditional GPG (with hardware key support) or modern Sigstore (keyless OIDC).
* **Preserves PDF integrity**: Original PDF content unchanged; signatures appended after `%%EOF`.
* **Multi-signer workflow**: Supports multiple signatures (GPG + Sigstore) on the same document, and/or multi-party signing.
* **Privacy-preserving**: No extra PII embedded; library never logs sensitive data.
## Quickstart
### Install with Nix
```bash
nix profile install github:0x77dev/pdf-sign#pdf-sign
pdf-sign --help
```
### Install with Cargo
```bash
cargo install --git https://github.com/0x77dev/pdf-sign --locked
# GPG signing (default backend)
pdf-sign sign document.pdf --key 0xDEADBEEF
# Sigstore keyless signing
pdf-sign sign --backend sigstore document.pdf
# Verify (automatically handles both GPG and Sigstore)
pdf-sign verify document_signed.pdf
```
### Build from Source
```bash
# Clone and build
git clone https://github.com/0x77dev/pdf-sign
cd pdf-sign
cargo build --release
./target/release/pdf-sign --help
# Or with Nix flake
nix build
./result/bin/pdf-sign --help
```
## Commands
### `sign` - Sign a PDF
Unified signing command with backend selection.
**GPG backend (default):**
```bash
pdf-sign sign contract.pdf --key 0xF1171FAAAA237211
# or explicitly:
pdf-sign sign --backend gpg contract.pdf --key user@example.com
```
**Sigstore backend:**
```bash
pdf-sign sign --backend sigstore document.pdf
```
**Common options:**
* `--output, -o`: Output path (default: `<input>_signed.pdf`)
* `--backend, -b`: Backend to use (`gpg` or `sigstore`, default: `gpg`)
* `--json`: Machine-readable JSON output
**GPG-specific options:**
* `--key, -k`: Key spec (file, fingerprint, key ID, or email) - **required for GPG**
* `--embed-uid`: Embed signer UID as notation
**Sigstore-specific options:**
* `--oidc-issuer <URL>`: OIDC provider (default: `https://oauth2.sigstore.dev/auth`)
* `--fulcio-url <URL>`: Fulcio CA (default: `https://fulcio.sigstore.dev`)
* `--rekor-url <URL>`: Rekor log (default: `https://rekor.sigstore.dev`)
* `--oidc-client-id <ID>`: Client ID (default: `sigstore`)
* `--oidc-client-secret <SECRET>`: Client secret
* `--identity-token <JWT>`: Non-interactive (CI mode)
* `--digest-algorithm <ALG>`: Hash (default: `sha512`)
### `verify` - Verify signatures
Automatically detects and verifies **both GPG and Sigstore** signatures in a single pass.
```bash
# Verify GPG signatures (uses keybox by default)
pdf-sign verify contract_signed.pdf
# Verify GPG with specific cert
pdf-sign verify contract_signed.pdf --cert signer.asc
# Verify Sigstore signatures (requires identity policy)
pdf-sign verify document_signed.pdf \
--certificate-identity user@example.com \
--certificate-oidc-issuer https://accounts.google.com
# Verify both GPG and Sigstore in one PDF
pdf-sign verify multi_signed.pdf \
--cert alice.asc \
--certificate-identity bob@example.com \
--certificate-oidc-issuer https://accounts.google.com
```
**GPG verification options:**
* `--cert, -c`: Optional cert spec (can repeat)
**Sigstore verification options:**
* `--certificate-identity <EMAIL|URI>`: Expected signer identity (required if Sigstore sigs present)
* `--certificate-identity-regexp <REGEX>`: Identity regex
* `--certificate-oidc-issuer <URL>`: Expected issuer (required if Sigstore sigs present)
* `--certificate-oidc-issuer-regexp <REGEX>`: Issuer regex
* `--offline`: Skip Rekor verification
**Common options:**
* `--json`: Machine-readable JSON output
### `challenge` - Prepare signing challenge for remote/air-gapped GPG signing
Create a challenge file for signing on a remote or air-gapped machine.
```bash
pdf-sign challenge document.pdf --key 0xDEADBEEF --output challenge.json
```
**Options:**
* `--key, -k`: Key specification (required)
* `--output, -o`: Output path for challenge JSON (default: stdout)
* `--embed-uid`: Embed signer UID into signature
* `--json`: Machine-readable JSON output
**Challenge format:**
```json
{
"version": 1,
"fingerprint": "ABCD1234...",
"data_base64": "SGVsbG8...",
"gpg_command": "echo 'SGVsbG8...' | base64 -d | gpg --detach-sign --armor -u 0xDEADBEEF > signature.asc",
"created_at": "2025-12-13T10:00:00Z",
"embed_uid": false
}
```
### `apply-response` - Apply signature response from challenge-response workflow
Apply a signature created on a remote machine to complete the signing process.
```bash
pdf-sign apply-response document.pdf \
--challenge challenge.json \
--signature signature.asc \
--output signed.pdf
```
**Options:**
* `--challenge, -c`: Path to challenge JSON file (required)
* `--signature, -s`: Path to signature file (.asc) (required)
* `--output, -o`: Output path for signed PDF (default: `<input>_signed.pdf`)
* `--json`: Machine-readable JSON output
## Features
### OpenPGP Backend
* **GPG agent integration**: All private key operations delegated to `gpg-agent`.
* **Hardware key support**: Smartcards and YubiKeys work seamlessly.
* **Keybox lookups**: Reads your `~/.gnupg/pubring.kbx` for verification.
* **Privacy by default**: Signer UIDs only embedded if explicitly requested.
### Sigstore Backend
* **Keyless signing**: No long-lived keys—authenticate with your existing OIDC account.
* **Transparency logging**: All signatures publicly logged to Rekor.
* **Short-lived certificates**: Fulcio issues ephemeral certs tied to your verified identity.
* **Strict verification**: Requires explicit identity and issuer constraints (prevents identity confusion).
* **Customizable endpoints**: Use public Sigstore or private deployments.
### Challenge-Response Workflow
* **Air-gapped signing**: Keep private keys isolated on secure machines
* **Remote signing**: Sign on different servers without copying keys
* **HSM support**: Sign with hardware security modules
* **Audit trail**: Clear separation between digest preparation and signing
* **Standard format**: Uses standard OpenPGP detached signatures
### Architecture
* **Portable core**: PDF splitting, suffix parsing, digest abstraction (no CLI/UI deps).
* **Pluggable backends**: Clean separation between GPG and Sigstore signing logic.
* **Hash agility**: SHA-512 default with SRI-style encoding (`sha512-<base64>`).
* **Versioned format**: Sigstore blocks use bilrost (efficient binary) with version tagging.
* **Structured tracing**: Full `tracing` instrumentation (never logs sensitive data).
* **Multi-signer**: Multiple signatures (GPG + Sigstore) can coexist on one PDF.
## Security Model
### OpenPGP
* **No private keys in tool**: All signing via `gpg-agent`.
* **Reduced exposure**: Private keys stay in agent or on hardware.
* **Explicit verification**: Uses keybox by default or provided certs.
### Sigstore
* **Identity-based**: Signatures tied to verified OIDC identity (email/URI).
* **Transparency**: Rekor ensures signatures are publicly auditable.
* **Strict by default**: Verification fails unless expected identity/issuer provided.
* **Privacy-aware**: Library code never logs tokens or identity material.
## Embedded Signature Formats
### OpenPGP Format
Standard ASCII-armored blocks appended after `%%EOF`:
```text
%%EOF
-----BEGIN PGP SIGNATURE-----
...
-----END PGP SIGNATURE-----
```
### Sigstore Format
Versioned bilrost-encoded blocks with digest binding:
```text
%%EOF
-----BEGIN PDF-SIGN SIGSTORE-----
<base64-encoded bilrost payload>
-----END PDF-SIGN SIGSTORE-----
```
**Bilrost payload** (v1):
* `version`: Format version (1)
* `signed_range_len`: Length of clean PDF bytes
* `digest_alg`: Hash algorithm (1 = SHA-512)
* `digest`: Raw digest bytes (for integrity binding)
* `bundle_json`: Sigstore bundle (signature + cert + Rekor proof)
## Requirements
### For OpenPGP Signing
* Rust toolchain (`cargo`)
* Running `gpg-agent`
* Public cert importable or in keyring
* Private key in `gpg-agent` (software or hardware)
### For Sigstore Signing
* Rust toolchain (`cargo`)
* Web browser (for OIDC auth) or `--identity-token` for CI
* Network access (Fulcio, Rekor, OIDC provider)
* No keys/certs required
## Environment Variables
### General
* `GNUPGHOME`: GPG keybox location (default: `~/.gnupg`)
* `RUST_LOG`: Tracing verbosity (e.g., `RUST_LOG=debug`)
### Sigstore-Specific
* `SIGSTORE_IDENTITY_TOKEN`: Pre-obtained OIDC identity token (JWT) for CI workflows (bypasses interactive browser flow)
* `OIDC_REDIRECT_PORT`: Local port for OIDC callback listener (default: OS-assigned dynamic port). Set to a fixed port (e.g., `8080`) if you need predictable port forwarding or firewall rules
### Output Channels
* `stderr`: Progress, status, errors
* `stdout`: Result paths (sign) or "OK" (verify) for pipelines
## Examples
### GPG signing with YubiKey
```bash
# Sign with hardware key (default backend)
pdf-sign sign contract.pdf --key user@example.com
# Verify
pdf-sign verify contract_signed.pdf
```
### Sigstore keyless signing
```bash
# Interactive signing (opens browser for OIDC)
pdf-sign sign --backend sigstore document.pdf
# Verify with strict identity policy
pdf-sign verify document_signed.pdf \
--certificate-identity user@example.com \
--certificate-oidc-issuer https://accounts.google.com
```
### CI/CD with Sigstore
```bash
# Non-interactive signing with pre-obtained token
pdf-sign sign --backend sigstore release.pdf --identity-token "$OIDC_TOKEN"
# Verify in CI
pdf-sign verify release_signed.pdf \
--certificate-identity https://github.com/org/repo/.github/workflows/release.yml@refs/tags/v1.0.0 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--json
```
### Multi-signer workflow
```bash
# Alice signs with GPG
pdf-sign sign contract.pdf --key alice@example.com
# Bob adds Sigstore signature to the same PDF
pdf-sign sign --backend sigstore contract_signed.pdf --output contract_multi.pdf
# Verify both signatures in one command
pdf-sign verify contract_multi.pdf \
--cert alice.asc \
--certificate-identity bob@example.com \
--certificate-oidc-issuer https://accounts.google.com
```
### Challenge-response for air-gapped signing
```bash
# 1. On connected machine: Prepare challenge
pdf-sign challenge sensitive.pdf --key 0xABCD1234 --output challenge.json
# 2. Transfer challenge.json to air-gapped machine
# 3. On air-gapped machine: Sign the challenge
cat challenge.json | jq -r '.data_base64' | base64 -d | \
gpg --detach-sign --armor -u 0xABCD1234 > signature.asc
# 4. Transfer signature.asc back to connected machine
# 5. On connected machine: Apply signature
pdf-sign apply-response sensitive.pdf \
--challenge challenge.json \
--signature signature.asc \
--output sensitive_signed.pdf
# 6. Verify the result
pdf-sign verify sensitive_signed.pdf
```
## License
GPL-3.0-only
UPD: v0.2 with Sigstore