@@ -329,6 +329,204 @@ fn ec_sign_verify_with_gpg() {
329329 sign_verify_roundtrip ( & env, & env. ec_label , "Test EC <ec@example.com>" ) ;
330330}
331331
332+ // ---------------------------------------------------------------------------
333+ // sq (Sequoia CLI) parity tests.
334+ //
335+ // The gpg-based tests above prove our outputs are accepted by GnuPG.
336+ // The tests below assert the same artefacts verify cleanly through
337+ // Sequoia's own `sq` CLI — different OpenPGP implementation, different
338+ // crypto backend, useful as a sanity check that we don't accidentally
339+ // produce GnuPG-leniency-shaped artefacts.
340+ // ---------------------------------------------------------------------------
341+
342+ /// `sq` invocation pointed at an isolated home directory so tests don't
343+ /// touch the operator's real Sequoia keystore.
344+ fn sq_cli ( home : & Path ) -> StdCommand {
345+ let mut c = StdCommand :: new ( "sq" ) ;
346+ c. arg ( "--home" ) . arg ( home) . arg ( "--batch" ) ;
347+ c
348+ }
349+
350+ /// Set up a fresh sq home directory inside `tmp` and return its path.
351+ fn fresh_sq_home ( tmp : & TempDir ) -> PathBuf {
352+ let home = tmp. path ( ) . join ( "sq" ) ;
353+ std:: fs:: create_dir_all ( & home) . unwrap ( ) ;
354+ #[ cfg( unix) ]
355+ {
356+ use std:: os:: unix:: fs:: PermissionsExt ;
357+ std:: fs:: set_permissions ( & home, std:: fs:: Permissions :: from_mode ( 0o700 ) ) . unwrap ( ) ;
358+ }
359+ home
360+ }
361+
362+ fn sq_sign_verify_roundtrip ( env : & TestEnv , key_label : & str , userid : & str ) {
363+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
364+ let cert_path = tmp. path ( ) . join ( "cert.asc" ) ;
365+ let payload = tmp. path ( ) . join ( "payload.txt" ) ;
366+ let signature = tmp. path ( ) . join ( "payload.txt.asc" ) ;
367+ let sq_home = fresh_sq_home ( & tmp) ;
368+
369+ std:: fs:: write ( & payload, b"test payload bytes\n " ) . unwrap ( ) ;
370+
371+ // 1. Export the cert via sq-pkcs11.
372+ sq_pkcs11 ( env)
373+ . args ( [ "cert-export" ] )
374+ . args ( [ "--key-label" , key_label] )
375+ . args ( [ "--userid" , userid] )
376+ . args ( [ "--creation-time" , STABLE_TIME ] )
377+ . args ( [ "--output" ] )
378+ . arg ( & cert_path)
379+ . assert ( )
380+ . success ( ) ;
381+
382+ // 2. Produce a detached signature via sq-pkcs11.
383+ sq_pkcs11 ( env)
384+ . args ( [ "sign" ] )
385+ . args ( [ "--key-label" , key_label] )
386+ . args ( [ "--creation-time" , STABLE_TIME ] )
387+ . arg ( & payload)
388+ . assert ( )
389+ . success ( ) ;
390+ assert ! ( signature. exists( ) , "sign did not create {signature:?}" ) ;
391+
392+ // 3. Verify with sq. --signer-file gives sq the cert directly so
393+ // we don't have to import into a keystore first; --signature-file
394+ // points at the detached signature. sq exits 0 on success.
395+ let verify = sq_cli ( & sq_home)
396+ . arg ( "verify" )
397+ . arg ( "--signer-file" )
398+ . arg ( & cert_path)
399+ . arg ( "--signature-file" )
400+ . arg ( & signature)
401+ . arg ( & payload)
402+ . output ( )
403+ . expect ( "sq verify" ) ;
404+ assert ! (
405+ verify. status. success( ) ,
406+ "sq verify failed for {key_label}:\n stdout: {}\n stderr: {}" ,
407+ String :: from_utf8_lossy( & verify. stdout) ,
408+ String :: from_utf8_lossy( & verify. stderr) ,
409+ ) ;
410+ }
411+
412+ #[ test]
413+ fn rsa_sign_verify_with_sq ( ) {
414+ let env = require_env ! ( ) ;
415+ sq_sign_verify_roundtrip ( & env, & env. rsa_label , "SQ Test RSA <sq-rsa@example.com>" ) ;
416+ }
417+
418+ #[ test]
419+ fn ec_sign_verify_with_sq ( ) {
420+ let env = require_env ! ( ) ;
421+ sq_sign_verify_roundtrip ( & env, & env. ec_label , "SQ Test EC <sq-ec@example.com>" ) ;
422+ }
423+
424+ #[ test]
425+ fn sq_honours_standalone_subkey_revocation ( ) {
426+ // Sequoia's sq (unlike GnuPG — see README's caveat under
427+ // "Caveat: GnuPG ignores standalone subkey-revocation files")
428+ // *does* apply a SubkeyRevocation packet imported on its own.
429+ // This test confirms our subkey-revoke output is structurally
430+ // correct and would Just Work with any Sequoia-based verifier.
431+ //
432+ // Flow: build a two-tier cert, sign a payload with the subkey,
433+ // verify (must succeed), then issue a "compromised" subkey
434+ // revocation, merge cert+revocation into one file, and verify
435+ // again (must fail because the signing subkey is now revoked
436+ // and "compromised" invalidates past signatures).
437+
438+ let env = require_env ! ( ) ;
439+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
440+ let cert_path = tmp. path ( ) . join ( "cert.asc" ) ;
441+ let merged_cert = tmp. path ( ) . join ( "cert-with-revocation.asc" ) ;
442+ let payload = tmp. path ( ) . join ( "payload.txt" ) ;
443+ let signature = tmp. path ( ) . join ( "payload.txt.asc" ) ;
444+ let revocation = tmp. path ( ) . join ( "subkey-revocation.asc" ) ;
445+ let sq_home = fresh_sq_home ( & tmp) ;
446+
447+ std:: fs:: write ( & payload, b"sq subkey-revocation parity\n " ) . unwrap ( ) ;
448+
449+ // 1. Two-tier cert.
450+ sq_pkcs11 ( & env)
451+ . args ( [ "cert-export" ] )
452+ . args ( [ "--key-label" , & env. primary_label ] )
453+ . args ( [ "--subkey-label" , & env. subkey_label ] )
454+ . args ( [ "--userid" , "SQ Subkey Revoke <sq-skrev@example.com>" ] )
455+ . args ( [ "--creation-time" , STABLE_TIME ] )
456+ . args ( [ "--subkey-creation-time" , STABLE_TIME ] )
457+ . args ( [ "--output" ] )
458+ . arg ( & cert_path)
459+ . assert ( )
460+ . success ( ) ;
461+
462+ // 2. Sign with the subkey.
463+ sq_pkcs11 ( & env)
464+ . args ( [ "sign" ] )
465+ . args ( [ "--key-label" , & env. subkey_label ] )
466+ . args ( [ "--creation-time" , STABLE_TIME ] )
467+ . arg ( & payload)
468+ . assert ( )
469+ . success ( ) ;
470+
471+ // 3. Verify against the un-revoked cert — must succeed.
472+ let verify_before = sq_cli ( & sq_home)
473+ . arg ( "verify" )
474+ . arg ( "--signer-file" )
475+ . arg ( & cert_path)
476+ . arg ( "--signature-file" )
477+ . arg ( & signature)
478+ . arg ( & payload)
479+ . output ( )
480+ . expect ( "sq verify (before revocation)" ) ;
481+ assert ! (
482+ verify_before. status. success( ) ,
483+ "sq verify against the un-revoked cert must succeed:\n stderr: {}" ,
484+ String :: from_utf8_lossy( & verify_before. stderr) ,
485+ ) ;
486+
487+ // 4. Issue a "compromised" subkey revocation.
488+ sq_pkcs11 ( & env)
489+ . args ( [ "subkey-revoke" ] )
490+ . args ( [ "--key-label" , & env. primary_label ] )
491+ . args ( [ "--subkey-label" , & env. subkey_label ] )
492+ . args ( [ "--creation-time" , STABLE_TIME ] )
493+ . args ( [ "--subkey-creation-time" , STABLE_TIME ] )
494+ . args ( [ "--reason" , "compromised" ] )
495+ . args ( [ "--message" , "sq subkey revocation parity test" ] )
496+ . args ( [ "--output" ] )
497+ . arg ( & revocation)
498+ . assert ( )
499+ . success ( ) ;
500+
501+ // 5. Merge cert + standalone revocation into one file. Sequoia's
502+ // Cert parser handles the concatenation natively (a TPK followed
503+ // by a SubkeyRevocation Signature merges into a cert with the
504+ // subkey marked revoked).
505+ let cert_bytes = std:: fs:: read ( & cert_path) . unwrap ( ) ;
506+ let rev_bytes = std:: fs:: read ( & revocation) . unwrap ( ) ;
507+ std:: fs:: write ( & merged_cert, [ & cert_bytes[ ..] , & rev_bytes[ ..] ] . concat ( ) ) . unwrap ( ) ;
508+
509+ // 6. Verify against the merged cert — must FAIL because the signing
510+ // subkey is now revoked with reason "compromised", which
511+ // invalidates past signatures.
512+ let verify_after = sq_cli ( & sq_home)
513+ . arg ( "verify" )
514+ . arg ( "--signer-file" )
515+ . arg ( & merged_cert)
516+ . arg ( "--signature-file" )
517+ . arg ( & signature)
518+ . arg ( & payload)
519+ . output ( )
520+ . expect ( "sq verify (after revocation)" ) ;
521+ assert ! (
522+ !verify_after. status. success( ) ,
523+ "sq verify against the cert-with-subkey-revocation must FAIL when \
524+ the subkey was revoked as compromised:\n stdout: {}\n stderr: {}",
525+ String :: from_utf8_lossy( & verify_after. stdout) ,
526+ String :: from_utf8_lossy( & verify_after. stderr) ,
527+ ) ;
528+ }
529+
332530// ---------------------------------------------------------------------------
333531// Output format
334532// ---------------------------------------------------------------------------
0 commit comments