Skip to content

Commit 19863eb

Browse files
authored
OCI distro (#64)
fixes #9
1 parent 382e361 commit 19863eb

8 files changed

Lines changed: 214 additions & 23 deletions

File tree

.github/workflows/ci.yml

Lines changed: 128 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,38 @@ name: CI
33
on:
44
push:
55
branches: [main]
6+
tags: ["v*"]
67
pull_request:
78
types: [opened, synchronize, reopened, ready_for_review]
89

10+
env:
11+
IMAGE: ghcr.io/${{ github.repository }}
12+
913
jobs:
1014
build:
1115
name: Build (${{ matrix.name }})
1216
runs-on: ${{ matrix.runs_on }}
17+
defaults:
18+
run:
19+
shell: nix develop --command bash -eo pipefail {0}
1320
permissions:
1421
id-token: write
1522
contents: read
1623
attestations: write
24+
packages: write
1725
strategy:
1826
fail-fast: false
1927
matrix:
2028
include:
2129
- name: linux-amd64
2230
runs_on: ubuntu-24.04
31+
platform: linux-amd64
2332
- name: linux-arm64
2433
runs_on: ubuntu-24.04-arm
34+
platform: linux-arm64
2535
- name: macos-arm64
2636
runs_on: macos-26
37+
platform: darwin-arm64
2738

2839
steps:
2940
- name: Checkout
@@ -65,20 +76,55 @@ jobs:
6576
name: pdf-sign-${{ matrix.name }}
6677
path: dist/*
6778

68-
shell:
69-
name: Dev Shell (${{ matrix.name }})
70-
runs-on: ${{ matrix.runs_on }}
71-
strategy:
72-
fail-fast: false
73-
matrix:
74-
include:
75-
- name: linux-amd64
76-
runs_on: ubuntu-24.04
77-
- name: linux-arm64
78-
runs_on: ubuntu-24.04-arm
79-
- name: macos-arm64
80-
runs_on: macos-26
79+
# --- OCI image build and push (all platforms) ---
80+
81+
- name: Build streaming image
82+
run: |
83+
nix build .#image --out-link image
8184
85+
- name: Login to GHCR
86+
if: github.event.pull_request.head.repo.fork != true
87+
run: |
88+
skopeo login ghcr.io \
89+
--username "${{ github.actor }}" \
90+
--password "${{ secrets.GITHUB_TOKEN }}" \
91+
--compat-auth-file "$HOME/.docker/config.json"
92+
93+
- name: Push image to GHCR
94+
id: push-image
95+
if: github.event.pull_request.head.repo.fork != true
96+
run: |
97+
printf '{"default":[{"type":"reject"}],"transports":{"docker-archive":{"":[{"type":"insecureAcceptAnything"}]},"docker":{"ghcr.io/%s":[{"type":"insecureAcceptAnything"}]}}}\n' \
98+
"${{ github.repository }}" > "$RUNNER_TEMP/skopeo-policy.json"
99+
./image | gzip --fast | skopeo \
100+
--policy "$RUNNER_TEMP/skopeo-policy.json" \
101+
copy \
102+
--digestfile "$RUNNER_TEMP/image-digest" \
103+
docker-archive:/dev/stdin \
104+
"docker://$IMAGE:sha-${GITHUB_SHA::7}-${{ matrix.platform }}"
105+
echo "digest=$(cat "$RUNNER_TEMP/image-digest")" >> "$GITHUB_OUTPUT"
106+
107+
- name: Attest image
108+
if: github.event.pull_request.head.repo.fork != true
109+
uses: actions/attest-build-provenance@v4
110+
with:
111+
subject-name: ${{ env.IMAGE }}
112+
subject-digest: ${{ steps.push-image.outputs.digest }}
113+
push-to-registry: true
114+
115+
docker:
116+
name: Docker Manifest
117+
if: github.event.pull_request.head.repo.fork != true
118+
needs: build
119+
runs-on: ubuntu-24.04
120+
defaults:
121+
run:
122+
shell: nix develop --command bash -eo pipefail {0}
123+
permissions:
124+
id-token: write
125+
contents: read
126+
packages: write
127+
attestations: write
82128
steps:
83129
- name: Checkout
84130
uses: actions/checkout@v6
@@ -95,7 +141,74 @@ jobs:
95141
name: pdf-sign
96142
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
97143

98-
- name: Build dev shell
144+
- name: Login to GHCR
145+
run: |
146+
skopeo login ghcr.io \
147+
--username "${{ github.actor }}" \
148+
--password "${{ secrets.GITHUB_TOKEN }}" \
149+
--compat-auth-file "$HOME/.docker/config.json"
150+
151+
- name: Compute tags
152+
id: tags
153+
run: |
154+
SHA_SHORT="${GITHUB_SHA::7}"
155+
TAGS="$IMAGE:sha-$SHA_SHORT"
156+
157+
if [[ "$GITHUB_REF" == refs/heads/main ]]; then
158+
TAGS="$TAGS,$IMAGE:latest"
159+
fi
160+
161+
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
162+
VERSION="${GITHUB_REF#refs/tags/}"
163+
TAGS="$TAGS,$IMAGE:$VERSION"
164+
TAGS="$TAGS,$IMAGE:${VERSION#v}"
165+
166+
MINOR="${VERSION%.*}"
167+
TAGS="$TAGS,$IMAGE:${MINOR#v}"
168+
169+
TAGS="$TAGS,$IMAGE:latest"
170+
fi
171+
172+
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
173+
174+
- name: Create multi-platform manifest
175+
run: |
176+
SHA_SHORT="${GITHUB_SHA::7}"
177+
IFS=',' read -ra TAG_ARRAY <<< "${{ steps.tags.outputs.tags }}"
178+
179+
FIRST_TAG="${TAG_ARRAY[0]}"
180+
EXTRA_TAGS=()
181+
for tag in "${TAG_ARRAY[@]:1}"; do
182+
EXTRA_TAGS+=(--tags "${tag##*:}")
183+
done
184+
185+
manifest-tool push from-args \
186+
--platforms linux/amd64,linux/arm64,darwin/arm64 \
187+
--template "$IMAGE:sha-$SHA_SHORT-OS-ARCH" \
188+
--target "$FIRST_TAG" \
189+
"${EXTRA_TAGS[@]}"
190+
191+
- name: Get manifest digest
192+
id: digest
99193
run: |
100-
nix develop --command true
194+
SHA_SHORT="${GITHUB_SHA::7}"
195+
DIGEST=$(skopeo inspect "docker://$IMAGE:sha-$SHA_SHORT" | jq -r '.Digest')
196+
echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
101197
198+
- name: Attest manifest
199+
uses: actions/attest-build-provenance@v4
200+
with:
201+
subject-name: ${{ env.IMAGE }}
202+
subject-digest: ${{ steps.digest.outputs.digest }}
203+
push-to-registry: true
204+
205+
- name: Verify attestations
206+
env:
207+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
208+
run: |
209+
sleep 5
210+
IFS=',' read -ra TAG_ARRAY <<< "${{ steps.tags.outputs.tags }}"
211+
for tag in "${TAG_ARRAY[@]}"; do
212+
echo "Verifying $tag..."
213+
gh attestation verify "oci://$tag" --repo "${{ github.repository }}"
214+
done

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# pdf-sign
22

3+
[![built with garnix](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fgarnix.io%2Fapi%2Fbadges%2F0x77dev%2Fpdf-sign%3Fbranch%3Dmain)](https://garnix.io/repo/0x77dev/pdf-sign) [![CI](https://github.com/0x77dev/pdf-sign/actions/workflows/ci.yml/badge.svg)](https://github.com/0x77dev/pdf-sign/actions/workflows/ci.yml)
4+
35
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.
46

7+
[![Watch the demo on X](https://pbs.twimg.com/amplify_video_thumb/2000478980236013569/img/FpmQTXfyxCD5yxaW.jpg)](https://twitter.com/0x77dev/status/2000481268258205919)
8+
59
[![asciicast](https://asciinema.org/a/JXR1crpqtcbMT1DIhD3dzXFB9.svg)](https://asciinema.org/a/JXR1crpqtcbMT1DIhD3dzXFB9)
610

711
## Why pdf-sign?
@@ -55,6 +59,54 @@ nix build
5559
./result/bin/pdf-sign --help
5660
```
5761

62+
### Docker
63+
64+
Multi-platform images (`linux/amd64`, `linux/arm64`, `darwin/arm64`) are published to GHCR with [build provenance attestations](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations).
65+
66+
```bash
67+
docker pull ghcr.io/0x77dev/pdf-sign
68+
69+
# Verify attestation
70+
gh attestation verify oci://ghcr.io/0x77dev/pdf-sign:latest --repo 0x77dev/pdf-sign
71+
```
72+
73+
**GPG signing** — mount your host GPG agent socket and keyring (Linux only — [macOS Docker Desktop cannot forward Unix sockets](https://github.com/docker/for-mac/issues/483)):
74+
75+
```bash
76+
docker run --rm \
77+
-v "$(gpgconf --list-dirs agent-socket)":/gnupg/S.gpg-agent \
78+
-v ~/.gnupg/pubring.kbx:/gnupg/pubring.kbx:ro \
79+
-v "$PWD":/data \
80+
ghcr.io/0x77dev/pdf-sign sign --key 0xDEADBEEF document.pdf
81+
```
82+
83+
**Sigstore signing (interactive)** — forward the OIDC callback port:
84+
85+
```bash
86+
docker run --rm \
87+
-p 127.0.0.1:8080:8080 -e OIDC_REDIRECT_PORT=8080 \
88+
-v "$PWD":/data \
89+
ghcr.io/0x77dev/pdf-sign sign --backend sigstore document.pdf
90+
```
91+
92+
**Sigstore signing (CI/non-interactive)** — pass a pre-obtained identity token:
93+
94+
```bash
95+
docker run --rm \
96+
-e SIGSTORE_IDENTITY_TOKEN="$OIDC_TOKEN" \
97+
-v "$PWD":/data \
98+
ghcr.io/0x77dev/pdf-sign sign --backend sigstore document.pdf
99+
```
100+
101+
**Verify signatures:**
102+
103+
```bash
104+
docker run --rm -v "$PWD":/data \
105+
ghcr.io/0x77dev/pdf-sign verify document_signed.pdf \
106+
--certificate-identity user@example.com \
107+
--certificate-oidc-issuer https://accounts.google.com
108+
```
109+
58110
## Commands
59111

60112
### `sign` - Sign a PDF

crates/gpg/src/sign.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ pub async fn create_signature(
6464
tracing::debug!("Connecting to GPG agent");
6565
use sequoia_gpg_agent as agent;
6666

67+
prepare_agent_pinentry();
68+
6769
let ctx = agent::Context::new().context("Failed to create GPG agent context")?;
6870
let agent = agent::Agent::connect(&ctx)
6971
.await

crates/sigstore/src/sign.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,10 @@ async fn obtain_identity_token(
256256
.await
257257
.context("Failed to create OIDC authorization URL")?;
258258

259-
// Open browser for user authorization
260-
webbrowser::open(oidc_url.0.as_ref()).context("Failed to open browser for OIDC authorization")?;
259+
let auth_url = oidc_url.0.as_ref();
260+
if webbrowser::open(auth_url).is_err() {
261+
eprintln!("\nOpen this URL in your browser to authenticate:\n\n {auth_url}\n");
262+
}
261263

262264
tracing::debug!(port = port, "Waiting for OIDC callback");
263265
let listener = sigstore::oauth::openidflow::RedirectListener::new(

flake.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
{
7070
default = package.pdfSign;
7171
pdf-sign = package.pdfSign;
72+
image = package.image;
7273
inherit autocast;
7374
};
7475

nix/git-hooks.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ git-hooks.lib.${system}.run {
1212
nixfmt.enable = true;
1313
shellcheck.enable = true;
1414
rustfmt.enable = true;
15+
actionlint.enable = true;
1516
};
1617

1718
package = pkgs.prek;

nix/package.nix

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,27 +57,42 @@ rec {
5757
}
5858
);
5959

60-
image = pkgs.dockerTools.buildLayeredImage {
60+
image = pkgs.dockerTools.streamLayeredImage {
6161
name = "ghcr.io/0x77dev/pdf-sign";
62-
tag = "latest";
6362

64-
contents = [ pdfSign ];
63+
contents = [
64+
pdfSign
65+
pkgs.dockerTools.caCertificates
66+
pkgs.dockerTools.fakeNss
67+
pkgs.iana-etc
68+
pkgs.gnupg
69+
];
70+
71+
fakeRootCommands = ''
72+
mkdir -p tmp
73+
chmod 1777 tmp
74+
'';
6575

6676
config = {
67-
Cmd = [ "${lib.getExe pdfSign}" ];
77+
Entrypoint = [ "${lib.getExe pdfSign}" ];
6878
WorkingDir = "/data";
6979
Env = [
7080
"GNUPGHOME=/gnupg"
71-
# OIDC_REDIRECT_PORT can be set at runtime for Sigstore signing
81+
"SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt"
7282
];
7383
Volumes = {
7484
"/gnupg" = { };
7585
"/data" = { };
7686
};
7787
ExposedPorts = {
78-
# Dynamic OIDC redirect port (can be mapped with -p)
7988
"8080/tcp" = { };
8089
};
90+
Labels = {
91+
"org.opencontainers.image.source" = "https://github.com/0x77dev/pdf-sign";
92+
"org.opencontainers.image.description" =
93+
"Lightweight PDF signing with OpenPGP (GPG) and Sigstore (keyless OIDC)";
94+
"org.opencontainers.image.licenses" = "GPL-3.0-only";
95+
};
8196
};
8297
};
8398
}

nix/shell.nix

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ pkgs.mkShell {
3030
bun
3131
nodejs_24
3232

33+
# OCI tooling
34+
skopeo
35+
manifest-tool
36+
jq
37+
3338
# Demo tools
3439
asciinema
3540
autocast

0 commit comments

Comments
 (0)