Mock signature verification for application testing

classic Classic list List threaded Threaded
1 message Options
Reply | Threaded
Open this post in threaded view
|

Mock signature verification for application testing

GnuPG - Dev mailing list
Hello all,

I answered a call for volunteers to help with the FTP upload handling
tool that runs ftp.gnu.org and I have been developing a testsuite for
that tool.  To support automated testing I have written a mockup of
gpgv, attached to this message.

I am sending this message on a recommendation from Ian at the FSF to ask
the gnupg list when I requested that someone review the mock gpgv script
that I had written.  I apologize in advance if this is not the
appropriate list for this request.  The mock gpgv's role inside the
testsuite is to provide an easy way to test and verify the upload
handler's behavior with various combinations of valid and invalid
signatures, without having to actually generate those signatures.  The
mock gpgv itself should be self-contained, although some additional
DejaGnu interface code for driving it has also been written.

The mock gpgv was written mostly by examining GPG source code, and there
is a chance that I may have misunderstood the program flow, although I
believe I have it right, including some surprising (to me) results with
the exit code returned by gpgv:  a valid signature produces a successful
exit code, even if the key has expired or been revoked, or the signature
itself has expired.  An unsuccessful exit code occurs only if (code 1)
the data is found to not match the signature or (code 2) an error
prevents checking the signature at all.  It seems to me that the proper
way to distinguish the other situations (matching signature from
questionable key) involves using the --status-fd mechanism.

Do I understand correctly?  Does the attached shell script accurately
mimic gpgv's behavior?  Are there further elements that should be added
to better support testing of programs that use gpgv to perform automatic
signature verification against a set of approved keys?

As a maintainer of DejaGnu, I may eventually put this script and its
associated DejaGnu code into DejaGnu's contrib/ directory, if it is or
can be made sufficiently general, or I would be happy to contribute this
support for automated testing to the GnuPG project.


-- Jacob

#!/bin/bash
# -*- bash -*-

# Copyright (C) 2021 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}

# parse command line
while :; do
    case "$1" in
        --version)
            echo 'gpgv (Testing mock)'
            exit 0
            ;;
        --keyring)
            case "$2" in
                */*)
                    Keyrings="$Keyrings \"$2\""
                    ;;
                *)
                    Keyrings="$Keyrings \"${GNUPGHOME}/$2\""
                    ;;
            esac
            shift 2
            ;;
        --homedir)
            GNUPGHOME=$2
            shift 2
            ;;
        --status-fd)
            StatusFd=$2
            shift 2
            ;;
        --logger-fd)
            LogFd=$2
            shift 2
            ;;
        --*)
            echo bogus option $1
            shift
            ;;
        *)
            break
            ;;
    esac
done

### Output helpers
function status {
    if [ x$StatusFd != x ]; then
        echo '[GNUPG:]' "$@" >&${StatusFd}
    fi
}
function log {
    echo gpgv: "$@" >&${LogFd-2}
}

### Keyring handling

# Mock keyrings consist of line-oriented records, in the format:
#  <long key ID>:<status>:<primary long key ID>:<user ID>:<expiration timestamp>
#   The <long key ID> field is 16 hex digits.
#   The <status> is 'V' for a valid key or 'R' for a revoked key.
#   The <primary long key ID> field may be omitted if same as key ID.
#   The <user ID> field is free-form text.
#   The <expiration timestamp> is empty for no expiration.

# search keyrings for the given long key ID
# return success and load global KeyRec if found
function search_keyrings {
    KeyRec=$(eval grep -he \"^$1:\" $Keyrings | head -1; \
        return ${PIPESTATUS[0]})
    return $?
}
# access fields from global KeyRec
function keyrec_id() (IFS=: ; set -- $KeyRec ; echo $1)
function keyrec_status()(IFS=: ; set -- $KeyRec ; echo $2)
function keyrec_pri() (IFS=: ; set -- $KeyRec ; echo ${3:-$1})
function keyrec_user() (IFS=: ; set -- $KeyRec ; echo $4)
function keyrec_exp() (IFS=: ; set -- $KeyRec ; echo $5)

function keyrec_expired() {
    [ -n "$(keyrec_exp)" \
        -a $(date -d "$(keyrec_exp)" +%s) -lt $(date -d now +%s) ]
}
function keyrec_revoked() { [ "$(keyrec_status)" = R ]; }

### Signature handling

ExitCode=0

# handle SIGNED record in mock data
function handle_SIGNED {
    # SIGNED <status> <sigID> <keyID> "<timestamp>" \
    # "<expiration timestamp>"
    local status="$1" sigID="$2" keyID="$3" tstamp="$4"
    if [ "x$5" = x ]; then local exptstamp=0
    else local exptstamp=$(date -d "$5" +%s) exptstamp_raw="$5"
    fi

    log Signature made $(date -d "$tstamp" +'%a %b %e %T %Y %Z') \
        using RSA key ID ${keyID:8}
    if search_keyrings $keyID; then
        # key was found in the keyrings

        # handle caveats (expired signature, expired key, revoked key)
        local chktype=GOOD desctype=Good
        keyrec_revoked && chktype=REVKEY
        keyrec_expired && chktype=EXPKEY
        # Note that signatures from expired and revoked keys are still
        # considered "Good" apparently ...
        [ $exptstamp -gt 0 \
            -a $exptstamp -lt $(date -d now +%s) ] \
            && chktype=EXP desctype=Expired
        # ... but a signature that has *itself* expired is reported.

        case "$status" in
            V) # valid signature
                if keyrec_expired; then
                    status KEYEXPIRED $(date -d "$(keyrec_exp)" +%s)
                fi
                status SIG_ID testmock/sig/"$sigID"/id \
                    $(date -d "$tstamp" +'%Y-%m-%d %s')
                status ${chktype}SIG $keyID $(keyrec_user)
                log ${desctype} signature from \"$(keyrec_user)\"
                status VALIDSIG 000000000000000000000000$keyID \
                    $(date -d "$tstamp" +'%Y-%m-%d %s') \
                    $exptstamp 3 0 1 2 01 \
                    000000000000000000000000$(keyrec_pri)
                if [ $chktype = EXP ]; then
                    log Signature expired \
                        $(date -d "$exptstamp_raw" +'%a %b %e %T %Y %Z')
                elif [ $exptstamp -gt 0 ]; then
                    log Signature expires \
                        $(date -d "$exptstamp_raw" +'%a %b %e %T %Y %Z')
                fi
                ;;
            B) # bogus signature
                status BADSIG $keyID $(keyrec_user)
                log BAD signature from \"$(keyrec_user)\"
                ExitCode=1
                ;;
        esac

    else
        # key not found
        status ERRSIG $keyID 1 2 01 $(date -d "$tstamp" +%s) 9
        status NO_PUBKEY $keyID
        log "Can't check signature:" public key not found
        ExitCode=2
    fi
}

# process a "signature" file for a testing run
function process_sig_file {
    while read line; do
        case "$line" in
            -----BEGIN?PGP?SIGNATURE-----)
                break
                ;;
            DETACHED?SIGNATURE)
                DataFile="$1"
                shift
                break
        esac
    done
    while read token rest; do
        case "$token" in
            -----END)
                break
                ;;
            Version:)
                # this looks like a GPG signature instead of mock data...
                # complain... loudly
                echo mockgpgv: looks like someone fed a real signature \
                    to the mock tool
                log mock: real GPG signature detected instead of mock test data
                status NODATA 3
                exit 120
                ;;
            SIGNED_FILE)
                if [ x"$DataFile" = x ]; then
                    log no signed data
                    log "can't hash datafile:" file open error
                    ExitCode=2
                    break
                elif [ ! -f "$DataFile" ]; then
                    log "can't open signed data" $DataFile
                    log "can't hash datafile:" file open error
                    ExitCode=2
                    break
                fi
                ;;
            SIGNED)
                eval handle_SIGNED $rest
                ;;
            *)
                echo $token $rest
                ;;
        esac
    done
} <"$SigFile"

# check that we were actually given a file to process
if [ x"$*" = x ]; then
    status NODATA 2
    log verify signatures failed: eof
    exit 2
fi

# check that the file is readable and process it
SigFile="$1"
shift
if [ -r "$SigFile" ] ; then
    process_sig_file "$@"
else
    log "can't" open '`'"$SigFile'"
    log verify signatures failed: file open error
    exit 2
fi

exit $ExitCode

#EOF

_______________________________________________
Gnupg-devel mailing list
[hidden email]
http://lists.gnupg.org/mailman/listinfo/gnupg-devel