2023-06-02 01:22:52 +03:00
#[ cfg(test) ]
mod integration {
use std ::path ;
use assert_cmd ::Command ;
use predicates ::prelude ::* ;
2024-05-28 15:33:27 +03:00
use tempfile ::TempDir ;
2023-06-02 01:22:52 +03:00
use sequoia_openpgp as openpgp ;
use openpgp ::Cert ;
use openpgp ::Packet ;
use openpgp ::parse ::Parse ;
fn dir ( ) -> path ::PathBuf {
2024-02-09 20:36:29 +03:00
path ::Path ::new ( " tests " ) . join ( " data " ) . join ( " cert-lint " )
2023-06-02 01:22:52 +03:00
}
const FROZEN_TIME : & str = " 20220101 " ;
2024-01-08 16:13:03 +03:00
/// Returns an assert_cmd::Command for sq with the console detached.
#[ cfg(unix) ]
fn sq ( ) -> Command {
use std ::os ::fd ::AsRawFd ;
use std ::os ::unix ::process ::CommandExt ;
use libc ::{ TIOCNOTTY , ioctl } ;
let mut c =
std ::process ::Command ::new ( assert_cmd ::cargo ::cargo_bin ( " sq " ) ) ;
unsafe {
c . pre_exec ( | | {
// Best-effort, ignores errors.
if let Ok ( h ) = std ::fs ::File ::open ( " /dev/tty " ) {
2024-03-02 21:54:30 +03:00
ioctl ( h . as_raw_fd ( ) , TIOCNOTTY . into ( ) ) ;
2024-01-08 16:13:03 +03:00
} else {
2024-03-02 21:54:30 +03:00
ioctl ( std ::io ::stdin ( ) . as_raw_fd ( ) , TIOCNOTTY . into ( ) ) ;
2024-01-08 16:13:03 +03:00
}
Ok ( ( ) )
} ) ;
}
c . into ( )
}
/// Returns an assert_cmd::Command for sq with the console detached.
#[ cfg(windows) ]
fn sq ( ) -> Command {
use std ::os ::windows ::process ::CommandExt ;
let mut c =
std ::process ::Command ::new ( assert_cmd ::cargo ::cargo_bin ( " sq " ) ) ;
const DETACHED_PROCESS : u32 = 0x00000008 ;
c . creation_flags ( DETACHED_PROCESS ) ;
c . into ( )
}
2023-06-02 01:22:52 +03:00
// passwords: one '-p' option per element.
// required_fixes: the number of fixes (= new top-level signatures) needed.
// expected_fixes: the number of them that we can create.
fn t ( base : & str , prv : Option < & str > , passwords : & [ & str ] ,
required_fixes : usize , expected_fixes : usize )
{
assert! ( required_fixes > = expected_fixes ) ;
let dir = dir ( ) ;
let mut suffixes = vec! [ " pub " ] ;
if let Some ( prv ) = prv {
suffixes . push ( prv ) ;
}
for suffix in suffixes . iter ( ) {
2024-05-28 15:33:27 +03:00
for keystore in [ false , true ] {
let home = TempDir ::new ( ) . unwrap ( ) ;
let home = home . path ( ) . display ( ) . to_string ( ) ;
// Lint it.
let filename = & format! ( " {} - {} .pgp " , base , suffix ) ;
eprintln! ( " Linting {} " , filename ) ;
let cert = Cert ::from_file ( dir . join ( filename ) )
. expect ( & format! ( " Can parse {} " , filename ) ) ;
if keystore {
// When using the keystore, we need to import the key.
if suffix = = & " pub " {
eprintln! ( " Import certificate from {} " , filename ) ;
let mut cmd = sq ( ) ;
cmd
. current_dir ( & dir )
. args ( [
" --home " , & home ,
" cert " ,
" import " ,
& filename ,
] ) ;
let output = cmd . output ( ) . expect ( " can sq cert import " ) ;
if ! output . status . success ( ) {
panic! (
" sq exited with non-zero status code: {} " ,
String ::from_utf8_lossy ( & output . stderr )
) ;
}
2023-06-02 01:22:52 +03:00
} else {
2024-05-28 15:33:27 +03:00
eprintln! ( " Import key from {} " , filename ) ;
let mut cmd = sq ( ) ;
cmd
2023-06-02 01:22:52 +03:00
. current_dir ( & dir )
2024-05-28 15:33:27 +03:00
. args ( [
" --home " , & home ,
" key " ,
" import " ,
& filename ,
] ) ;
let output = cmd . output ( ) . expect ( " can sq key import " ) ;
if ! output . status . success ( ) {
panic! (
" sq exited with non-zero status code: {} " ,
String ::from_utf8_lossy ( & output . stderr )
) ;
}
}
}
let mut cmd = sq ( ) ;
cmd
. current_dir ( & dir )
. arg ( " --home " ) . arg ( & home )
. arg ( " cert " ) . arg ( " lint " )
. arg ( " --time " ) . arg ( FROZEN_TIME ) ;
if keystore {
cmd . arg ( " --cert " ) . arg ( & cert . fingerprint ( ) . to_string ( ) ) ;
} else {
cmd . arg ( " --cert-file " ) . arg ( filename ) ;
}
cmd
. assert ( )
. code ( if required_fixes > 0 { 2 } else { 0 } ) ;
// Fix it.
let filename = & format! ( " {} - {} .pgp " , base , suffix ) ;
eprint! ( " Fixing {} " , filename ) ;
if passwords . len ( ) > 0 {
eprint! ( " (passwords: " ) ;
for ( i , p ) in passwords . iter ( ) . enumerate ( ) {
if i > 0 {
eprint! ( " , " ) ;
2023-06-02 01:22:52 +03:00
}
2024-05-28 15:33:27 +03:00
eprint! ( " {:?} " , p )
2023-06-02 01:22:52 +03:00
}
2024-05-28 15:33:27 +03:00
eprint! ( " ) " ) ;
}
eprintln! ( " . " ) ;
let expected_fixes = if suffix = = & " pub " {
// We only have public key material: we won't be able
// to fix anything.
0
} else {
expected_fixes
} ;
eprintln! ( " {} expected fixes, {} required fixes " ,
expected_fixes , required_fixes ) ;
let mut cmd = sq ( ) ;
let mut cmd = cmd . current_dir ( & dir )
. args ( & [
" --home " , & home ,
" cert " , " lint " ,
" --time " , FROZEN_TIME ,
" --fix " ,
] ) ;
if keystore {
cmd . args ( [
" --cert " , & cert . fingerprint ( ) . to_string ( ) ,
] ) ;
} else {
cmd . args ( [
" --cert-file " , & format! ( " {} - {} .pgp " , base , suffix ) ,
] ) ;
}
for p in passwords . iter ( ) {
cmd = cmd . arg ( " -p " ) . arg ( p )
}
cmd . assert ( )
// If not everything can be fixed, then --fix's exit code is 3.
. code ( if expected_fixes = = required_fixes { 0 } else { 3 } )
. stdout ( predicate ::function ( | output : & [ u8 ] | -> bool {
if expected_fixes = = 0 {
// If there are no fixes, nothing is printed.
output = = b " "
} else {
// Pass the result through the linter.
let mut cmd = Command ::cargo_bin ( " sq " ) . unwrap ( ) ;
cmd
. current_dir ( & dir )
. arg ( " --home " ) . arg ( & home )
. arg ( " cert " ) . arg ( " lint " )
. arg ( " --time " ) . arg ( FROZEN_TIME ) ;
if keystore {
cmd . arg ( " --cert " )
. arg ( & cert . fingerprint ( ) . to_string ( ) ) ;
} else {
cmd . arg ( " --cert-file " ) . arg ( " - " )
. write_stdin ( output ) ;
}
cmd . assert ( )
. code (
if expected_fixes = = required_fixes {
// Everything should have been fixed.
0
} else {
// There are still issues.
2
} ) ;
// Check that the number of new signatures equals
// the number of expected new signatures.
let orig_sigs : isize = cert
. clone ( )
. into_packets2 ( )
. map ( | p | {
if let Packet ::Signature ( _ ) = p {
1
} else {
0
}
} )
. sum ( ) ;
let updated_cert = if keystore {
let mut cmd = sq ( ) ;
let cmd = cmd . current_dir ( & dir )
. args ( & [
" --home " , & home ,
" cert " , " export " ,
" --cert " , & cert . fingerprint ( ) . to_string ( ) ,
] ) ;
let output = cmd . output ( )
. expect ( & format! ( " Can run sq cert export " ) ) ;
if ! output . status . success ( ) {
panic! (
" sq exited with non-zero status code: {} " ,
String ::from_utf8_lossy ( & output . stderr )
) ;
}
Cert ::from_bytes ( & output . stdout )
} else {
// When not using the keystore, `sq
// cert lint --fix` emits the fixed
// certificate on stdout.
Cert ::from_bytes ( output )
} ;
let fixed_sigs : isize = updated_cert
. map ( | cert | {
cert . into_packets2 ( )
. map ( | p | {
match p {
Packet ::Signature ( _ ) = > 1 ,
Packet ::SecretKey ( _ )
| Packet ::SecretSubkey ( _ ) = >
panic! ( " Secret key material \
should not be exported ! " ),
_ = > 0 ,
}
} )
. sum ( )
} )
. map_err ( | err | {
eprintln! ( " Parsing fixed certificate: {} " , err ) ;
0
} )
. unwrap ( ) ;
let fixes = fixed_sigs - orig_sigs ;
if expected_fixes as isize ! = fixes {
eprintln! ( " Expected {} fixes, \
found { } additional signatures " ,
expected_fixes , fixes ) ;
false
} else {
true
}
}
} ) ) ;
}
2023-06-02 01:22:52 +03:00
}
}
#[ test ]
fn known_good ( ) {
t ( " gnupg-rsa-normal " , Some ( " priv " ) , & [ ] , 0 , 0 ) ;
t ( " gnupg-ecc-normal " , Some ( " priv " ) , & [ ] , 0 , 0 ) ;
}
#[ test ]
fn userid_certification ( ) {
// User ID: SHA256
// User ID: SHA1
// Enc Subkey: SHA256
t ( " sha1-userid " , Some ( " priv " ) , & [ ] , 1 , 1 ) ;
}
#[ test ]
fn revoked_userid_certification ( ) {
// A revoked User ID shouldn't be updated.
// User ID: SHA256
// User ID: SHA1 (revoked)
// Enc Subkey: SHA256
t ( " sha1-userid-revoked " , Some ( " priv " ) , & [ ] , 0 , 0 ) ;
}
#[ test ]
fn signing_subkey_binding_signature ( ) {
// User ID: SHA256
// Enc Subkey: SHA256
// Sig Subkey: SHA1
t ( " sha1-signing-subkey " , Some ( " priv " ) , & [ ] , 1 , 1 ) ;
}
#[ test ]
fn encryption_subkey_binding_signature ( ) {
// User ID: SHA256
// Enc Subkey: SHA256
// Enc Subkey: SHA1
t ( " sha1-encryption-subkey " , Some ( " priv " ) , & [ ] , 1 , 1 ) ;
}
#[ test ]
fn subkey_backsig ( ) {
// User ID: SHA256
// Enc Subkey: SHA256
// Sig Subkey: SHA256, backsig: SHA1
t ( " sha1-backsig-signing-subkey " , Some ( " priv " ) , & [ ] , 1 , 1 ) ;
}
#[ test ]
fn all_bad ( ) {
// User ID: SHA1
// Enc Subkey: SHA1
t ( " only-sha1 " , Some ( " priv " ) , & [ ] , 2 , 2 ) ;
// We don't fix MD5 signatures.
//
// User ID: MD5
// Enc Subkey: MD5
t ( " only-md5 " , Some ( " priv " ) , & [ ] , 2 , 0 ) ;
}
2024-02-07 15:49:32 +03:00
/// XXX: Disabled because there is no non-interactive way to feed
/// passwords to it.
#[ allow(dead_code) ]
2023-06-02 01:22:52 +03:00
fn passwords ( ) {
// User ID: SHA1
// Enc Subkey: SHA1
// Wrong password.
t ( " all-sha1-password-Foobar " , Some ( " priv " ) , & [ " foobar " ] , 2 , 0 ) ;
// Right password.
t ( " all-sha1-password-Foobar " , Some ( " priv " ) , & [ " Foobar " ] , 2 , 2 ) ;
// Try multiple passwords.
t ( " all-sha1-password-Foobar " , Some ( " priv " ) , & [ " Foobar " , " bar " ] , 2 , 2 ) ;
t ( " all-sha1-password-Foobar " , Some ( " priv " ) , & [ " bar " , " Foobar " ] , 2 , 2 ) ;
}
2024-02-07 15:49:32 +03:00
/// XXX: Disabled because there is no non-interactive way to feed
/// passwords to it.
#[ allow(dead_code) ]
2023-06-02 01:22:52 +03:00
fn multiple_passwords ( ) {
// The primary is encrypted with foo and the signing subkey
// with bar. We need to provide both, because the signing
// subkey needs its backsig updated.
// User ID: SHA256
// Enc Subkey: SHA256
// Enc Subkey: SHA1
// Sig Subkey: SHA1
// We only have the password for the signing subkey: we can't
// update anything.
t ( " multiple-passwords " , Some ( " priv " ) , & [ " bar " , " Foobar " ] , 2 , 0 ) ;
// We only have the password for the primary key: we can't
// update the backsig.
t ( " multiple-passwords " , Some ( " priv " ) , & [ " foo " , " Foobar " ] , 2 , 1 ) ;
// We have all passwords: we can fix everything.
t ( " multiple-passwords " , Some ( " priv " ) , & [ " bar " , " Foobar " , " foo " ] , 2 , 2 ) ;
}
#[ test ]
fn offline_subkeys ( ) {
// The User ID, the encryption subkey, and the signing subkey
// all need new signatures. With just the primary key, we are
// able to create two of the three required signatures.
// User ID: SHA1
// Enc Subkey: SHA1
// Sig Subkey: SHA1
// We can't update the backsig.
t ( " sha1-offline-subkeys " , Some ( " offline " ) , & [ ] , 3 , 2 ) ;
// We can fix everything.
t ( " sha1-offline-subkeys " , Some ( " priv " ) , & [ ] , 3 , 3 ) ;
}
#[ test ]
fn sha1_authentication_subkey ( ) {
// User ID: SHA1
// Enc Subkey: SHA1
// Auth Subkey: SHA1
t ( " sha1-authentication-subkey " , Some ( " priv " ) , & [ ] , 3 , 3 ) ;
}
#[ test ]
fn authentication_subkey ( ) {
// An authentication subkey doesn't require a backsig. Make
// sure we don't flag a missing backsig as an error.
// User ID: SHA512
// Enc Subkey: SHA512
// Auth Subkey: SHA512
t ( " authentication-subkey " , Some ( " priv " ) , & [ ] , 0 , 0 ) ;
}
#[ test ]
fn sha1_userid_sha256_subkeys ( ) {
// The User ID is protected with a SHA-1 signature, but two
// subkeys are protected with SHA256. Make sure the subkeys
// don't get new binding signatures.
// User ID: SHA1
// Enc Subkey: SHA1
// Sig Subkey: SHA256
// Enc Subkey: SHA256
t ( " sha1-userid-sha256-subkeys " , Some ( " priv " ) , & [ ] , 2 , 2 ) ;
}
#[ test ]
fn no_backsig ( ) {
// If a key doesn't have a backsig and needs one, it won't be
// detected as an issue, because it is not valid under
// SHA1+SP. That's okay.
// User ID: SHA512
// Sig Subkey: SHA512, no backsig.
t ( " no-backsig " , Some ( " priv " ) , & [ ] , 0 , 0 ) ;
}
#[ test ]
fn sha512_self_sig_sha1_revocation ( ) {
// Under the standard policy, SHA1 revocations are considered
// bad. We assume that SP+SHA-1 is strictly more liberal than
// SP (i.e., it accepts at least everything that SP accepts).
// User ID: SHA512, SHA-1 revocation.
t ( " sha512-self-sig-sha1-revocation " , None , & [ ] , 0 , 0 ) ;
}
#[ test ]
fn revoked_certificate ( ) {
// The certificate is only valid under SP+SHA1, and the
// revocation certificate uses SHA1. There is no need to
// upgrade the certificate or the revocation certificate.
// User ID: SHA1
// Enc Subkey: SHA1
// Revocation: SHA1
t ( " sha1-cert-sha1-revocation " , Some ( " priv " ) , & [ ] , 0 , 0 ) ;
// The certificate is only valid under SP+SHA1, and the
// revocation certificate uses SHA256. There is no need to
// upgrade the certificate or the revocation certificate.
// User ID: SHA1
// Enc Subkey: SHA1
// Revocation: SHA256
t ( " sha1-cert-sha256-revocation " , Some ( " priv " ) , & [ ] , 0 , 0 ) ;
// The certificate is valid under SP (the signatures use
// SHA512), but there are two revocation certificates that use
// SHA1. Make sure we upgrade them.
// User ID: SHA512
// Enc Subkey: SHA512
// Revocation: SHA1
// Revocation: SHA1
t ( " sha512-cert-sha1-revocation " , Some ( " priv " ) , & [ ] , 2 , 2 ) ;
// The certificate is valid under SP (the signatures use
// SHA256), and it is revoked using a SHA256 revocation
// certificate, which is also valid under SP. It also has a
// SHA-1 protected signing subkey. Because the certificate is
// revoked and the revocation certificate uses SHA256, we
// don't need to fix the SHA-1 signature. Make sure we don't.
// User ID: SHA256
// Enc Subkey: SHA256
// Sig Subkey: SHA1
// Revocation: SHA256
t ( " sha256-cert-sha256-revocation " , Some ( " priv " ) , & [ ] , 0 , 0 ) ;
}
#[ test ]
fn expired_certificates ( ) {
// User ID: SHA256 (expired)
// Enc Subkey: SHA256
t ( " sha256-expired " , Some ( " priv " ) , & [ ] , 0 , 0 ) ;
// User ID: SHA1 (expired)
// Enc Subkey: SHA1
t ( " sha1-expired " , Some ( " priv " ) , & [ ] , 0 , 0 ) ;
// User ID: SHA256 (old, expired), SHA1 (new, live)
// Enc Subkey: SHA256
t ( " sha256-expired-sha1-live " , Some ( " priv " ) , & [ ] , 1 , 1 ) ;
}
#[ test ]
fn list_keys ( ) {
Command ::cargo_bin ( " sq " ) . unwrap ( )
. current_dir ( & dir ( ) )
. args ( & [
" --no-cert-store " ,
2024-01-18 20:09:59 +03:00
" --no-key-store " ,
2024-02-09 20:36:29 +03:00
" cert " , " lint " ,
2023-06-02 01:22:52 +03:00
" --time " , FROZEN_TIME ,
" --list-keys " ,
// 94F19D3CB5656E0BC3977C09A8AC5ACC2FB87104
2024-05-28 15:08:50 +03:00
" --cert-file " , " sha1-userid-pub.pgp " ,
2023-06-02 01:22:52 +03:00
// 55EF7181C288067AE189FF12F5A5CD01D8070917
2024-05-28 15:08:50 +03:00
" --cert-file " , " gnupg-rsa-normal-pub.pgp "
2023-06-02 01:22:52 +03:00
] )
. assert ( )
// If there are issues, the exit code is 2.
. code ( 2 )
. stdout ( predicate ::eq ( " 94F19D3CB5656E0BC3977C09A8AC5ACC2FB87104 \n " ) ) ;
}
#[ test ]
fn signature ( ) {
Command ::cargo_bin ( " sq " ) . unwrap ( )
. current_dir ( & dir ( ) )
. args ( & [
" --no-cert-store " ,
2024-01-18 20:09:59 +03:00
" --no-key-store " ,
2024-02-09 20:36:29 +03:00
" cert " , " lint " ,
2023-06-02 01:22:52 +03:00
" --time " , FROZEN_TIME ,
2024-05-28 15:08:50 +03:00
" --cert-file " , " msg.sig " ,
2023-06-02 01:22:52 +03:00
] )
. assert ( )
// If there are issues, the exit code is 1.
. code ( 1 ) ;
}
}