Resolve security audit findings

ober

281eae6bf12359766c50fc9a845520fa6213c63c

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d5d9eaf..0622b8f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,7 +17,7 @@ jobs:
   build-test-audit:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
 
       - name: Install system tools
         run: |
diff --git a/.github/workflows/security-baseline.yml b/.github/workflows/security-baseline.yml
index 28a713e..5381c31 100644
--- a/.github/workflows/security-baseline.yml
+++ b/.github/workflows/security-baseline.yml
@@ -13,7 +13,7 @@ jobs:
   baseline:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
 
       - name: Required release files
         run: |
diff --git a/.jerbuild b/.jerbuild
index ed26f77..2b519bc 100644
--- a/.jerbuild
+++ b/.jerbuild
@@ -1,12 +1,9 @@
 ;; Build jpg with a standalone jerbuild binary.
 
 (entry "support/binary-entry.ss")
-(output "jpg")
-(requires "cc" "cargo" "bundled jerboa-native-rs")
-(notes "Builds pgp-native plus jerboa-native-rs with tls,sqlite,crypto. DuckDB is not required. The legacy OS-specific scripts still document extra static FFI symbol registration details.")
+(output "jpg-bin")
+(requires "cc" "cargo")
+(notes "Builds pgp-native as a static archive and registers only its reviewed C ABI symbols. No runtime native-library search is used.")
 (libdirs ".")
-(rust-crates
-  "pgp-native/Cargo.toml"
-  ("@bundle/jerboa-native-rs/Cargo.toml"
-   features: "tls,sqlite,crypto"
-   no-default-features: #t))
+(ffi-symbols "support/ffi-symbols.txt")
+(rust-crates "pgp-native/Cargo.toml")
diff --git a/Makefile b/Makefile
index 9545fd1..51bd98c 100644
--- a/Makefile
+++ b/Makefile
@@ -59,14 +59,18 @@ ensure-jerboa-tools:
 	fi
 
 run: build-native ensure-jerboa-tools
-	JERBOA_HOME=$(JERBOA_HOME) JPGP_DIR=$(CURDIR) \
+	JERBOA_HOME=$(JERBOA_HOME) JPGP_ALLOW_DYNAMIC_NATIVE=1 JPGP_DIR=$(CURDIR) \
 		$(JERBUILD) exec --libdirs $(CURDIR):$(JERBOA_HOME)/lib \
 			pgp/main.ss -- $(ARGS)
 
 test: build-native ensure-jerboa-tools
-	JERBOA_HOME=$(JERBOA_HOME) JPGP_DIR=$(CURDIR) \
+	cd $(NATIVE_DIR) && cargo test --release
+	JERBOA_HOME=$(JERBOA_HOME) JPGP_ALLOW_DYNAMIC_NATIVE=1 JPGP_DIR=$(CURDIR) \
 		$(JERBUILD) exec --libdirs $(CURDIR):$(JERBOA_HOME)/lib \
 			test/test-all.ss
+	JERBUILD="$(JERBUILD)" JERBOA_HOME="$(JERBOA_HOME)" \
+		JPGP_ALLOW_DYNAMIC_NATIVE=1 JPGP_DIR="$(CURDIR)" \
+		sh test/verify-cli.sh
 
 test-interop: build-native
 	JERBOA_HOME=$(JERBOA_HOME) JPGP_REPO=$(CURDIR) SCHEME=$(SCHEME) \
@@ -104,16 +108,15 @@ BINARY_OUTPUT ?= jpg-bin
 binary: build-native
 	@echo "==> Building self-contained $(BINARY_OUTPUT) binary"
 	JPGP_REPO=$(CURDIR) JERBOA_HOME=$(JERBOA_HOME) \
-		$(JERBUILD) binary --libdirs "$(CURDIR):$(JERBOA_HOME)/lib" support/binary-entry.ss $(BINARY_OUTPUT)
+		$(JERBUILD) build --config .jerbuild
+	@if [ "$(BINARY_OUTPUT)" != "jpg-bin" ]; then cp jpg-bin "$(BINARY_OUTPUT)"; fi
 	@echo ""
-	@echo "Built ./$(BINARY_OUTPUT). The native lib is still required at runtime:"
-	@echo "  $(NATIVE_LIB)"
-	@echo "  /usr/local/lib/libjpgp_native.{dylib,so}"
-	@echo "  /opt/homebrew/lib/libjpgp_native.{dylib,so}"
-	@echo "  or set JPGP_NATIVE_LIB=/path/to/libjpgp_native.{dylib,so}"
+	@echo "Built ./$(BINARY_OUTPUT) with libjpgp_native statically linked."
 
 binary-version-smoke: binary
-	JPGP_DIR=$(CURDIR) JPGP_NATIVE_LIB="$(NATIVE_LIB)" ./$(BINARY_OUTPUT) version
+	./$(BINARY_OUTPUT) version
+	JPGP_DIR="$(CURDIR)" JPGP_BINARY="$(CURDIR)/$(BINARY_OUTPUT)" CC="$(CC)" \
+		sh test/binary-loader-security.sh
 
 sbom: ensure-jerboa-tools
 	@REPO_ROOT="$(CURDIR)" \
@@ -154,8 +157,8 @@ release-evidence:
 
 install-script: build-native
 	mkdir -p $(BIN_DIR) $(MAN_DIR)
-	printf '#!/bin/sh\nexec %s -q --libdirs %s:%s/lib --script %s/pgp/main.ss -- "$$@"\n' \
-		"$(SCHEME)" "$(CURDIR)" "$(JERBOA_HOME)" "$(CURDIR)" \
+	printf '#!/bin/sh\nJPGP_ALLOW_DYNAMIC_NATIVE=1 JPGP_DIR=%s exec %s -q --libdirs %s:%s/lib --script %s/pgp/main.ss -- "$$@"\n' \
+		"$(CURDIR)" "$(SCHEME)" "$(CURDIR)" "$(JERBOA_HOME)" "$(CURDIR)" \
 		> $(BIN_DIR)/jpg
 	chmod +x $(BIN_DIR)/jpg
 	cp doc/jpg.1 $(MAN_DIR)/jpg.1
@@ -169,15 +172,13 @@ install-script: build-native
 	@echo "(This wraps the dev interpreter; for a static binary see PLAN.md)"
 
 install: binary
-	mkdir -p $(BIN_DIR) $(LIB_DIR) $(MAN_DIR)
+	mkdir -p $(BIN_DIR) $(MAN_DIR)
 	cp $(BINARY_OUTPUT)        $(BIN_DIR)/jpg
 	chmod +x                   $(BIN_DIR)/jpg
-	cp $(NATIVE_LIB)           $(LIB_DIR)/$(notdir $(NATIVE_LIB))
 	cp doc/jpg.1               $(MAN_DIR)/jpg.1
 	@echo ""
 	@echo "Installed:"
 	@echo "  $(BIN_DIR)/jpg"
-	@echo "  $(LIB_DIR)/$(notdir $(NATIVE_LIB))"
 	@echo "  $(MAN_DIR)/jpg.1"
 	@echo ""
 	@echo "Shell completions are in completions/ — copy to your distro's path:"
diff --git a/PLAN.md b/PLAN.md
index 44f1168..7be7259 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -82,7 +82,8 @@ jpg fingerprint [--identity P]       Print SHA256: fingerprint
 jpg encrypt [-r R ...] [-s] [-i I] [-o O]   Encrypt to age/jpgp recipients
 jpg decrypt [-s] [-i I] [-o O] [--identity P]
 jpg sign    [-i I] [-o O] [--identity P]
-jpg verify  SIG [-i I] [--pubkey P]
+jpg verify  SIG [-i I] --pubkey P
+jpg verify  SIG [-i I] --trust-embedded-key  # untrusted check, exits 3
 jpg version
 ```
 
@@ -121,7 +122,7 @@ make run ARGS='keygen'   # interpreter
 make test                # smoke tests
 make test-interop        # disabled OpenPGP interop status
 make binary              # native binary `jpg`
-make install             # → ~/.local/bin/jpg + ~/.local/lib/libjpgp_native
+make install             # → ~/.local/bin/jpg (native crypto is linked in)
 ```
 
 See [docs/BUILDING.md](docs/BUILDING.md) for what the binary build
diff --git a/README.md b/README.md
index c27540e..0e3eb50 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ jpg keygen
 jpg encrypt -r alice.pub.jpgp -i secret.txt -o secret.txt.age
 jpg decrypt -i secret.txt.age
 jpg sign -i release.tar.gz -o release.tar.gz.sig
-jpg verify release.tar.gz.sig -i release.tar.gz
+jpg verify release.tar.gz.sig -i release.tar.gz --pubkey release-signer.pub.jpgp
 ```
 
 Native crypto is [age]-style (X25519 + ChaCha20-Poly1305) for encryption and
@@ -58,7 +58,7 @@ make audit                # cargo-audit for the Rust backend
 make test-interop         # reports OpenPGP interop disabled/skipped
 make binary               # native `jpg` binary (5MB Mach-O / ELF)
 make release-evidence     # tests, audit, SBOM, reproducibility
-make install              # → ~/.local/bin/jpg + ~/.local/lib/libjpgp_native
+make install              # → ~/.local/bin/jpg (native crypto is linked in)
 ```
 
 Release evidence is documented in
diff --git a/completions/_jpg b/completions/_jpg
index 14da2bf..d035d81 100644
--- a/completions/_jpg
+++ b/completions/_jpg
@@ -74,6 +74,7 @@ _jpg() {
                 '-i[input file]:file:_files' \
                 '--in[input file]:file:_files' \
                 '--pubkey[jpgp1 pubkey]:file:_files' \
+                '--trust-embedded-key[untrusted embedded-key self-check]' \
                 '*:signature file:_files'
             ;;
     esac
diff --git a/completions/jpg.bash b/completions/jpg.bash
index c84c3a6..cd917c6 100644
--- a/completions/jpg.bash
+++ b/completions/jpg.bash
@@ -52,7 +52,7 @@ _jpg() {
                 -i|--in|--pubkey) _filedir ; return ;;
                 verify) _filedir ; return ;;
             esac
-            COMPREPLY=( $(compgen -W "-i --in --pubkey" -- "$cur") )
+            COMPREPLY=( $(compgen -W "-i --in --pubkey --trust-embedded-key" -- "$cur") )
             ;;
     esac
 }
diff --git a/doc/jpg.1 b/doc/jpg.1
index 18d1fd1..70ec706 100644
--- a/doc/jpg.1
+++ b/doc/jpg.1
@@ -93,14 +93,18 @@ OpenPGP signing options are rejected in production builds.
 .Cm verify
 .Ar SIG
 .Op Fl i Ar IN
-.Op Fl -pubkey Ar FILE
+.Fl -pubkey Ar FILE
 .Xc
 Verify the signature at
 .Ar SIG
 against
 .Ar IN .
-jpgp blobs are verified with the embedded pubkey
-.Pq or one supplied via Fl -pubkey .
+Trusted verification requires an externally authenticated key supplied with
+.Fl -pubkey .
+The alternative
+.Fl -trust-embedded-key
+performs an untrusted self-consistency check and exits with status 3 even when
+the signature is cryptographically valid.
 OpenPGP signatures are detected and rejected.
 Exit status is 0 on success, 2 on bad signature.
 .It Cm version
@@ -136,6 +140,7 @@ $ jpg encrypt -r age1xyz... -i secrets.txt -o secrets.age
 A bad signature in
 .Cm verify
 exits with status 2.
+An untrusted embedded-key self-check exits with status 3.
 .Sh INTEROPERABILITY
 .Nm
 does not currently interoperate with GnuPG.
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 8f20646..9095e19 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -159,7 +159,7 @@ best-effort, and uses `(pgp armor)` to see whether it is an OpenPGP message.
 If it is, the tool exits with the disabled-OpenPGP error. Otherwise it calls
 `jpgp-age-decrypt`.
 
-### `jpg verify SIG -i FILE`
+### `jpg verify SIG -i FILE --pubkey SIGNER.jpgp`
 
 `cmd-verify` reads `SIG` as text and inspects the armor header:
 
@@ -168,10 +168,11 @@ If it is, the tool exits with the disabled-OpenPGP error. Otherwise it calls
   SIGNATURE-----` with `pk:` and `sig:` lines), extracts the bytes,
   and calls `jpgp-ed25519-verify`.
 
-If the user passes `--pubkey FILE`, the embedded `pk:` from the blob is
-ignored in favour of the supplied `jpgp1` line — this defends against
-the trivial attack of replacing the embedded pubkey with one whose
-holder will sign anything.
+Trusted verification requires `--pubkey FILE`; the selected `jpgp1` key must
+also match the blob's embedded metadata. `--trust-embedded-key` is a separate
+self-consistency mode that reports `trust=untrusted-embedded` and exits 3 even
+when the signature is valid. Every outcome includes a stable `JPGP_VERIFY`
+status line with result, trust source, and signer fingerprint.
 
 ## The `(buf, buf_len, *out_len)` pattern
 
@@ -197,10 +198,8 @@ See [FFI.md](FFI.md) for the full ABI.
 `support/build-binary.sh` does what Jerboa's own `build-binary.sh` does,
 except it passes *both* the project's lib directory and Jerboa's lib so
 WPO can resolve `(pgp ...)` *and* `(jerboa prelude)` / `(std ...)`. The
-output is a single static binary that bundles `petite.boot`,
-`scheme.boot`, and a whole-program-optimised `program.boot` containing
-all the project Scheme code. The native crypto lib still lives outside
-the binary and is loaded at runtime — set `JPGP_NATIVE_LIB` to override
-the search.
+output is a single static binary that bundles `petite.boot`, `scheme.boot`, a
+whole-program-optimised `program.boot`, and the Rust native crypto archive.
+Runtime library search is disabled in the production artifact.
 
 See [BUILDING.md](BUILDING.md) for the step-by-step.
diff --git a/docs/BUILDING.md b/docs/BUILDING.md
index cb870aa..c6ba3e8 100644
--- a/docs/BUILDING.md
+++ b/docs/BUILDING.md
@@ -49,8 +49,9 @@ strict-audit-clean OpenPGP backend exists.
 `make run` invokes Chez `--script` against `pgp/main.ss` with both the
 project root and `$JERBOA_HOME/lib` on the `--libdirs` path. Sources
 are compiled on demand into `.so` files alongside the `.ss` files (Chez
-caches these), and `libjpgp_native.dylib` is dynamically loaded by
-`(pgp ffi)` on first call.
+caches these). `make run` explicitly enables the development-only loader and
+points it at the repository's absolute Cargo target path; ordinary launches do
+not search the working directory or platform loader path.
 
 This path is fast — about 100 ms cold start including library compile
 on a first invocation, then under 30 ms for subsequent runs. Good for
@@ -58,10 +59,12 @@ development; not what you want to ship.
 
 ## The binary build
 
-`make binary` runs `support/build-binary.sh` and produces a single
-~5 MB executable that bundles the Chez runtime and all project Scheme
-code. It still needs `libjpgp_native.{dylib,so}` at runtime, loaded
-from `~/.local/lib` (or the cargo target dir, or `$JPGP_NATIVE_LIB`).
+`make binary` invokes the checked-in `.jerbuild` configuration, which links the
+Rust crate and registers the allowlisted `support/ffi-symbols.txt` ABI, and
+produces a single
+~5 MB executable that bundles the Chez runtime, all project Scheme code, and
+the Rust `libjpgp_native` static archive. It does not search for or load a
+native crypto library at runtime.
 
 The script does four steps:
 
@@ -165,7 +168,6 @@ shaves another ~200 KB but slows startup measurably.
 
 ```
 ~/.local/bin/jpg                          (the binary, 5 MB)
-~/.local/lib/libjpgp_native.{dylib,so}    (Rust crypto, 3 MB)
 ~/.local/share/man/man1/jpg.1             (mandoc man page)
 ```
 
@@ -185,16 +187,13 @@ on the source tree continuing to exist at the same path.
 
 ## Static / musl builds
 
-The binary build is dynamic: `libkernel.a` is the only Chez-side static
-input, but `libjpgp_native.dylib` is loaded at runtime and `$OS_LIBS`
-links against system shared libraries. For a fully static musl build
-you'd need to:
+The normal binary embeds `libjpgp_native.a`, but Chez and `$OS_LIBS` may still
+link against system shared libraries. For a fully static musl build you still
+need to:
 
 - Build Chez against musl (non-trivial — Chez's bootstrapping uses gcc
   intrinsics that musl handles, but the build scripts have Linux-glibc
   assumptions).
-- Build `libjpgp_native` as a `staticlib` and link it directly into the
-  final binary instead of dyloading.
 - Replace `-lncurses` / `-ltinfo` with their static equivalents from
   the musl toolchain.
 
diff --git a/docs/FFI.md b/docs/FFI.md
index 119c06a..5386cfd 100644
--- a/docs/FFI.md
+++ b/docs/FFI.md
@@ -11,7 +11,7 @@ the wire format below is what's actually crossed.
 ## ABI version
 
 ```c
-uint32_t jpgp_abi_version(void);   // currently returns 1
+uint32_t jpgp_abi_version(void);   // currently returns 2
 ```
 
 Bumped on any incompatible change to the surface below. The Jerboa side
@@ -138,21 +138,25 @@ These wrap age's `scrypt` recipient mode. `_decrypt` returns
 ### Ed25519
 
 ```c
-int32_t jpgp_ed25519_keygen(uint8_t *out_sk /*32*/, uint8_t *out_pk /*32*/);
+int32_t jpgp_ed25519_keygen(
+    uint8_t *out_sk, uint32_t out_sk_len,
+    uint8_t *out_pk, uint32_t out_pk_len);
 
 int32_t jpgp_ed25519_sign(
-    const uint8_t *sk /*32*/,
+    const uint8_t *sk, uint32_t sk_len,
     const uint8_t *msg, uint32_t msg_len,
-    uint8_t       *out_sig /*64*/);
+    uint8_t       *out_sig, uint32_t out_sig_len);
 
 int32_t jpgp_ed25519_verify(
-    const uint8_t *pk /*32*/,
+    const uint8_t *pk, uint32_t pk_len,
     const uint8_t *msg, uint32_t msg_len,
-    const uint8_t *sig /*64*/);
+    const uint8_t *sig, uint32_t sig_len);
 ```
 
 `verify` returns `JPGP_OK` on a good signature, `JPGP_E_VERIFY` on a
-bad one. All buffer sizes are fixed; there is no size-query path.
+bad one. The ABI requires the key/signature capacities to be exactly 32/64
+bytes and returns `JPGP_E_INVALID_INPUT` before constructing Rust slices when
+they differ.
 
 ### OpenPGP compatibility stubs
 
@@ -207,16 +211,13 @@ no callback or async path.
 
 ## Loading the library
 
-`pgp/ffi.ss` tries paths in this order, falling through to the next on
-any error:
+Release binaries link `libjpgp_native` statically and resolve only the symbols
+registered by the executable. There is no CWD, `HOME`, installation-prefix, or
+bare-name fallback.
 
-1. `$JPGP_NATIVE_LIB` (if set)
-2. `$JPGP_DIR/pgp-native/target/release/libjpgp_native.{dylib,so}`
-3. `$JPGP_DIR/pgp-native/target/debug/...`
-4. `/usr/local/lib/...`
-5. `/opt/homebrew/lib/...`
-6. `$HOME/.local/lib/...`
-7. plain `libjpgp_native.{dylib,so}` (system loader)
-
-`(jpgp-available?)` returns `#f` if all paths failed, and every wrapper
-calls `need-lib` first to give a clean error.
+Interpreter-based development must explicitly set
+`JPGP_ALLOW_DYNAMIC_NATIVE=1` and either an absolute `JPGP_NATIVE_LIB` file or
+an absolute `JPGP_DIR` repository root. The loader rejects relative paths,
+symlinks (including symlinked path components), and all dynamic loading in root
+or real/effective-ID-mismatched processes. This development escape hatch is
+not a supported production deployment mechanism.
diff --git a/docs/FORMATS.md b/docs/FORMATS.md
index c1ec22c..1f54390 100644
--- a/docs/FORMATS.md
+++ b/docs/FORMATS.md
@@ -156,11 +156,12 @@ encoding, no headers — Ed25519 hashes its full input internally). The
 blob's verifier byte stream must match the original byte-for-byte for
 verification to succeed.
 
-`pk:` is embedded for convenience. By default `jpg verify` uses the
-embedded `pk:` to verify, which means a "signature OK" line by itself
-only proves the signer **claimed** this key. If you care that the
-signer is a specific party, supply `--pubkey FILE` so verification
-checks against an externally-trusted `jpgp1` line.
+`pk:` is embedded for diagnostics, not identity. Trusted `jpg verify` requires
+`--pubkey FILE` pointing at an externally authenticated `jpgp1` line. An
+explicit `--trust-embedded-key` performs only a self-consistency check, prints
+`trust=untrusted-embedded`, and exits 3 even for a valid signature. The parser
+rejects duplicate fields, invalid base64, blobs over 16 KiB, and decoded keys
+or signatures whose lengths are not exactly 32/64 bytes.
 
 Code: `pgp/cli.ss` (`format-sig-blob`, `parse-sig-blob`, `cmd-verify`).
 
diff --git a/docs/THREAT-MODEL.md b/docs/THREAT-MODEL.md
index 1a779ed..74dc9c6 100644
--- a/docs/THREAT-MODEL.md
+++ b/docs/THREAT-MODEL.md
@@ -167,15 +167,13 @@ sig: <base64 of Ed25519 signature>
 -----END JPGP SIGNATURE-----
 ```
 
-This is **convenience, not trust**. By default `jpg verify` uses the
-embedded `pk:` — which means a "Signature OK" line by itself just
-means "this signer signed this with the key they claim to hold." It
-proves nothing about whose key that is.
-
-For meaningful verification, pass `--pubkey FILE` pointing at a
+This is **diagnostic metadata, not trust**. `jpg verify` therefore refuses to
+report trusted success unless `--pubkey FILE` points at a
 `jpgp1` line you've authenticated out-of-band (paste, SSH-style
-TOFU on first contact, fingerprint exchange). `cmd-verify` then
-ignores the embedded `pk:` in favour of the supplied one.
+TOFU on first contact, fingerprint exchange). The embedded key must also match
+the pinned key. `--trust-embedded-key` is an explicit self-consistency mode;
+it reports `UNTRUSTED SELF-SIGNATURE`, emits `trust=untrusted-embedded`, and
+returns status 3 rather than trusted success.
 
 This is the same property as `ssh-keygen -Y verify`, which is fine for
 release artifact signing if users actually pin a key. We do not
@@ -188,15 +186,15 @@ provide a known_hosts-equivalent yet.
 | Read identity file only | Nothing — file is scrypt-encrypted                           |
 | Identity file + passphrase | Full secret keys. Equivalent to compromising the user. |
 | Read process memory     | Plaintext keys + passphrase while the process is running     |
-| Replace `libjpgp_native`| Full control — silently weaken keygen, leak signed data, etc. |
+| Replace a development `libjpgp_native`| Full control in an explicitly opted-in interpreter run |
 | Replace `jpg` binary    | Same as above                                                |
 | Supply OpenPGP armor | Operation is rejected; no OpenPGP runtime parser is linked |
 | MITM during `verify`    | Cannot forge a signature; can only cause false `BAD`         |
 
-The `libjpgp_native` replacement risk is why the Makefile installs it
-to `~/.local/lib` (user-writable but not world-writable) and the dylib
-search order in `pgp/ffi.ss` looks at user-owned paths before system
-paths. There is no signed-binary check; that's a packaging concern.
+Release binaries statically link `libjpgp_native` and perform no runtime native
+library search. Interpreter development requires an explicit opt-in plus an
+absolute, canonical repository/file path; it is rejected for privileged
+processes and is not a production packaging mechanism.
 
 ## What we deliberately don't do
 
diff --git a/pgp-native/src/lib.rs b/pgp-native/src/lib.rs
index cbcbafb..f1ddbd2 100644
--- a/pgp-native/src/lib.rs
+++ b/pgp-native/src/lib.rs
@@ -170,9 +170,14 @@ pub unsafe extern "C" fn jpgp_pass_decrypt(
 /// Generate a fresh Ed25519 keypair. Writes 32 secret bytes to `out_sk`
 /// and 32 public bytes to `out_pk`.
 #[no_mangle]
-pub unsafe extern "C" fn jpgp_ed25519_keygen(out_sk: *mut u8, out_pk: *mut u8) -> i32 {
+pub unsafe extern "C" fn jpgp_ed25519_keygen(
+    out_sk: *mut u8,
+    out_sk_len: u32,
+    out_pk: *mut u8,
+    out_pk_len: u32,
+) -> i32 {
     guard(|| {
-        if out_sk.is_null() || out_pk.is_null() {
+        if out_sk.is_null() || out_pk.is_null() || out_sk_len != 32 || out_pk_len != 32 {
             return JPGP_E_INVALID_INPUT;
         }
         let (sk, pk) = sig_mod::keygen();
@@ -188,20 +193,29 @@ pub unsafe extern "C" fn jpgp_ed25519_keygen(out_sk: *mut u8, out_pk: *mut u8) -
 #[no_mangle]
 pub unsafe extern "C" fn jpgp_ed25519_sign(
     sk: *const u8,
+    sk_len: u32,
     msg: *const u8,
     msg_len: u32,
     out_sig: *mut u8,
+    out_sig_len: u32,
 ) -> i32 {
     guard(|| {
-        if sk.is_null() || out_sig.is_null() {
+        if out_sig.is_null() || out_sig_len != 64 {
             return JPGP_E_INVALID_INPUT;
         }
+        let sk_slice = match unsafe { slice_from(sk, sk_len) } {
+            Some(s) if s.len() == 32 => s,
+            _ => return JPGP_E_INVALID_INPUT,
+        };
         let m = match unsafe { slice_from(msg, msg_len) } {
             Some(s) => s,
             None => return JPGP_E_INVALID_INPUT,
         };
-        let sk_arr: [u8; 32] = unsafe { *(sk as *const [u8; 32]) };
-        let sig = sig_mod::sign(&sk_arr, m);
+        let sk_arr: &[u8; 32] = match sk_slice.try_into() {
+            Ok(key) => key,
+            Err(_) => return JPGP_E_INVALID_INPUT,
+        };
+        let sig = sig_mod::sign(sk_arr, m);
         unsafe { slice::from_raw_parts_mut(out_sig, 64).copy_from_slice(&sig) };
         JPGP_OK
     })
@@ -212,21 +226,34 @@ pub unsafe extern "C" fn jpgp_ed25519_sign(
 #[no_mangle]
 pub unsafe extern "C" fn jpgp_ed25519_verify(
     pk: *const u8,
+    pk_len: u32,
     msg: *const u8,
     msg_len: u32,
     sig: *const u8,
+    sig_len: u32,
 ) -> i32 {
     guard(|| {
-        if pk.is_null() || sig.is_null() {
-            return JPGP_E_INVALID_INPUT;
-        }
+        let pk_slice = match unsafe { slice_from(pk, pk_len) } {
+            Some(s) if s.len() == 32 => s,
+            _ => return JPGP_E_INVALID_INPUT,
+        };
+        let sig_slice = match unsafe { slice_from(sig, sig_len) } {
+            Some(s) if s.len() == 64 => s,
+            _ => return JPGP_E_INVALID_INPUT,
+        };
         let m = match unsafe { slice_from(msg, msg_len) } {
             Some(s) => s,
             None => return JPGP_E_INVALID_INPUT,
         };
-        let pk_arr: [u8; 32] = unsafe { *(pk as *const [u8; 32]) };
-        let sig_arr: [u8; 64] = unsafe { *(sig as *const [u8; 64]) };
-        if sig_mod::verify(&pk_arr, m, &sig_arr) {
+        let pk_arr: &[u8; 32] = match pk_slice.try_into() {
+            Ok(key) => key,
+            Err(_) => return JPGP_E_INVALID_INPUT,
+        };
+        let sig_arr: &[u8; 64] = match sig_slice.try_into() {
+            Ok(signature) => signature,
+            Err(_) => return JPGP_E_INVALID_INPUT,
+        };
+        if sig_mod::verify(pk_arr, m, sig_arr) {
             JPGP_OK
         } else {
             JPGP_E_VERIFY
@@ -333,5 +360,114 @@ pub unsafe extern "C" fn jpgp_sha256(input: *const u8, input_len: u32, out: *mut
 /// ABI version. Bump when the FFI surface changes incompatibly.
 #[no_mangle]
 pub extern "C" fn jpgp_abi_version() -> u32 {
-    1
+    2
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn ed25519_ffi_rejects_wrong_fixed_lengths() {
+        let mut sk = [0u8; 32];
+        let mut pk = [0u8; 32];
+        let mut sig = [0u8; 64];
+        let msg = b"ffi boundary";
+
+        unsafe {
+            assert_eq!(
+                jpgp_ed25519_keygen(sk.as_mut_ptr(), 31, pk.as_mut_ptr(), 32),
+                JPGP_E_INVALID_INPUT
+            );
+            assert_eq!(
+                jpgp_ed25519_keygen(sk.as_mut_ptr(), 32, pk.as_mut_ptr(), 32),
+                JPGP_OK
+            );
+            assert_eq!(
+                jpgp_ed25519_keygen(std::ptr::null_mut(), 32, pk.as_mut_ptr(), 32),
+                JPGP_E_INVALID_INPUT
+            );
+            assert_eq!(
+                jpgp_ed25519_sign(
+                    sk.as_ptr(),
+                    32,
+                    msg.as_ptr(),
+                    msg.len() as u32,
+                    sig.as_mut_ptr(),
+                    64,
+                ),
+                JPGP_OK
+            );
+            assert_eq!(
+                jpgp_ed25519_verify(
+                    pk.as_ptr(),
+                    32,
+                    msg.as_ptr(),
+                    msg.len() as u32,
+                    sig.as_ptr(),
+                    64,
+                ),
+                JPGP_OK
+            );
+            for bad_len in [0, 1, 31, 33] {
+                assert_eq!(
+                    jpgp_ed25519_sign(
+                        sk.as_ptr(),
+                        bad_len,
+                        msg.as_ptr(),
+                        msg.len() as u32,
+                        sig.as_mut_ptr(),
+                        64,
+                    ),
+                    JPGP_E_INVALID_INPUT
+                );
+                assert_eq!(
+                    jpgp_ed25519_verify(
+                        pk.as_ptr(),
+                        bad_len,
+                        msg.as_ptr(),
+                        msg.len() as u32,
+                        sig.as_ptr(),
+                        64,
+                    ),
+                    JPGP_E_INVALID_INPUT
+                );
+            }
+            for bad_len in [0, 1, 63, 65] {
+                assert_eq!(
+                    jpgp_ed25519_verify(
+                        pk.as_ptr(),
+                        32,
+                        msg.as_ptr(),
+                        msg.len() as u32,
+                        sig.as_ptr(),
+                        bad_len,
+                    ),
+                    JPGP_E_INVALID_INPUT
+                );
+            }
+            assert_eq!(
+                jpgp_ed25519_sign(
+                    sk.as_ptr(),
+                    32,
+                    msg.as_ptr(),
+                    msg.len() as u32,
+                    sig.as_mut_ptr(),
+                    63,
+                ),
+                JPGP_E_INVALID_INPUT
+            );
+            assert_eq!(
+                jpgp_ed25519_verify(
+                    std::ptr::null(),
+                    32,
+                    msg.as_ptr(),
+                    msg.len() as u32,
+                    sig.as_ptr(),
+                    64,
+                ),
+                JPGP_E_INVALID_INPUT
+            );
+        }
+    }
 }
diff --git a/pgp/cli.ss b/pgp/cli.ss
index b208b89..81835a2 100644
--- a/pgp/cli.ss
+++ b/pgp/cli.ss
@@ -5,7 +5,7 @@
 ;;; a thin script wrapper.
 
 (library (pgp cli)
-  (export run-cli jpgp-version-string)
+  (export run-cli jpgp-version-string format-sig-blob parse-sig-blob)
 
   (import (rnrs)
           (except (jerboa prelude)
@@ -110,8 +110,12 @@ Commands:
   sign --pgp-key SECRET.asc [--pgp-pass PASS] [-i IN] [-o OUT]
       Rejected in production builds; OpenPGP interop is disabled.
 
-  verify SIG [-i IN] [--pubkey FILE]
-      Verify a jpgp signature against IN.
+  verify SIG [-i IN] --pubkey FILE
+      Verify a jpgp signature against an externally trusted key.
+
+  verify SIG [-i IN] --trust-embedded-key
+      Check only self-consistency using the signature's embedded key.
+      Reports an untrusted status and exits 3 even when cryptographically valid.
 
   version
       Print version.
@@ -309,25 +313,62 @@ Globally, -i/--in '-' means stdin and -o/--out '-' means stdout.
       "sig: " (u8vector->base64-string sig-bv) "\n"
       "-----END JPGP SIGNATURE-----\n"))
 
+  (def max-sig-blob-size 16384)
+
+  (def (decode-fixed-sig-field field encoded expected-len)
+    (when (> (string-length encoded) 256)
+      (jpgp-error 'parse-sig "~a field is too large" field))
+    (let ([decoded
+           (guard (e [#t (jpgp-error 'parse-sig "invalid base64 in ~a field" field)])
+             (base64-string->u8vector encoded))])
+      (unless (= (bytevector-length decoded) expected-len)
+        (jpgp-error 'parse-sig "~a field must decode to exactly ~a bytes"
+                    field expected-len))
+      decoded))
+
   (def (parse-sig-blob text)
+    (when (> (string-length text) max-sig-blob-size)
+      (jpgp-error 'parse-sig "signature blob exceeds ~a bytes" max-sig-blob-size))
     (let loop ([lines (string-split text #\newline)]
-               [pk #f] [sig #f])
+               [header? #f] [version? #f] [pk #f] [sig #f] [footer? #f])
       (cond
         [(null? lines)
-         (unless (and pk sig)
+         (unless (and header? version? pk sig footer?)
            (jpgp-error 'parse-sig "malformed signature blob"))
          (values pk sig)]
         [else
          (let ([line (string-trim (car lines))])
            (cond
-             [(string-prefix? "pk: " line)
-              (loop (cdr lines)
-                    (base64-string->u8vector (substring line 4 (string-length line)))
-                    sig)]
-             [(string-prefix? "sig: " line)
-              (loop (cdr lines) pk
-                    (base64-string->u8vector (substring line 5 (string-length line))))]
-             [else (loop (cdr lines) pk sig)]))])))
+             [(string=? line "")
+              (loop (cdr lines) header? version? pk sig footer?)]
+             [(string=? line "-----BEGIN JPGP SIGNATURE-----")
+              (when (or header? version? pk sig footer?)
+                (jpgp-error 'parse-sig "misplaced or duplicate signature header"))
+              (loop (cdr lines) #t version? pk sig footer?)]
+             [(string=? line "v: 1")
+              (unless (and header? (not version?) (not pk) (not sig) (not footer?))
+                (jpgp-error 'parse-sig "misplaced or duplicate version field"))
+              (loop (cdr lines) header? #t pk sig footer?)]
+             [(string-prefix? "pk:" line)
+              (unless (and header? version? (not pk) (not footer?))
+                (jpgp-error 'parse-sig "misplaced or duplicate pk field"))
+              (loop (cdr lines) header? version?
+                    (decode-fixed-sig-field
+                      "pk" (string-trim (substring line 3 (string-length line))) 32)
+                    sig footer?)]
+             [(string-prefix? "sig:" line)
+              (unless (and header? version? (not sig) (not footer?))
+                (jpgp-error 'parse-sig "misplaced or duplicate sig field"))
+              (loop (cdr lines) header? version? pk
+                    (decode-fixed-sig-field
+                      "sig" (string-trim (substring line 4 (string-length line))) 64)
+                    footer?)]
+             [(string=? line "-----END JPGP SIGNATURE-----")
+              (unless (and header? version? pk sig (not footer?))
+                (jpgp-error 'parse-sig "misplaced or duplicate signature footer"))
+              (loop (cdr lines) header? version? pk sig #t)]
+             [else
+              (jpgp-error 'parse-sig "unexpected signature line: ~a" line)]))])))
 
   (def (cmd-sign rest)
     (let* ([parsed (parse-opts rest
@@ -356,10 +397,21 @@ Globally, -i/--in '-' means stdin and -o/--out '-' means stdout.
   (def (pgp-signature-armor? s)
     (string-prefix? "-----BEGIN PGP SIGNATURE-----" (string-trim s)))
 
+  (def (signature-key-fingerprint pk)
+    (string-append "SHA256:"
+      (strip-trailing-padding
+        (u8vector->base64-string (jpgp-sha256 pk)))))
+
+  (def (emit-verify-status result trust pk)
+    (fprintf (current-error-port)
+             "JPGP_VERIFY result=~a trust=~a signer=~a~%"
+             result trust (signature-key-fingerprint pk)))
+
   (def (cmd-verify rest)
     (let* ([parsed (parse-opts rest
                      '(("-i" . #t) ("--in" . #t)
                        ("--pubkey" . #t)
+                       ("--trust-embedded-key" . #f)
                        ("--pgp-pubkey" . #t)))]
            [opts (car parsed)]
            [pos  (cdr parsed)]
@@ -367,31 +419,62 @@ Globally, -i/--in '-' means stdin and -o/--out '-' means stdout.
                        [(pair? pos) (car pos)]
                        [else (die "verify: SIG_FILE required")])]
            [inp  (or (opt opts "-i") (opt opts "--in") "-")]
-           [sig-text (utf8->string (read-file-bytes sig-path))])
-      (cond
-        [(pgp-signature-armor? sig-text)
-         (openpgp-disabled "verify")]
-        [else
-         (let ([msg (read-file-bytes inp)])
-         (let-values ([(pk-claim sig) (parse-sig-blob sig-text)])
-           (let ([pk (cond
-                       [(opt opts "--pubkey")
-                        => (lambda (p)
-                             (let* ([raw (utf8->string (read-file-bytes p))]
-                                    [rec (recipient-from-string raw)])
-                               (case (car rec)
-                                 [(jpgp)
-                                  (cond
-                                    [(assq 'ed25519 (cadr rec)) => cdr]
-                                    [else (die "pubkey file missing ed25519 field")])]
-                                 [else (die "--pubkey must point to a jpgp1 line")])))]
-                       [else pk-claim])])
-             (cond
-               [(jpgp-ed25519-verify pk msg sig)
-               (display "Signature OK\n" (current-error-port))]
-               [else
-                (display "Signature BAD\n" (current-error-port))
-                (exit 2)]))))])))
+           [sig-bytes (read-file-bytes sig-path)]
+           [pinned-path (opt opts "--pubkey")]
+           [trust-embedded? (opt opts "--trust-embedded-key")])
+      (when (> (bytevector-length sig-bytes) max-sig-blob-size)
+        (die "verify: signature blob exceeds ~a bytes" max-sig-blob-size))
+      (when (and pinned-path trust-embedded?)
+        (die "verify: --pubkey and --trust-embedded-key are mutually exclusive"))
+      (unless (or pinned-path trust-embedded?)
+        (die "verify: --pubkey FILE is required for trusted verification (or explicitly use --trust-embedded-key for an untrusted self-check)"))
+      (let ([sig-text (utf8->string sig-bytes)])
+        (cond
+          [(pgp-signature-armor? sig-text)
+           (openpgp-disabled "verify")]
+          [else
+           (let ([msg (read-file-bytes inp)])
+             (let-values ([(pk-claim sig) (parse-sig-blob sig-text)])
+               (let ([pk
+                      (cond
+                        [pinned-path
+                         (let* ([raw (utf8->string (read-file-bytes pinned-path))]
+                                [rec (recipient-from-string raw)])
+                           (case (car rec)
+                             [(jpgp)
+                              (cond
+                                [(assq 'ed25519 (cadr rec)) => cdr]
+                                [else (die "pubkey file missing ed25519 field")])]
+                             [else (die "--pubkey must point to a jpgp1 line")]))]
+                        [else pk-claim])])
+                 (unless (= (bytevector-length pk) 32)
+                   (die "verify: selected public key must be exactly 32 bytes"))
+                 (when (and pinned-path (not (bytevector=? pk pk-claim)))
+                   (display "Signature BAD (embedded key does not match pinned signer)\n"
+                            (current-error-port))
+                   (emit-verify-status "metadata-mismatch" "pinned" pk)
+                   (exit 2))
+                 (cond
+                   [(jpgp-ed25519-verify pk msg sig)
+                    (if pinned-path
+                        (begin
+                          (fprintf (current-error-port)
+                                   "Signature OK (trusted pinned key ~a)~%"
+                                   (signature-key-fingerprint pk))
+                          (emit-verify-status "valid" "pinned" pk))
+                        (begin
+                          (fprintf (current-error-port)
+                                   "UNTRUSTED SELF-SIGNATURE (embedded key ~a)~%"
+                                   (signature-key-fingerprint pk))
+                          (emit-verify-status "valid" "untrusted-embedded" pk)
+                          (exit 3)))]
+                   [else
+                    (display "Signature BAD\n" (current-error-port))
+                    (emit-verify-status
+                      "invalid"
+                      (if pinned-path "pinned" "untrusted-embedded")
+                      pk)
+                    (exit 2)]))))]))))
 
   ;; ── dispatcher ─────────────────────────────────────────────────────────
 
diff --git a/pgp/ffi.ss b/pgp/ffi.ss
index 0b32ac5..c341561 100644
--- a/pgp/ffi.ss
+++ b/pgp/ffi.ss
@@ -31,7 +31,11 @@
                   read-file-string path-join)
           (only (jerboa core) getenv)
           (only (jerboa ffi) c-lambda load-shared-object*)
+          (only (std os exec-id) exec-id-realpath-of)
           (only (std os platform) platform-macos?)
+          (only (std os posix) posix-getuid posix-geteuid)
+          (only (chezscheme)
+                file-symbolic-link? foreign-procedure)
           (pgp util))
 
   ;; ── Error codes (mirror src/error.rs) ───────────────────────────────────
@@ -73,34 +77,90 @@
 
   (define ext (if macos? ".dylib" ".so"))
 
-  (define here (or (getenv "JPGP_DIR") "."))
-
-  ;; JPGP_NATIVE_LIB, if set, is tried first — used by the standalone
-  ;; binary to point at a sibling-installed dylib.
-  (define *jpgp-lib-paths*
-    (let ([base
-            (list
-              (string-append here "/pgp-native/target/release/libjpgp_native" ext)
-              (string-append here "/pgp-native/target/debug/libjpgp_native"   ext)
-              (string-append "/usr/local/lib/libjpgp_native" ext)
-              (string-append "/opt/homebrew/lib/libjpgp_native" ext)
-              (string-append (or (getenv "HOME") ".") "/.local/lib/libjpgp_native" ext)
-              (string-append "libjpgp_native" ext))])
+  ;; Release binaries link libjpgp_native statically. Dynamic loading exists
+  ;; only for an explicitly opted-in development interpreter and never falls
+  ;; back to CWD, HOME, installation guesses, or a bare platform-loader name.
+  (def (dynamic-native-paths)
+    (let ([enabled (getenv "JPGP_ALLOW_DYNAMIC_NATIVE")]
+          [explicit (getenv "JPGP_NATIVE_LIB")]
+          [repo (getenv "JPGP_DIR")])
       (cond
-        [(getenv "JPGP_NATIVE_LIB") => (lambda (p) (cons p base))]
-        [else base])))
+        [(not (string=? (or enabled "") "1"))
+         (if (or explicit repo)
+             (jpgp-error 'jpgp-loader
+                         "dynamic native loading is disabled; use the statically linked binary")
+             '())]
+        [(or (= (posix-geteuid) 0)
+             (not (= (posix-getuid) (posix-geteuid))))
+         (jpgp-error 'jpgp-loader
+                     "dynamic native loading is forbidden in privileged processes")]
+        [explicit
+         (unless (path-absolute? explicit)
+           (jpgp-error 'jpgp-loader "JPGP_NATIVE_LIB must be absolute: ~a" explicit))
+         (list explicit)]
+        [repo
+         (unless (path-absolute? repo)
+           (jpgp-error 'jpgp-loader "JPGP_DIR must be absolute: ~a" repo))
+         (list
+           (string-append repo "/pgp-native/target/release/libjpgp_native" ext)
+           (string-append repo "/pgp-native/target/debug/libjpgp_native" ext))]
+        [else
+         (jpgp-error 'jpgp-loader
+                     "development loading requires JPGP_NATIVE_LIB or JPGP_DIR")])))
 
   (define *jpgp-loaded?* #f)
   (define *jpgp-bindings-ready?* #f)
 
+  (def c-lstat (foreign-procedure "lstat" (string u8*) int))
+
+  ;; Dynamic loading is development-only, but still reject replaceable files
+  ;; and directories.  A canonical leaf is insufficient: an attacker who can
+  ;; write one of its parent directories can swap the file after validation.
+  (def (trusted-path-component? path expected-type)
+    (let ([stat-buffer (make-bytevector 256 0)])
+      (and (= 0 (c-lstat path stat-buffer))
+           (let* ([mode (if macos?
+                            (bytevector-u16-native-ref stat-buffer 4)
+                            (bytevector-u32-native-ref stat-buffer 24))]
+                  [uid (bytevector-u32-native-ref stat-buffer
+                         (if macos? 16 28))]
+                  [kind (bitwise-and mode #o170000)])
+             (and (= kind (if (eq? expected-type 'directory)
+                              #o040000 #o100000))
+                  (or (= uid 0) (= uid (posix-getuid)))
+                  (= 0 (bitwise-and mode #o022)))))))
+
+  (def (trusted-parent-chain? path)
+    (let loop ([directory (path-directory path)])
+      (and (trusted-path-component? directory 'directory)
+           (let ([parent (path-directory directory)])
+             (or (string=? parent directory)
+                 (loop parent))))))
+
+  (def (trusted-native-path? path)
+    (and (not (file-symbolic-link? path))
+         (let ([canonical (exec-id-realpath-of path)])
+           (and (string=? canonical path)
+                (trusted-path-component? canonical 'regular)
+                (trusted-parent-chain? canonical)))))
+
   (def (try-load-one! path)
+    (and (file-exists? path)
+         (begin
+           (unless (trusted-native-path? path)
+             (jpgp-error 'jpgp-loader
+                         "untrusted native library path: ~a" path))
+           (guard (e [(condition? e) #f])
+             (load-shared-object* path)
+             #t))))
+
+  (def (prelinked-abi-valid?)
     (guard (e [(condition? e) #f])
-      (load-shared-object* path)
-      #t))
+      (= ((c-lambda () unsigned-32 "jpgp_abi_version")) 2)))
 
   (def (try-load-jpgp!)
     (or *jpgp-loaded?*
-        (let loop ([ps *jpgp-lib-paths*])
+        (let loop ([ps (dynamic-native-paths)])
           (cond
             [(null? ps) #f]
             [(try-load-one! (car ps))
@@ -126,7 +186,13 @@
   (define c-abi-version #f)
 
   (def (ensure-bindings!)
-    (when (and (try-load-jpgp!) (not *jpgp-bindings-ready?*))
+    (when (and (not *jpgp-bindings-ready?*)
+               (or (prelinked-abi-valid?)
+                   (and (try-load-jpgp!) (prelinked-abi-valid?))))
+      (set! c-abi-version
+            (c-lambda () unsigned-32 "jpgp_abi_version"))
+      (unless (= (c-abi-version) 2)
+        (jpgp-error 'jpgp-loader "native ABI mismatch; expected version 2"))
       (set! c-age-keygen
             (c-lambda (u8* unsigned-32 u8* u8* unsigned-32 u8*) int "jpgp_age_keygen"))
       (set! c-age-encrypt
@@ -138,11 +204,11 @@
       (set! c-pass-decrypt
             (c-lambda (u8* unsigned-32 string u8* unsigned-32 u8*) int "jpgp_pass_decrypt"))
       (set! c-ed25519-keygen
-            (c-lambda (u8* u8*) int "jpgp_ed25519_keygen"))
+            (c-lambda (u8* unsigned-32 u8* unsigned-32) int "jpgp_ed25519_keygen"))
       (set! c-ed25519-sign
-            (c-lambda (u8* u8* unsigned-32 u8*) int "jpgp_ed25519_sign"))
+            (c-lambda (u8* unsigned-32 u8* unsigned-32 u8* unsigned-32) int "jpgp_ed25519_sign"))
       (set! c-ed25519-verify
-            (c-lambda (u8* u8* unsigned-32 u8*) int "jpgp_ed25519_verify"))
+            (c-lambda (u8* unsigned-32 u8* unsigned-32 u8* unsigned-32) int "jpgp_ed25519_verify"))
       (set! c-pgp-encrypt
             (c-lambda (string u8* unsigned-32 u8* unsigned-32 u8*) int "jpgp_pgp_encrypt"))
       (set! c-pgp-decrypt
@@ -153,8 +219,6 @@