[RFC] ima-evm-utils: Add some tests for evmctl
diff mbox series

Message ID 20190725061855.3734-1-vt@altlinux.org
State New
Headers show
Series
  • [RFC] ima-evm-utils: Add some tests for evmctl
Related show

Commit Message

Vitaly Chikunov July 25, 2019, 6:18 a.m. UTC
Run `make check' to execute the tests.
Currently only ima_hash, ima_sign (v2), and ima_verify are tested.

Signed-off-by: Vitaly Chikunov <vt@altlinux.org>
---
 Makefile.am           |   2 +-
 configure.ac          |   1 +
 tests/Makefile.am     |  14 ++++
 tests/functions       | 135 ++++++++++++++++++++++++++++++++++++
 tests/gen-keys.sh     |  75 ++++++++++++++++++++
 tests/ima_hash.test   |  84 +++++++++++++++++++++++
 tests/ima_sign.test   | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++
 tests/ima_verify.test |  79 +++++++++++++++++++++
 8 files changed, 575 insertions(+), 1 deletion(-)
 create mode 100644 tests/Makefile.am
 create mode 100755 tests/functions
 create mode 100755 tests/gen-keys.sh
 create mode 100755 tests/ima_hash.test
 create mode 100755 tests/ima_sign.test
 create mode 100755 tests/ima_verify.test

Comments

Mimi Zohar July 25, 2019, 2:46 p.m. UTC | #1
On Thu, 2019-07-25 at 09:18 +0300, Vitaly Chikunov wrote:
> Run `make check' to execute the tests.
> Currently only ima_hash, ima_sign (v2), and ima_verify are tested.
> 
> Signed-off-by: Vitaly Chikunov <vt@altlinux.org>

Nice!  As much as I would like to include this patch in this release,
let's hold off and add it to the next release.

Reviewing shorter patches is a lot easier, at least for me.  Could you
break this patch up?  Perhaps by defining the tests separately, and
then adding the autotools support to run the test afterwards?

thanks,

Mimi

> ---
>  Makefile.am           |   2 +-
>  configure.ac          |   1 +
>  tests/Makefile.am     |  14 ++++
>  tests/functions       | 135 ++++++++++++++++++++++++++++++++++++
>  tests/gen-keys.sh     |  75 ++++++++++++++++++++
>  tests/ima_hash.test   |  84 +++++++++++++++++++++++
>  tests/ima_sign.test   | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++
>  tests/ima_verify.test |  79 +++++++++++++++++++++
>  8 files changed, 575 insertions(+), 1 deletion(-)
>  create mode 100644 tests/Makefile.am
>  create mode 100755 tests/functions
>  create mode 100755 tests/gen-keys.sh
>  create mode 100755 tests/ima_hash.test
>  create mode 100755 tests/ima_sign.test
>  create mode 100755 tests/ima_verify.test
> 
> diff --git a/Makefile.am b/Makefile.am
> index dba408d..45c6f82 100644
> --- a/Makefile.am
> +++ b/Makefile.am
> @@ -1,4 +1,4 @@
> -SUBDIRS = src
> +SUBDIRS = src tests
>  dist_man_MANS = evmctl.1
> 
>  doc_DATA =  examples/ima-genkey-self.sh examples/ima-genkey.sh examples/ima-gen-local-ca.sh
> diff --git a/configure.ac b/configure.ac
> index 3fc63b3..dccdd92 100644
> --- a/configure.ac
> +++ b/configure.ac
> @@ -67,6 +67,7 @@ EVMCTL_MANPAGE_DOCBOOK_XSL
> 
>  AC_CONFIG_FILES([Makefile
>  		src/Makefile
> +		tests/Makefile
>  		packaging/ima-evm-utils.spec
>  		])
>  AC_OUTPUT
> diff --git a/tests/Makefile.am b/tests/Makefile.am
> new file mode 100644
> index 0000000..8c6256c
> --- /dev/null
> +++ b/tests/Makefile.am
> @@ -0,0 +1,14 @@
> +check_SCRIPTS =
> +TESTS = $(check_SCRIPTS)
> +
> +check_SCRIPTS += ima_hash.test ima_verify.test ima_sign.test
> +
> +# ima_verify depends on results of ima_hash
> +ima_verify.log: ima_sign.log
> +
> +clean-local:
> +	-rm -f *.txt *.out *.sig *.sig2
> +distclean: distclean-keys
> +.PHONY: distclean-keys
> +distclean-keys:
> +	-rm -f *.pub *.key *.cer
> diff --git a/tests/functions b/tests/functions
> new file mode 100755
> index 0000000..7ad6c7c
> --- /dev/null
> +++ b/tests/functions
> @@ -0,0 +1,135 @@
> +#!/bin/bash
> +#
> +# ima-evm-utils tests bash functions
> +#
> +# Copyright (C) 2019 Vitaly Chikunov <vt@altlinux.org>
> +#
> +# This program 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 2, 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.
> +
> +declare -i pass=0 fail=0 skip=0
> +
> +_require() {
> +  RET=
> +  for i; do
> +    if ! type $i; then
> +      echo "$i is required for test"
> +      RET=1
> +    fi
> +  done
> +  [ $RET ] && exit 99
> +}
> +
> +if tty -s; then
> +     RED=$'\e[1;31m'
> +  YELLOW=$'\e[1;33m'
> +    BLUE=$'\e[1;34m'
> +    NORM=$'\e[m'
> +fi
> +
> +# Define EXITEARLY to exit testing on the first error.
> +exit_early() {
> +  if [ $EXITEARLY ]; then
> +    exit $1
> +  fi
> +}
> +
> +# Defaults
> +MODE=+
> +HOWFAILED=
> +pos() {
> +  MODE=+
> +  HOWFAILED=
> +  eval "$@"
> +  case $? in
> +    0)  pass+=1 ;;
> +    77) skip+=1 ;;
> +    99) fail+=1; exit_early 1 ;;
> +    *)  fail+=1; exit_early 2 ;;
> +  esac
> +}
> +
> +neg() {
> +  MODE=-
> +  HOWFAILED=properly
> +  eval "$@"
> +  case $? in
> +    0)  fail+=1; exit_early 3 ;;
> +    77) skip+=1 ;;
> +    99) fail+=1; exit_early 4 ;;
> +    *)  pass+=1 ;;
> +  esac
> +# Restore defaults
> +  MODE=+
> +  HOWFAILED=
> +}
> +
> +_ispos() {
> +  [ -z "$HOWFAILED" ]
> +}
> +
> +_isneg() {
> +  [ "$HOWFAILED" ]
> +}
> +
> +red_if_pos() {
> +  _ispos && echo $@ $RED
> +}
> +
> +norm_if_pos() {
> +  _ispos && echo $@ $NORM
> +}
> +
> +_evmctl_catch() {
> +  local ret=$1 out=$2 cmd=$3 for=$4 del=${@:5}
> +
> +  if [ $ret -gt 128 -a $ret -lt 255 ]; then
> +    echo $RED
> +    echo "evmctl $cmd failed hard with $ret for $for"
> +    sed 's/^/  /' $out
> +    echo $NORM
> +    rm -f $out $del
> +    return 99
> +  elif [ $ret -gt 0 ]; then
> +    red_if_pos
> +    echo "evmctl $cmd failed" $HOWFAILED "with $ret for $for"
> +    sed 's/^/  /' $out
> +    norm_if_pos
> +    rm -f $out $del
> +    return 1
> +  elif [ -n "$HOWFAILED" ]; then
> +    echo $RED
> +    echo "evmctl $cmd wrongly succeeded for $for"
> +    sed 's/^/  /' $out
> +    echo $NORM
> +  fi
> +  rm -f $out
> +  return 0
> +}
> +
> +_enable_gost_engine() {
> +  # Do not enable if it already enabled by user
> +  if ! openssl md_gost12_256 /dev/null >/dev/null 2>&1 \
> +    && openssl engine gost >/dev/null 2>&1; then
> +    ENGINE=gost
> +  fi
> +}
> +
> +_report_exit() {
> +  echo "PASS: $pass SKIP: $skip FAIL: $fail"
> +  if [ $fail -gt 0 ]; then
> +    exit 1
> +  elif [ $pass -gt 0 ]; then
> +    exit 0
> +  else
> +    exit 77
> +  fi
> +}
> +
> diff --git a/tests/gen-keys.sh b/tests/gen-keys.sh
> new file mode 100755
> index 0000000..c4073a2
> --- /dev/null
> +++ b/tests/gen-keys.sh
> @@ -0,0 +1,75 @@
> +#!/bin/bash
> +#
> +# Generate keys for the tests
> +#
> +# Copyright (C) 2019 Vitaly Chikunov <vt@altlinux.org>
> +#
> +# This program 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 2, 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.
> +
> +cd $(dirname $0)
> +PATH=../src:$PATH
> +type openssl
> +
> +log() {
> +  echo - "$*"
> +  eval "$@"
> +}
> +
> +if [ ! -e test-ca.conf ]; then
> +cat > test-ca.conf <<- EOF
> +	[ req ]
> +	distinguished_name = req_distinguished_name
> +	prompt = no
> +	string_mask = utf8only
> +	x509_extensions = v3_ca
> +
> +	[ req_distinguished_name ]
> +	O = IMA-CA
> +	CN = IMA/EVM certificate signing key
> +	emailAddress = ca@ima-ca
> +
> +	[ v3_ca ]
> +	basicConstraints=CA:TRUE
> +	subjectKeyIdentifier=hash
> +	authorityKeyIdentifier=keyid:always,issuer
> +EOF
> +fi
> +
> +# RSA
> +# Second key will be used for wrong key tests.
> +for m in 1024 2048; do
> +  if [ ! -e test-rsa$m.key ]; then
> +    log openssl req -verbose -new -nodes -utf8 -sha1 -days 10000 -batch -x509 \
> +      -config test-ca.conf \
> +      -newkey rsa:$m \
> +      -out test-rsa$m.cer -outform DER \
> +      -keyout test-rsa$m.key
> +  fi
> +done
> +
> +# EC-RDSA
> +for m in \
> +  gost2012_256:A \
> +  gost2012_256:B \
> +  gost2012_256:C \
> +  gost2012_512:A \
> +  gost2012_512:B; do
> +    IFS=':' read -r algo param <<< "$m"
> +    [ -e test-$algo-$param.key ] && continue
> +    log openssl req -nodes -x509 -utf8 -days 10000 -batch \
> +      -config test-ca.conf \
> +      -newkey $algo \
> +      -pkeyopt paramset:$param \
> +      -out    test-$algo-$param.cer -outform DER \
> +      -keyout test-$algo-$param.key
> +    log openssl pkey -in test-$algo-$param.key -out test-$algo-$param.pub -pubout
> +done
> +
> diff --git a/tests/ima_hash.test b/tests/ima_hash.test
> new file mode 100755
> index 0000000..cf4af57
> --- /dev/null
> +++ b/tests/ima_hash.test
> @@ -0,0 +1,84 @@
> +#!/bin/bash
> +#
> +# evmctl ima_hash tests
> +#
> +# Copyright (C) 2019 Vitaly Chikunov <vt@altlinux.org>
> +#
> +# This program 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 2, 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.
> +
> +cd $(dirname $0)
> +PATH=../src:$PATH
> +source ./functions
> +_require evmctl openssl
> +
> +# Check with constant
> +check_const() {
> +  local a=$1 p=$2 h=$3 f=$4
> +
> +  cmd=(evmctl ima_hash -v ${ENGINE:+--engine $ENGINE} -a $a --xattr-user $f)
> +  echo $YELLOW$MODE "${cmd[@]}" $NORM
> +  eval "${cmd[@]}" >$a.out 2>&1
> +  _evmctl_catch $? $a.out ima_hash $a $f || return
> +
> +  if ! getfattr -n user.ima -e hex $f | grep -qx user.ima=$p$h; then
> +    red_if_pos
> +    echo "Did not find expected hash value for $a:"
> +    echo "    user.ima=$p$h"
> +    echo ""
> +    echo "Actual output below:"
> +    getfattr -n user.ima -e hex $f | sed 's/^/    /'
> +    norm_if_pos
> +    rm -f $f
> +    return 1
> +  fi
> +  rm -f $f
> +  return 0
> +}
> +
> +check() {
> +  local a=$1 p=$2 h=$3
> +  local f=$a-hash.txt
> +
> +  rm -f $f
> +  touch $f
> +  cmd=(openssl dgst ${ENGINE:+--engine $ENGINE} -$a $f)
> +  echo $BLUE - "${cmd[@]}" $NORM
> +  h=$(set -o pipefail; eval "${cmd[@]}" 2>/dev/null | cut -d' ' -f2)
> +  if [ $? -ne 0 -a -z "$HOWFAILED" ]; then
> +    echo "$a test is skipped"
> +    rm -f $f
> +    return 77
> +  fi
> +  check_const $a $p "$h" $f
> +}
> +
> +# check args: algo prefix hex-hash
> +pos check md4    0x01
> +pos check md5    0x01
> +pos check sha1   0x01
> +neg check SHA1   0x01 # uppercase
> +neg check sha512-224 0x01 # valid for pkcs1
> +neg check sha512-256 0x01 # valid for pkcs1
> +neg check unknown 0x01 # nonexistent
> +pos check sha224 0x0407
> +pos check sha256 0x0404
> +pos check sha384 0x0405
> +pos check sha512 0x0406
> +pos check rmd160 0x0403
> +neg check sm3     0x01
> +neg check sm3-256 0x01
> +_enable_gost_engine
> +pos check md_gost12_256 0x0412
> +pos check streebog256   0x0412
> +pos check md_gost12_512 0x0413
> +pos check streebog512   0x0413
> +
> +_report_exit
> diff --git a/tests/ima_sign.test b/tests/ima_sign.test
> new file mode 100755
> index 0000000..5d8edd5
> --- /dev/null
> +++ b/tests/ima_sign.test
> @@ -0,0 +1,186 @@
> +#!/bin/bash
> +#
> +# evmctl ima_sign tests
> +#
> +# Copyright (C) 2019 Vitaly Chikunov <vt@altlinux.org>
> +#
> +# This program 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 2, 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.
> +
> +cd $(dirname $0)
> +PATH=../src:$PATH
> +source ./functions
> +_require evmctl openssl xxd getfattr
> +./gen-keys.sh >/dev/null 2>&1
> +
> +# Determine keyid from .cer
> +keyid() {
> +  id=$(openssl x509 ${ENGINE:+-engine $ENGINE} \
> +      -in $1.cer -inform DER -pubkey -noout 2>/dev/null \
> +    | openssl asn1parse \
> +    | grep BIT.STRING \
> +    | cut -d: -f1)
> +  if [ -z "$id" ]; then
> +    echo "Cannot asn1parse $1.cer" >&2
> +    exit 1
> +  fi
> +  openssl x509 ${ENGINE:+-engine $ENGINE} \
> +      -in $1.cer -inform DER -pubkey -noout 2>/dev/null \
> +    | openssl asn1parse -strparse $id -out - -noout \
> +    | openssl dgst -c -sha1 \
> +    | cut -d' ' -f2 \
> +    | grep -o ":..:..:..:..$" \
> +    | tr -d :
> +}
> +
> +_ima_sign() {
> +  local k=$1 a=$2 f=$3
> +
> +  cmd=(evmctl ima_sign ${ENGINE:+--engine $ENGINE} -a $a \
> +	   -k $k.key --xattr-user --sigfile $f)
> +  echo $YELLOW$MODE ${cmd[@]} $NORM
> +  eval "${cmd[@]}" >$a.out 2>&1
> +  _evmctl_catch $? $a.out ima_sign "$a ($k.key)" $f || return
> +
> +  # Check that detached signature matches xattr signature
> +  if [ ! -e $f.sig ]; then
> +    echo "evmctl ima_sign: no detached signature $f.sig"
> +    return 1
> +  fi
> +
> +  getfattr -n user.ima --only-values $f > $f.sig2
> +  if ! cmp -bl $f.sig $f.sig2; then
> +    echo "evmctl ima_sign: xattr signature differ from detached $f.sig"
> +    rm -f $f.sig $f.sig2
> +    return 1
> +  fi
> +
> +  rm -f $f.sig $f.sig2
> +}
> +
> +check_rsa() {
> +  local k=test-rsa1024 a=$1 p=$2
> +  local f=$a.txt
> +
> +  # Append suffix to files for negative tests, because we need
> +  # to leave only good files for ima_verify.test
> +  _isneg && f+=-
> +  rm -f $f
> +
> +  keyid=$(keyid $k)
> +  if [ $? -ne 0 ]; then
> +    echo "Unable to determine keyid for $k"
> +    return 99
> +  fi
> +  p=$(echo $p | sed "s/K/$keyid/")
> +
> +  if ! openssl dgst -$a /dev/null >/dev/null 2>&1; then
> +    echo "$a ($k.key) test is skipped (openssl cannot handle $a digest)"
> +    return 77
> +  fi
> +  touch $f
> +  cmd=(openssl dgst -$a -sign $k.key -hex $f)
> +  echo $BLUE - "${cmd[@]}" $NORM
> +  sig=$(set -o pipefail; eval "${cmd[@]}" 2>/dev/null | cut -d' ' -f2)
> +  if [ $? -ne 0 ] && _ispos; then
> +    echo "$a ($k.key) test is skipped (openssl cannot sign with $a+$k.key)"
> +    rm -f $f
> +    return 77
> +  fi
> +
> +  _ima_sign $k $a $f || return
> +  if ! getfattr -n user.ima -e hex $f | grep -qx user.ima=$p$sig; then
> +    red_if_pos
> +    echo "Did not find expected hash value for $a:"
> +    echo "    user.ima=$p$sig"
> +    echo ""
> +    echo "Actual output below:"
> +    getfattr -n user.ima -e hex $f | sed 's/^/    /'
> +    norm_if_pos
> +    rm -f $f
> +    return 1
> +  fi
> +  return 0
> +}
> +
> +check_ecrdsa() {
> +  local k=$1 a=$2 p=$3
> +
> +  # Sign different files not only depending on a hash algo,
> +  # but also on a key. Append curve letter to the hash algo.
> +  curve=${k##*-}
> +  f=$a${curve,,}.txt
> +
> +  # Append suffix to files for negative tests, because we need
> +  # to leave only good files for ima_verify.test
> +  _isneg && f+=-
> +  rm -f $f
> +
> +  # Older openssl unable to parse 512-bit keys
> +  if ! openssl pkey ${ENGINE:+-engine $ENGINE} -in $k.key >/dev/null 2>&1; then
> +    echo "$a ($k.key) test is skipped"
> +    return 77
> +  fi
> +
> +  keyid=$(keyid $k)
> +  if [ $? -ne 0 ]; then
> +    echo "Unable to determine keyid for $k"
> +    return 99
> +  fi
> +  p=$(echo $p | sed "s/K/$keyid/")
> +
> +  touch $f
> +  _ima_sign $k $a $f || return
> +  # Only verify prefix here.
> +  if ! getfattr -n user.ima -e hex $f | grep -q ^user.ima=$p; then
> +    red_if_pos
> +    echo "Signature prefix does not match for $a ($k):"
> +    echo "Expected:  user.ima=$p..."
> +    echo ""
> +    echo "Actual output below:"
> +    getfattr -n user.ima -e hex $f | sed 's/^/    /'
> +    norm_if_pos
> +    rm -f $f
> +    return 1
> +  fi
> +
> +  # Extract signature from xattr
> +  getfattr -n user.ima -e hex $f \
> +    | grep ^user.ima= \
> +    | sed s/^user.ima=$p// \
> +    | xxd -r -p > $a.sig2
> +
> +  # Verify with openssl
> +  if ! openssl dgst ${ENGINE:+-engine $ENGINE} -$a \
> +	-verify $k.pub -signature $a.sig2 $f >/dev/null 2>&1; then
> +    return 1
> +  fi
> +  rm $a.sig2
> +}
> +
> +# check args: algo prefix hex-signature-prefix (K in place of keyid.)
> +pos check_rsa md5    0x030201K0080
> +pos check_rsa sha1   0x030202K0080
> +pos check_rsa sha224 0x030207K0080
> +pos check_rsa sha256 0x030204K0080
> +pos check_rsa sha384 0x030205K0080
> +pos check_rsa sha512 0x030206K0080
> +pos check_rsa rmd160 0x030203K0080
> +neg check_rsa invalid-hash-algo  0x030202K0080
> +_enable_gost_engine
> +pos check_ecrdsa test-gost2012_256-A md_gost12_256 0x030212K0040
> +pos check_ecrdsa test-gost2012_256-B md_gost12_256 0x030212K0040
> +pos check_ecrdsa test-gost2012_256-C md_gost12_256 0x030212K0040
> +pos check_ecrdsa test-gost2012_512-A md_gost12_512 0x030213K0080
> +pos check_ecrdsa test-gost2012_512-B md_gost12_512 0x030213K0080
> +neg check_ecrdsa test-gost2012_256-A md_gost12_512 0x030212K0040
> +neg check_ecrdsa test-gost2012_512-A md_gost12_256 0x030212K0040
> +
> +_report_exit
> diff --git a/tests/ima_verify.test b/tests/ima_verify.test
> new file mode 100755
> index 0000000..6a9c876
> --- /dev/null
> +++ b/tests/ima_verify.test
> @@ -0,0 +1,79 @@
> +#!/bin/bash
> +#
> +# evmctl ima_verify tests
> +#
> +# Copyright (C) 2019 Vitaly Chikunov <vt@altlinux.org>
> +#
> +# This program 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 2, 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.
> +
> +cd $(dirname $0)
> +PATH=../src:$PATH
> +source ./functions
> +_require evmctl openssl
> +
> +# Check with constant
> +check() {
> +  local k=$1 a=$2 f=$3
> +
> +  if [ ! -e $f ]; then
> +    echo "Signed file $f is not found. Skipping verify $a ($k) test."
> +    return 77
> +  fi
> +  if ! openssl dgst ${ENGINE:+--engine $ENGINE} -$a /dev/null >/dev/null 2>&1; then
> +    echo "$a ($k.key) test is skipped (openssl does not support $a)"
> +    rm -f $f
> +    return 77
> +  fi
> +
> +  cmd=(evmctl ima_verify ${ENGINE:+--engine $ENGINE} -v -k $k --xattr-user $f)
> +  echo $YELLOW$MODE "${cmd[@]}" $NORM
> +  eval "${cmd[@]}" >$f.out 2>&1
> +  _evmctl_catch $? $f.out ima_verify "$f ($k)"
> +}
> +
> +# Tests (check args: key hash-algo signed-file
> +for a in md5 sha1 sha224 sha256 sha384 sha512 rmd160; do
> +  f=$a.txt
> +  pos check test-rsa1024.cer $a $f
> +  pos check /dev/zero,test-rsa1024.cer $a $f
> +  pos check /dev/null,test-rsa1024.cer $a $f
> +  pos check test-rsa1024.cer,test-rsa2048.cer $a $f
> +  pos check test-rsa2048.cer,test-rsa1024.cer $a $f
> +  pos check ,,test-rsa1024.cer $a $f
> +  pos check test-rsa1024.cer,,, $a $f
> +  neg check test-rsa2048.cer $a $f
> +  neg check /dev/absent $a $f
> +  neg check /dev/null $a $f
> +  neg check /dev/zero $a $f
> +done
> +
> +_enable_gost_engine
> +wk=test-gost2012_512-B.cer # first wrong key
> +for k in gost2012_256-A gost2012_256-B gost2012_256-C \
> +         gost2012_512-A gost2012_512-B; do
> +  tmp=${k#gost2012_}
> +  bits=${tmp%-?}
> +  wbits=$(( $bits == 256 ? 512 : 256 ))
> +  curve=${k#*-}
> +  a=md_gost12_$bits   # proper hash algo
> +  f=$a${curve,,}.txt  # file signed in ima_sign test
> +  k=test-$k.cer
> +  wwk=test-gost2012_$wbits-$curve.cer # key with wrong bit length
> +
> +  pos check $k     $a $f
> +  pos check $wk,$k $a $f
> +  pos check $k,$wk $a $f
> +  neg check $wk    $a $f
> +  neg check $wwk   $a $f
> +  wk=$k # previous key is always wrong
> +done
> +
> +_report_exit
Vitaly Chikunov July 25, 2019, 3:38 p.m. UTC | #2
Mimi,

On Thu, Jul 25, 2019 at 10:46:31AM -0400, Mimi Zohar wrote:
> On Thu, 2019-07-25 at 09:18 +0300, Vitaly Chikunov wrote:
> > Run `make check' to execute the tests.
> > Currently only ima_hash, ima_sign (v2), and ima_verify are tested.
> > 
> > Signed-off-by: Vitaly Chikunov <vt@altlinux.org>
> 
> Nice!  As much as I would like to include this patch in this release,
> let's hold off and add it to the next release.

You may include it if you wish, this should work good as is (over my
latest patches).

> Reviewing shorter patches is a lot easier, at least for me.  Could you
> break this patch up?  Perhaps by defining the tests separately, and
> then adding the autotools support to run the test afterwards?

This is just tests, so they don't alter any other code, don't produce
user visible features, and don't complicate anything. Each file inside
of the patch could be understood separately.
Bruno E. O. Meneguele July 25, 2019, 9:58 p.m. UTC | #3
On Thu, Jul 25, 2019 at 09:18:55AM +0300, Vitaly Chikunov wrote:
> Run `make check' to execute the tests.
> Currently only ima_hash, ima_sign (v2), and ima_verify are tested.
> 
> Signed-off-by: Vitaly Chikunov <vt@altlinux.org>
> ---
>  Makefile.am           |   2 +-
>  configure.ac          |   1 +
>  tests/Makefile.am     |  14 ++++
>  tests/functions       | 135 ++++++++++++++++++++++++++++++++++++
>  tests/gen-keys.sh     |  75 ++++++++++++++++++++
>  tests/ima_hash.test   |  84 +++++++++++++++++++++++
>  tests/ima_sign.test   | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++
>  tests/ima_verify.test |  79 +++++++++++++++++++++
>  8 files changed, 575 insertions(+), 1 deletion(-)
>  create mode 100644 tests/Makefile.am
>  create mode 100755 tests/functions
>  create mode 100755 tests/gen-keys.sh
>  create mode 100755 tests/ima_hash.test
>  create mode 100755 tests/ima_sign.test
>  create mode 100755 tests/ima_verify.test
> 
> diff --git a/Makefile.am b/Makefile.am
> index dba408d..45c6f82 100644
> --- a/Makefile.am
> +++ b/Makefile.am
> @@ -1,4 +1,4 @@
> -SUBDIRS = src
> +SUBDIRS = src tests
>  dist_man_MANS = evmctl.1
>  
>  doc_DATA =  examples/ima-genkey-self.sh examples/ima-genkey.sh examples/ima-gen-local-ca.sh
> diff --git a/configure.ac b/configure.ac
> index 3fc63b3..dccdd92 100644
> --- a/configure.ac
> +++ b/configure.ac
> @@ -67,6 +67,7 @@ EVMCTL_MANPAGE_DOCBOOK_XSL
>  
>  AC_CONFIG_FILES([Makefile
>  		src/Makefile
> +		tests/Makefile
>  		packaging/ima-evm-utils.spec
>  		])
>  AC_OUTPUT
> diff --git a/tests/Makefile.am b/tests/Makefile.am
> new file mode 100644
> index 0000000..8c6256c
> --- /dev/null
> +++ b/tests/Makefile.am
> @@ -0,0 +1,14 @@
> +check_SCRIPTS =
> +TESTS = $(check_SCRIPTS)
> +
> +check_SCRIPTS += ima_hash.test ima_verify.test ima_sign.test
> +
> +# ima_verify depends on results of ima_hash
> +ima_verify.log: ima_sign.log

Small nit: comment and code don't match, ima_hash vs ima_sign :). 

Maybe:

-# ima_verify depends on results of ima_hash
+# ima_verify depends on results of ima_sign

> +
> +clean-local:
> +	-rm -f *.txt *.out *.sig *.sig2
> +distclean: distclean-keys
> +.PHONY: distclean-keys
> +distclean-keys:
> +	-rm -f *.pub *.key *.cer
> diff --git a/tests/functions b/tests/functions
> new file mode 100755
> index 0000000..7ad6c7c
> --- /dev/null
> +++ b/tests/functions
> @@ -0,0 +1,135 @@
> +#!/bin/bash
> +#
> +# ima-evm-utils tests bash functions
> +#
> +# Copyright (C) 2019 Vitaly Chikunov <vt@altlinux.org>
> +#
> +# This program 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 2, 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.
> +
> +declare -i pass=0 fail=0 skip=0
> +
> +_require() {
> +  RET=
> +  for i; do
> +    if ! type $i; then
> +      echo "$i is required for test"
> +      RET=1
> +    fi
> +  done
> +  [ $RET ] && exit 99
> +}
> +
> +if tty -s; then
> +     RED=$'\e[1;31m'
> +  YELLOW=$'\e[1;33m'
> +    BLUE=$'\e[1;34m'
> +    NORM=$'\e[m'
> +fi
> +
> +# Define EXITEARLY to exit testing on the first error.
> +exit_early() {
> +  if [ $EXITEARLY ]; then
> +    exit $1
> +  fi
> +}
> +
> +# Defaults
> +MODE=+
> +HOWFAILED=
> +pos() {
> +  MODE=+
> +  HOWFAILED=
> +  eval "$@"
> +  case $? in
> +    0)  pass+=1 ;;
> +    77) skip+=1 ;;
> +    99) fail+=1; exit_early 1 ;;
> +    *)  fail+=1; exit_early 2 ;;
> +  esac
> +}
> +
> +neg() {
> +  MODE=-
> +  HOWFAILED=properly
> +  eval "$@"
> +  case $? in
> +    0)  fail+=1; exit_early 3 ;;
> +    77) skip+=1 ;;
> +    99) fail+=1; exit_early 4 ;;
> +    *)  pass+=1 ;;
> +  esac
> +# Restore defaults
> +  MODE=+
> +  HOWFAILED=
> +}
> +
> +_ispos() {
> +  [ -z "$HOWFAILED" ]
> +}
> +
> +_isneg() {
> +  [ "$HOWFAILED" ]
> +}
> +
> +red_if_pos() {
> +  _ispos && echo $@ $RED
> +}
> +
> +norm_if_pos() {
> +  _ispos && echo $@ $NORM
> +}
> +
> +_evmctl_catch() {
> +  local ret=$1 out=$2 cmd=$3 for=$4 del=${@:5}
> +
> +  if [ $ret -gt 128 -a $ret -lt 255 ]; then
> +    echo $RED
> +    echo "evmctl $cmd failed hard with $ret for $for"
> +    sed 's/^/  /' $out
> +    echo $NORM
> +    rm -f $out $del
> +    return 99
> +  elif [ $ret -gt 0 ]; then
> +    red_if_pos
> +    echo "evmctl $cmd failed" $HOWFAILED "with $ret for $for"
> +    sed 's/^/  /' $out
> +    norm_if_pos
> +    rm -f $out $del
> +    return 1
> +  elif [ -n "$HOWFAILED" ]; then
> +    echo $RED
> +    echo "evmctl $cmd wrongly succeeded for $for"
> +    sed 's/^/  /' $out
> +    echo $NORM
> +  fi
> +  rm -f $out
> +  return 0
> +}
> +
> +_enable_gost_engine() {
> +  # Do not enable if it already enabled by user
> +  if ! openssl md_gost12_256 /dev/null >/dev/null 2>&1 \
> +    && openssl engine gost >/dev/null 2>&1; then
> +    ENGINE=gost
> +  fi
> +}
> +
> +_report_exit() {
> +  echo "PASS: $pass SKIP: $skip FAIL: $fail"
> +  if [ $fail -gt 0 ]; then
> +    exit 1
> +  elif [ $pass -gt 0 ]; then
> +    exit 0
> +  else
> +    exit 77
> +  fi
> +}
> +
> diff --git a/tests/gen-keys.sh b/tests/gen-keys.sh
> new file mode 100755
> index 0000000..c4073a2
> --- /dev/null
> +++ b/tests/gen-keys.sh
> @@ -0,0 +1,75 @@
> +#!/bin/bash
> +#
> +# Generate keys for the tests
> +#
> +# Copyright (C) 2019 Vitaly Chikunov <vt@altlinux.org>
> +#
> +# This program 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 2, 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.
> +
> +cd $(dirname $0)
> +PATH=../src:$PATH
> +type openssl
> +
> +log() {
> +  echo - "$*"
> +  eval "$@"
> +}
> +
> +if [ ! -e test-ca.conf ]; then
> +cat > test-ca.conf <<- EOF
> +	[ req ]
> +	distinguished_name = req_distinguished_name
> +	prompt = no
> +	string_mask = utf8only
> +	x509_extensions = v3_ca
> +
> +	[ req_distinguished_name ]
> +	O = IMA-CA
> +	CN = IMA/EVM certificate signing key
> +	emailAddress = ca@ima-ca
> +
> +	[ v3_ca ]
> +	basicConstraints=CA:TRUE
> +	subjectKeyIdentifier=hash
> +	authorityKeyIdentifier=keyid:always,issuer
> +EOF
> +fi
> +
> +# RSA
> +# Second key will be used for wrong key tests.
> +for m in 1024 2048; do
> +  if [ ! -e test-rsa$m.key ]; then
> +    log openssl req -verbose -new -nodes -utf8 -sha1 -days 10000 -batch -x509 \
> +      -config test-ca.conf \
> +      -newkey rsa:$m \
> +      -out test-rsa$m.cer -outform DER \
> +      -keyout test-rsa$m.key
> +  fi
> +done
> +
> +# EC-RDSA
> +for m in \
> +  gost2012_256:A \
> +  gost2012_256:B \
> +  gost2012_256:C \
> +  gost2012_512:A \
> +  gost2012_512:B; do
> +    IFS=':' read -r algo param <<< "$m"
> +    [ -e test-$algo-$param.key ] && continue
> +    log openssl req -nodes -x509 -utf8 -days 10000 -batch \
> +      -config test-ca.conf \
> +      -newkey $algo \
> +      -pkeyopt paramset:$param \
> +      -out    test-$algo-$param.cer -outform DER \
> +      -keyout test-$algo-$param.key
> +    log openssl pkey -in test-$algo-$param.key -out test-$algo-$param.pub -pubout
> +done
> +
> diff --git a/tests/ima_hash.test b/tests/ima_hash.test
> new file mode 100755
> index 0000000..cf4af57
> --- /dev/null
> +++ b/tests/ima_hash.test
> @@ -0,0 +1,84 @@
> +#!/bin/bash
> +#
> +# evmctl ima_hash tests
> +#
> +# Copyright (C) 2019 Vitaly Chikunov <vt@altlinux.org>
> +#
> +# This program 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 2, 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.
> +
> +cd $(dirname $0)
> +PATH=../src:$PATH
> +source ./functions
> +_require evmctl openssl
> +
> +# Check with constant
> +check_const() {
> +  local a=$1 p=$2 h=$3 f=$4
> +
> +  cmd=(evmctl ima_hash -v ${ENGINE:+--engine $ENGINE} -a $a --xattr-user $f)
> +  echo $YELLOW$MODE "${cmd[@]}" $NORM
> +  eval "${cmd[@]}" >$a.out 2>&1
> +  _evmctl_catch $? $a.out ima_hash $a $f || return
> +
> +  if ! getfattr -n user.ima -e hex $f | grep -qx user.ima=$p$h; then
> +    red_if_pos
> +    echo "Did not find expected hash value for $a:"
> +    echo "    user.ima=$p$h"
> +    echo ""
> +    echo "Actual output below:"
> +    getfattr -n user.ima -e hex $f | sed 's/^/    /'
> +    norm_if_pos
> +    rm -f $f
> +    return 1
> +  fi
> +  rm -f $f
> +  return 0
> +}
> +
> +check() {
> +  local a=$1 p=$2 h=$3
> +  local f=$a-hash.txt
> +
> +  rm -f $f
> +  touch $f
> +  cmd=(openssl dgst ${ENGINE:+--engine $ENGINE} -$a $f)
> +  echo $BLUE - "${cmd[@]}" $NORM
> +  h=$(set -o pipefail; eval "${cmd[@]}" 2>/dev/null | cut -d' ' -f2)
> +  if [ $? -ne 0 -a -z "$HOWFAILED" ]; then
> +    echo "$a test is skipped"
> +    rm -f $f
> +    return 77
> +  fi
> +  check_const $a $p "$h" $f
> +}
> +
> +# check args: algo prefix hex-hash
> +pos check md4    0x01
> +pos check md5    0x01
> +pos check sha1   0x01
> +neg check SHA1   0x01 # uppercase
> +neg check sha512-224 0x01 # valid for pkcs1
> +neg check sha512-256 0x01 # valid for pkcs1
> +neg check unknown 0x01 # nonexistent
> +pos check sha224 0x0407
> +pos check sha256 0x0404
> +pos check sha384 0x0405
> +pos check sha512 0x0406
> +pos check rmd160 0x0403
> +neg check sm3     0x01
> +neg check sm3-256 0x01
> +_enable_gost_engine
> +pos check md_gost12_256 0x0412
> +pos check streebog256   0x0412
> +pos check md_gost12_512 0x0413
> +pos check streebog512   0x0413
> +
> +_report_exit
> diff --git a/tests/ima_sign.test b/tests/ima_sign.test
> new file mode 100755
> index 0000000..5d8edd5
> --- /dev/null
> +++ b/tests/ima_sign.test
> @@ -0,0 +1,186 @@
> +#!/bin/bash
> +#
> +# evmctl ima_sign tests
> +#
> +# Copyright (C) 2019 Vitaly Chikunov <vt@altlinux.org>
> +#
> +# This program 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 2, 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.
> +
> +cd $(dirname $0)
> +PATH=../src:$PATH
> +source ./functions
> +_require evmctl openssl xxd getfattr
> +./gen-keys.sh >/dev/null 2>&1
> +
> +# Determine keyid from .cer
> +keyid() {
> +  id=$(openssl x509 ${ENGINE:+-engine $ENGINE} \
> +      -in $1.cer -inform DER -pubkey -noout 2>/dev/null \
> +    | openssl asn1parse \
> +    | grep BIT.STRING \
> +    | cut -d: -f1)
> +  if [ -z "$id" ]; then
> +    echo "Cannot asn1parse $1.cer" >&2
> +    exit 1
> +  fi
> +  openssl x509 ${ENGINE:+-engine $ENGINE} \
> +      -in $1.cer -inform DER -pubkey -noout 2>/dev/null \
> +    | openssl asn1parse -strparse $id -out - -noout \
> +    | openssl dgst -c -sha1 \
> +    | cut -d' ' -f2 \
> +    | grep -o ":..:..:..:..$" \
> +    | tr -d :
> +}
> +
> +_ima_sign() {
> +  local k=$1 a=$2 f=$3
> +
> +  cmd=(evmctl ima_sign ${ENGINE:+--engine $ENGINE} -a $a \
> +	   -k $k.key --xattr-user --sigfile $f)
> +  echo $YELLOW$MODE ${cmd[@]} $NORM
> +  eval "${cmd[@]}" >$a.out 2>&1
> +  _evmctl_catch $? $a.out ima_sign "$a ($k.key)" $f || return
> +
> +  # Check that detached signature matches xattr signature
> +  if [ ! -e $f.sig ]; then
> +    echo "evmctl ima_sign: no detached signature $f.sig"
> +    return 1
> +  fi
> +
> +  getfattr -n user.ima --only-values $f > $f.sig2
> +  if ! cmp -bl $f.sig $f.sig2; then
> +    echo "evmctl ima_sign: xattr signature differ from detached $f.sig"
> +    rm -f $f.sig $f.sig2
> +    return 1
> +  fi
> +
> +  rm -f $f.sig $f.sig2
> +}
> +
> +check_rsa() {
> +  local k=test-rsa1024 a=$1 p=$2
> +  local f=$a.txt
> +
> +  # Append suffix to files for negative tests, because we need
> +  # to leave only good files for ima_verify.test
> +  _isneg && f+=-
> +  rm -f $f
> +
> +  keyid=$(keyid $k)
> +  if [ $? -ne 0 ]; then
> +    echo "Unable to determine keyid for $k"
> +    return 99
> +  fi
> +  p=$(echo $p | sed "s/K/$keyid/")
> +
> +  if ! openssl dgst -$a /dev/null >/dev/null 2>&1; then
> +    echo "$a ($k.key) test is skipped (openssl cannot handle $a digest)"
> +    return 77
> +  fi
> +  touch $f
> +  cmd=(openssl dgst -$a -sign $k.key -hex $f)
> +  echo $BLUE - "${cmd[@]}" $NORM
> +  sig=$(set -o pipefail; eval "${cmd[@]}" 2>/dev/null | cut -d' ' -f2)
> +  if [ $? -ne 0 ] && _ispos; then
> +    echo "$a ($k.key) test is skipped (openssl cannot sign with $a+$k.key)"
> +    rm -f $f
> +    return 77
> +  fi
> +
> +  _ima_sign $k $a $f || return
> +  if ! getfattr -n user.ima -e hex $f | grep -qx user.ima=$p$sig; then
> +    red_if_pos
> +    echo "Did not find expected hash value for $a:"
> +    echo "    user.ima=$p$sig"
> +    echo ""
> +    echo "Actual output below:"
> +    getfattr -n user.ima -e hex $f | sed 's/^/    /'
> +    norm_if_pos
> +    rm -f $f
> +    return 1
> +  fi
> +  return 0
> +}
> +
> +check_ecrdsa() {
> +  local k=$1 a=$2 p=$3
> +
> +  # Sign different files not only depending on a hash algo,
> +  # but also on a key. Append curve letter to the hash algo.
> +  curve=${k##*-}
> +  f=$a${curve,,}.txt
> +
> +  # Append suffix to files for negative tests, because we need
> +  # to leave only good files for ima_verify.test
> +  _isneg && f+=-
> +  rm -f $f
> +
> +  # Older openssl unable to parse 512-bit keys
> +  if ! openssl pkey ${ENGINE:+-engine $ENGINE} -in $k.key >/dev/null 2>&1; then
> +    echo "$a ($k.key) test is skipped"
> +    return 77
> +  fi
> +
> +  keyid=$(keyid $k)
> +  if [ $? -ne 0 ]; then
> +    echo "Unable to determine keyid for $k"
> +    return 99
> +  fi
> +  p=$(echo $p | sed "s/K/$keyid/")
> +
> +  touch $f
> +  _ima_sign $k $a $f || return
> +  # Only verify prefix here.
> +  if ! getfattr -n user.ima -e hex $f | grep -q ^user.ima=$p; then
> +    red_if_pos
> +    echo "Signature prefix does not match for $a ($k):"
> +    echo "Expected:  user.ima=$p..."
> +    echo ""
> +    echo "Actual output below:"
> +    getfattr -n user.ima -e hex $f | sed 's/^/    /'
> +    norm_if_pos
> +    rm -f $f
> +    return 1
> +  fi
> +
> +  # Extract signature from xattr
> +  getfattr -n user.ima -e hex $f \
> +    | grep ^user.ima= \
> +    | sed s/^user.ima=$p// \
> +    | xxd -r -p > $a.sig2
> +
> +  # Verify with openssl
> +  if ! openssl dgst ${ENGINE:+-engine $ENGINE} -$a \
> +	-verify $k.pub -signature $a.sig2 $f >/dev/null 2>&1; then
> +    return 1
> +  fi
> +  rm $a.sig2
> +}
> +
> +# check args: algo prefix hex-signature-prefix (K in place of keyid.)
> +pos check_rsa md5    0x030201K0080
> +pos check_rsa sha1   0x030202K0080
> +pos check_rsa sha224 0x030207K0080
> +pos check_rsa sha256 0x030204K0080
> +pos check_rsa sha384 0x030205K0080
> +pos check_rsa sha512 0x030206K0080
> +pos check_rsa rmd160 0x030203K0080
> +neg check_rsa invalid-hash-algo  0x030202K0080
> +_enable_gost_engine
> +pos check_ecrdsa test-gost2012_256-A md_gost12_256 0x030212K0040
> +pos check_ecrdsa test-gost2012_256-B md_gost12_256 0x030212K0040
> +pos check_ecrdsa test-gost2012_256-C md_gost12_256 0x030212K0040
> +pos check_ecrdsa test-gost2012_512-A md_gost12_512 0x030213K0080
> +pos check_ecrdsa test-gost2012_512-B md_gost12_512 0x030213K0080
> +neg check_ecrdsa test-gost2012_256-A md_gost12_512 0x030212K0040
> +neg check_ecrdsa test-gost2012_512-A md_gost12_256 0x030212K0040
> +
> +_report_exit
> diff --git a/tests/ima_verify.test b/tests/ima_verify.test
> new file mode 100755
> index 0000000..6a9c876
> --- /dev/null
> +++ b/tests/ima_verify.test
> @@ -0,0 +1,79 @@
> +#!/bin/bash
> +#
> +# evmctl ima_verify tests
> +#
> +# Copyright (C) 2019 Vitaly Chikunov <vt@altlinux.org>
> +#
> +# This program 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 2, 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.
> +
> +cd $(dirname $0)
> +PATH=../src:$PATH
> +source ./functions
> +_require evmctl openssl
> +
> +# Check with constant
> +check() {
> +  local k=$1 a=$2 f=$3
> +
> +  if [ ! -e $f ]; then
> +    echo "Signed file $f is not found. Skipping verify $a ($k) test."
> +    return 77
> +  fi
> +  if ! openssl dgst ${ENGINE:+--engine $ENGINE} -$a /dev/null >/dev/null 2>&1; then
> +    echo "$a ($k.key) test is skipped (openssl does not support $a)"
> +    rm -f $f
> +    return 77
> +  fi
> +
> +  cmd=(evmctl ima_verify ${ENGINE:+--engine $ENGINE} -v -k $k --xattr-user $f)
> +  echo $YELLOW$MODE "${cmd[@]}" $NORM
> +  eval "${cmd[@]}" >$f.out 2>&1
> +  _evmctl_catch $? $f.out ima_verify "$f ($k)"
> +}
> +
> +# Tests (check args: key hash-algo signed-file
> +for a in md5 sha1 sha224 sha256 sha384 sha512 rmd160; do
> +  f=$a.txt
> +  pos check test-rsa1024.cer $a $f
> +  pos check /dev/zero,test-rsa1024.cer $a $f
> +  pos check /dev/null,test-rsa1024.cer $a $f
> +  pos check test-rsa1024.cer,test-rsa2048.cer $a $f
> +  pos check test-rsa2048.cer,test-rsa1024.cer $a $f
> +  pos check ,,test-rsa1024.cer $a $f
> +  pos check test-rsa1024.cer,,, $a $f
> +  neg check test-rsa2048.cer $a $f
> +  neg check /dev/absent $a $f
> +  neg check /dev/null $a $f
> +  neg check /dev/zero $a $f
> +done
> +
> +_enable_gost_engine
> +wk=test-gost2012_512-B.cer # first wrong key
> +for k in gost2012_256-A gost2012_256-B gost2012_256-C \
> +         gost2012_512-A gost2012_512-B; do
> +  tmp=${k#gost2012_}
> +  bits=${tmp%-?}
> +  wbits=$(( $bits == 256 ? 512 : 256 ))
> +  curve=${k#*-}
> +  a=md_gost12_$bits   # proper hash algo
> +  f=$a${curve,,}.txt  # file signed in ima_sign test
> +  k=test-$k.cer
> +  wwk=test-gost2012_$wbits-$curve.cer # key with wrong bit length
> +
> +  pos check $k     $a $f
> +  pos check $wk,$k $a $f
> +  pos check $k,$wk $a $f
> +  neg check $wk    $a $f
> +  neg check $wwk   $a $f
> +  wk=$k # previous key is always wrong
> +done
> +
> +_report_exit
> -- 
> 2.11.0
> 

The test cases looks pretty straightforward to me, although some helper
functions took me some time to digest :-).

I would say that's a nice and good start for testing the package.
Tested in my local build and worked fine with no errors whatsoever.

I don't think the minor nit in the comment at Makefile.am I pointed
should stop it from being pulled, thus:

Reviewed-by: Bruno E. O. Meneguele <bmeneg@redhat.com>

Thanks.
Vitaly Chikunov July 25, 2019, 10:31 p.m. UTC | #4
Bruno,

On Thu, Jul 25, 2019 at 06:58:43PM -0300, Bruno E. O. Meneguele wrote:
> On Thu, Jul 25, 2019 at 09:18:55AM +0300, Vitaly Chikunov wrote:
> > Run `make check' to execute the tests.
> > Currently only ima_hash, ima_sign (v2), and ima_verify are tested.
> > 
> > Signed-off-by: Vitaly Chikunov <vt@altlinux.org>
> > ---
> >  Makefile.am           |   2 +-
> >  configure.ac          |   1 +
> >  tests/Makefile.am     |  14 ++++
> >  tests/functions       | 135 ++++++++++++++++++++++++++++++++++++
> >  tests/gen-keys.sh     |  75 ++++++++++++++++++++
> >  tests/ima_hash.test   |  84 +++++++++++++++++++++++
> >  tests/ima_sign.test   | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++
> >  tests/ima_verify.test |  79 +++++++++++++++++++++
> >  8 files changed, 575 insertions(+), 1 deletion(-)
> >  create mode 100644 tests/Makefile.am
> >  create mode 100755 tests/functions
> >  create mode 100755 tests/gen-keys.sh
> >  create mode 100755 tests/ima_hash.test
> >  create mode 100755 tests/ima_sign.test
> >  create mode 100755 tests/ima_verify.test
> > 
> > diff --git a/tests/Makefile.am b/tests/Makefile.am
> > new file mode 100644
> > index 0000000..8c6256c
> > --- /dev/null
> > +++ b/tests/Makefile.am
> > @@ -0,0 +1,14 @@
> > +check_SCRIPTS =
> > +TESTS = $(check_SCRIPTS)
> > +
> > +check_SCRIPTS += ima_hash.test ima_verify.test ima_sign.test
> > +
> > +# ima_verify depends on results of ima_hash
> > +ima_verify.log: ima_sign.log
> 
> Small nit: comment and code don't match, ima_hash vs ima_sign :). 
> 
> Maybe:
> 
> -# ima_verify depends on results of ima_hash
> +# ima_verify depends on results of ima_sign

Oh, yes. Thanks!
Mimi Zohar July 26, 2019, 1:03 a.m. UTC | #5
On Thu, 2019-07-25 at 18:38 +0300, Vitaly Chikunov wrote:
> Mimi,
> 
> On Thu, Jul 25, 2019 at 10:46:31AM -0400, Mimi Zohar wrote:
> > On Thu, 2019-07-25 at 09:18 +0300, Vitaly Chikunov wrote:
> > > Run `make check' to execute the tests.
> > > Currently only ima_hash, ima_sign (v2), and ima_verify are tested.
> > > 
> > > Signed-off-by: Vitaly Chikunov <vt@altlinux.org>
> > 
> > Nice!  As much as I would like to include this patch in this release,
> > let's hold off and add it to the next release.
> 
> You may include it if you wish, this should work good as is (over my
> latest patches).

Yes, it applies cleanly on top of the other patches.
> 
> > Reviewing shorter patches is a lot easier, at least for me.  Could you
> > break this patch up?  Perhaps by defining the tests separately, and
> > then adding the autotools support to run the test afterwards?
> 
> This is just tests, so they don't alter any other code, don't produce
> user visible features, and don't complicate anything. Each file inside
> of the patch could be understood separately.

I understand that.  However, without comments, with short function
names, and single letter variable names, makes reviewing the code more
difficult than needed.

Mimi
Petr Vorel July 26, 2019, 1:07 p.m. UTC | #6
Hi Vitaly,

generally LGTM, nice work!
Reviewed-by: Petr Vorel <pvorel@suse.cz>

I'd be also for splitting the test into more commits (if possible).

Other suggestions:
* rename function names and variables to be more understandable
(e.g. a=$1 p=$2 h=$3 f=$4 isn't much readable).
* there should be some records in .gitignore
* rename tests/functions to tests/functions.sh
* I'd define exit codes (77, 99, ...) as variables
* Do you plan to use XFAIL:, XPASS: and ERROR: ? IMHO these are redundant.
* posix compatible shell code (does really arrays and 'declare -i'
and other bashisms needed?), but that'd take some effort, so it's up to you and Mimi.
IMHO posix shell syntax is easier to read.


Kind regards,
Petr
Vitaly Chikunov July 28, 2019, 4:53 a.m. UTC | #7
Petr, Bruno,

Thanks for reviews! I think I will rework the tests one more time. So
that they can support EVM signatures too w/o too much code duplication.

Thanks,

Patch
diff mbox series

diff --git a/Makefile.am b/Makefile.am
index dba408d..45c6f82 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,4 +1,4 @@ 
-SUBDIRS = src
+SUBDIRS = src tests
 dist_man_MANS = evmctl.1
 
 doc_DATA =  examples/ima-genkey-self.sh examples/ima-genkey.sh examples/ima-gen-local-ca.sh
diff --git a/configure.ac b/configure.ac
index 3fc63b3..dccdd92 100644
--- a/configure.ac
+++ b/configure.ac
@@ -67,6 +67,7 @@  EVMCTL_MANPAGE_DOCBOOK_XSL
 
 AC_CONFIG_FILES([Makefile
 		src/Makefile
+		tests/Makefile
 		packaging/ima-evm-utils.spec
 		])
 AC_OUTPUT
diff --git a/tests/Makefile.am b/tests/Makefile.am
new file mode 100644
index 0000000..8c6256c
--- /dev/null
+++ b/tests/Makefile.am
@@ -0,0 +1,14 @@ 
+check_SCRIPTS =
+TESTS = $(check_SCRIPTS)
+
+check_SCRIPTS += ima_hash.test ima_verify.test ima_sign.test
+
+# ima_verify depends on results of ima_hash
+ima_verify.log: ima_sign.log
+
+clean-local:
+	-rm -f *.txt *.out *.sig *.sig2
+distclean: distclean-keys
+.PHONY: distclean-keys
+distclean-keys:
+	-rm -f *.pub *.key *.cer
diff --git a/tests/functions b/tests/functions
new file mode 100755
index 0000000..7ad6c7c
--- /dev/null
+++ b/tests/functions
@@ -0,0 +1,135 @@ 
+#!/bin/bash
+#
+# ima-evm-utils tests bash functions
+#
+# Copyright (C) 2019 Vitaly Chikunov <vt@altlinux.org>
+#
+# This program 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 2, 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.
+
+declare -i pass=0 fail=0 skip=0
+
+_require() {
+  RET=
+  for i; do
+    if ! type $i; then
+      echo "$i is required for test"
+      RET=1
+    fi
+  done
+  [ $RET ] && exit 99
+}
+
+if tty -s; then
+     RED=$'\e[1;31m'
+  YELLOW=$'\e[1;33m'
+    BLUE=$'\e[1;34m'
+    NORM=$'\e[m'
+fi
+
+# Define EXITEARLY to exit testing on the first error.
+exit_early() {
+  if [ $EXITEARLY ]; then
+    exit $1
+  fi
+}
+
+# Defaults
+MODE=+
+HOWFAILED=
+pos() {
+  MODE=+
+  HOWFAILED=
+  eval "$@"
+  case $? in
+    0)  pass+=1 ;;
+    77) skip+=1 ;;
+    99) fail+=1; exit_early 1 ;;
+    *)  fail+=1; exit_early 2 ;;
+  esac
+}
+
+neg() {
+  MODE=-
+  HOWFAILED=properly
+  eval "$@"
+  case $? in
+    0)  fail+=1; exit_early 3 ;;
+    77) skip+=1 ;;
+    99) fail+=1; exit_early 4 ;;
+    *)  pass+=1 ;;
+  esac
+# Restore defaults
+  MODE=+
+  HOWFAILED=
+}
+
+_ispos() {
+  [ -z "$HOWFAILED" ]
+}
+
+_isneg() {
+  [ "$HOWFAILED" ]
+}
+
+red_if_pos() {
+  _ispos && echo $@ $RED
+}
+
+norm_if_pos() {
+  _ispos && echo $@ $NORM
+}
+
+_evmctl_catch() {
+  local ret=$1 out=$2 cmd=$3 for=$4 del=${@:5}
+
+  if [ $ret -gt 128 -a $ret -lt 255 ]; then
+    echo $RED
+    echo "evmctl $cmd failed hard with $ret for $for"
+    sed 's/^/  /' $out
+    echo $NORM
+    rm -f $out $del
+    return 99
+  elif [ $ret -gt 0 ]; then
+    red_if_pos
+    echo "evmctl $cmd failed" $HOWFAILED "with $ret for $for"
+    sed 's/^/  /' $out
+    norm_if_pos
+    rm -f $out $del
+    return 1
+  elif [ -n "$HOWFAILED" ]; then
+    echo $RED
+    echo "evmctl $cmd wrongly succeeded for $for"
+    sed 's/^/  /' $out
+    echo $NORM
+  fi
+  rm -f $out
+  return 0
+}
+
+_enable_gost_engine() {
+  # Do not enable if it already enabled by user
+  if ! openssl md_gost12_256 /dev/null >/dev/null 2>&1 \
+    && openssl engine gost >/dev/null 2>&1; then
+    ENGINE=gost
+  fi
+}
+
+_report_exit() {
+  echo "PASS: $pass SKIP: $skip FAIL: $fail"
+  if [ $fail -gt 0 ]; then
+    exit 1
+  elif [ $pass -gt 0 ]; then
+    exit 0
+  else
+    exit 77
+  fi
+}
+
diff --git a/tests/gen-keys.sh b/tests/gen-keys.sh
new file mode 100755
index 0000000..c4073a2
--- /dev/null
+++ b/tests/gen-keys.sh
@@ -0,0 +1,75 @@ 
+#!/bin/bash
+#
+# Generate keys for the tests
+#
+# Copyright (C) 2019 Vitaly Chikunov <vt@altlinux.org>
+#
+# This program 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 2, 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.
+
+cd $(dirname $0)
+PATH=../src:$PATH
+type openssl
+
+log() {
+  echo - "$*"
+  eval "$@"
+}
+
+if [ ! -e test-ca.conf ]; then
+cat > test-ca.conf <<- EOF
+	[ req ]
+	distinguished_name = req_distinguished_name
+	prompt = no
+	string_mask = utf8only
+	x509_extensions = v3_ca
+
+	[ req_distinguished_name ]
+	O = IMA-CA
+	CN = IMA/EVM certificate signing key
+	emailAddress = ca@ima-ca
+
+	[ v3_ca ]
+	basicConstraints=CA:TRUE
+	subjectKeyIdentifier=hash
+	authorityKeyIdentifier=keyid:always,issuer
+EOF
+fi
+
+# RSA
+# Second key will be used for wrong key tests.
+for m in 1024 2048; do
+  if [ ! -e test-rsa$m.key ]; then
+    log openssl req -verbose -new -nodes -utf8 -sha1 -days 10000 -batch -x509 \
+      -config test-ca.conf \
+      -newkey rsa:$m \
+      -out test-rsa$m.cer -outform DER \
+      -keyout test-rsa$m.key
+  fi
+done
+
+# EC-RDSA
+for m in \
+  gost2012_256:A \
+  gost2012_256:B \
+  gost2012_256:C \
+  gost2012_512:A \
+  gost2012_512:B; do
+    IFS=':' read -r algo param <<< "$m"
+    [ -e test-$algo-$param.key ] && continue
+    log openssl req -nodes -x509 -utf8 -days 10000 -batch \
+      -config test-ca.conf \
+      -newkey $algo \
+      -pkeyopt paramset:$param \
+      -out    test-$algo-$param.cer -outform DER \
+      -keyout test-$algo-$param.key
+    log openssl pkey -in test-$algo-$param.key -out test-$algo-$param.pub -pubout
+done
+
diff --git a/tests/ima_hash.test b/tests/ima_hash.test
new file mode 100755
index 0000000..cf4af57
--- /dev/null
+++ b/tests/ima_hash.test
@@ -0,0 +1,84 @@ 
+#!/bin/bash
+#
+# evmctl ima_hash tests
+#
+# Copyright (C) 2019 Vitaly Chikunov <vt@altlinux.org>
+#
+# This program 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 2, 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.
+
+cd $(dirname $0)
+PATH=../src:$PATH
+source ./functions
+_require evmctl openssl
+
+# Check with constant
+check_const() {
+  local a=$1 p=$2 h=$3 f=$4
+
+  cmd=(evmctl ima_hash -v ${ENGINE:+--engine $ENGINE} -a $a --xattr-user $f)
+  echo $YELLOW$MODE "${cmd[@]}" $NORM
+  eval "${cmd[@]}" >$a.out 2>&1
+  _evmctl_catch $? $a.out ima_hash $a $f || return
+
+  if ! getfattr -n user.ima -e hex $f | grep -qx user.ima=$p$h; then
+    red_if_pos
+    echo "Did not find expected hash value for $a:"
+    echo "    user.ima=$p$h"
+    echo ""
+    echo "Actual output below:"
+    getfattr -n user.ima -e hex $f | sed 's/^/    /'
+    norm_if_pos
+    rm -f $f
+    return 1
+  fi
+  rm -f $f
+  return 0
+}
+
+check() {
+  local a=$1 p=$2 h=$3
+  local f=$a-hash.txt
+
+  rm -f $f
+  touch $f
+  cmd=(openssl dgst ${ENGINE:+--engine $ENGINE} -$a $f)
+  echo $BLUE - "${cmd[@]}" $NORM
+  h=$(set -o pipefail; eval "${cmd[@]}" 2>/dev/null | cut -d' ' -f2)
+  if [ $? -ne 0 -a -z "$HOWFAILED" ]; then
+    echo "$a test is skipped"
+    rm -f $f
+    return 77
+  fi
+  check_const $a $p "$h" $f
+}
+
+# check args: algo prefix hex-hash
+pos check md4    0x01
+pos check md5    0x01
+pos check sha1   0x01
+neg check SHA1   0x01 # uppercase
+neg check sha512-224 0x01 # valid for pkcs1
+neg check sha512-256 0x01 # valid for pkcs1
+neg check unknown 0x01 # nonexistent
+pos check sha224 0x0407
+pos check sha256 0x0404
+pos check sha384 0x0405
+pos check sha512 0x0406
+pos check rmd160 0x0403
+neg check sm3     0x01
+neg check sm3-256 0x01
+_enable_gost_engine
+pos check md_gost12_256 0x0412
+pos check streebog256   0x0412
+pos check md_gost12_512 0x0413
+pos check streebog512   0x0413
+
+_report_exit
diff --git a/tests/ima_sign.test b/tests/ima_sign.test
new file mode 100755
index 0000000..5d8edd5
--- /dev/null
+++ b/tests/ima_sign.test
@@ -0,0 +1,186 @@ 
+#!/bin/bash
+#
+# evmctl ima_sign tests
+#
+# Copyright (C) 2019 Vitaly Chikunov <vt@altlinux.org>
+#
+# This program 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 2, 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.
+
+cd $(dirname $0)
+PATH=../src:$PATH
+source ./functions
+_require evmctl openssl xxd getfattr
+./gen-keys.sh >/dev/null 2>&1
+
+# Determine keyid from .cer
+keyid() {
+  id=$(openssl x509 ${ENGINE:+-engine $ENGINE} \
+      -in $1.cer -inform DER -pubkey -noout 2>/dev/null \
+    | openssl asn1parse \
+    | grep BIT.STRING \
+    | cut -d: -f1)
+  if [ -z "$id" ]; then
+    echo "Cannot asn1parse $1.cer" >&2
+    exit 1
+  fi
+  openssl x509 ${ENGINE:+-engine $ENGINE} \
+      -in $1.cer -inform DER -pubkey -noout 2>/dev/null \
+    | openssl asn1parse -strparse $id -out - -noout \
+    | openssl dgst -c -sha1 \
+    | cut -d' ' -f2 \
+    | grep -o ":..:..:..:..$" \
+    | tr -d :
+}
+
+_ima_sign() {
+  local k=$1 a=$2 f=$3
+
+  cmd=(evmctl ima_sign ${ENGINE:+--engine $ENGINE} -a $a \
+	   -k $k.key --xattr-user --sigfile $f)
+  echo $YELLOW$MODE ${cmd[@]} $NORM
+  eval "${cmd[@]}" >$a.out 2>&1
+  _evmctl_catch $? $a.out ima_sign "$a ($k.key)" $f || return
+
+  # Check that detached signature matches xattr signature
+  if [ ! -e $f.sig ]; then
+    echo "evmctl ima_sign: no detached signature $f.sig"
+    return 1
+  fi
+
+  getfattr -n user.ima --only-values $f > $f.sig2
+  if ! cmp -bl $f.sig $f.sig2; then
+    echo "evmctl ima_sign: xattr signature differ from detached $f.sig"
+    rm -f $f.sig $f.sig2
+    return 1
+  fi
+
+  rm -f $f.sig $f.sig2
+}
+
+check_rsa() {
+  local k=test-rsa1024 a=$1 p=$2
+  local f=$a.txt
+
+  # Append suffix to files for negative tests, because we need
+  # to leave only good files for ima_verify.test
+  _isneg && f+=-
+  rm -f $f
+
+  keyid=$(keyid $k)
+  if [ $? -ne 0 ]; then
+    echo "Unable to determine keyid for $k"
+    return 99
+  fi
+  p=$(echo $p | sed "s/K/$keyid/")
+
+  if ! openssl dgst -$a /dev/null >/dev/null 2>&1; then
+    echo "$a ($k.key) test is skipped (openssl cannot handle $a digest)"
+    return 77
+  fi
+  touch $f
+  cmd=(openssl dgst -$a -sign $k.key -hex $f)
+  echo $BLUE - "${cmd[@]}" $NORM
+  sig=$(set -o pipefail; eval "${cmd[@]}" 2>/dev/null | cut -d' ' -f2)
+  if [ $? -ne 0 ] && _ispos; then
+    echo "$a ($k.key) test is skipped (openssl cannot sign with $a+$k.key)"
+    rm -f $f
+    return 77
+  fi
+
+  _ima_sign $k $a $f || return
+  if ! getfattr -n user.ima -e hex $f | grep -qx user.ima=$p$sig; then
+    red_if_pos
+    echo "Did not find expected hash value for $a:"
+    echo "    user.ima=$p$sig"
+    echo ""
+    echo "Actual output below:"
+    getfattr -n user.ima -e hex $f | sed 's/^/    /'
+    norm_if_pos
+    rm -f $f
+    return 1
+  fi
+  return 0
+}
+
+check_ecrdsa() {
+  local k=$1 a=$2 p=$3
+
+  # Sign different files not only depending on a hash algo,
+  # but also on a key. Append curve letter to the hash algo.
+  curve=${k##*-}
+  f=$a${curve,,}.txt
+
+  # Append suffix to files for negative tests, because we need
+  # to leave only good files for ima_verify.test
+  _isneg && f+=-
+  rm -f $f
+
+  # Older openssl unable to parse 512-bit keys
+  if ! openssl pkey ${ENGINE:+-engine $ENGINE} -in $k.key >/dev/null 2>&1; then
+    echo "$a ($k.key) test is skipped"
+    return 77
+  fi
+
+  keyid=$(keyid $k)
+  if [ $? -ne 0 ]; then
+    echo "Unable to determine keyid for $k"
+    return 99
+  fi
+  p=$(echo $p | sed "s/K/$keyid/")
+
+  touch $f
+  _ima_sign $k $a $f || return
+  # Only verify prefix here.
+  if ! getfattr -n user.ima -e hex $f | grep -q ^user.ima=$p; then
+    red_if_pos
+    echo "Signature prefix does not match for $a ($k):"
+    echo "Expected:  user.ima=$p..."
+    echo ""
+    echo "Actual output below:"
+    getfattr -n user.ima -e hex $f | sed 's/^/    /'
+    norm_if_pos
+    rm -f $f
+    return 1
+  fi
+
+  # Extract signature from xattr
+  getfattr -n user.ima -e hex $f \
+    | grep ^user.ima= \
+    | sed s/^user.ima=$p// \
+    | xxd -r -p > $a.sig2
+
+  # Verify with openssl
+  if ! openssl dgst ${ENGINE:+-engine $ENGINE} -$a \
+	-verify $k.pub -signature $a.sig2 $f >/dev/null 2>&1; then
+    return 1
+  fi
+  rm $a.sig2
+}
+
+# check args: algo prefix hex-signature-prefix (K in place of keyid.)
+pos check_rsa md5    0x030201K0080
+pos check_rsa sha1   0x030202K0080
+pos check_rsa sha224 0x030207K0080
+pos check_rsa sha256 0x030204K0080
+pos check_rsa sha384 0x030205K0080
+pos check_rsa sha512 0x030206K0080
+pos check_rsa rmd160 0x030203K0080
+neg check_rsa invalid-hash-algo  0x030202K0080
+_enable_gost_engine
+pos check_ecrdsa test-gost2012_256-A md_gost12_256 0x030212K0040
+pos check_ecrdsa test-gost2012_256-B md_gost12_256 0x030212K0040
+pos check_ecrdsa test-gost2012_256-C md_gost12_256 0x030212K0040
+pos check_ecrdsa test-gost2012_512-A md_gost12_512 0x030213K0080
+pos check_ecrdsa test-gost2012_512-B md_gost12_512 0x030213K0080
+neg check_ecrdsa test-gost2012_256-A md_gost12_512 0x030212K0040
+neg check_ecrdsa test-gost2012_512-A md_gost12_256 0x030212K0040
+
+_report_exit
diff --git a/tests/ima_verify.test b/tests/ima_verify.test
new file mode 100755
index 0000000..6a9c876
--- /dev/null
+++ b/tests/ima_verify.test
@@ -0,0 +1,79 @@ 
+#!/bin/bash
+#
+# evmctl ima_verify tests
+#
+# Copyright (C) 2019 Vitaly Chikunov <vt@altlinux.org>
+#
+# This program 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 2, 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.
+
+cd $(dirname $0)
+PATH=../src:$PATH
+source ./functions
+_require evmctl openssl
+
+# Check with constant
+check() {
+  local k=$1 a=$2 f=$3
+
+  if [ ! -e $f ]; then
+    echo "Signed file $f is not found. Skipping verify $a ($k) test."
+    return 77
+  fi
+  if ! openssl dgst ${ENGINE:+--engine $ENGINE} -$a /dev/null >/dev/null 2>&1; then
+    echo "$a ($k.key) test is skipped (openssl does not support $a)"
+    rm -f $f
+    return 77
+  fi
+
+  cmd=(evmctl ima_verify ${ENGINE:+--engine $ENGINE} -v -k $k --xattr-user $f)
+  echo $YELLOW$MODE "${cmd[@]}" $NORM
+  eval "${cmd[@]}" >$f.out 2>&1
+  _evmctl_catch $? $f.out ima_verify "$f ($k)"
+}
+
+# Tests (check args: key hash-algo signed-file
+for a in md5 sha1 sha224 sha256 sha384 sha512 rmd160; do
+  f=$a.txt
+  pos check test-rsa1024.cer $a $f
+  pos check /dev/zero,test-rsa1024.cer $a $f
+  pos check /dev/null,test-rsa1024.cer $a $f
+  pos check test-rsa1024.cer,test-rsa2048.cer $a $f
+  pos check test-rsa2048.cer,test-rsa1024.cer $a $f
+  pos check ,,test-rsa1024.cer $a $f
+  pos check test-rsa1024.cer,,, $a $f
+  neg check test-rsa2048.cer $a $f
+  neg check /dev/absent $a $f
+  neg check /dev/null $a $f
+  neg check /dev/zero $a $f
+done
+
+_enable_gost_engine
+wk=test-gost2012_512-B.cer # first wrong key
+for k in gost2012_256-A gost2012_256-B gost2012_256-C \
+         gost2012_512-A gost2012_512-B; do
+  tmp=${k#gost2012_}
+  bits=${tmp%-?}
+  wbits=$(( $bits == 256 ? 512 : 256 ))
+  curve=${k#*-}
+  a=md_gost12_$bits   # proper hash algo
+  f=$a${curve,,}.txt  # file signed in ima_sign test
+  k=test-$k.cer
+  wwk=test-gost2012_$wbits-$curve.cer # key with wrong bit length
+
+  pos check $k     $a $f
+  pos check $wk,$k $a $f
+  pos check $k,$wk $a $f
+  neg check $wk    $a $f
+  neg check $wwk   $a $f
+  wk=$k # previous key is always wrong
+done
+
+_report_exit