1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
|
#!/bin/sh
# Simple, positive self-test for Stateless OpenPGP implementations
# https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/
# This does not test all possible combinations of options or
# argument structures, it merely confirms that the standard
# subcommands and options are all implemented.
# This code makes many simplifying assumptions (e.g., there is no
# whitespace or metacharacters in filenames; filenames follow a
# strict convention) in order to be simple POSIX-compliant shell.
# The invocations are not necessarily safe shell programming if
# those assumptions are not met. Please use caution when borrowing
# from this test script.
# Author: Daniel Kahn Gillmor
# License: CC-0
SOP=$1
if [ -z "$SOP" ]; then
cat >&2 <<EOF
Usage: $0 SOP
SOP should refer (either by \$PATH or by absolute path) to an
implementation of the Stateless OpenPGP command-line interface.
See https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/
EOF
exit 1
fi
if ! COMMAND_OUTPUT=$(command -v "$SOP"); then
printf >&2 "No such command: %s\n" "$SOP"
exit 1
fi
shift
# We skip commands whose inputs are not available.
# Return 0 if the test should be skipped, 1 otherwise.
# missing inputs are printed to stdout.
skip_test() {
# do not skip commands that consume no input.
if [ "$1" = generate-key \
-o "$1" = list-profiles \
-o "$1" = version ]; then
return 1
fi
shift
local arg=""
local ret=1
for arg in $SIN "$@"; do
local noninput='^--\(\(.*out\|as\|profile\|userid\|'
noninput="${noninput}"'validate-at\|\(verify-\|\)'
noninput="${noninput}"'not-\(before\|after\)\)=\|[^=]*$\)'
if printf %s "$arg" | grep -q "$noninput" ; then
continue
fi
arg=$(printf %s "$arg" | sed 's/^--.*=\(.*\)$/\1/')
if ! [ -r "$arg" ]; then
ret=0
printf ' %s' "$arg"
fi
done
return "$ret"
}
sop() {
local suffix=""
if [ -n "$SIN" ]; then
suffix=" < $SIN"
fi
if [ -n "$SOUT" ]; then
suffix="$suffix > $SOUT"
fi
local missing=""
if missing=$(skip_test "$@"); then
printf "⛅ skipped [%s %s%s] due to missing inputs%s\n" \
"$SOP" "$*" "$suffix" "$missing"
SKIPCOUNT=$(( $SKIPCOUNT + 1 ))
return
fi
printf "🔒 [%s %s%s]\n" "$SOP" "$*" "$suffix"
if ! ( if [ -n "$SIN" ]; then exec < "$SIN"; fi;
if [ -n "$SOUT" ]; then exec > "$SOUT"; fi;
$SOP "$@") ; then
printf "💣 Failed: %s%s\n" "$*" "$suffix"
rm -f "$SOUT"
ERRORS="$ERRORS
$*$suffix"
else
PASSCOUNT=$(( $PASSCOUNT + 1 ))
fi
}
sop_fail() {
local suffix=""
if [ -n "$SIN" ]; then
suffix=" < $SIN"
fi
if [ -n "$SOUT" ]; then
printf 'ERROR: do not call sop_fail with expected stdout\n'
exit 1
fi
local missing=""
if missing=$(skip_test "$@"); then
printf "⛅ skipped failing test [%s %s%s] due to %s%s\n" \
"$SOP" "$*" "$suffix" "missing input" "$missing"
SKIPCOUNT=$(( $SKIPCOUNT + 1 ))
return
fi
printf "🔒⚠ [%s %s%s]\n" "$SOP" "$*" "$suffix"
if ( if [ -n "$SIN" ]; then exec < "$SIN"; fi; $SOP "$@"); then
printf >&2 "💣 succeeded when it should have failed: %s%s\n" \
"$*" "$suffix"
ERRORS="$ERRORS
! $*$suffix"
else
PASSCOUNT=$(( $PASSCOUNT + 1 ))
fi
}
compare() {
local args=""
if [ "$1" = text -o "$1" = clearsigned ]; then
args=--ignore-trailing-space
fi
comptype="$1"
shift
if ! [ -r "$1" -a -r "$2" ]; then
printf "⛅ skipped %s comparison (%s) of %s and %s\n" \
"missing inputs" "$comptype" "$1" "$2"
SKIPCOUNT=$(( $SKIPCOUNT + 1 ))
return
fi
if diff --unified $args "$1" "$2"; then
printf "👍 %s and %s match!\n" "$1" "$2"
PASSCOUNT=$(( $PASSCOUNT + 1 ))
else
printf " 💣 %s and %s do not match!\n" "$1" "$2"
ERRORS="$ERRORS
Mismatch ($*)"
fi
}
show_errs() {
if [ -z "$1" ]; then
if [ 0 -ne $SKIPCOUNT ]; then
printf "No errors, but %d tests skipped somehow\n" \
$SKIPCOUNT
else
printf "No errors!\n"
fi
else
local SKIPMSG=''
if [ 0 -ne $SKIPCOUNT ]; then
SKIPMSG=$(printf "%d tests skipped due to prior errors" \
$SKIPCOUNT)
fi
cat <<EOF
$PASSCOUNT tests passed.
$SKIPMSG
=== ERRORS ===
$1
=== Error summary ===
EOF
E=$(echo "$1" | grep -v '^$')
printf "%d Errors:\n" $(echo "$E" | wc -l)
echo "$E" | sed 's/^! //' | cut -f1 -d\ | sort | uniq -c
fi
}
DEARMORED=""
dearmor() {
SIN="$1" SOUT="$1.bin" sop dearmor
DEARMORED="$DEARMORED $1.bin"
}
ERRORS=""
SKIPCOUNT=0
PASSCOUNT=0
WORKDIR=$(mktemp -d)
printf "Working in: %s\n" "$WORKDIR"
cd "$WORKDIR"
sop version
sop version --extended
sop version --backend
sop version --sop-spec
sop version --sopv
sop list-profiles generate-key
sop list-profiles encrypt
SOUT=test.key sop generate-key "Example User <user@example.net>"
dearmor test.key
SIN=test.key SOUT=test.cert sop extract-cert
dearmor test.cert
SOUT=zeina.key sop generate-key "Zeina <zeina@example.net>"
dearmor zeina.key
SIN=zeina.key SOUT=zeina.cert sop extract-cert
dearmor zeina.cert
for f in cert key; do
cat zeina.$f.bin test.$f.bin > both.$f.bin
SIN=both.$f.bin SOUT=both.$f sop armor
done
SIN=test.key SOUT=test-revoked.cert sop revoke-key
dearmor test-revoked.cert
echo b4n4n4s > pw-orig.txt
SIN=test.key SOUT=test-locked.key sop change-key-password \
--new-key-password=pw-orig.txt
dearmor test-locked.key
# ensure that the key password is based on content, not filename
mv pw-orig.txt pw.txt
echo no-bananas > wrong-pw.txt
SIN=test-locked.key sop_fail change-key-password \
--old-key-password=wrong-pw.txt
SIN=test-locked.key SOUT=test-unlocked.key sop change-key-password \
--old-key-password=pw.txt
dearmor test-unlocked.key
compare binary test.key.bin test-unlocked.key.bin
cat > test.txt <<EOF
This is a test message.
We all ♥ OpenPGP!
EOF
for as in '' binary text; do
asarg=''
if [ -n "$as" ]; then
asarg=--as=$as
fi
SIN=test.txt SOUT=test.$as.sig sop sign $asarg test.key
dearmor test.$as.sig
# should fail because no password is supplied.
SIN=test.txt sop_fail sign $asarg test-locked.key
# should fail because wrong password is supplied.
SIN=test.txt sop_fail sign $asarg \
--with-key-password=wrong-pw.txt test-locked.key
SIN=test.txt SOUT=test.$as.siglocked sop sign $asarg \
--with-key-password=pw.txt test-locked.key
dearmor test.$as.siglocked
for sig in test.$as.sig test.$as.sig.bin test.$as.siglocked \
test.$as.siglocked.bin; do
for cert in test.cert test.cert.bin \
both.cert both.cert.bin; do
SIN=test.txt sop verify $sig $cert
done
for cert in test-revoked.cert test-revoked.cert.bin; do
SIN=test.txt sop_fail verify $sig $cert
done
done
done
for as in '' binary text clearsigned; do
asarg=''
cmparg=binary
if [ -n "$as" ]; then
asarg=--as=$as
cmparg=$as
fi
SIN=test.txt SOUT=test.$as.signed sop inline-sign $asarg test.key
msgs=test.$as.signed
if [ "$as" != clearsigned ]; then
dearmor test.$as.signed
msgs="$msgs test.$as.signed.bin"
fi
for msg in $msgs; do
SIN=$msg SOUT=$msg.body sop inline-detach \
--signatures-out=$msg.detached-sigs
compare $cmparg test.txt $msg.body
for cert in test.cert test.cert.bin both.cert \
both.cert.bin; do
SIN=$msg SOUT=$msg.$cert.verified.txt sop \
inline-verify $cert
compare $cmparg test.txt $msg.$cert.verified.txt
SIN=$msg.body sop verify $msg.detached-sigs $cert
done
for cert in test-revoked.cert test-revoked.cert.bin; do
SIN=$msg sop_fail inline-verify $cert
done
done
done
SIN=test.txt SOUT=test.msg sop encrypt test.cert
dearmor test.msg
SIN=test.txt SOUT=test.both.msg sop encrypt both.cert.bin
dearmor test.both.msg
for msg in test.msg test.msg.bin test.both.msg test.both.msg.bin; do
SIN=$msg SOUT=$msg.decrypted.txt sop decrypt test.key
compare binary test.txt $msg.decrypted.txt
SIN=$msg sop_fail decrypt test-locked.key
SIN=$msg sop_fail decrypt --with-key-password=wrong-pw.txt \
test-locked.key
SIN=$msg SOUT=$msg.locked-decrypted.txt sop decrypt \
--with-key-password=pw.txt test-locked.key
compare binary test.txt $msg.decrypted.txt
done
for x in $DEARMORED ; do
SIN=$x SOUT=$x.asc sop armor
SIN=$x.asc SOUT=$x.asc.bin sop dearmor
compare binary $x $x.asc.bin
done
# TODO (subcommands still untested):
# merge-certs
# update-key
# certify-userid
# validate-userid
# TODO (sop features still untested):
# symmetric encryption/decryption (with password)
# using --no-armor explicitly
# sop generate-key --signing-only
# sop generate-key --with-key-password
# sop revoke-key --with-key-password
# using the -- delimiter between options and positional args
# sop sign --micalg-out
# signing and encrypting at the same time
# decrypting and verifying at the same time
# using profiles
# using session keys
# using date ranges
# using special designators (@FD: and @ENV:)
# using piped input instead of material in the filesystem
# confirming error codes for expected failures
# put multiple TSKs in a KEYS object
# sop_fail when KEYS is offered where CERTS should be
# sop_fail when CERTS are offered where KEYS should be
# This script does not test different algorithms or protocol-layer
# subtleties For more complete testing, see the OpenPGP
# Interoperability Test Suite, at https://tests.sequoia-pgp.org/
show_errs "$ERRORS"
if [ -d "$WORKDIR" ]; then
rm -rf "$WORKDIR"
fi
|