Add mock gpg tool for listing and manipulating keyrings
authorJacob Bachmeyer <jcb@gnu.org>
Sat, 21 Jan 2023 02:59:59 +0000 (20:59 -0600)
committerJacob Bachmeyer <jcb@gnu.org>
Sat, 21 Jan 2023 02:59:59 +0000 (20:59 -0600)
testsuite/lib/exec/mockgpg [new file with mode: 0755]
testsuite/lib/mockgpg.exp
testsuite/mock.gpg/keymgr.exp [new file with mode: 0644]
testsuite/mock.gpg/listkeys.exp [new file with mode: 0644]

diff --git a/testsuite/lib/exec/mockgpg b/testsuite/lib/exec/mockgpg
new file mode 100755 (executable)
index 0000000..d323b71
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+
+: ${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 <<EOF
+This testing mock only supports the subset of GPG functionality needed
+for testing the tools in this package.  Other options and commands are
+not supported.
+EOF
+           exit 2
+           ;;
+       *)
+           Files=("${Files[@]}" "$1")
+           shift
+           ;;
+    esac
+done
+
+$OmitDefaultKeyring || Keyrings=( ${GNUPGHOME}/pubring.gpg "${Keyrings[@]}" )
+
+function list_keyring_basic() (
+    echo "$1"
+    echo ${1//?/-}
+    last_id=
+    while read record; do
+       id=$(IFS=:      ; set -- $record ; echo $1)
+       status=$(IFS=:  ; set -- $record ; echo $2)
+       primary=$(IFS=: ; set -- $record ; echo ${3:-$1})
+       user=$(IFS=:    ; set -- $record ; echo $4)
+       expire=$(IFS=:  ; set -- $record ; echo $5)
+               #
+       [ x$id = x ] && continue
+       if [ x$primary = x$id ]; then type=pub; else type=sub; fi
+               #
+       if [ x$id != x$last_id ]; then
+           if [ x$status = xR ]; then
+               stat=" [revoked: $(date +'%Y-%m-%d')]"
+           elif [ -n "$expire" ]; then
+               if [ $(date -d "$expire" +%s) -lt $(date -d now +%s) ]; then
+                   stat=" [expired: $(date +'%Y-%m-%d')]"
+               else
+                   stat=" [expires: $(date +'%Y-%m-%d')]"
+               fi
+           else
+               stat=''
+           fi
+           printf "%-6s4096R/%8s %s${stat}\n" \
+               $type ${id:0-8} $(date +'%Y-%m-%d')
+           $WantFingerprints && [ x$type = xpub ] && printf \
+               '%-6sKey Fingerprint = 0000 0000 0000 0000 0000  %s\n' \
+               '' "0000 ${id:0:4} ${id:4:4} ${id:8:4} ${id:12:4}"
+       fi
+       [ x$type = xpub ] && printf '%-20s %s\n' uid "$user"
+               #
+       last_id=$id
+    done <"$1"
+)
+
+function list_keyring_colon_delimited() (
+    keydate=$(date -d '1 day ago' +%s)
+    echo tru::1:$keydate:0
+    # TODO
+    while read record; do
+       id=$(IFS=:      ; set -- $record ; echo $1)
+       status=$(IFS=:  ; set -- $record ; echo $2)
+       primary=$(IFS=: ; set -- $record ; echo ${3:-$1})
+       user=$(IFS=:    ; set -- $record ; echo $4)
+       expire=$(IFS=:  ; set -- $record ; echo $5)
+               #
+       [ x$id = x ] && continue
+               #
+       if [ x$primary = x$id ]; then
+           type=pub; keycaps=scSC; otr=u ;
+       else
+           type=sub; keycaps=s   ; otr='';
+       fi
+       #
+       if [ -n "$expire" ]; then
+           keyexpire=$(date -d "$expire" +%s)
+       else
+           keyexpire=''
+       fi
+       #
+       if [ x$status = xR ]; then
+           trust=r
+       elif [ -n "$expire" ] \
+           && [ $keyexpire -lt $(date -d now +%s) ]; then
+           trust=e
+       else
+           trust=u
+       fi
+       #
+       uidsha1=$(echo -n "$user" | sha1sum - | awk '{print toupper($1)}')
+               #
+       if [ x$id != x$last_id ]; then
+           echo $type:$trust:4096:1:$id:$keydate:$keyexpire::$otr:::$keycaps:
+           if $WantFingerprints \
+               && [ x$type = xpub ] || $WantSubkeyFingerprints; then
+               echo fpr:::::::::$(printf '%40s' $id | tr ' ' 0):
+           fi
+       fi
+       if [ x$type = xpub ]; then
+           echo uid:u::::$keydate::$uidsha1::$user:
+       fi
+               #
+       last_id=$id
+    done <"$1"
+)
+
+function list_keyring() {
+    if $WantColonDelimited; then
+       list_keyring_colon_delimited "$@"
+    else
+       list_keyring_basic "$@"
+    fi
+}
+
+case ${Command:?no command given} in
+    import)
+       # The mock keyring format is such that import is accomplished by
+       # simply appending the import files to each keyring.
+       if [ "${#Files[*]}" -gt 0 ]; then
+           for file in "${Files[@]}"; do
+               for keyring in "${Keyrings[@]}"; do
+                   [ -f "$file" ] && cat "$file" >>"$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
index a2f702e312c5491f1f487ce2020355daf78e3489..cbef9f908a6f3fa9e58a0e70e80378261a9c6003 100644 (file)
@@ -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 <test-name> /some/file/some/where {
+#     { id <subkey-long-ID> name <user> [is <state>]
+#      [subkey-of <primary-key-long-ID>]
+#      [expires <expiration>] }...
+# }
+#
+# If the name clause is repeated, the key is required to be duplicated with
+# one entry for each name given.  If no <state> 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 (file)
index 0000000..fe66f5a
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+
+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 (file)
index 0000000..f5efb26
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+
+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