Skip to content

Conversation

@0x77dev
Copy link
Owner

@0x77dev 0x77dev commented Dec 15, 2025

Summary by CodeRabbit

  • New Features

    • Sigstore keyless (OIDC) signing and verification alongside OpenPGP
    • Challenge–response workflow for remote/air‑gapped signing
    • Multi-signature support (mix GPG + Sigstore)
    • WebAssembly bindings for browser/JS usage
    • Docker image output
  • Enhancements

    • Rich CLI: sign, verify, challenge, apply-response with JSON-mode outputs
  • Documentation

    • Expanded README with multi-backend examples and guides

✏️ Tip: You can customize this high-level summary in your review settings.

@0x77dev 0x77dev self-assigned this Dec 15, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 15, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Restructures the project from a monolithic library into a Rust workspace with specialized crates (core, cli, gpg, sigstore, wasm), adds Sigstore keyless signing alongside OpenPGP, and migrates CLI, core primitives, and cryptographic backends into modular, feature-gated components with expanded challenge-response workflows and WASM bindings.

Changes

Cohort / File(s) Summary
Configuration & Build
\.cargo/config.toml`, `.envrc`, .github/workflows/ci.yml`, .gitignore`, .vscode/settings.json`, rustfmt.toml, tsconfig.json, flake.nix, nix/checks.nix, nix/package.nix, nix/shell.nix, nix/demo.nix, nix/git-hooks.nix`
Add workspace and WASM build flags, CI permissions and attestation steps, Nix inputs/overlays and packaging (docker image), environment watchers, formatting and TS configs, and git-hook/package wiring.
Workspace Root & Tooling
Cargo.toml, package.json
Convert root manifest to a workspace (resolver = "2"), centralize dependencies, add Node monorepo tooling via package.json and workspaces.
Core Library (new crate)
crates/core/Cargo.toml, crates/core/src/lib.rs, crates/core/src/digest.rs, crates/core/src/pdf.rs, crates/core/src/suffix.rs, crates/core/src/types.rs
New pdf-sign-core crate: digest/SRI utilities, PDF EOF splitting, suffix block parsing/encoding (OpenPGP and Sigstore), and verification result types.
GPG/OpenPGP Backend (new crate)
crates/gpg/Cargo.toml, crates/gpg/src/lib.rs, crates/gpg/src/verify.rs, crates/gpg/src/sign.rs, crates/gpg/src/keybox.rs, crates/gpg/src/challenge.rs, crates/gpg/tests/challenge_response.rs
New pdf-sign-gpg crate implementing keybox loading, detached signing via gpg-agent, verification, and a challenge-response flow (WASM-friendly), with feature gates (native, challenge).
Sigstore Backend (new crate)
crates/sigstore/Cargo.toml, crates/sigstore/src/lib.rs, crates/sigstore/src/sign.rs, crates/sigstore/src/verify.rs
New pdf-sign-sigstore crate providing OIDC-based keyless signing, identity token acquisition (interactive OIDC flow), bundle creation, Rekor interaction, and bundle verification with identity/issuer policy matching.
CLI Application (new crate)
crates/cli/Cargo.toml, crates/cli/src/main.rs, crates/cli/src/cli.rs, crates/cli/src/app.rs, crates/cli/src/sign.rs, crates/cli/src/commands.rs, crates/cli/src/json.rs, crates/cli/src/util.rs
New pdf-sign CLI: clap-based subcommands (sign, verify, challenge, apply-response), dual-backend dispatch (GPG/Sigstore), JSON output types, error formatting, and utilities.
WebAssembly Bindings (new crate)
crates/wasm/Cargo.toml, crates/wasm/.gitignore, crates/wasm/LICENSE, crates/wasm/src/lib.rs, crates/wasm/tests/web.rs
New wasm-bindgen crate exposing Challenge, signing and verification functions to JS/TS, optional Sigstore features, and wasm tests; includes GPL license file for the wasm package.
Demo & Scripts
scripts/autocast.sh, scripts/autocast.yaml, scripts/e2e.sh
Add demo/autocast recording script and YAML, expand e2e script to multi-key and conditional Sigstore tests.
Removed Monolith (migrated)
src/app.rs, src/cli.rs, src/json.rs, src/keybox.rs, src/lib.rs, src/main.rs, src/pdf.rs, src/sign.rs, src/util.rs, src/verify.rs
Remove previous single-crate source tree and CLI; functionality migrated into new workspace crates.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI
    participant Core
    participant GPG
    participant Sigstore
    participant Agent
    participant OIDC
    participant Rekor

    Note over User,CLI: User requests signing
    User->>CLI: sign --backend=gpg|sigstore input.pdf
    CLI->>Core: split_pdf(input)
    Core-->>CLI: clean_pdf, suffix_data

    alt backend == gpg
        CLI->>GPG: sign_gpg(clean_pdf, key_spec,...)
        GPG->>Agent: request keypair (gpg-agent)
        Agent-->>GPG: keypair
        GPG-->>CLI: SignResult {fingerprint, uids, signature}
    else backend == sigstore
        CLI->>Sigstore: sign_sigstore(clean_pdf, endpoints,...)
        Sigstore->>OIDC: obtain_identity_token()
        OIDC-->>Sigstore: identity_token
        Sigstore->>Rekor: submit bundle
        Rekor-->>Sigstore: log_index
        Sigstore-->>CLI: SignResult {certificate_identity, issuer, log_index}
    end

    CLI->>Core: encode_suffix_block(SignResult)
    Core-->>CLI: encoded_block
    CLI->>CLI: append encoded_block to clean_pdf + suffix_data
    CLI-->>User: signed PDF output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Areas requiring extra attention:

  • crates/sigstore/src/{sign,verify}.rs — OIDC flow, token handling, bundle parsing, Rekor interactions, and policy matching.
  • crates/gpg/{sign,keybox,challenge,verify} — agent integration, keybox parsing, challenge-response correctness, and cross-target feature gating.
  • crates/core/src/{suffix.rs,digest.rs} — bilrost encoding/decoding, SRI parsing/encoding, and newline/encoding edge cases.
  • Workspace/Cargo/Nix changes — ensure build targets, feature flags, and CI attestation steps are consistent.
  • wasm bindings (crates/wasm/src/lib.rs) — wasm-bindgen surface, async bridges, and error marshaling.

Poem

🐰
I nibbled through the monolith, stitched crates anew,
GPG hops and Sigstore bounds, bundles in my stew,
Challenges prepared with care, responses checked with grace,
Workspaces sprout like carrot rows, each module finds its place,
A tiny rabbit cheers the builds — signed PDFs, embrace!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically summarizes the main changes: multi-crate refactoring, Sigstore support addition, challenge-response workflow, and Docker image support.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@0x77dev 0x77dev changed the title v0.2 v0.2 – multi-crate, wasm, sigstore support, challenge-response, web app, docker image Dec 15, 2025
@0x77dev 0x77dev changed the title v0.2 – multi-crate, wasm, sigstore support, challenge-response, web app, docker image feat: v0.2 – multi-crate, sigstore support, challenge-response, docker image Dec 15, 2025
@0x77dev 0x77dev changed the title feat: v0.2 – multi-crate, sigstore support, challenge-response, docker image feat: v0.2 - multi-crate, sigstore support, challenge-response, docker image Dec 15, 2025
@0x77dev 0x77dev marked this pull request as ready for review December 15, 2025 05:33
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (28)
scripts/e2e.sh (2)

49-63: Add error handling for invalid Sigstore token.

The conditional block runs Sigstore operations only if SIGSTORE_IDENTITY_TOKEN is set, but if the token is invalid or expired at runtime, the sign and verify commands may fail with unclear errors. The set -e will catch stderr, but consider adding explicit validation or error messaging.

 # Sigstore tests require network and OIDC token
 if [[ -n "${SIGSTORE_IDENTITY_TOKEN:-}" ]]; then
+  # Validate token is not empty after expansion
+  if [[ -z "$SIGSTORE_IDENTITY_TOKEN" ]]; then
+    echo "ERROR: SIGSTORE_IDENTITY_TOKEN is set but empty" >&2
+    exit 1
+  fi
+
   echo "==> Test 3: Sigstore sign + verify"
-  sigstore_signed="$("$PDF_SIGN" sign --backend sigstore input.pdf --identity-token "$SIGSTORE_IDENTITY_TOKEN")"
+  sigstore_signed="$("$PDF_SIGN" sign --backend sigstore input.pdf --identity-token "$SIGSTORE_IDENTITY_TOKEN")" || {
+    echo "ERROR: Sigstore sign failed (token may be invalid or expired)" >&2
+    exit 1
+  }
   "$PDF_SIGN" verify "$sigstore_signed" | grep -x OK >/dev/null

1-13: Add validation for required binary before first use.

The script assumes $PDF_SIGN is available but does not check if the binary exists or is executable. This can lead to unclear errors later.

 #!/usr/bin/env bash
 # End-to-end tests for pdf-sign.
 # Runs in Nix sandbox (offline) for GPG tests, and optionally with network for Sigstore.
 set -euo pipefail
+
+# Verify pdf-sign binary is available
+if [[ -z "${PDF_SIGN:-}" ]] || ! command -v "$PDF_SIGN" &>/dev/null; then
+  echo "ERROR: PDF_SIGN binary not found or not in PATH" >&2
+  exit 1
+fi
scripts/autocast.yaml (1)

54-61: Consider making tamper offset more defensive or documented.

The fixed byte offset (4096) and value ('X') may not reliably corrupt the signature if the PDF structure changes or varies. For a demo, consider either:

  1. Adding a comment explaining why offset 4096 is safe, or
  2. Using a more robust tampering approach (e.g., truncating the file or modifying a later offset in the signature area)

This is a low-priority refinement since the demo's primary goal is illustrative rather than exhaustive.

scripts/autocast.sh (1)

48-48: Improve error diagnostics for PDF download failure.

The curl command suppresses all output (>/dev/null 2>&1), which means if the download fails, the user receives no helpful error message when the script exits. Consider capturing curl's stderr for visibility into network/download failures.

-curl -fsSL -o "${_PDF_SIGN_DEMO_DIR}/bitcoin.pdf" https://bitcoin.org/bitcoin.pdf >/dev/null 2>&1
+curl -fSL -o "${_PDF_SIGN_DEMO_DIR}/bitcoin.pdf" https://bitcoin.org/bitcoin.pdf \
+  || { echo "Failed to download bitcoin.pdf from https://bitcoin.org/bitcoin.pdf" >&2; exit 1; }

This change:

  • Removes the silent -s flag to show progress/errors from curl
  • Keeps -S to show errors even with -s
  • Adds explicit error message with exit code
  • Directs error to stderr (>&2)
README.md (1)

188-191: Clarify GPG agent requirement and OpenPGP/Sigstore block parsing.

Minor clarity suggestions:

  • Line 188: "All private key operations delegated to gpg-agent" — consider adding that this requires gpg-agent to be running and accessible via $GNUPGHOME (documented on line 283, but could be mentioned earlier).
  • Lines 237–244: The PGP block example shows standard ASCII-armored format. Confirm the tool correctly skips/validates existing -----BEGIN PGP SIGNATURE----- blocks and appends new ones.
  • Lines 248–255: The Sigstore block format uses a custom header (-----BEGIN PDF-SIGN SIGSTORE-----). Confirm parsing correctly distinguishes this from PGP blocks.

No breaking issues, but early clarity prevents user confusion.

Consider adding a note like:

GPG Signing Requirements: Ensure gpg-agent is running (gpg-agent --daemon) and your private key is in the agent. See Configuring GPG Agent for GNUPGHOME.

This bridges lines 188 and 283.

Also applies to: 237-244, 248-255

.cargo/config.toml (1)

2-5: Clarify or update the comment about hardening flags.

The comment states "Disable hardening flags that don't work with WASM", but the configuration only sets a stack size and doesn't explicitly disable any hardening flags. Either update the comment to accurately describe the stack size configuration, or add the relevant hardening flag disablement if needed.

-# Disable hardening flags that don't work with WASM
+# Configure WASM target with appropriate stack size
 rustflags = [
     "-C", "link-arg=-zstack-size=4194304",
 ]
tsconfig.json (1)

24-27: Consider enabling noPropertyAccessFromIndexSignature for type safety.

While the current configuration disables some stricter checks, enabling noPropertyAccessFromIndexSignature would prevent accessing potentially undefined properties on indexed types, reducing runtime errors in WASM bindings.

     // Some stricter flags (disabled by default)
     "noUnusedLocals": false,
     "noUnusedParameters": false,
-    "noPropertyAccessFromIndexSignature": false
+    "noPropertyAccessFromIndexSignature": true
crates/gpg/src/keybox.rs (2)

55-72: Silent error skipping may hide important failures.

Line 56 uses filter_map(|r| r.ok()) which silently discards keybox record parsing errors. If individual records fail to parse, this could silently return fewer certificates than expected without any indication to the user.

Consider logging skipped errors:

     let certs = match kbx
-      .filter_map(|r| r.ok())
+      .filter_map(|r| match r {
+        Ok(record) => Some(record),
+        Err(e) => {
+          tracing::warn!("Skipping malformed keybox record: {}", e);
+          None
+        }
+      })
       .filter_map(|r| match r {

133-139: Add context to file read error.

The std::fs::read(path)? error propagates without indicating which operation failed.

-    return Cert::from_bytes(&std::fs::read(path)?)
-      .with_context(|| format!("Failed to load certificate from file: {}", path.display()));
+    let bytes = std::fs::read(path)
+      .with_context(|| format!("Failed to read certificate file: {}", path.display()))?;
+    return Cert::from_bytes(&bytes)
+      .with_context(|| format!("Failed to parse certificate from file: {}", path.display()));
crates/cli/src/util.rs (1)

3-15: Document the rounding threshold rationale.

The threshold value 1_048_525 appears carefully chosen to prevent displaying "1024.0 KB", but the derivation isn't immediately obvious to future maintainers.

Consider adding a comment explaining the threshold:

  const KB: f64 = 1024.0;
  const MB: f64 = KB * 1024.0;
+ // Threshold chosen to avoid displaying "1024.0 KB" due to rounding.
+ // At 1_048_524 bytes (1023.99 KB), we still show KB.
+ // At 1_048_525 bytes and above, we switch to MB display.
  const KB_TO_MB_ROUNDING_THRESHOLD: usize = 1_048_525;
crates/wasm/Cargo.toml (1)

37-37: Inconsistent dependency version specification.

Line 37 uses an explicit version "0.1.7" for console_error_panic_hook while all other dependencies use workspace resolution. Consider moving this to the workspace dependencies for consistency, or document why an explicit version is required.

If no specific version requirement exists, apply this change:

-console_error_panic_hook = { version = "0.1.7", optional = true }
+console_error_panic_hook = { workspace = true, optional = true }

And add it to the workspace dependencies in the root Cargo.toml.

crates/wasm/tests/web.rs (1)

45-63: Consider consolidating duplicate serialization tests.

A nearly identical test_challenge_serialization appears in both crates/wasm/src/lib.rs (lines 319-334) and this file. While having both unit and integration tests is valid, consider whether both are necessary or if one provides sufficient coverage.

Cargo.toml (1)

60-61: Consider documenting tokio feature expectations.

tokio is declared with default-features = false and no features at the workspace level. Ensure each crate that depends on tokio specifies the required features (e.g., rt, rt-multi-thread, macros).

crates/core/src/pdf.rs (1)

26-48: Consider adding an error case test.

The tests cover happy paths well. Consider adding a test for the error case when no %%EOF marker exists to verify the error message.

#[test]
fn errors_when_no_eof_marker() {
  let invalid = b"%%PDF-1.4\n...content...";
  assert!(find_eof_offset(invalid).is_err());
}
flake.nix (1)

56-75: Consider deduplicating the autocast import.

The autocast derivation is imported twice from ./nix/demo.nix — once in the packages block (lines 58-61) and again in devShells.default (lines 72-75). While Nix's lazy evaluation prevents redundant computation, this duplicates the import expression.

You could lift the let autocast = ... binding outside both blocks or reference it from the packages set:

       devShells.default = import ./nix/shell.nix {
         inherit pkgs;
         pdfSign = package.pdfSign;
-        autocast = import ./nix/demo.nix {
-          inherit pkgs craneLib;
-          lib = pkgs.lib;
-        };
+        inherit (self.packages.${system}) autocast;
         pre-commit-check = import ./nix/git-hooks.nix {
           inherit git-hooks system pkgs;
           src = ./.;
         };
       };

Alternatively, move autocast to a shared let binding at the system level.

crates/core/src/digest.rs (1)

99-104: Consider verifying the actual digest value in the test.

The test only asserts the output length. Adding a check against a known SHA-512 hash of "hello" would catch potential algorithm implementation issues.

   #[test]
   fn sha512_computes_correctly() {
     let data = b"hello";
     let digest = compute_digest(DigestAlgorithm::Sha512, data);
     assert_eq!(digest.len(), 64);
+    // Known SHA-512 of "hello"
+    let expected_hex = "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043";
+    let expected: Vec<u8> = (0..64).map(|i| u8::from_str_radix(&expected_hex[i*2..i*2+2], 16).unwrap()).collect();
+    assert_eq!(digest, expected);
   }
crates/cli/src/sign.rs (1)

170-171: Consider refactoring to reduce parameter count.

The sign_sigstore function has 10 parameters. While #[allow(clippy::too_many_arguments)] suppresses the lint, grouping related parameters into a struct would improve maintainability and make the API cleaner.

You could create a SigstoreSignCommand struct to bundle the endpoint-related options and pass it instead of individual parameters.

nix/package.nix (1)

60-77: Docker image configuration looks solid.

The layered image setup with volumes for /gnupg and /data is appropriate for GPG key management and PDF processing. Using lib.getExe ensures the correct binary path.

One minor consideration: the tag = "latest" is hardcoded. For reproducible builds and proper versioning, consider using the version variable.

   image = pkgs.dockerTools.buildLayeredImage {
     name = "ghcr.io/0x77dev/pdf-sign";
-    tag = "latest";
+    tag = version;
crates/core/src/suffix.rs (1)

123-131: Consider extracting find_subslice to avoid duplication.

This helper function is identical to the one in crates/gpg/src/verify.rs (lines 225-233). Consider moving it to pdf-sign-core and re-exporting it for both crates to use.

crates/sigstore/Cargo.toml (2)

9-12: Feature flags appear unused.

The native and wasm features are defined but don't currently gate any dependencies or conditional compilation. If these are placeholders for future work, consider adding a TODO comment or removing them until needed to avoid confusion.


28-31: Consider scoping Tokio features.

tokio = { workspace = true, features = ["full"] } includes all Tokio features. If only specific features are needed (e.g., rt-multi-thread, macros, time), narrowing the feature set can reduce compile times and binary size.

crates/cli/src/cli.rs (2)

72-74: Consider validating digest_algorithm at parse time.

The digest_algorithm is a String with a default of "sha512". Based on the codebase context (DigestAlgorithm::Sha512), only SHA-512 appears supported. Consider using a clap::ValueEnum to restrict valid values at parse time rather than runtime.

+#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
+pub enum DigestAlgorithmArg {
+  Sha512,
+}

     /// Digest algorithm (Sigstore backend, default: sha512)
-    #[arg(long, default_value = "sha512")]
-    digest_algorithm: String,
+    #[arg(long, value_enum, default_value = "sha512")]
+    digest_algorithm: DigestAlgorithmArg,

110-126: Challenge command is GPG-specific but not explicitly documented.

The Challenge subcommand requires --key and --embed_uid, which are GPG-specific options. The help text mentions "GPG signing" but consider adding a note that this is OpenPGP-only, or add backend support in the future for consistency with Sign.

crates/gpg/src/challenge.rs (1)

184-205: Test coverage is minimal.

The serialization test is useful, but consider adding integration tests for the challenge-response flow with actual GPG signatures to ensure cryptographic correctness.

crates/sigstore/src/verify.rs (1)

190-265: Significant code duplication between extract_identity_from_bundle and extract_bundle_info.

Both functions parse the certificate, extract SAN, and extract the OIDC issuer using nearly identical code. The only differences are minor: extract_identity_from_bundle errors on missing identity while extract_bundle_info returns "unknown", and extract_identity_from_bundle has slightly different whitespace handling.

Consider extracting common certificate parsing logic into a shared helper:

struct CertInfo {
    identity: Option<String>,
    issuer: Option<String>,
}

fn extract_cert_info(bundle: &sigstore::bundle::Bundle) -> Result<CertInfo> {
    // Common certificate parsing, SAN extraction, and issuer extraction...
}

fn extract_identity_from_bundle(bundle: &sigstore::bundle::Bundle) -> Result<(String, String)> {
    let info = extract_cert_info(bundle)?;
    let identity = info.identity.ok_or_else(|| anyhow::anyhow!("No email or URI found in certificate SAN"))?;
    let issuer = info.issuer.unwrap_or_else(|| "unknown".to_string());
    Ok((identity, issuer))
}

fn extract_bundle_info(bundle: &sigstore::bundle::Bundle) -> Result<(String, String)> {
    let info = extract_cert_info(bundle)?;
    Ok((
        info.identity.unwrap_or_else(|| "unknown".to_string()),
        info.issuer.unwrap_or_else(|| "unknown".to_string()),
    ))
}

Also applies to: 267-337

crates/wasm/src/lib.rs (1)

41-48: VerificationResult uses GPG-centric field naming for Sigstore verification.

The gpg_signatures field name is misleading when used for Sigstore verification results. The struct also lacks Sigstore-specific information like certificate_identity and rekor_log_index.

Consider a more generic structure or separate result types:

 #[derive(Tsify, Serialize, Deserialize)]
 #[tsify(into_wasm_abi, from_wasm_abi)]
 #[serde(rename_all = "camelCase")]
 pub struct VerificationResult {
   pub valid: bool,
-  pub gpg_signatures: Vec<SignatureInfo>,
+  pub signatures: Vec<SignatureInfo>,
+  #[serde(skip_serializing_if = "Option::is_none")]
+  pub sigstore_identity: Option<String>,
+  #[serde(skip_serializing_if = "Option::is_none")]
+  pub sigstore_issuer: Option<String>,
 }

Also applies to: 249-304

crates/sigstore/src/sign.rs (2)

107-111: Consider defensive handling for log_index cast.

The cast entry.log_index as u64 could produce unexpected results if log_index is ever negative (though unlikely in practice). A defensive check or try_into() would be safer.

-    .map(|entry| entry.log_index as u64);
+    .and_then(|entry| u64::try_from(entry.log_index).ok());

139-205: Code duplication with verify.rs::extract_bundle_info.

This function is nearly identical to extract_bundle_info in crates/sigstore/src/verify.rs. Consider extracting the shared certificate parsing logic into a common helper in this crate to reduce duplication.

Note a subtle behavioral difference: this version errors if no email/URI is found in SAN (line 190), while verify.rs falls back to "unknown". Ensure this stricter behavior during signing is intentional.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3cb1868 and 10eca9b.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (60)
  • .cargo/config.toml (1 hunks)
  • .envrc (1 hunks)
  • .github/workflows/ci.yml (2 hunks)
  • .gitignore (1 hunks)
  • .vscode/settings.json (1 hunks)
  • Cargo.toml (1 hunks)
  • LICENSE (1 hunks)
  • README.md (1 hunks)
  • crates/cli/Cargo.toml (1 hunks)
  • crates/cli/src/app.rs (1 hunks)
  • crates/cli/src/cli.rs (1 hunks)
  • crates/cli/src/commands.rs (1 hunks)
  • crates/cli/src/json.rs (1 hunks)
  • crates/cli/src/main.rs (1 hunks)
  • crates/cli/src/sign.rs (1 hunks)
  • crates/cli/src/util.rs (1 hunks)
  • crates/core/Cargo.toml (1 hunks)
  • crates/core/src/digest.rs (1 hunks)
  • crates/core/src/lib.rs (1 hunks)
  • crates/core/src/pdf.rs (1 hunks)
  • crates/core/src/suffix.rs (1 hunks)
  • crates/core/src/types.rs (1 hunks)
  • crates/gpg/Cargo.toml (1 hunks)
  • crates/gpg/src/challenge.rs (1 hunks)
  • crates/gpg/src/keybox.rs (1 hunks)
  • crates/gpg/src/lib.rs (1 hunks)
  • crates/gpg/src/sign.rs (1 hunks)
  • crates/gpg/src/verify.rs (1 hunks)
  • crates/gpg/tests/challenge_response.rs (1 hunks)
  • crates/sigstore/Cargo.toml (1 hunks)
  • crates/sigstore/src/lib.rs (1 hunks)
  • crates/sigstore/src/sign.rs (1 hunks)
  • crates/sigstore/src/verify.rs (1 hunks)
  • crates/wasm/.gitignore (1 hunks)
  • crates/wasm/Cargo.toml (1 hunks)
  • crates/wasm/LICENSE (1 hunks)
  • crates/wasm/src/lib.rs (1 hunks)
  • crates/wasm/tests/web.rs (1 hunks)
  • flake.nix (2 hunks)
  • nix/checks.nix (2 hunks)
  • nix/demo.nix (1 hunks)
  • nix/git-hooks.nix (1 hunks)
  • nix/package.nix (2 hunks)
  • nix/shell.nix (2 hunks)
  • package.json (1 hunks)
  • rustfmt.toml (1 hunks)
  • scripts/autocast.sh (1 hunks)
  • scripts/autocast.yaml (1 hunks)
  • scripts/e2e.sh (2 hunks)
  • src/app.rs (0 hunks)
  • src/cli.rs (0 hunks)
  • src/json.rs (0 hunks)
  • src/keybox.rs (0 hunks)
  • src/lib.rs (0 hunks)
  • src/main.rs (0 hunks)
  • src/pdf.rs (0 hunks)
  • src/sign.rs (0 hunks)
  • src/util.rs (0 hunks)
  • src/verify.rs (0 hunks)
  • tsconfig.json (1 hunks)
💤 Files with no reviewable changes (10)
  • src/util.rs
  • src/main.rs
  • src/cli.rs
  • src/app.rs
  • src/sign.rs
  • src/keybox.rs
  • src/verify.rs
  • src/pdf.rs
  • src/lib.rs
  • src/json.rs
🧰 Additional context used
🧬 Code graph analysis (14)
crates/cli/src/main.rs (1)
crates/cli/src/app.rs (1)
  • run (6-99)
crates/gpg/tests/challenge_response.rs (1)
crates/gpg/src/challenge.rs (2)
  • prepare_challenge (51-84)
  • validate_response (146-182)
crates/cli/src/commands.rs (7)
crates/core/src/pdf.rs (1)
  • split_pdf (21-24)
crates/core/src/suffix.rs (2)
  • encode_suffix_block (100-121)
  • parse_suffix_blocks (39-96)
crates/gpg/src/keybox.rs (1)
  • load_cert (133-160)
crates/sigstore/src/verify.rs (1)
  • verify_blob (51-133)
crates/gpg/src/verify.rs (1)
  • verify_signatures (128-195)
crates/gpg/src/challenge.rs (2)
  • prepare_challenge (51-84)
  • apply_response (91-139)
crates/wasm/src/lib.rs (2)
  • prepare_challenge (52-92)
  • apply_response (96-135)
crates/cli/src/app.rs (2)
crates/cli/src/sign.rs (2)
  • sign_gpg (30-168)
  • sign_sigstore (171-317)
crates/cli/src/commands.rs (3)
  • verify_pdf (25-240)
  • prepare_challenge (243-353)
  • apply_response (356-503)
crates/core/src/lib.rs (3)
crates/core/src/digest.rs (3)
  • compute_digest (51-59)
  • encode_sri (62-69)
  • parse_sri (74-93)
crates/core/src/pdf.rs (2)
  • find_eof_offset (9-15)
  • split_pdf (21-24)
crates/core/src/suffix.rs (2)
  • encode_suffix_block (100-121)
  • parse_suffix_blocks (39-96)
crates/core/src/suffix.rs (1)
crates/gpg/src/verify.rs (1)
  • find_subslice (226-234)
crates/wasm/tests/web.rs (2)
crates/gpg/tests/challenge_response.rs (1)
  • test_challenge_serialization (43-58)
crates/wasm/src/lib.rs (1)
  • test_challenge_serialization (320-335)
crates/sigstore/src/lib.rs (2)
crates/sigstore/src/sign.rs (1)
  • sign_blob (68-137)
crates/sigstore/src/verify.rs (1)
  • verify_blob (51-133)
crates/wasm/src/lib.rs (5)
crates/core/src/pdf.rs (1)
  • split_pdf (21-24)
crates/core/src/suffix.rs (2)
  • encode_suffix_block (100-121)
  • parse_suffix_blocks (39-96)
crates/gpg/src/verify.rs (1)
  • verify_signatures (128-195)
crates/sigstore/src/verify.rs (1)
  • verify_blob (51-133)
crates/gpg/tests/challenge_response.rs (1)
  • test_challenge_serialization (43-58)
crates/gpg/src/challenge.rs (3)
crates/cli/src/commands.rs (2)
  • prepare_challenge (243-353)
  • apply_response (356-503)
crates/wasm/src/lib.rs (2)
  • prepare_challenge (52-92)
  • apply_response (96-135)
crates/gpg/src/verify.rs (2)
  • get_certs (24-67)
  • check (69-90)
crates/cli/src/cli.rs (1)
crates/core/src/digest.rs (1)
  • name (16-20)
crates/sigstore/src/verify.rs (2)
crates/core/src/digest.rs (3)
  • compute_digest (51-59)
  • try_from (41-46)
  • name (16-20)
crates/sigstore/src/sign.rs (1)
  • extract_bundle_info (140-205)
crates/gpg/src/verify.rs (1)
crates/gpg/src/keybox.rs (2)
  • find_certs_in_keybox (98-127)
  • load_keybox_certs (23-84)
crates/sigstore/src/sign.rs (2)
crates/core/src/digest.rs (2)
  • compute_digest (51-59)
  • name (16-20)
crates/sigstore/src/verify.rs (1)
  • extract_bundle_info (268-337)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Dev Shell (macos-arm64)
  • GitHub Check: Build (linux-amd64)
  • GitHub Check: Build (linux-arm64)
  • GitHub Check: Build (macos-arm64)
  • GitHub Check: Build (macos-arm64)
  • GitHub Check: Build (linux-arm64)
  • GitHub Check: Dev Shell (macos-arm64)
  • GitHub Check: Build (linux-amd64)
🔇 Additional comments (83)
rustfmt.toml (2)

4-9: Formatting configuration is well-aligned.

The spacing (2-space indentation), tab settings, and Unix newline convention are appropriate and consistent with the referenced .editorconfig file. This should provide consistent formatting across the workspace.


2-2: No issues found. Edition "2024" is a valid and stable Rust edition, released on February 20, 2025 with Rust 1.85. The configuration is correct.

scripts/e2e.sh (1)

41-46: The test correctly validates multi-signature PDFs. The implementation confirms all three assumptions:

  1. PDF format supports multiple signatures appended sequentially in suffix blocks
  2. The verify command with multiple --cert flags validates all loaded certificates against signatures found in the PDF, returning an error if any validation fails
  3. The output format matches the expected pattern—"OK" is printed to stdout when piped

The test will correctly pass only when both signatures are successfully added and validate against their respective certificates.

scripts/autocast.yaml (2)

6-30: LGTM on settings and environment configuration.

The terminal settings, custom shell configuration with stable prompt, and environment variables are well-suited for reproducible demo capture. The 900-second timeout appropriately accounts for GPG key generation and network operations.


41-41: All CLI flags used in the demo are correctly implemented.

The flags --key, --backend sigstore, and --output in the Sign command, as well as the Verify command, are all properly defined in crates/cli/src/cli.rs and match their usage in the demo script.

scripts/autocast.sh (4)

13-19: LGTM on cleanup implementation.

The cleanup function is well-protected with nested error suppression, and the unconditional trap at exit ensures temporary files are removed regardless of how the script terminates.

Also applies to: 50-50


30-32: LGTM on GPG home setup.

The isolated GNUPGHOME with strict 700 permissions follows GPG best practices and ensures the demo uses an ephemeral, secure keyring.


43-45: LGTM on ephemeral GPG key generation.

The non-interactive key generation with empty passphrase is appropriate for a demo. The batch mode with loopback pinentry-mode ensures automation-friendly behavior.


55-55: The autocast invocation on line 55 correctly uses documented flags for autocast v0.1.0. The -d 88ms flag is the short form of --type-speed (sets delay between key presses to 88ms), and --overwrite is a valid flag to overwrite the output file. No issues found.

README.md (7)

213-217: Verify multi-signer signature isolation and ordering.

Line 216 states "Multiple signatures (GPG + Sigstore) can coexist on one PDF." The README example (lines 329–340) shows sequential signing (Alice GPG, then Bob Sigstore).

Confirm:

  • PDF parsing correctly isolates and verifies each signature independently
  • Verification reports which signature(s) passed/failed
  • No cross-contamination if a later signer corrupts the PDF
  • The order of verification does not affect security guarantees

This is especially important for legal/audit workflows where signature provenance matters.


345-362: Air-gapped challenge-response workflow is clear; verify digest stability.

Lines 345–362 document a realistic air-gapped signing workflow. The steps are:

  1. Prepare challenge (compute digest)
  2. Transfer challenge JSON
  3. Sign on isolated machine
  4. Transfer signature back
  5. Apply signature
  6. Verify

Confirm:

  • The digest algorithm (SHA-512 by default, line 97) is locked in the challenge JSON and cannot be changed during response
  • The signed_range_len (line 260) ensures the PDF bytes haven't drifted between challenge and apply-response
  • If the PDF is modified between challenge and apply-response, the operation clearly fails

These guarantees prevent subtle signing errors in high-security workflows.


155-164: and


70-70: All hardcoded Sigstore and OIDC endpoints in README.md are verified as current production URLs (as of 2024–2025): Fulcio (https://fulcio.sigstore.dev), Rekor (https://rekor.sigstore.dev), Sigstore OIDC issuer (https://oauth2.sigstore.dev/auth), Google OIDC (https://accounts.google.com), and GitHub Actions OIDC (https://token.actions.githubusercontent.com) are all correct and match official documentation. No action needed.


35-35: No changes needed. The README CLI documentation is accurate and consistent with the implementation in crates/cli/src/cli.rs. All documented command names, options, backend identifiers (gpg, sigstore), and flags match the actual Clap CLI definitions.


5-5: Recording is publicly accessible; verify it demonstrates both signing backends.

The asciinema recording URL is publicly accessible and available for download as both .cast and .txt formats on asciinema.org. However, confirm the recording demonstrates both the GPG and Sigstore signing workflows, as both are core features described in the README's quickstart. If the recording shows only one backend, consider whether it should be re-recorded to showcase both sign and verify operations across both backends.


9-19: Verification reveals the implementation differs from the stated enforcement model.

The CLI does not enforce identity/issuer constraints as described. When users omit --certificate-identity and --certificate-oidc-issuer, verification succeeds by extracting and using the embedded identity/issuer from the Sigstore bundle itself (see commands.rs lines 139–147). The README's claim on line 230 that "Verification fails unless expected identity/issuer provided" is misleading—it only fails if constraints are explicitly provided and don't match, not if the user provides nothing.

Additionally, identity and issuer values are logged via tracing::info!() in verify.rs (line 118), contradicting the README's assertion that the library "never logs tokens or identity material." While OIDC tokens are correctly not logged, identity material is included in logs.

The security model is less strict than documented: pinned verification (user-provided constraints) is optional rather than required, with fallback to bundle-embedded values.

Likely an incorrect or invalid review comment.

.gitignore (1)

7-9: LGTM!

Standard ignore patterns for Node.js dependencies and build artifacts, appropriate for the WASM and TypeScript integration introduced in this PR.

.envrc (1)

5-8: LGTM!

The watch directives ensure the development environment reloads when Nix configuration changes, and the --accept-flake-config flag aligns with the CI workflow configuration.

.vscode/settings.json (1)

1-6: LGTM!

Appropriate Rust analyzer configuration for the new multi-crate workspace, enabling comprehensive IDE support across all targets and features.

.github/workflows/ci.yml (3)

13-16: LGTM!

Appropriate permissions for artifact attestation generation, aligning with the Sigstore support introduced in this PR.


57-60: LGTM!

Artifact attestation step properly generates build provenance for all artifacts in the dist directory, providing supply chain security for the built binaries.


68-100: LGTM!

The Dev Shell job ensures the development environment works correctly across all supported platforms, complementing the build validation.

crates/core/src/types.rs (2)

5-11: LGTM!

The VerifiedSignature structure appropriately captures OpenPGP verification results with key fingerprint, user IDs, and source information.


13-19: LGTM!

The VerifiedSigstoreSignature structure correctly represents Sigstore verification results, including certificate identity, OIDC issuer, and optional Rekor log index.

nix/checks.nix (1)

25-27: LGTM!

Setting CARGO_BUILD_TARGET and --workspace --all-features ensures comprehensive testing across all workspace members with full feature coverage.

crates/gpg/src/keybox.rs (2)

152-159: LGTM - Safe unwrap with appropriate warning.

The unwrap() on line 159 is safe because it's guarded by the is_empty() check on line 145. The warning for multiple matches is helpful for user awareness.


9-15: LGTM - Standard GnuPG home resolution.

Correctly prioritizes GNUPGHOME environment variable before falling back to ~/.gnupg, following GnuPG conventions.

crates/wasm/.gitignore (1)

1-5: LGTM!

Standard gitignore entries for wasm-pack projects - ignoring the pkg/ output directory and compiled .wasm binaries.

crates/core/Cargo.toml (1)

1-17: LGTM - Clean workspace-based crate manifest.

Good use of workspace inheritance for metadata and dependencies. This ensures consistent versioning across the multi-crate workspace.

nix/shell.nix (1)

21-35: LGTM - Well-organized tooling additions.

Good separation of concerns with clear comments. The WASM tooling (wasm-pack, wasm-bindgen-cli, binaryen, lld), web development tools (bun, nodejs_24), and demo tools align well with the PR's goals.

crates/cli/src/util.rs (1)

17-38: LGTM!

The test coverage is thorough, validating both the rounding behavior and exact boundary transitions.

crates/gpg/Cargo.toml (4)

1-7: LGTM!

Package metadata follows workspace conventions correctly.


9-12: LGTM!

Feature flags are well-structured. The empty challenge feature serves as a marker for conditional compilation, which is a standard Rust pattern.


28-32: LGTM!

Platform-specific Tokio feature configuration is appropriate. Restricting WASM builds to sync and macros avoids incompatible runtime features.


14-26: LGTM!

Workspace dependency declarations are correct, with appropriate optional gating for sequoia-gpg-agent.

crates/cli/src/main.rs (1)

11-26: LGTM!

The CLI entry point follows best practices: tracing to stderr, clean separation of concerns, and proper error handling. The error display logic in app::run ensures users see detailed error messages before the process exits.

crates/wasm/Cargo.toml (2)

1-11: LGTM!

Package configuration is correct. The dual crate types (cdylib and rlib) properly support both WASM output and workspace integration.


20-23: LGTM!

Disabling wasm-opt is a reasonable workaround for the bulk-memory operations issue. The comment clearly explains the trade-off between size optimization and build reproducibility.

crates/cli/Cargo.toml (1)

1-26: LGTM!

The CLI manifest is well-configured with appropriate dependencies and feature flags for both signing backends.

crates/gpg/tests/challenge_response.rs (4)

17-29: LGTM!

Basic happy path coverage for challenge preparation is appropriate for an integration test.


31-40: LGTM!

Test validates that the embed_uid option propagates correctly through the challenge preparation flow.


42-58: LGTM!

Serialization round-trip test provides good coverage of the serde implementation for the Challenge type.


60-75: LGTM!

Error path testing for invalid signatures is appropriate and well-structured.

crates/wasm/tests/web.rs (2)

12-26: LGTM!

Test appropriately validates the WASM-specific Challenge structure, which uses base64-encoded strings instead of raw bytes.


28-43: LGTM!

Test validates the VerificationResult structure correctly.

crates/cli/src/app.rs (3)

1-4: LGTM!

Imports are clean and appropriate for the CLI orchestration layer.


76-98: LGTM!

Error handling is well-structured with proper separation between JSON and human-readable output modes. The cause chain extraction correctly skips the top-level error.


24-27: No changes needed. The --key requirement is properly enforced by clap's required_if_eq("backend", "gpg") attribute (line 40 of cli.rs). The .expect() at line 25 is safe and will not panic during normal operation.

Cargo.toml (2)

69-73: LGTM!

Release profile settings are well-suited for a production CLI binary, balancing binary size and performance.


13-13: The Rust 2024 edition is stable.

The edition = "2024" stabilized in Rust 1.85 (released February 20, 2025) and does not require a nightly compiler. Ensure your CI and build environments use Rust 1.85 or later.

Likely an incorrect or invalid review comment.

crates/core/src/lib.rs (1)

1-13: LGTM!

Clean module organization with appropriate re-exports. The core crate properly establishes a dependency-free foundation for the workspace.

crates/core/src/pdf.rs (1)

8-15: LGTM!

The implementation correctly finds the last %%EOF marker using windows(5).rposition(). This properly handles PDFs with multiple EOF markers from incremental saves.

crates/gpg/src/sign.rs (4)

36-47: LGTM!

Key selection logic correctly applies the standard policy to find alive, non-revoked, signing-capable keys. The .clone() on line 47 ensures the key outlives the iterator.


55-62: LGTM!

The conditional UID embedding correctly takes only the first UID when embed_uid is enabled.


64-75: LGTM!

GPG agent connection and keypair retrieval are properly implemented with descriptive error context.


77-105: LGTM!

Signature creation correctly uses the sequoia-openpgp API with proper finalization order (signer before armor writer).

crates/sigstore/src/lib.rs (1)

1-13: LGTM! Clean module structure and well-organized public API.

The crate root properly exposes the sign/verify modules with appropriate re-exports and documents the default Sigstore public endpoints. The API surface is minimal and well-scoped.

crates/core/src/digest.rs (1)

1-93: LGTM! Well-designed digest abstraction with SRI support.

The implementation correctly handles algorithm agility, SRI encoding/decoding with both padded and unpadded base64, and proper length validation. The tracing instrumentation on compute_digest is a nice touch for observability.

crates/cli/src/sign.rs (2)

61-67: Asymmetric block preservation between GPG and Sigstore signing.

sign_gpg (lines 61-67) only preserves existing OpenPGP signature blocks, dropping any Sigstore bundles. In contrast, sign_sigstore (lines 280-283) preserves all existing blocks regardless of type.

If a PDF was previously signed with Sigstore and then signed with GPG, the Sigstore signature would be lost. This may be intentional, but if not, consider aligning the behavior.

Is this asymmetry intentional? If mixed-signature PDFs should be supported, sign_gpg should preserve all block types:

-  let existing_pgp_sigs: Vec<_> = existing_blocks
-    .iter()
-    .filter_map(|b| match b {
-      SuffixBlock::OpenPgpSig(data) => Some(data.clone()),
-      _ => None,
-    })
-    .collect();
+  // Preserve all existing blocks
+  let existing_blocks_to_preserve = existing_blocks.clone();

And update the write loop accordingly.

Also applies to: 280-283


19-28: LGTM! Good helper function with solid test coverage.

The default_signed_output_path function handles edge cases well, and the tests cover normal paths, dotfiles, and error cases appropriately.

Also applies to: 319-348

crates/gpg/src/lib.rs (1)

1-27: LGTM! Well-structured feature-gated module organization.

The feature gates are appropriately applied:

  • verify is unconditional, enabling verification in all environments (including WASM)
  • native gates keybox and sign which require GPG agent access
  • challenge gates the challenge-response workflow separately

This structure supports both native CLI usage and cross-platform verification scenarios cleanly.

nix/package.nix (1)

7-18: LGTM! Workspace-aware source filtering.

The filter correctly includes Rust sources, Cargo files, the crates directory, and falls back to crane's cargo source filtering. This properly supports the new multi-crate workspace structure.

crates/core/src/suffix.rs (2)

137-168: Good test coverage for round-trip encoding.

The tests verify both PGP block parsing and Sigstore block round-tripping. This ensures the encode/decode cycle is consistent.


43-61: Let-chains syntax requires Rust 1.88+.

The if let ... && let ... syntax (let-chains) is used here. This was stabilized in Rust 1.88. Ensure the project's minimum supported Rust version supports this feature, as edition 2024 alone only requires Rust 1.80+.

Likely an incorrect or invalid review comment.

crates/cli/src/json.rs (1)

5-56: Well-structured JSON output types.

The structs are appropriately designed for CLI output serialization. Using lifetime parameters for constant strings (status, command) avoids unnecessary allocations.

crates/cli/src/cli.rs (1)

4-21: Clean CLI structure with well-documented arguments.

The global flags and subcommand pattern are well-organized. The use of global = true for --json and --verbose ensures they work with all subcommands.

crates/gpg/src/challenge.rs (1)

51-84: LGTM!

The prepare_challenge function correctly validates that the certificate has a signing-capable key using StandardPolicy, extracts the fingerprint, and constructs a well-formed challenge with proper metadata.

crates/cli/src/commands.rs (3)

25-240: LGTM!

The verify_pdf function correctly handles dual-backend verification (OpenPGP and Sigstore), with proper policy construction for Sigstore and appropriate JSON/terminal output modes. The Tokio runtime usage is acceptable for CLI context.


243-353: LGTM!

The prepare_challenge function correctly loads the certificate, prepares the challenge via the GPG module, and provides helpful output including the executable GPG command for air-gapped signing workflows.


356-503: LGTM!

The apply_response function correctly reconstructs the challenge, validates the signature via the GPG module, preserves existing signature blocks, and writes the signed PDF. The output path defaulting logic is sensible.

crates/sigstore/src/verify.rs (2)

50-133: LGTM!

The verify_blob function implements a robust verification flow: version validation, signed range length check, digest validation, and proper integration with the Sigstore verifier. The pre-extraction of identity for regex matching before invoking the verifier is a sound approach.


135-188: LGTM!

The build_identity_policy function correctly enforces the strict policy requirement, validates matchers (including regex compilation), and creates an exact Identity policy with the actual certificate values after validation passes.

crates/wasm/src/lib.rs (3)

51-92: LGTM!

The prepare_challenge function correctly parses the armored certificate, splits the PDF, and delegates to the GPG challenge module. The helper GPG command generation is useful for users.


137-192: LGTM!

The verify_gpg function correctly handles the case of no signatures (returning valid: false), parses optional certificates, and properly maps verification results to the WASM-friendly SignatureInfo struct.


194-247: LGTM!

The sign_sigstore function correctly handles the feature gate, prepares signing options, and properly constructs the signed PDF with existing blocks preserved and the new Sigstore bundle appended.

crates/gpg/src/verify.rs (4)

61-63: Returning all keybox certificates when no specific match is found may be overly permissive.

When ids don't match any certificates in the keybox, the code returns the entire keybox. This allows verification to succeed against any key in the user's keybox, which may not be the intended security posture.

Is this fallback intentional? If the signature's key ID doesn't match any known certificate, it might be more secure to fail:

       if out.is_empty() {
-        return Ok(keybox.clone());
+        // No matching certificate found for the signature's key IDs
+        return Err(openpgp::Error::InvalidOperation(
+          "No matching certificate found in keybox".into()
+        ).into());
       }

Alternatively, if the intent is to allow any trusted key, consider documenting this behavior.


128-195: LGTM!

The verify_signatures function correctly handles multiple signature blocks, properly initializes the verification helper per signature, and extracts signer information. The cert_source tracking is useful for reporting.


199-224: LGTM!

The extract_pgp_signatures function correctly handles both CRLF and LF line endings and extracts multiple signature blocks. The accompanying tests provide good coverage for these edge cases.


236-263: LGTM!

The tests adequately cover the CRLF and LF line ending handling in extract_pgp_signatures.

crates/sigstore/src/sign.rs (4)

9-28: LGTM!

The endpoint configuration struct with sensible defaults from crate-level constants is well-structured. Empty default for oidc_client_secret is appropriate for public OAuth client flows.


30-47: LGTM!

Clean options struct with good documentation on the identity_token field explaining the CI vs interactive flow distinction.


49-56: LGTM!

Result struct is well-defined with appropriate optionality for rekor_log_index.


58-67: Good documentation and instrumentation.

The doc comments clearly describe the signing workflow steps, and the tracing instrumentation with skip(data, options) and field extraction is well done for observability.

Comment on lines +72 to +74
let signing_ctx = sigstore::bundle::sign::SigningContext::async_production()
.await
.context("Failed to initialize Sigstore signing context")?;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the sign.rs file around the mentioned lines
cat -n crates/sigstore/src/sign.rs | head -150 | tail -100

Repository: 0x77dev/pdf-sign

Length of output: 4159


🏁 Script executed:

# Search for how SigningContext is implemented and if it supports custom endpoints
fd "\.rs" | xargs grep -l "SigningContext" | head -5

Repository: 0x77dev/pdf-sign

Length of output: 87


🏁 Script executed:

# Look for any custom endpoint configuration in the sign.rs file
rg "endpoints|fulcio_url|rekor_url" crates/sigstore/src/sign.rs -A 2 -B 2

Repository: 0x77dev/pdf-sign

Length of output: 1734


🏁 Script executed:

# Search for SigningContext implementation to see if there's a way to specify custom endpoints
rg "SigningContext" --type rust -A 5 -B 2 | head -100

Repository: 0x77dev/pdf-sign

Length of output: 594


🏁 Script executed:

# Check if there's documentation or usage of sigstore-rs library
fd "Cargo.toml" | xargs grep -l sigstore

Repository: 0x77dev/pdf-sign

Length of output: 142


🏁 Script executed:

# Look for any mention of custom endpoints or alternative initialization methods
rg "async_production|SigningContext" crates/ -A 3 -B 1

Repository: 0x77dev/pdf-sign

Length of output: 412


🏁 Script executed:

# Check the sigstore-rs crate dependency version
cat crates/sigstore/Cargo.toml | grep -A 5 "sigstore"

Repository: 0x77dev/pdf-sign

Length of output: 583


🏁 Script executed:

# Search for any documentation or comments about endpoint configuration
rg "production|staging|custom|endpoint" crates/sigstore/ -i

Repository: 0x77dev/pdf-sign

Length of output: 1230


🏁 Script executed:

# Check if there are any other SigningContext methods available by searching for imports
rg "use.*sigstore" crates/sigstore/src/sign.rs -B 2 -A 10

Repository: 0x77dev/pdf-sign

Length of output: 42


🏁 Script executed:

# Check main Cargo.toml for sigstore version
rg "sigstore" Cargo.toml -A 2 -B 2

Repository: 0x77dev/pdf-sign

Length of output: 515


🏁 Script executed:

# Let's search for any alternative methods to create signing context
rg "SigningContext::" crates/ -o

Repository: 0x77dev/pdf-sign

Length of output: 104


🏁 Script executed:

# Check if there's documentation in comments about why async_production is used
rg "async_production|SigningContext" crates/sigstore/src/ -B 5 -A 5

Repository: 0x77dev/pdf-sign

Length of output: 891


🌐 Web query:

sigstore-rs 0.13 SigningContext custom endpoints configuration

💡 Result:

Yes — sigstore-rs lets you configure custom endpoints in the SigningContext via a SigningConfig (or by loading a ClientTrustConfig / trusted-root). You can either:

  • Construct a SigningConfig with explicit endpoints (fulcio, rekor, TSA, OIDC) and build a signer from it; or
  • Load a trusted root / signing config (TUF trust bundle) and create a SigningContext from that, which encapsulates the service endpoints and keys.

Minimal example (illustrative — adjust types/async as needed):

  • Create a custom SigningConfig with custom URLs (fulcio/rekor/…).
  • Make a SigningContext (or Signer) from that config and use it to sign.

Rust-style pseudo-code:
let cfg = SigningConfig {
fulcio_url: "https://fulcio.example.com".into(),
rekor_url: "https://rekor.example.com".into(),
/* set tsa/oidc etc as needed */
};
let ctx = SigningContext::from(cfg);
let signer = ctx.signer(identity_token);
let bundle = signer.sign(artifact).await?;

See the crate docs for field names and constructors:

  • sigstore-sign (SigningConfig / SigningContext examples). [1]
  • sigstore-trust-root (signing config & trusted-root support). [2]
  • Sigstore system docs on custom components / using custom endpoints or TUF trusted roots. [3]

References:
[1] sigstore-sign crate docs (SigningConfig / SigningContext).
[2] sigstore-trust-root crate docs (Signing Config / trusted root).
[3] Sigstore docs — Configuring Cosign / custom components (custom Fulcio/Rekor/TSA via TUF or env vars).


🏁 Script executed:

# Let's also check the verify.rs to see if it uses production()
cat crates/sigstore/src/verify.rs | grep -A 5 -B 5 "production"

Repository: 0x77dev/pdf-sign

Length of output: 447


🏁 Script executed:

# Check if there are any TODOs or FIXMEs related to endpoints
rg "TODO|FIXME|XXX" crates/sigstore/src/sign.rs

Repository: 0x77dev/pdf-sign

Length of output: 42


🌐 Web query:

sigstore-rs 0.13.0 SigningConfig API fulcio_url rekor_url

💡 Result:

Short answer: You can configure Fulcio and Rekor endpoints with SigningConfig — the crate exposes helpers (SigningConfig::production(), ::staging()) and also a plain struct form where you set fulcio_url and rekor_url directly (e.g. SigningConfig { fulcio_url: "...".into(), rekor_url: "...".into(), ... }). See the sigstore-sign docs and the trust-root/signing-config descriptions. [1][2]

Sources:

  • sigstore-sign (SigningConfig examples showing fulcio_url / rekor_url). [1]
  • sigstore-trust-root / SigningConfig description (service endpoints, CA / Rekor URLs). [2]

🏁 Script executed:

# Check if SigningConfig is imported or used anywhere in the codebase
rg "SigningConfig|ClientTrustConfig" crates/

Repository: 0x77dev/pdf-sign

Length of output: 42


🏁 Script executed:

# Let's look at the full sign.rs file structure to see if there's any setup
wc -l crates/sigstore/src/sign.rs

Repository: 0x77dev/pdf-sign

Length of output: 91


🏁 Script executed:

# Check the imports at the top of sign.rs
head -30 crates/sigstore/src/sign.rs

Repository: 0x77dev/pdf-sign

Length of output: 938


Configured endpoints are ignored; signing always uses production Sigstore.

SigningContext::async_production() hardcodes production Fulcio/Rekor URLs, making options.endpoints.fulcio_url and options.endpoints.rekor_url ineffective. The SigstoreEndpoints configuration is only used for the OIDC token flow, not for the signing context initialization. This prevents using custom or staging Sigstore instances.

Use SigningConfig with custom endpoints instead: SigningConfig { fulcio_url: options.endpoints.fulcio_url, rekor_url: options.endpoints.rekor_url, ... } to respect the configured endpoints.

🤖 Prompt for AI Agents
In crates/sigstore/src/sign.rs around lines 72 to 74, the code calls
SigningContext::async_production(), which forces production Fulcio/Rekor
endpoints and ignores options.endpoints; replace this by constructing a
SigningConfig that uses options.endpoints.fulcio_url and
options.endpoints.rekor_url (plus any other required fields from options), then
initialize the SigningContext from that config (the async variant) so the
configured endpoints are honored; ensure proper await and error context
propagation when creating the signing context.

@0x77dev 0x77dev added documentation Improvements or additions to documentation enhancement New feature or request openpgp GPG related sigstore Sigstore related labels Dec 15, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
flake.nix (1)

62-86: Remove duplicated autocast import.

The autocast derivation is imported twice: once in packages (lines 64-67) and again in devShells.default (lines 78-81). This causes the same derivation to be evaluated twice unnecessarily.

Refactor to import autocast once at the let binding level:

       in
       {
         checks = import ./nix/checks.nix {
           inherit
             pkgs
             craneLib
             package
             git-hooks
             system
             ;
         };

-        packages =
-          let
-            autocast = import ./nix/demo.nix {
-              inherit pkgs craneLib;
-              lib = pkgs.lib;
-            };
-          in
-          {
+        packages = {
             default = package.pdfSign;
             pdf-sign = package.pdfSign;
             inherit autocast;
           };

         devShells.default = import ./nix/shell.nix {
           inherit pkgs;
           pdfSign = package.pdfSign;
-          autocast = import ./nix/demo.nix {
-            inherit pkgs craneLib;
-            lib = pkgs.lib;
-          };
+          inherit autocast;
           pre-commit-check = import ./nix/git-hooks.nix {
             inherit git-hooks system pkgs;
             src = ./.;
           };
         };
       }

And move autocast to the outer let block alongside package:

let
  pkgs = import nixpkgs { ... };
  craneLib = crane.mkLib pkgs;
  package = import ./nix/package.nix { ... };
  autocast = import ./nix/demo.nix {
    inherit pkgs craneLib;
    lib = pkgs.lib;
  };
in
♻️ Duplicate comments (2)
Cargo.toml (1)

32-33: Bilrost versions are now valid.

The versions have been updated to 0.1010.2, which exists on crates.io, addressing the previous review feedback.

crates/cli/src/commands.rs (1)

120-136: Previous issue has been addressed.

The conflicting identity/issuer options now correctly bail with clear error messages when both exact and regexp variants are provided. This prevents the silent ignore behavior flagged in the previous review.

🧹 Nitpick comments (10)
scripts/e2e.sh (2)

54-59: Remove redundant null check of SIGSTORE_IDENTITY_TOKEN.

Line 55 already checks that SIGSTORE_IDENTITY_TOKEN is non-empty; the second check at line 56 is redundant.

 if [[ -n "${SIGSTORE_IDENTITY_TOKEN:-}" ]]; then
-  if [[ -z "$SIGSTORE_IDENTITY_TOKEN" ]]; then
-    echo "ERROR: SIGSTORE_IDENTITY_TOKEN is set but empty" >&2
-    exit 1
-  fi
-
   echo "==> Test 3: Sigstore sign + verify"

69-77: Test multi-backend verification incompletely.

The multi-backend test (Test 4) adds a Sigstore signature to a GPG-signed PDF but only verifies the GPG signature. To ensure both backends function in the combined scenario, verify the Sigstore signature as well.

   "$PDF_SIGN" verify "$multi_backend" --cert cert1.asc | grep -x OK >/dev/null
+  # Also verify Sigstore signature is present
+  "$PDF_SIGN" verify "$multi_backend" | grep -x OK >/dev/null
   echo "    [OK] Multi-backend signing passed"

Confirm whether the intent is to verify both GPG and Sigstore signatures in the multi-backend test, or only ensure backward compatibility.

nix/package.nix (1)

60-82: Consider using the version for the Docker image tag instead of "latest".

Using tag = "latest" makes it difficult to track which version of the application is deployed. Consider using the version variable for reproducibility and traceability.

   image = pkgs.dockerTools.buildLayeredImage {
     name = "ghcr.io/0x77dev/pdf-sign";
-    tag = "latest";
+    tag = version;

Alternatively, expose both tags if you need latest for convenience:

passthru.imageLatest = image;
passthru.imageVersioned = pkgs.dockerTools.buildLayeredImage {
  name = "ghcr.io/0x77dev/pdf-sign";
  tag = version;
  # ... rest of config
};
crates/gpg/tests/challenge_response.rs (1)

57-73: Consider adding a test for valid signature acceptance.

The test suite validates that invalid signatures are rejected, but there's no test verifying that a correctly signed response is accepted. Adding a positive test case would improve coverage of the happy path.

Would you like me to help generate a test that creates a valid signature using the test certificate's secret key and verifies it passes validate_response? This would require using sequoia's signing capabilities to produce a detached signature over the challenge data.

crates/cli/src/cli.rs (1)

38-76: Consider adding conflicts_with for backend-specific options.

The Sign command mixes GPG-specific options (key, embed_uid) with Sigstore-specific options (oidc_issuer, fulcio_url, etc.). While the required_if_eq constraint ensures --key is provided for GPG, there's no validation preventing users from passing GPG-specific options when using Sigstore backend (or vice versa). These would be silently ignored.

Consider adding conflicts_with attributes to prevent confusion:

     /// Key specification: file path (.asc), fingerprint, key ID, or email (GPG backend only)
-    #[arg(short, long, required_if_eq("backend", "gpg"))]
+    #[arg(short, long, required_if_eq("backend", "gpg"), conflicts_with_all = ["oidc_issuer", "fulcio_url", "rekor_url", "oidc_client_id", "oidc_client_secret", "identity_token"])]
     key: Option<String>,

     /// Embed signer UID into the OpenPGP signature (GPG backend only)
-    #[arg(long)]
+    #[arg(long, conflicts_with_all = ["oidc_issuer", "fulcio_url", "rekor_url", "oidc_client_id", "oidc_client_secret", "identity_token"])]
     embed_uid: bool,

Alternatively, this could be documented as acceptable behavior where unused options are simply ignored.

crates/cli/src/commands.rs (2)

145-162: Consider warning users when auto-trusting embedded identity.

When no explicit identity constraints are provided, the code extracts the identity from the Sigstore bundle itself and verifies against it (lines 152-157). This means verification succeeds for any valid Sigstore signature, regardless of the signer's identity.

While this behavior is documented in the README (which requires explicit identity policy), a CLI warning when falling back to auto-trust would help prevent accidental approval of unexpected signers.

       let options = if has_sigstore_constraints {
         let policy = policy.clone();
         SigstoreVerifyOptions { policy, offline }
       } else {
+        eprintln!(
+          "{}",
+          style("    Warning: No identity policy provided - trusting embedded signer identity")
+            .yellow()
+        );
         let (id, iss) = pdf_sign_sigstore::verify::extract_identity_from_block(bundle_block)?;
         let policy = VerifyPolicy {
           certificate_identity: Some(CertificateIdentityMatcher::Exact(id)),
           certificate_oidc_issuer: Some(OidcIssuerMatcher::Exact(iss)),
         };
         SigstoreVerifyOptions { policy, offline }
       };

304-312: Consider extracting ChallengeJson to a shared module.

The ChallengeJson struct is defined twice - once for serialization in prepare_challenge (with gpg_command) and once for deserialization in apply_response (without it). While the current approach works, extracting to crates/cli/src/json.rs would improve consistency and maintainability.

Also applies to: 398-405

crates/cli/src/sign.rs (1)

83-96: Consider reusing the spinner instance.

A new ProgressBar spinner is created at lines 83-90 before connecting to GPG agent, but the previous spinner (lines 39-45) was already finished. This is fine but creates slightly repetitive code. The same pattern appears at lines 108-114 and 252-258 for writing the output file.

This is a minor stylistic observation - the current approach is clear and works correctly.

crates/gpg/src/challenge.rs (1)

204-225: Test coverage is minimal but adequate for the core serialization contract.

Consider adding tests for prepare_challenge and validate_response edge cases (invalid certs, version mismatch, fingerprint mismatch) in follow-up work.

crates/wasm/src/lib.rs (1)

261-324: Sigstore verification works but has minor API polish issues.

  1. TODO on line 307: Only the first Sigstore signature is verified; multiple signature support is deferred.

  2. Confusing result structure: The gpg_signatures field is always empty for Sigstore verification (line 315). Consider adding a sigstore_signatures field or a more generic signatures field to VerificationResult for consistency across backends.

Would you like me to propose a unified VerificationResult structure that works for both GPG and Sigstore signatures?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 10eca9b and 72f88ca.

⛔ Files ignored due to path filters (1)
  • flake.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • .cargo/config.toml (1 hunks)
  • Cargo.toml (1 hunks)
  • README.md (1 hunks)
  • crates/cli/src/cli.rs (1 hunks)
  • crates/cli/src/commands.rs (1 hunks)
  • crates/cli/src/sign.rs (1 hunks)
  • crates/cli/src/util.rs (1 hunks)
  • crates/core/src/digest.rs (1 hunks)
  • crates/core/src/pdf.rs (1 hunks)
  • crates/gpg/src/challenge.rs (1 hunks)
  • crates/gpg/src/keybox.rs (1 hunks)
  • crates/gpg/tests/challenge_response.rs (1 hunks)
  • crates/sigstore/src/sign.rs (1 hunks)
  • crates/wasm/src/lib.rs (1 hunks)
  • flake.nix (4 hunks)
  • nix/checks.nix (2 hunks)
  • nix/package.nix (2 hunks)
  • scripts/e2e.sh (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/cli/src/util.rs
  • crates/gpg/src/keybox.rs
  • crates/core/src/pdf.rs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-15T05:48:24.869Z
Learnt from: 0x77dev
Repo: 0x77dev/pdf-sign PR: 8
File: nix/git-hooks.nix:17-17
Timestamp: 2025-12-15T05:48:24.869Z
Learning: In Nix configurations, prefer using pkgs.prek as a drop-in replacement for pkgs.pre-commit when available in nixpkgs. prek is a Rust-based reimplementation of pre-commit designed for better performance (single binary, no Python runtime). Update references in Nix files (e.g., nix/git-hooks.nix) to use pkgs.prek instead of pkgs.pre-commit, and verify that prek is present in your nixpkgs version before making the switch.

Applied to files:

  • nix/package.nix
  • nix/checks.nix
  • flake.nix
🧬 Code graph analysis (7)
crates/gpg/tests/challenge_response.rs (1)
crates/gpg/src/challenge.rs (2)
  • prepare_challenge (51-84)
  • validate_response (146-202)
scripts/e2e.sh (1)
crates/sigstore/src/sign.rs (2)
  • default (20-28)
  • default (41-47)
crates/cli/src/cli.rs (1)
crates/core/src/digest.rs (1)
  • name (16-20)
crates/gpg/src/challenge.rs (1)
crates/gpg/src/verify.rs (2)
  • get_certs (24-67)
  • check (69-90)
crates/cli/src/sign.rs (7)
crates/core/src/digest.rs (3)
  • encode_sri (62-69)
  • name (16-20)
  • from_name (23-28)
crates/core/src/pdf.rs (1)
  • split_pdf (21-24)
crates/core/src/suffix.rs (2)
  • encode_suffix_block (100-121)
  • parse_suffix_blocks (39-96)
crates/cli/src/util.rs (1)
  • format_bytes (3-18)
crates/gpg/src/keybox.rs (1)
  • load_cert (139-168)
crates/gpg/src/sign.rs (1)
  • create_signature (31-106)
crates/wasm/src/lib.rs (1)
  • sign_sigstore (208-259)
crates/sigstore/src/sign.rs (2)
crates/core/src/digest.rs (3)
  • compute_digest (51-59)
  • try_from (41-46)
  • name (16-20)
crates/sigstore/src/verify.rs (1)
  • extract_bundle_info (268-337)
crates/wasm/src/lib.rs (7)
crates/cli/src/commands.rs (2)
  • prepare_challenge (249-359)
  • apply_response (362-509)
crates/gpg/src/challenge.rs (2)
  • prepare_challenge (51-84)
  • apply_response (91-139)
crates/core/src/pdf.rs (1)
  • split_pdf (21-24)
crates/core/src/suffix.rs (2)
  • encode_suffix_block (100-121)
  • parse_suffix_blocks (39-96)
crates/gpg/src/verify.rs (1)
  • verify_signatures (128-195)
crates/cli/src/sign.rs (1)
  • sign_sigstore (162-308)
crates/sigstore/src/sign.rs (1)
  • sign_blob (69-156)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build (linux-amd64)
  • GitHub Check: Dev Shell (macos-arm64)
  • GitHub Check: Build (macos-arm64)
  • GitHub Check: Build (linux-arm64)
🔇 Additional comments (34)
scripts/e2e.sh (2)

61-67: Clarify Sigstore verification behavior.

The Sigstore verify call (line 66) does not pass a --cert flag, unlike the GPG verify calls. If Sigstore certs are embedded in the signature bundle and verified automatically, this is correct. If a cert is required, add --cert to line 66 for consistency and explicit validation.

Confirm whether verify on a Sigstore-signed PDF requires an explicit cert flag or validates using embedded credentials.


1-40: Script structure and setup are solid.

The binary validation, GPG environment configuration, and dual-key generation are well-handled. The previous review's concern about CLI argument ordering has been resolved—all invocations now correctly place positional arguments before flags.

nix/package.nix (1)

7-18: LGTM! Comprehensive source filtering for workspace builds.

The filter correctly includes Rust sources, Cargo files, the crates directory, and delegates to craneLib.filterCargoSources for additional Cargo-related files.

Cargo.toml (1)

1-9: LGTM! Well-structured workspace configuration.

The workspace is properly configured with resolver "2" and clear member organization separating core, backend-specific (gpg, sigstore), CLI, and WASM crates.

flake.nix (1)

20-23: LGTM! Proper rust-overlay integration.

The rust-overlay input correctly follows nixpkgs to avoid dependency conflicts, and is properly wired into the overlays.

.cargo/config.toml (1)

1-5: LGTM! Appropriate WASM stack size configuration.

The 4MB stack size is reasonable for WASM builds that include cryptographic operations (OpenPGP/Sigstore), which can have deep call stacks.

crates/gpg/tests/challenge_response.rs (2)

7-12: LGTM! Clean test certificate helper.

The make_test_cert helper appropriately generates a general-purpose certificate for testing purposes.


14-26: LGTM! Comprehensive test for challenge preparation.

The test properly validates all key fields: version, data_to_sign content, non-empty fingerprint, and the embed_uid option.

README.md (1)

1-375: LGTM!

The documentation is comprehensive and well-structured, covering both GPG and Sigstore backends, challenge-response workflows, security considerations, and practical examples. The organization with clear sections for commands, features, and examples makes it easy to navigate.

crates/core/src/digest.rs (1)

1-131: LGTM!

The digest abstraction is well-designed with:

  • Clear enum representation with repr(u8) for efficient serialization
  • Flexible parsing that accepts common algorithm name variants
  • Proper validation of digest length in parse_sri
  • Good test coverage including edge cases like unpadded base64

The SRI format provides a standard, interoperable way to represent digests with algorithm agility.

crates/cli/src/cli.rs (1)

1-154: LGTM for overall CLI structure.

The Clap-based CLI is well-organized with clear subcommands and appropriate argument definitions. The use of value_enum for SigningBackend, global flags for --json and --verbose, and proper documentation strings make for a good user experience.

crates/cli/src/commands.rs (1)

1-509: LGTM for the overall implementation.

The verify, challenge, and apply-response flows are well-implemented with proper error handling, progress indication, and JSON output support. The code correctly preserves existing signature blocks when applying new signatures.

crates/cli/src/sign.rs (2)

310-339: Good test coverage for edge cases.

The tests properly validate the default output path logic including the edge case where a root path has no filename. This ensures robust error handling.


1-339: LGTM for the overall sign module.

The implementation cleanly separates GPG and Sigstore signing paths while maintaining consistent structure. Key features:

  • Proper preservation of existing signature blocks for multi-signer support
  • Good error context with anyhow
  • JSON output option for automation
  • Sensible defaults for Sigstore endpoints
crates/gpg/src/challenge.rs (4)

17-44: Well-structured data types for the challenge-response workflow.

The Challenge, ChallengeOptions, and SignResult structs are appropriately designed with clear serialization support and documentation. The separation of concerns is clean.


46-84: LGTM!

The prepare_challenge function correctly validates that the certificate has a signing-capable key before creating the challenge. The instrumentation and error handling are appropriate.


86-139: Previous cryptographic verification issue has been addressed.

The function now correctly passes the certificate to validate_response (line 120), enabling proper cryptographic verification of the signature against the expected key. The version and fingerprint validation logic is sound.


146-202: Cryptographic verification is now properly implemented.

The Helper implementation correctly provides the certificate via get_certs and validates signature results in check(). This aligns with the pattern used in crates/gpg/src/verify.rs and addresses the previous review concern about missing cryptographic verification.

crates/sigstore/src/sign.rs (6)

9-48: Well-structured configuration types with sensible defaults.

The SigstoreEndpoints and SignOptions structs provide good configurability. Default implementations use production constants appropriately.


72-93: Good documentation of sigstore-rs limitation.

The code now explicitly documents that SigningContext::async_production() hardcodes production endpoints, and logs a warning when custom endpoints are configured. This is an acceptable mitigation until sigstore-rs exposes a public API for custom endpoint configuration.


95-105: Clean token acquisition flow.

The dual-path approach (provided token for CI, interactive OIDC otherwise) is well-designed and documented.


107-156: LGTM!

The signing session creation, data signing, and bundle construction are implemented correctly. The digest computation aligns with the core crate's DigestAlgorithm implementation.


158-224: Certificate parsing implementation is sound.

The extraction of SAN identity and OIDC issuer from the Fulcio certificate follows the expected Sigstore certificate structure. The fallback to "unknown" for missing issuer is a reasonable default.


232-276: Port allocation and timeout issues from previous review are addressed.

Dynamic port allocation (line 237) and the 5-minute timeout (lines 270-276) resolve the previous concerns.

Minor TOCTOU race on port availability: The pattern of binding, extracting the port, then dropping the listener (line 245) before sigstore's RedirectListener::new creates a small window where another process could claim the port. This is low risk in practice and an inherent limitation of the sigstore-rs API.

crates/wasm/src/lib.rs (7)

11-17: Good initialization pattern for WASM.

The #[wasm_bindgen(start)] hook with conditional panic hook setup is appropriate for browser environments.


19-48: TypeScript-friendly data structures are well-designed.

The use of tsify with into_wasm_abi and from_wasm_abi enables seamless TypeScript integration. The camelCase rename convention aligns with JavaScript conventions.


50-92: LGTM!

The prepare_challenge function correctly bridges the GPG challenge API to WASM. The generated gpg_command uses the certificate's fingerprint which is a hex string, making it safe for shell interpolation.


94-147: Previous concern about ignored challenge parameter is now addressed.

The function now validates that challenge.data_base64 matches the PDF bytes being signed (lines 117-123), preventing accidental application of signatures to wrong documents. The validation and signature appending logic is correct.


149-204: LGTM!

The GPG verification correctly handles optional certificate provision and maps verification results to the WASM-friendly VerificationResult structure.


206-259: Well-implemented feature-gated Sigstore signing.

The function correctly preserves existing signature blocks while appending the new Sigstore bundle. The feature gate with explicit error message for disabled builds is appropriate.


326-348: Basic test coverage is present.

The serialization test validates the core Challenge structure. Consider adding tests for error paths (invalid cert, malformed PDF) in follow-up work.

nix/checks.nix (3)

26-27: LGTM! Comprehensive workspace testing.

The --workspace --all-features flags ensure thorough testing across all crates in the new multi-crate structure, which aligns well with the workspace refactoring mentioned in the PR objectives.


25-25: The code is correct as-is. CARGO_BUILD_TARGET is appropriately set only for cargo-test (line 25), not for pre-commit-check, because the environment variable is only relevant for Rust cargo operations, not for git hooks. The cargoTarget variable ensures consistent cross-platform Rust builds in CI contexts, which applies to the cargo test invocation but not to pre-commit hooks that run linters and other checks.

Likely an incorrect or invalid review comment.


8-14: Architecture hardcoding issue resolved.

The implementation correctly uses platform-native target detection (rust.rustcTarget with fallback to hostPlatform.config), which properly handles Intel Macs, ARM Linux, and other platform combinations that the previous hardcoded approach failed on.

Since your project uses nixos-unstable, the rust.rustcTarget attribute is available and this approach will work across all supported platforms without issues.

Comment on lines +181 to +234
let spinner = ProgressBar::new_spinner();
spinner.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.cyan} {msg}")
.unwrap(),
);
spinner.enable_steady_tick(Duration::from_millis(80));

spinner.set_message(format!("Reading PDF {}", style(input.display()).cyan()));
let mut pdf_data = Vec::new();
let mut file = BufReader::new(
File::open(&input).with_context(|| format!("Failed to open PDF: {}", input.display()))?,
);
file.read_to_end(&mut pdf_data)?;
spinner.finish_with_message(format!(
"[OK] Read PDF ({})",
style(format_bytes(pdf_data.len())).cyan()
));

let (clean_pdf, suffix) = split_pdf(&pdf_data)?;
let existing_blocks = parse_suffix_blocks(suffix)?;

let digest_alg = DigestAlgorithm::from_name(&digest_algorithm)?;

let endpoints = pdf_sign_sigstore::SigstoreEndpoints {
oidc_issuer: oidc_issuer.unwrap_or_else(|| pdf_sign_sigstore::DEFAULT_OIDC_ISSUER.to_string()),
fulcio_url: fulcio_url.unwrap_or_else(|| pdf_sign_sigstore::DEFAULT_FULCIO_URL.to_string()),
rekor_url: rekor_url.unwrap_or_else(|| pdf_sign_sigstore::DEFAULT_REKOR_URL.to_string()),
oidc_client_id: oidc_client_id
.unwrap_or_else(|| pdf_sign_sigstore::DEFAULT_OIDC_CLIENT_ID.to_string()),
oidc_client_secret: oidc_client_secret.unwrap_or_default(),
};

eprintln!(" OIDC Issuer: {}", style(&endpoints.oidc_issuer).dim());
eprintln!(" Fulcio: {}", style(&endpoints.fulcio_url).dim());
eprintln!(" Rekor: {}", style(&endpoints.rekor_url).dim());

if identity_token.is_none() {
eprintln!(
"\n{}",
style("Your browser will open for OIDC authentication...").yellow()
);
}

spinner.set_message("Performing keyless signing...");

let rt = tokio::runtime::Runtime::new()?;
let sign_options = pdf_sign_sigstore::sign::SignOptions {
endpoints,
digest_algorithm: digest_alg,
identity_token,
};

let sign_result = rt.block_on(pdf_sign_sigstore::sign::sign_blob(clean_pdf, &sign_options))?;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Spinner reuse after finish_with_message may not work as expected.

The spinner created at line 181 is finished at line 195-198 with finish_with_message. At line 225, spinner.set_message is called on the same finished spinner before signing. A finished ProgressBar may not display new messages.

Consider creating a new spinner for the signing phase:

   spinner.finish_with_message(format!(
     "[OK] Read PDF ({})",
     style(format_bytes(pdf_data.len())).cyan()
   ));

   // ... endpoint configuration ...

   if identity_token.is_none() {
     eprintln!(
       "\n{}",
       style("Your browser will open for OIDC authentication...").yellow()
     );
   }

-  spinner.set_message("Performing keyless signing...");
+  let spinner = ProgressBar::new_spinner();
+  spinner.set_style(
+    ProgressStyle::default_spinner()
+      .template("{spinner:.cyan} {msg}")
+      .unwrap(),
+  );
+  spinner.enable_steady_tick(Duration::from_millis(80));
+  spinner.set_message("Performing keyless signing...");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let spinner = ProgressBar::new_spinner();
spinner.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.cyan} {msg}")
.unwrap(),
);
spinner.enable_steady_tick(Duration::from_millis(80));
spinner.set_message(format!("Reading PDF {}", style(input.display()).cyan()));
let mut pdf_data = Vec::new();
let mut file = BufReader::new(
File::open(&input).with_context(|| format!("Failed to open PDF: {}", input.display()))?,
);
file.read_to_end(&mut pdf_data)?;
spinner.finish_with_message(format!(
"[OK] Read PDF ({})",
style(format_bytes(pdf_data.len())).cyan()
));
let (clean_pdf, suffix) = split_pdf(&pdf_data)?;
let existing_blocks = parse_suffix_blocks(suffix)?;
let digest_alg = DigestAlgorithm::from_name(&digest_algorithm)?;
let endpoints = pdf_sign_sigstore::SigstoreEndpoints {
oidc_issuer: oidc_issuer.unwrap_or_else(|| pdf_sign_sigstore::DEFAULT_OIDC_ISSUER.to_string()),
fulcio_url: fulcio_url.unwrap_or_else(|| pdf_sign_sigstore::DEFAULT_FULCIO_URL.to_string()),
rekor_url: rekor_url.unwrap_or_else(|| pdf_sign_sigstore::DEFAULT_REKOR_URL.to_string()),
oidc_client_id: oidc_client_id
.unwrap_or_else(|| pdf_sign_sigstore::DEFAULT_OIDC_CLIENT_ID.to_string()),
oidc_client_secret: oidc_client_secret.unwrap_or_default(),
};
eprintln!(" OIDC Issuer: {}", style(&endpoints.oidc_issuer).dim());
eprintln!(" Fulcio: {}", style(&endpoints.fulcio_url).dim());
eprintln!(" Rekor: {}", style(&endpoints.rekor_url).dim());
if identity_token.is_none() {
eprintln!(
"\n{}",
style("Your browser will open for OIDC authentication...").yellow()
);
}
spinner.set_message("Performing keyless signing...");
let rt = tokio::runtime::Runtime::new()?;
let sign_options = pdf_sign_sigstore::sign::SignOptions {
endpoints,
digest_algorithm: digest_alg,
identity_token,
};
let sign_result = rt.block_on(pdf_sign_sigstore::sign::sign_blob(clean_pdf, &sign_options))?;
let spinner = ProgressBar::new_spinner();
spinner.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.cyan} {msg}")
.unwrap(),
);
spinner.enable_steady_tick(Duration::from_millis(80));
spinner.set_message(format!("Reading PDF {}", style(input.display()).cyan()));
let mut pdf_data = Vec::new();
let mut file = BufReader::new(
File::open(&input).with_context(|| format!("Failed to open PDF: {}", input.display()))?,
);
file.read_to_end(&mut pdf_data)?;
spinner.finish_with_message(format!(
"[OK] Read PDF ({})",
style(format_bytes(pdf_data.len())).cyan()
));
let (clean_pdf, suffix) = split_pdf(&pdf_data)?;
let existing_blocks = parse_suffix_blocks(suffix)?;
let digest_alg = DigestAlgorithm::from_name(&digest_algorithm)?;
let endpoints = pdf_sign_sigstore::SigstoreEndpoints {
oidc_issuer: oidc_issuer.unwrap_or_else(|| pdf_sign_sigstore::DEFAULT_OIDC_ISSUER.to_string()),
fulcio_url: fulcio_url.unwrap_or_else(|| pdf_sign_sigstore::DEFAULT_FULCIO_URL.to_string()),
rekor_url: rekor_url.unwrap_or_else(|| pdf_sign_sigstore::DEFAULT_REKOR_URL.to_string()),
oidc_client_id: oidc_client_id
.unwrap_or_else(|| pdf_sign_sigstore::DEFAULT_OIDC_CLIENT_ID.to_string()),
oidc_client_secret: oidc_client_secret.unwrap_or_default(),
};
eprintln!(" OIDC Issuer: {}", style(&endpoints.oidc_issuer).dim());
eprintln!(" Fulcio: {}", style(&endpoints.fulcio_url).dim());
eprintln!(" Rekor: {}", style(&endpoints.rekor_url).dim());
if identity_token.is_none() {
eprintln!(
"\n{}",
style("Your browser will open for OIDC authentication...").yellow()
);
}
let spinner = ProgressBar::new_spinner();
spinner.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.cyan} {msg}")
.unwrap(),
);
spinner.enable_steady_tick(Duration::from_millis(80));
spinner.set_message("Performing keyless signing...");
let rt = tokio::runtime::Runtime::new()?;
let sign_options = pdf_sign_sigstore::sign::SignOptions {
endpoints,
digest_algorithm: digest_alg,
identity_token,
};
let sign_result = rt.block_on(pdf_sign_sigstore::sign::sign_blob(clean_pdf, &sign_options))?;
🤖 Prompt for AI Agents
In crates/cli/src/sign.rs around lines 181 to 234, the ProgressBar created at
the top is finished at the PDF-read section (finish_with_message) but then
reused at line ~225 via spinner.set_message("Performing keyless signing..."),
which may not update because a finished ProgressBar should not be reused; create
a new ProgressBar for the signing phase instead of reusing the finished one:
after spinner.finish_with_message(...) keep that spinner done, then instantiate
a new ProgressBar (apply the same ProgressStyle, enable_steady_tick) and set its
message to "Performing keyless signing..." before starting the signing runtime,
and finally finish that new spinner (e.g., finish_with_message) after sign_blob
completes.

renovate bot added 7 commits December 16, 2025 21:00
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
@0x77dev 0x77dev closed this Dec 30, 2025
@0x77dev 0x77dev deleted the v0.2 branch December 30, 2025 10:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request openpgp GPG related sigstore Sigstore related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants