2023-06-02 01:22:52 +03:00
#[ cfg(test) ]
mod integration {
use std ::path ;
use assert_cmd ::Command ;
use predicates ::prelude ::* ;
use sequoia_openpgp as openpgp ;
use openpgp ::Cert ;
use openpgp ::Packet ;
use openpgp ::parse ::Parse ;
fn dir ( ) -> path ::PathBuf {
2023-06-29 19:23:01 +03:00
path ::Path ::new ( " tests " ) . join ( " data " ) . join ( " keyring-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 " ) {
ioctl ( h . as_raw_fd ( ) , TIOCNOTTY ) ;
} else {
ioctl ( std ::io ::stdin ( ) . as_raw_fd ( ) , TIOCNOTTY ) ;
}
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 ( ) {
// Lint it.
let filename = & format! ( " {} - {} .pgp " , base , suffix ) ;
eprintln! ( " Linting {} " , filename ) ;
2024-01-08 16:13:03 +03:00
sq ( )
2023-06-02 01:22:52 +03:00
. current_dir ( & dir )
. arg ( " --no-cert-store " )
2023-06-29 19:23:01 +03:00
. arg ( " keyring " ) . arg ( " lint " )
2023-06-02 01:22:52 +03:00
. arg ( " --time " ) . arg ( FROZEN_TIME )
. arg ( filename )
. 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! ( " , " ) ;
}
eprint! ( " {:?} " , p )
}
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
} ;
2024-01-08 16:13:03 +03:00
let mut cmd = sq ( ) ;
2023-06-02 01:22:52 +03:00
let mut cmd = cmd . current_dir ( & dir )
. args ( & [
" --no-cert-store " ,
2023-06-29 19:23:01 +03:00
" keyring " , " lint " ,
2023-06-02 01:22:52 +03:00
" --time " , FROZEN_TIME ,
" --fix " , & 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 {
// We got a certificate on stdout. Pass it
// through the linter.
Command ::cargo_bin ( " sq " ) . unwrap ( )
. current_dir ( & dir )
. arg ( " --no-cert-store " )
2023-06-29 19:23:01 +03:00
. arg ( " keyring " ) . arg ( " lint " )
2023-06-02 01:22:52 +03:00
. arg ( " --time " ) . arg ( FROZEN_TIME )
. arg ( " - " )
. write_stdin ( output )
. 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 ::from_file ( dir . clone ( ) . join ( filename ) ) . unwrap ( )
2024-01-29 21:45:07 +03:00
. into_packets2 ( )
2023-06-02 01:22:52 +03:00
. map ( | p | {
if let Packet ::Signature ( _ ) = p {
1
} else {
0
}
} )
. sum ( ) ;
let fixed_sigs : isize = Cert ::from_bytes ( output )
. map ( | cert | {
2024-01-29 21:45:07 +03:00
cert . into_packets2 ( )
2023-06-02 01:22:52 +03:00
. 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
}
}
} ) ) ;
}
}
#[ 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 ) ;
}
#[ test ]
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 ) ;
}
#[ test ]
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 " ,
2023-06-29 19:23:01 +03:00
" keyring " , " lint " ,
2023-06-02 01:22:52 +03:00
" --time " , FROZEN_TIME ,
" --list-keys " ,
// 94F19D3CB5656E0BC3977C09A8AC5ACC2FB87104
" sha1-userid-pub.pgp " ,
// 55EF7181C288067AE189FF12F5A5CD01D8070917
" gnupg-rsa-normal-pub.pgp "
] )
. 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 " ,
2023-06-29 19:23:01 +03:00
" keyring " , " lint " ,
2023-06-02 01:22:52 +03:00
" --time " , FROZEN_TIME ,
" msg.sig " ,
] )
. assert ( )
// If there are issues, the exit code is 1.
. code ( 1 ) ;
}
}