PGP I/O: decrypt, sign, verify against gpg
ober
284e72204228f6feb5eb00d211378b3a94cb98e3
--- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ else NATIVE_LIB := $(NATIVE_RELEASE)/libjpgp_native.so endif -.PHONY: help run test build-native binary install clean +.PHONY: help run test test-interop build-native binary install clean .DEFAULT_GOAL := help help: @@ -20,6 +20,7 @@ help: @echo " make build-native Build pure-Rust crypto backend" @echo " make run ARGS='version' Run jpgp under the interpreter" @echo " make test Run smoke tests" + @echo " make test-interop Run bidirectional gpg interop tests" @echo "" @echo "Distribution:" @echo " make binary Build native binary (requires Chez+Jerboa)" @@ -45,6 +46,10 @@ test: build-native $(SCHEME) -q --libdirs $(CURDIR):$(JERBOA_HOME)/lib \ --script test/test-all.ss +test-interop: build-native + JERBOA_HOME=$(JERBOA_HOME) JPGP_REPO=$(CURDIR) SCHEME=$(SCHEME) \ + bash test/interop-gpg.sh + binary: build-native @echo "Native binary build not yet implemented — use 'make run' or 'make install-script'" --- a/pgp-native/Cargo.toml +++ b/pgp-native/Cargo.toml @@ -12,7 +12,7 @@ crate-type = ["staticlib", "cdylib"] [dependencies] age = { version = "0.10", default-features = false, features = ["armor"] } ed25519-dalek = { version = "2.1", default-features = false, features = ["rand_core", "std"] } -pgp = { version = "0.13", default-features = false } +pgp = { version = "0.14", default-features = false } zeroize = "1.7" rand = "0.8" rand_core = "0.6" --- a/pgp-native/src/lib.rs +++ b/pgp-native/src/lib.rs @@ -11,6 +11,7 @@ mod age_mod; mod error; mod pass_mod; mod pgp_mod; +mod pgp_io; mod sig_mod; mod util; @@ -264,6 +265,100 @@ pub unsafe extern "C" fn jpgp_pgp_encrypt( }) } +// ── OpenPGP inbound / sign / verify ───────────────────────────────────────── + +/// Decrypt an ASCII-armored OpenPGP message using an armored secret key. +/// `passphrase` may be the empty string for unencrypted secret keys. +#[no_mangle] +pub unsafe extern "C" fn jpgp_pgp_decrypt( + secret_armor_cstr: *const c_char, + passphrase_cstr: *const c_char, + cipher: *const u8, + cipher_len: u32, + out: *mut u8, + out_buf_len: u32, + out_len: *mut u32, +) -> i32 { + guard(|| { + let sec = match unsafe { cstr_to_str(secret_armor_cstr) } { + Some(s) => s, + None => return JPGP_E_INVALID_INPUT, + }; + let pass = match unsafe { cstr_to_str(passphrase_cstr) } { + Some(s) => s, + None => return JPGP_E_INVALID_INPUT, + }; + let ct = match unsafe { slice_from(cipher, cipher_len) } { + Some(s) => s, + None => return JPGP_E_INVALID_INPUT, + }; + match pgp_io::decrypt(sec, pass, ct) { + Ok(pt) => unsafe { write_out(&pt, out, out_buf_len, out_len) }, + Err(code) => code, + } + }) +} + +/// Produce a detached, ASCII-armored OpenPGP signature over `msg`. +#[no_mangle] +pub unsafe extern "C" fn jpgp_pgp_sign( + secret_armor_cstr: *const c_char, + passphrase_cstr: *const c_char, + msg: *const u8, + msg_len: u32, + out: *mut u8, + out_buf_len: u32, + out_len: *mut u32, +) -> i32 { + guard(|| { + let sec = match unsafe { cstr_to_str(secret_armor_cstr) } { + Some(s) => s, + None => return JPGP_E_INVALID_INPUT, + }; + let pass = match unsafe { cstr_to_str(passphrase_cstr) } { + Some(s) => s, + None => return JPGP_E_INVALID_INPUT, + }; + let m = match unsafe { slice_from(msg, msg_len) } { + Some(s) => s, + None => return JPGP_E_INVALID_INPUT, + }; + match pgp_io::sign_detached(sec, pass, m) { + Ok(sig) => unsafe { write_out(sig.as_bytes(), out, out_buf_len, out_len) }, + Err(code) => code, + } + }) +} + +/// Verify a detached ASCII-armored OpenPGP signature against `msg`. +/// Returns JPGP_OK on success, JPGP_E_VERIFY on bad signature. +#[no_mangle] +pub unsafe extern "C" fn jpgp_pgp_verify( + pubkey_armor_cstr: *const c_char, + sig_armor_cstr: *const c_char, + msg: *const u8, + msg_len: u32, +) -> i32 { + guard(|| { + let pk = match unsafe { cstr_to_str(pubkey_armor_cstr) } { + Some(s) => s, + None => return JPGP_E_INVALID_INPUT, + }; + let sig = match unsafe { cstr_to_str(sig_armor_cstr) } { + Some(s) => s, + None => return JPGP_E_INVALID_INPUT, + }; + let m = match unsafe { slice_from(msg, msg_len) } { + Some(s) => s, + None => return JPGP_E_INVALID_INPUT, + }; + match pgp_io::verify_detached(pk, sig, m) { + Ok(()) => JPGP_OK, + Err(code) => code, + } + }) +} + // ── SHA-256 (used for fingerprints) ───────────────────────────────────────── /// SHA-256 of `input` written to `out` (must be 32 bytes). new file mode 100644 --- /dev/null +++ b/pgp-native/src/pgp_io.rs @@ -0,0 +1,88 @@ +//! OpenPGP inbound + signing operations: decrypt, detached-sign, verify. +//! +//! `decrypt` parses SEIPDv1 (RFC 4880 packet 18, AES-CFB+MDC) only. +//! GnuPG 2.5's proprietary OCB packet (tag 20) is rejected by rPGP. To +//! get a gpg counterparty to emit SEIPDv1, its key must NOT advertise +//! AEAD in its self-sig prefs (subpacket 34 / features bit 0x02). +//! See `test/interop-gpg.sh` for the gpg.conf that produces a compatible key. + +use crate::error::*; +use chrono::SubsecRound; +use pgp::composed::{ + ArmorOptions, Deserializable, Message, SignedPublicKey, SignedSecretKey, StandaloneSignature, +}; +use pgp::crypto::hash::HashAlgorithm; +use pgp::packet::{SignatureConfig, SignatureType, Subpacket, SubpacketData}; +use pgp::types::PublicKeyTrait; + +/// Decrypt an ASCII-armored OpenPGP message using an armored secret key. +/// `passphrase` may be empty for unencrypted secret keys. +pub fn decrypt(secret_armor: &str, passphrase: &str, ciphertext: &[u8]) -> Result<Vec<u8>, i32> { + let (skey, _) = SignedSecretKey::from_armor_single(secret_armor.as_bytes()) + .map_err(|_| JPGP_E_PARSE_KEY)?; + skey.verify().map_err(|_| JPGP_E_PARSE_KEY)?; + + let (msg, _) = + Message::from_armor_single(ciphertext).map_err(|_| JPGP_E_PARSE_KEY)?; + + let pass = passphrase.to_owned(); + let (decrypted, _ids) = msg + .decrypt(|| pass.clone(), &[&skey]) + .map_err(|_| JPGP_E_DECRYPT)?; + + decrypted + .get_content() + .map_err(|_| JPGP_E_DECRYPT)? + .ok_or(JPGP_E_DECRYPT) +} + +/// Produce a detached, ASCII-armored OpenPGP signature over `data`. +pub fn sign_detached(secret_armor: &str, passphrase: &str, data: &[u8]) -> Result<String, i32> { + let (skey, _) = SignedSecretKey::from_armor_single(secret_armor.as_bytes()) + .map_err(|_| JPGP_E_PARSE_KEY)?; + skey.verify().map_err(|_| JPGP_E_PARSE_KEY)?; + + let now = chrono::Utc::now().trunc_subsecs(0); + let key_id = skey.key_id(); + let fpr = skey.fingerprint(); + + let mut config = SignatureConfig::v4( + SignatureType::Binary, + skey.algorithm(), + HashAlgorithm::SHA2_256, + ); + config.hashed_subpackets = vec![ + Subpacket::regular(SubpacketData::SignatureCreationTime(now)), + Subpacket::regular(SubpacketData::IssuerFingerprint(fpr)), + ]; + config.unhashed_subpackets = vec![Subpacket::regular(SubpacketData::Issuer(key_id))]; + + let pass = passphrase.to_owned(); + let sig = config + .sign(&skey, || pass.clone(), data) + .map_err(|_| JPGP_E_PGP)?; + + StandaloneSignature::new(sig) + .to_armored_string(ArmorOptions::default()) + .map_err(|_| JPGP_E_PGP) +} + +/// Verify a detached ASCII-armored OpenPGP signature. +pub fn verify_detached(pubkey_armor: &str, sig_armor: &str, data: &[u8]) -> Result<(), i32> { + let (pkey, _) = SignedPublicKey::from_armor_single(pubkey_armor.as_bytes()) + .map_err(|_| JPGP_E_PARSE_KEY)?; + pkey.verify().map_err(|_| JPGP_E_PARSE_KEY)?; + + let (sig, _) = StandaloneSignature::from_armor_single(sig_armor.as_bytes()) + .map_err(|_| JPGP_E_PARSE_KEY)?; + + if sig.verify(&pkey.primary_key, data).is_ok() { + return Ok(()); + } + for sub in &pkey.public_subkeys { + if sig.verify(sub, data).is_ok() { + return Ok(()); + } + } + Err(JPGP_E_VERIFY) +} --- a/pgp-native/src/pgp_mod.rs +++ b/pgp-native/src/pgp_mod.rs @@ -1,16 +1,15 @@ -//! OpenPGP outbound compatibility: encrypt-to-PGP-recipient via rPGP. -//! -//! v1 is encrypt-only — we never decrypt OpenPGP or sign in OpenPGP form. +//! OpenPGP outbound encryption — encrypt-to-PGP-recipient via rPGP. use crate::error::*; -use pgp::composed::{Deserializable, Message, SignedPublicKey}; +use pgp::composed::{ArmorOptions, Deserializable, Message, SignedPublicKey}; use pgp::crypto::sym::SymmetricKeyAlgorithm; -use pgp::types::KeyTrait; +use pgp::types::PublicKeyTrait; use rand::thread_rng; -/// Encrypt `plaintext` to an OpenPGP public key supplied as an ASCII-armored -/// `-----BEGIN PGP PUBLIC KEY BLOCK-----` string. Output is also ASCII-armored -/// (`-----BEGIN PGP MESSAGE-----`). +/// Encrypt `plaintext` to an ASCII-armored OpenPGP public key. Output is +/// ASCII-armored OpenPGP (`-----BEGIN PGP MESSAGE-----`). +/// +/// Uses SEIPDv1 (legacy) format for maximum interop with older GnuPG. pub fn encrypt(pubkey_armor: &str, plaintext: &[u8]) -> Result<String, i32> { let (pkey, _headers) = SignedPublicKey::from_armor_single(pubkey_armor.as_bytes()) .map_err(|_| JPGP_E_PARSE_KEY)?; @@ -19,24 +18,26 @@ pub fn encrypt(pubkey_armor: &str, plaintext: &[u8]) -> Result<String, i32> { let lit = Message::new_literal_bytes("msg.bin", plaintext); let mut rng = thread_rng(); - // Prefer an encryption subkey if present; otherwise use the primary. - let primary_can_encrypt = pkey.primary_key.is_encryption_key(); let enc_subkey = pkey .public_subkeys .iter() .find(|sk| sk.is_encryption_key()); let encrypted = if let Some(sub) = enc_subkey { - lit.encrypt_to_keys(&mut rng, SymmetricKeyAlgorithm::AES256, &[sub]) - .map_err(|_| JPGP_E_PGP)? - } else if primary_can_encrypt { - lit.encrypt_to_keys(&mut rng, SymmetricKeyAlgorithm::AES256, &[&pkey.primary_key]) + lit.encrypt_to_keys_seipdv1(&mut rng, SymmetricKeyAlgorithm::AES256, &[sub]) .map_err(|_| JPGP_E_PGP)? + } else if pkey.primary_key.is_encryption_key() { + lit.encrypt_to_keys_seipdv1( + &mut rng, + SymmetricKeyAlgorithm::AES256, + &[&pkey.primary_key], + ) + .map_err(|_| JPGP_E_PGP)? } else { return Err(JPGP_E_NO_RECIPIENT); }; encrypted - .to_armored_string(pgp::composed::ArmorOptions::default()) + .to_armored_string(ArmorOptions::default()) .map_err(|_| JPGP_E_PGP) } --- a/pgp/cli.ss +++ b/pgp/cli.ss @@ -283,7 +283,13 @@ Globally, -i/--in '-' means stdin and -o/--out '-' means stdout. [pt (jpgp-pass-decrypt ct pass)]) (write-file-bytes outp pt))] [pgp-key - (die "PGP decryption arrives in the next commit — wiring jpgp-pgp-decrypt FFI")] + (let* ([sec-armor (utf8->string (read-file-bytes pgp-key))] + [pgp-pass (or (opt opts "--pgp-pass") + (read-passphrase + (format "Passphrase for PGP key ~a (empty if unencrypted): " + pgp-key)))] + [pt (jpgp-pgp-decrypt sec-armor pgp-pass ct)]) + (write-file-bytes outp pt))] [else (let* ([pass (read-passphrase (format "Passphrase for ~a: " idp))] [id (load-identity idp pass)] @@ -329,51 +335,80 @@ Globally, -i/--in '-' means stdin and -o/--out '-' means stdout. (let* ([parsed (parse-opts rest '(("-i" . #t) ("--in" . #t) ("-o" . #t) ("--out" . #t) - ("--identity" . #t)))] + ("--identity" . #t) + ("--pgp-key" . #t) ("--pgp-pass" . #t)))] [opts (car parsed)] + [pgp-key (opt opts "--pgp-key")] [idp (opt-or opts "--identity" (default-identity-path))] [inp (or (opt opts "-i") (opt opts "--in") "-")] - [outp (or (opt opts "-o") (opt opts "--out") "-")]) - (let* ([pass (read-passphrase (format "Passphrase for ~a: " idp))] - [id (load-identity idp pass)] - [msg (read-file-bytes inp)] - [sig (jpgp-ed25519-sign (identity-ed25519-sk id) msg)] - [blob (format-sig-blob (identity-ed25519-pk id) sig)]) - (write-file-bytes outp (string->utf8 blob))))) + [outp (or (opt opts "-o") (opt opts "--out") "-")] + [msg (read-file-bytes inp)]) + (cond + [pgp-key + (let* ([sec-armor (utf8->string (read-file-bytes pgp-key))] + [pgp-pass (or (opt opts "--pgp-pass") + (read-passphrase + (format "Passphrase for PGP key ~a (empty if unencrypted): " + pgp-key)))] + [sig-armor (jpgp-pgp-sign sec-armor pgp-pass msg)]) + (write-file-bytes outp (string->utf8 sig-armor)))] + [else + (let* ([pass (read-passphrase (format "Passphrase for ~a: " idp))] + [id (load-identity idp pass)] + [sig (jpgp-ed25519-sign (identity-ed25519-sk id) msg)] + [blob (format-sig-blob (identity-ed25519-pk id) sig)]) + (write-file-bytes outp (string->utf8 blob)))]))) ;; ── verify ───────────────────────────────────────────────────────────── + (def (pgp-signature-armor? s) + (string-prefix? "-----BEGIN PGP SIGNATURE-----" (string-trim s))) + (def (cmd-verify rest) (let* ([parsed (parse-opts rest '(("-i" . #t) ("--in" . #t) - ("--pubkey" . #t)))] + ("--pubkey" . #t) + ("--pgp-pubkey" . #t)))] [opts (car parsed)] [pos (cdr parsed)] [sig-path (cond [(pair? pos) (car pos)] [else (die "verify: SIG_FILE required")])] - [inp (or (opt opts "-i") (opt opts "--in") "-")]) - (let* ([sig-text (utf8->string (read-file-bytes sig-path))] - [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)])))))) + [inp (or (opt opts "-i") (opt opts "--in") "-")] + [sig-text (utf8->string (read-file-bytes sig-path))] + [msg (read-file-bytes inp)]) + (cond + [(pgp-signature-armor? sig-text) + (let ([pgp-pub (opt opts "--pgp-pubkey")]) + (unless pgp-pub + (die "verify: OpenPGP signature needs --pgp-pubkey PUB.asc")) + (let ([pub-armor (utf8->string (read-file-bytes pgp-pub))]) + (cond + [(jpgp-pgp-verify pub-armor sig-text msg) + (display "Signature OK (OpenPGP)\n" (current-error-port))] + [else + (display "Signature BAD (OpenPGP)\n" (current-error-port)) + (exit 2)])))] + [else + (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)])))]))) ;; ── dispatcher ───────────────────────────────────────────────────────── --- a/pgp/ffi.ss +++ b/pgp/ffi.ss @@ -20,6 +20,9 @@ jpgp-ed25519-sign ;; sk-bv bv -> sig-bv jpgp-ed25519-verify ;; pk-bv bv sig-bv -> bool jpgp-pgp-encrypt ;; string bv -> bv + jpgp-pgp-decrypt ;; secret-armor passphrase ct-bv -> bv + jpgp-pgp-sign ;; secret-armor passphrase msg-bv -> string + jpgp-pgp-verify ;; pubkey-armor sig-armor msg-bv -> bool jpgp-sha256) ;; bv -> bv(32) (import (except (chezscheme) @@ -140,6 +143,21 @@ (foreign-procedure "jpgp_pgp_encrypt" (string u8* unsigned-32 u8* unsigned-32 u8*) integer-32))) + (define c-pgp-decrypt + (and *jpgp-loaded?* + (foreign-procedure "jpgp_pgp_decrypt" + (string string u8* unsigned-32 u8* unsigned-32 u8*) integer-32))) + + (define c-pgp-sign + (and *jpgp-loaded?* + (foreign-procedure "jpgp_pgp_sign" + (string string u8* unsigned-32 u8* unsigned-32 u8*) integer-32))) + + (define c-pgp-verify + (and *jpgp-loaded?* + (foreign-procedure "jpgp_pgp_verify" + (string string u8* unsigned-32) integer-32))) + (define c-sha256 (and *jpgp-loaded?* (foreign-procedure "jpgp_sha256" @@ -269,6 +287,32 @@ plaintext-bv (bytevector-length plaintext-bv) out buf-len out-len-bv)))) + (def (jpgp-pgp-decrypt secret-armor-str passphrase-str ciphertext-bv) + (need-lib 'jpgp-pgp-decrypt) + (call-with-buffer 'jpgp-pgp-decrypt + (lambda (out buf-len out-len-bv) + (c-pgp-decrypt secret-armor-str passphrase-str + ciphertext-bv (bytevector-length ciphertext-bv) + out buf-len out-len-bv)))) + + (def (jpgp-pgp-sign secret-armor-str passphrase-str msg-bv) + (need-lib 'jpgp-pgp-sign) + (utf8->string + (call-with-buffer 'jpgp-pgp-sign + (lambda (out buf-len out-len-bv) + (c-pgp-sign secret-armor-str passphrase-str + msg-bv (bytevector-length msg-bv) + out buf-len out-len-bv))))) + + (def (jpgp-pgp-verify pubkey-armor-str sig-armor-str msg-bv) + (need-lib 'jpgp-pgp-verify) + (let ([code (c-pgp-verify pubkey-armor-str sig-armor-str + msg-bv (bytevector-length msg-bv))]) + (cond + [(= code JPGP-OK) #t] + [(= code JPGP-E-VERIFY) #f] + [else (jpgp-error 'jpgp-pgp-verify "~a" (jpgp-error-message code))]))) + (def (jpgp-sha256 input-bv) (need-lib 'jpgp-sha256) (let ([out (make-bytevector 32 0)]) new file mode 100755 --- /dev/null +++ b/test/interop-gpg.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# Bidirectional OpenPGP interop test: jpgp ⇄ gpg +# +# Tests four directions: +# 1. jpgp encrypt → gpg decrypt +# 2. gpg encrypt → jpgp decrypt +# 3. jpgp sign → gpg verify +# 4. gpg sign → jpgp verify +# +# The gpg test keyring is configured to NOT advertise AEAD prefs, so gpg +# emits SEIPDv1 (RFC 4880, packet 18) which rPGP can parse. Modern GnuPG +# 2.5 defaults to AEAD/OCB (proprietary, packet 20), which rPGP does not +# support; the prefs override avoids that. +set -euo pipefail + +WORK=${WORK:-/tmp/jpgp-interop} +JPGP_REPO=${JPGP_REPO:-$(cd "$(dirname "$0")/.." && pwd)} +JERBOA_HOME=${JERBOA_HOME:-$(cd "$JPGP_REPO/../jerboa" && pwd)} +SCHEME=${SCHEME:-$JERBOA_HOME/.chez/bin/scheme} +JPGP_RUN=(env "JERBOA_HOME=$JERBOA_HOME" "JPGP_DIR=$JPGP_REPO" + "$SCHEME" -q --libdirs "$JPGP_REPO:$JERBOA_HOME/lib" + --script "$JPGP_REPO/pgp/main.ss" --) + +mkdir -p "$WORK" +export GNUPGHOME="$WORK/gnupg" +mkdir -p "$GNUPGHOME" +chmod 700 "$GNUPGHOME" + +# --- gpg config: loopback pinentry + no AEAD in default prefs ----------- +cat > "$GNUPGHOME/gpg.conf" <<EOF +pinentry-mode loopback +default-preference-list AES256 AES192 AES SHA512 SHA384 SHA256 ZLIB ZIP Uncompressed +personal-cipher-preferences AES256 AES192 AES +personal-digest-preferences SHA512 SHA384 SHA256 +personal-compress-preferences ZLIB ZIP Uncompressed +EOF +cat > "$GNUPGHOME/gpg-agent.conf" <<EOF +allow-loopback-pinentry +EOF +gpgconf --kill gpg-agent >/dev/null 2>&1 || true + +fail=0 +note() { printf '\n=== %s ===\n' "$*"; } +ok() { printf 'OK %s\n' "$*"; } +bad() { printf 'FAIL %s\n' "$*"; fail=$((fail+1)); } + +# --- gpg keygen (only if no key yet) ------------------------------------ +if ! gpg --list-keys jpgp-test@example.com >/dev/null 2>&1; then + note "Generating gpg test key" + cat > "$WORK/keygen.batch" <<EOF +%no-protection +Key-Type: EDDSA +Key-Curve: ed25519 +Key-Usage: sign +Subkey-Type: ECDH +Subkey-Curve: cv25519 +Subkey-Usage: encrypt +Name-Real: jpgp-test +Name-Email: jpgp-test@example.com +Expire-Date: 0 +%commit +EOF + gpg --batch --pinentry-mode loopback --rfc4880 --gen-key "$WORK/keygen.batch" 2>&1 \ + | grep -E "(revocation|key created|FAILED)" || true +fi + +gpg --batch --armor --export jpgp-test@example.com > "$WORK/gpg-pub.asc" +gpg --batch --pinentry-mode loopback --armor --export-secret-keys \ + jpgp-test@example.com > "$WORK/gpg-sec.asc" + +# --- jpgp keygen -------------------------------------------------------- +JPGP_PASS="test-pass-12345" +note "Generating jpgp identity" +if [[ ! -f "$WORK/jpgp.key" ]]; then + printf '%s\n%s\n' "$JPGP_PASS" "$JPGP_PASS" \ + | "${JPGP_RUN[@]}" keygen --out "$WORK/jpgp.key" >/dev/null 2>&1 +fi +printf '%s\n' "$JPGP_PASS" \ + | "${JPGP_RUN[@]}" pubkey --identity "$WORK/jpgp.key" --out "$WORK/jpgp.pub" 2>/dev/null + +# --- Test 1: jpgp encrypt → gpg decrypt -------------------------------- +note "Test 1: jpgp encrypt (to gpg pubkey) → gpg decrypt" +echo "hello from jpgp" > "$WORK/plain1.txt" +"${JPGP_RUN[@]}" encrypt -r "$WORK/gpg-pub.asc" \ + -i "$WORK/plain1.txt" -o "$WORK/jpgp-to-gpg.asc" +got=$(gpg --batch --pinentry-mode loopback --decrypt "$WORK/jpgp-to-gpg.asc" 2>/dev/null || echo "DECRYPT-FAIL") +[[ "$got" == "hello from jpgp" ]] && ok "test 1: gpg decrypted jpgp output" || bad "test 1: got '$got'" + +# --- Test 2: gpg encrypt → jpgp decrypt -------------------------------- +note "Test 2: gpg encrypt (to gpg pubkey) → jpgp decrypt" +echo "hello from gpg modern" > "$WORK/plain2.txt" +rm -f "$WORK/gpg-to-jpgp.asc" +gpg --batch --pinentry-mode loopback --rfc4880 \ + --recipient jpgp-test@example.com --trust-model always \ + -o "$WORK/gpg-to-jpgp.asc" --armor --encrypt "$WORK/plain2.txt" +"${JPGP_RUN[@]}" decrypt --pgp-key "$WORK/gpg-sec.asc" --pgp-pass "" \ + -i "$WORK/gpg-to-jpgp.asc" -o "$WORK/decrypted2.txt" 2>&1 +got=$(cat "$WORK/decrypted2.txt" 2>/dev/null || echo "") +[[ "$got" == "hello from gpg modern" ]] && ok "test 2: jpgp decrypted gpg output" || bad "test 2: got '$got'" + +# --- Test 3: jpgp sign → gpg verify ------------------------------------ +note "Test 3: jpgp sign → gpg verify" +echo "release v0.1.0" > "$WORK/release.txt" +"${JPGP_RUN[@]}" sign --pgp-key "$WORK/gpg-sec.asc" --pgp-pass "" \ + -i "$WORK/release.txt" -o "$WORK/release.sig.asc" +if gpg --batch --verify "$WORK/release.sig.asc" "$WORK/release.txt" 2>&1 | grep -q "Good signature"; then + ok "test 3: gpg verified jpgp signature" +else + bad "test 3: gpg rejected jpgp signature" +fi + +# --- Test 4: gpg sign → jpgp verify ------------------------------------ +note "Test 4: gpg sign → jpgp verify" +rm -f "$WORK/gpg-release.sig.asc" +gpg --batch --pinentry-mode loopback --armor --detach-sign \ + -u jpgp-test@example.com \ + -o "$WORK/gpg-release.sig.asc" "$WORK/release.txt" +if "${JPGP_RUN[@]}" verify --pgp-pubkey "$WORK/gpg-pub.asc" \ + -i "$WORK/release.txt" "$WORK/gpg-release.sig.asc" 2>&1 \ + | grep -q "Signature OK"; then + ok "test 4: jpgp verified gpg signature" +else + bad "test 4: jpgp rejected gpg signature" +fi + +# --- Summary ------------------------------------------------------------ +echo +if (( fail == 0 )); then + echo "All interop tests passed." + exit 0 +else + echo "$fail interop test(s) failed." + exit 1 +fi