From: Jacob Bachmeyer Date: Sat, 21 Jan 2023 02:59:59 +0000 (-0600) Subject: Add mock gpg tool for listing and manipulating keyrings X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=52c9f3d8f5dc2223e2c41bf1cac10b8db44b9ce8;p=gatekeeper.git Add mock gpg tool for listing and manipulating keyrings --- diff --git a/testsuite/lib/exec/mockgpg b/testsuite/lib/exec/mockgpg new file mode 100755 index 0000000..d323b71 --- /dev/null +++ b/testsuite/lib/exec/mockgpg @@ -0,0 +1,238 @@ +#!/bin/bash +# -*- bash -*- + +# Copyright (C) 2022, 2023 Jacob Bachmeyer +# +# This file is part of a testsuite for the GNU FTP upload system. +# +# This file is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +: ${GNUPGHOME:=~/.gnupg-mock} + +# parse command line +declare -a Files Keyrings +OmitDefaultKeyring=false +WantFingerprints=false +WantSubkeyFingerprints=false +WantColonDelimited=false +while [ $# -gt 0 ]; do + case $1 in + --version) + echo 'gpg (Testing mock)' + exit 0 + ;; + --homedir) + GNUPGHOME=$2 + shift 2 + ;; + --output|-o) + OutputFile=$2 + shift 2 + ;; + --no-default-keyring) + OmitDefaultKeyring=true + shift + ;; + --fingerprint|--with-fingerprint) + $WantFingerprints && WantSubkeyFingerprints=true + WantFingerprints=true + shift + ;; + --with-colons) + WantColonDelimited=true + shift + ;; + --fixed-list-mode) + # The testing mock always uses this mode. + shift + ;; + --keyring) + # unlike gpgv, gpg can create keyrings + case $2 in + */*) + Keyrings=( "${Keyrings[@]}" "$2" ) + ;; + *) + Keyrings=( "${Keyrings[@]}" "${GNUPGHOME}/$2" ) + ;; + esac + shift 2 + ;; + --import|--export|--list-keys|--delete-key) + Command=${1#--} + shift + ;; + -*) + echo bogus option $1 + cat <>"$keyring" + done + done + else + # Import with no files? Read stdin instead. + TmpFile=mockgpg-scratchpad.$$ + cat >"$TmpFile" + for keyring in "${Keyrings[@]}"; do + cat "$TmpFile" >>"$keyring" + done + rm -f "$TmpFile" + fi + ;; + export) + # The mock keyring format simply lists the appropriate lines here. + # The "files" are actually key IDs. + for keyid in "${Files[@]}"; do + [ ${#keyid} -gt 8 ] && keyid=${keyid:0-8} + if [ x${OutputFile:+set} = xset ]; then + grep -h '^0*'"$keyid": "${Keyrings[@]}" > "$OutputFile" + else + grep -h '^0*'"$keyid": "${Keyrings[@]}" + fi + done + ;; + delete-key) + # This simply requires removing the indicated ID from all keyrings. + # The "files" are actually key IDs. + for keyid in "${Files[@]}"; do + [ ${#keyid} -gt 8 ] && keyid=${keyid:0-8} + sed -i -e '/^0*'"$keyid"':/d' "${Keyrings[@]}" + done + ;; + list-keys) + for keyring in "${Keyrings[@]}"; do + list_keyring "$keyring" + done + ;; +esac + +#EOF diff --git a/testsuite/lib/mockgpg.exp b/testsuite/lib/mockgpg.exp index a2f702e..cbef9f9 100644 --- a/testsuite/lib/mockgpg.exp +++ b/testsuite/lib/mockgpg.exp @@ -1,6 +1,6 @@ # DejaGnu library file for mockGPG support procedures -# Copyright (C) 2021 Jacob Bachmeyer +# Copyright (C) 2021, 2022, 2023 Jacob Bachmeyer # # This file is part of a testsuite for the GNU FTP upload system. # @@ -26,6 +26,157 @@ proc _mockGPG_quote_value { val } { } } +# Match the keys in a keyring file against a list of expected keys +# +# Call as: +# check_test_keyring /some/file/some/where { +# { id name [is ] +# [subkey-of ] +# [expires ] }... +# } +# +# If the name clause is repeated, the key is required to be duplicated with +# one entry for each name given. If no is given, the key is +# required to be valid. If the subkey-of clause is not used, the key is +# required to be a primary key. If the expires clause is given, the +# expiration time must match; if not given, the key must not have an +# expiration time. +proc check_test_keyring { testname file keylist } { + array set allkeyids {} + + # prepare expected keys + array set wantkeys {} + foreach keyrec $keylist { + if { [lindex $keyrec 0] ne "id" } { + error "id clause must be listed first in key record: $keyrec" + } + set keyid [format "%016s" [string toupper [lindex $keyrec 1]]] + set wantkeys($keyid,state) V + foreach { fld val } $keyrec { + switch -- $fld { + id { + set allkeyids($keyid) 1 + } + name { + lappend wantkeys($keyid,names) $val + } + is { + switch -- $val { + valid { set wantkeys($keyid,state) V } + revoked { set wantkeys($keyid,state) R } + } + } + subkey-of { + set wantkeys($keyid,parent) $val + } + expired - + expires { + set wantkeys($keyid,expiration) $val + } + default { error "invalid key record: $keyrec" } + } + } + } + + # read keyring contents + array set havekeys {} + if { [catch {open $file} chan] } { + perror $chan + unresolved $testname + return + } + while { [gets $chan keyrec] >= 0 } { + if { [regexp -- {^(?:#|[[:space:]]*$)} $keyrec] } { continue } + + set fields [split $keyrec :] + set keyid [lindex $fields 0] + + set allkeyids($keyid) 1 + set havekeys($keyid,state) [lindex $fields 1] + if { [string length [lindex $fields 2]] > 0 + && [lindex $fields 2] ne $keyid} { + set havekeys($keyid,parent) [lindex $fields 2] + } + lappend havekeys($keyid,names) [lindex $fields 3] + if { [string length [lindex $fields 4]] > 0 } { + set havekeys($keyid,expiration) [lindex $fields 4] + } + } + close $chan + + # match keyring contents + set result pass + foreach keyid [lsort -dictionary [array names allkeyids]] { + + if { ![info exists wantkeys($keyid,state)] } { + verbose -log "unexpected key ID $keyid" + set result fail + continue + } elseif { ![info exists havekeys($keyid,state)] } { + verbose -log "missing key ID $keyid" + set result fail + continue + } + + if { $wantkeys($keyid,state) ne $havekeys($keyid,state) } { + verbose -log "key states differ for key ID $keyid" + verbose -log " wanted $wantkeys($keyid,state),\ + have $havekeys($keyid,state)" + set result fail + } + + if { [info exists wantkeys($keyid,parent)] + && ![info exists havekeys($keyid,parent)] } { + verbose -log "key ID $keyid should be a subkey\ + of $wantkeys($keyid,parent)" + set result fail + } elseif { [info exists havekeys($keyid,parent)] + && ![info exists wantkeys($keyid,parent)] } { + verbose -log "key ID $keyid should be a primary key,\ + not a subkey of $havekeys($keyid,parent)" + set result fail + } elseif { [info exists wantkeys($keyid,parent)] + && [info exists havekeys($keyid,parent)] + && $wantkeys($keyid,parent) ne $havekeys($keyid,parent) } { + verbose -log "parent keys differ for key ID $keyid" + verbose -log " want: $wantkeys($keyid,parent)" + verbose -log " have: $havekeys($keyid,parent)" + set result fail + } + + if { [lsort $wantkeys($keyid,names)] + ne [lsort $havekeys($keyid,names)] } { + verbose -log "name sets differ for key ID $keyid" + verbose -log " want: [lsort $wantkeys($keyid,names)]" + verbose -log " have: [lsort $havekeys($keyid,names)]" + set result fail + } + + if { [info exists wantkeys($keyid,expiration)] + && ![info exists havekeys($keyid,expiration)] } { + verbose -log "key ID $keyid should expire\ + at $wantkeys($keyid,expiration)" + set result fail + } elseif { [info exists havekeys($keyid,expiration)] + && ![info exists wantkeys($keyid,expiration)] } { + verbose -log "key ID $keyid should not expire\ + at $havekeys($keyid,expiration)" + set result fail + } elseif { [info exists wantkeys($keyid,expiration)] + && [info exists havekeys($keyid,expiration)] + && $wantkeys($keyid,expiration) \ + ne $havekeys($keyid,expiration) } { + verbose -log "expiration times differ for key ID $keyid" + verbose -log " want: $wantkeys($keyid,expiration)" + verbose -log " have: $havekeys($keyid,expiration)" + set result fail + } + + } + + $result $testname +} + # Prepare a keyring file suitable for the GPG mock tools. # Call as: # write_test_keyring /some/file/some/where { @@ -40,7 +191,7 @@ proc write_test_keyring { file keylist } { foreach { fld val } $keyrec { switch -- $fld { id { - lset keyfields 0 [format "%016s" $val] + lset keyfields 0 [format "%016s" [string toupper $val]] } name { lset keyfields 3 $val diff --git a/testsuite/mock.gpg/keymgr.exp b/testsuite/mock.gpg/keymgr.exp new file mode 100644 index 0000000..fe66f5a --- /dev/null +++ b/testsuite/mock.gpg/keymgr.exp @@ -0,0 +1,162 @@ +# Keyring management tests for GPG mock + +# Copyright (C) 2022, 2023 Jacob Bachmeyer +# +# This file is part of a testsuite for the GNU FTP upload system. +# +# This file is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +file mkdir $GNUPGHOME + +# + +proc run_gpg { keyrings args } { + global MOCKGPG + upvar 1 spawn_id spawn_id + set cmd [list spawn $MOCKGPG --no-default-keyring] + foreach ring $keyrings { lappend cmd --keyring $ring } + verbose -log "$cmd $args" + eval $cmd $args + expect eof + wait + catch close +} + +proc testfile { name } { + global GNUPGHOME + return [file join $GNUPGHOME $name] +} + +# + +write_test_keyring [testfile k1a.pgp] { + { id 1001 name {test 1-1} } +} +write_test_keyring [testfile k1b.pgp] { + { id 1002 name {test 1-2} } +} +catch {file delete [testfile K1.gpg]} + +run_gpg [testfile K1.gpg] --import [testfile k1a.pgp] [testfile k1b.pgp] + +check_test_keyring "simple import" [testfile K1.gpg] { + { id 1001 name {test 1-1} } + { id 1002 name {test 1-2} } +} + +# + +write_test_keyring [testfile K2.gpg] { + { id 1003 name {test 1-3} } + { id 1004 name {test 1-4} } +} + +run_gpg [testfile K2.gpg] --export 1004 -o [testfile k2b.pgp] + +check_test_keyring "simple export" [testfile k2b.pgp] { + { id 1004 name {test 1-4} } +} + +# + +write_test_keyring [testfile K3a.gpg] { + { id 1001 name {test 1-1} } + { id 1002 name {test 1-2} } +} +write_test_keyring [testfile K3b.gpg] { + { id 1003 name {test 1-3} } + { id 1004 name {test 1-4} } +} + +exec $MOCKGPG --no-default-keyring --keyring [testfile K3b.gpg] --export 1003 \ + | $MOCKGPG --no-default-keyring --keyring [testfile K3a.gpg] --import + +check_test_keyring "key transfer (source)" [testfile K3b.gpg] { + { id 1003 name {test 1-3} } + { id 1004 name {test 1-4} } +} +check_test_keyring "key transfer (target)" [testfile K3a.gpg] { + { id 1001 name {test 1-1} } + { id 1002 name {test 1-2} } + { id 1003 name {test 1-3} } +} + +# + +write_test_keyring [testfile K4a.gpg] { + { id 1001 name {test 1-1} } + { id 1002 name {test 1-2} } +} +write_test_keyring [testfile K4b.gpg] { + { id 1003 name {test 1-3} } + { id 1004 name {test 1-4} } +} + +exec $MOCKGPG --no-default-keyring --keyring [testfile K4b.gpg] \ + --export 1003 1004 \ + | $MOCKGPG --no-default-keyring --keyring [testfile K4a.gpg] \ + --import + +check_test_keyring "multiple key transfer (source)" [testfile K4b.gpg] { + { id 1003 name {test 1-3} } + { id 1004 name {test 1-4} } +} +check_test_keyring "multiple key transfer (target)" [testfile K4a.gpg] { + { id 1001 name {test 1-1} } + { id 1002 name {test 1-2} } + { id 1003 name {test 1-3} } + { id 1004 name {test 1-4} } +} + +# + +write_test_keyring [testfile K5.gpg] { + { id 1001 name {test 1-1} } + { id 1002 name {test 1-2} } + { id 1003 name {test 1-3} } + { id 1004 name {test 1-4} } +} + +run_gpg [testfile K5.gpg] --delete-key 00001002 00001003 + +check_test_keyring "key removal by ID" [testfile K5.gpg] { + { id 1001 name {test 1-1} } + { id 1004 name {test 1-4} } +} + +# + +write_test_keyring [testfile K6.gpg] { + { id 1001 name {test 1-1} } + { id 1002 name {test 1-2} } + { id 1003 name {test 1-3} } + { id 1004 name {test 1-4} } +} + +run_gpg [testfile K6.gpg] --delete-key \ + 0000000000000000000000000000000000001002 \ + 0000000000000000000000000000000000001003 + +check_test_keyring "key removal by fingerprint" [testfile K6.gpg] { + { id 1001 name {test 1-1} } + { id 1004 name {test 1-4} } +} + +# + +if { [mock_total_failures] == 0 } { + file delete -force $GNUPGHOME +} + +#EOF diff --git a/testsuite/mock.gpg/listkeys.exp b/testsuite/mock.gpg/listkeys.exp new file mode 100644 index 0000000..f5efb26 --- /dev/null +++ b/testsuite/mock.gpg/listkeys.exp @@ -0,0 +1,267 @@ +# Keyring listing tests for GPG mock + +# Copyright (C) 2022, 2023 Jacob Bachmeyer +# +# This file is part of a testsuite for the GNU FTP upload system. +# +# This file is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +file mkdir $GNUPGHOME + +# + +proc spawn_gpg { keyrings args } { + global MOCKGPG + upvar 1 spawn_id spawn_id + set cmd [list spawn $MOCKGPG --no-default-keyring] + foreach ring $keyrings { lappend cmd --keyring $ring } + verbose -log "$cmd $args" + eval $cmd $args +} + +proc testfile { name } { + global GNUPGHOME + return [file join $GNUPGHOME $name] +} + +# + +proc check_key_listing { testname keyring infolist } { + set result unresolved + set linenum 0 + + spawn_gpg $keyring --list-keys + + expect -ex $keyring { + set heading_len [string length $expect_out(0,string)] + } + expect -re "\\r*\\n-{$heading_len}\\r*\\n" { + set result pass + } + + expect { + -re {^[\r\n]+} { exp_continue } + -re {***:(?x) + ^(?: + ([ps]ub)\s+[[:alnum:]]+/([[:xdigit:]]+) + (?:\s[[]([^][]+)[]])?[^\n]+\n + |(uid)\s+([^\r\n]+)[\r\n]+ + ) + } { + set want_type [lindex $infolist $linenum 0] + if { $want_type eq $expect_out(1,string) } { + # pubkey/subkey ID line matching expected type + if { [format "%08s" [lindex $infolist $linenum 1]] + ne $expect_out(2,string) } { + verbose -log "key ID unexpected;\ + want [lindex $infolist $linenum 1];\ + have $expect_out(2,string)" + set result fail + } + if { [info exists expect_out(3,string)] + && (([lindex $infolist $linenum 2] eq "r" + && ![regexp revoked $expect_out(3,string)]) + || ([lindex $infolist $linenum 2] eq "e" + && ![regexp expired $expect_out(3,string)]) + || ($expect_out(3,string) ne "" + && ![regexp expires $expect_out(3,string)])) } { + verbose -log "key state unexpected" + set result fail + } + } elseif { $want_type eq $expect_out(4,string) } { + # uid line matching expected type + if { [lindex $infolist $linenum 1] + ne $expect_out(5,string) } { + verbose -log "uid name unexpected;\ + want {[lindex $infolist $linenum 1]};\ + have {$expect_out(4,string)}" + set result fail + } + } else { + verbose -log "line type unexpected\n \ + want: [lindex $infolist $linenum]\n \ + have: $expect_out(0,string)" + set result fail + } + incr linenum + exp_continue + } + eof { + if { $linenum < [llength $infolist] } { + verbose -log "EOF with more lines expected\ + (at $linenum of [llength $infolist])" + set result fail + } + wait + catch close + } + } + + $result $testname +} + +proc check_key_colon_listing { testname keyring infolist } { + set result unresolved + set linenum 0 + + spawn_gpg $keyring --list-keys --fixed-list-mode \ + --with-colons --with-fingerprint --with-fingerprint + + expect -re {tru::?:[^\n]+\n} { set result pass } + + expect { + -re {^[\r\n]+} { exp_continue } + -re {^((?:[^:\r\n]*:)+)\r*\n} { + # prepend an empty dummy field so the indexes will match the + # 1-based field numbers in the GPG documentation + set fields [split ":$expect_out(1,string)" :] + + if { [lindex $fields 1] eq [lindex $infolist $linenum 0] } { + switch -- [lindex $fields 1] { + pub - + sub { + set w_id [format "%016s" [lindex $infolist $linenum 1]] + if { $w_id ne [lindex $fields 5] } { + verbose -log "key ID unexpected;\ + want ${w_id};\ + have [lindex $fields 5]" + set result fail + } + if { [lindex $infolist $linenum 2] ne "" + && [lindex $infolist $linenum 2] \ + ne [lindex $fields 2] } { + verbose -log "key state unexpected;\ + want [lindex $infolist $linenum 2];\ + have [lindex $fields 2]" + set result fail + } + } + fpr { + set w_fp [format "%040s" [lindex $infolist $linenum 1]] + if { $w_fp ne [lindex $fields 10] } { + verbose -log "key fingerprint unexpected\n \ + want ${w_fp}\n \ + have [lindex $fields 10]" + set result fail + } + } + uid { + if { [lindex $infolist $linenum 1] \ + ne [lindex $fields 10] } { + verbose -log "uid name unexpected;\ + want {[lindex $infolist $linenum 1]};\ + have {[lindex $fields 10]}" + set result fail + } + } + } + } else { + verbose -log "line type unexpected\n \ + want: [lindex $infolist $linenum]\n \ + have: $expect_out(0,string)" + set result fail + } + + incr linenum + exp_continue + } + eof { + if { $linenum < [llength $infolist] } { + verbose -log "EOF with more lines expected\ + (at $linenum of [llength $infolist])" + set result fail + } + wait + catch close + } + } + + $result $testname +} + +# + +write_test_keyring [testfile L1.gpg] { + { id 1001 name {test 1 A} } + { id 1001 name {test 1 B} } + { id 1002 name {test 1 sub} subkey-of 1001 } +} + +check_key_listing "simple key listing (plain)" [testfile L1.gpg] { + { pub 1001 } + { uid {test 1 A} } + { uid {test 1 B} } + { sub 1002 } +} +check_key_colon_listing "simple key listing (colons)" [testfile L1.gpg] { + { pub 1001 } + { fpr 1001 } + { uid {test 1 A} } + { uid {test 1 B} } + { sub 1002 } + { fpr 1002 } +} + +# + +write_test_keyring [testfile L2.gpg] { + { id 2001 name {test 2} is revoked } +} + +check_key_listing "revoked key (plain)" [testfile L2.gpg] { + { pub 2001 r } + { uid {test 2} } +} +check_key_colon_listing "revoked key (colons)" [testfile L2.gpg] { + { pub 2001 r } + { fpr 2001 } + { uid {test 2} } +} + +# + +write_test_keyring [testfile L3a.gpg] { + { id 3001 name {test 3 A} expires {+5 minutes} } +} +write_test_keyring [testfile L3b.gpg] { + { id 3002 name {test 3 B} expired {5 minutes ago} } +} + +check_key_listing "key will expire (plain)" [testfile L3a.gpg] { + { pub 3001 } + { uid {test 3 A} } +} +check_key_colon_listing "key will expire (colons)" [testfile L3a.gpg] { + { pub 3001 } + { fpr 3001 } + { uid {test 3 A} } +} + +check_key_listing "expired key (plain)" [testfile L3b.gpg] { + { pub 3002 e } + { uid {test 3 B} } +} +check_key_colon_listing "expired key (colons)" [testfile L3b.gpg] { + { pub 3002 e } + { fpr 3002 } + { uid {test 3 B} } +} + +# + +if { [mock_total_failures] == 0 } { + file delete -force $GNUPGHOME +} + +#EOF