#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# evmctl {,ima_}{sign,verify} tests
#
# Copyright (C) 2020 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")" || exit 1
PATH=../src:$PATH

# set the env SIGV1=1 to execute the signature v1 tests
SIGV1=${SIGV1:-0}

source ./functions.sh

_require cmp evmctl getfattr openssl xxd

if cmp -b 2>&1 | grep -q "invalid option"; then
	echo "cmp does not support -b (cmp from busybox?) Use cmp from diffutils"
	exit "$HARDFAIL"
fi

./gen-keys.sh >/dev/null 2>&1

trap _report_exit_and_cleanup EXIT
WORKDIR=$(mktemp -d)
set -f # disable globbing

# Determine keyid from a cert
_keyid_from_cert() {
  local cer=${1%.*}.cer cmd
  local tmp

  cer=test-${cer#test-}
  # shellcheck disable=SC2086
  cmd="openssl x509 $OPENSSL_ENGINE \
          -in $cer -inform DER -pubkey -noout"
  id=$($cmd 2>/dev/null \
    | openssl asn1parse \
    | grep BIT.STRING \
    | tail -n1 \
    | cut -d: -f1)
  if [ -z "$id" ]; then
    echo - "$cmd" >&2
    echo "Cannot asn1parse $cer to determine keyid" >&2
    exit 1
  fi
  tmp=$(mktemp)
  # shellcheck disable=SC2086
  openssl x509 $OPENSSL_ENGINE \
      -in "$cer" -inform DER -pubkey -noout 2>/dev/null \
    | openssl asn1parse -strparse "$id" -out "$tmp" -noout
  # shellcheck disable=SC2002
  cat "$tmp" \
    | openssl dgst -c -sha1 \
    | cut -d' ' -f2 \
    | grep -o ":..:..:..:..$" \
    | tr -d :
  rm -f "$tmp"
}

# Convert test $type into evmctl op prefix
_op() {
  if [ "$1" = ima ]; then
    echo ima_
  fi
}

# Convert test $type into xattr name
_xattr() {
  if [ "$1" = ima ]; then
    echo user.ima
  else
    echo user.evm
  fi
}

# Check that detached signature matches xattr signature
_test_sigfile() {
  local file=$1 attr=$2 file_sig=$3 file_sig2=$4

  if [ ! -e "$file_sig" ]; then
    color_red
    echo "evmctl ima_sign: no detached signature $file_sig"
    color_restore
    rm "$file"
    return "$FAIL"
  fi

  _extract_xattr "$file" "$attr" "$file_sig2"
  if ! cmp -bl "$file_sig" "$file_sig2"; then
    color_red
    echo "evmctl ima_sign: xattr signature on $file differ from detached $file_sig"
    color_restore
    rm "$file" "$file_sig" "$file_sig2"
    return "$FAIL"
  fi

  # Leave '$file_sig' for ima_verify --sigfile test.
  rm "$file_sig2"
}

# Run single sign command
_evmctl_sign() {
  local type=$1 key=$2 alg=$3 file=$4 opts=$5

  # Can check --sigfile for ima_sign
  [ "$type" = ima ] && opts+=" --sigfile"

  # shellcheck disable=SC2086
  ADD_TEXT_FOR="$alg ($key)" ADD_DEL=$file \
    _evmctl_run "$(_op "$type")sign" $opts \
    --hashalgo "$alg" --key "$key" --xattr-user "$file" || return

  if [ "$type" = ima ]; then
    _test_sigfile "$file" "$(_xattr "$type")" "$file.sig" "$file.sig2"
  fi
}

# Run and test {ima_,}sign operation
check_sign() {
  # Arguments are passed via global vars:
  # TYPE (ima or evm),
  # KEY,
  # ALG (hash algo),
  # PREFIX (signature header prefix in hex),
  # OPTS (additional options for evmctl),
  # FILE (working file to sign).
  local "$@"
  local key verifykey
  local FILE=${FILE:-$ALG.txt}

  # Normalize key filename if it's not a pkcs11 URI
  if [ ${KEY:0:7} != pkcs11: ]; then
    key=${KEY%.*}.key
    key=test-${key#test-}
  else
    key=${KEY}
  fi

  # Append suffix to files for negative tests, because we may
  # leave only good files for verify tests.
  _test_expected_to_fail && FILE+='~'

  rm -f $FILE
  if ! touch $FILE; then
    color_red
    echo "Can't create test file: $FILE"
    color_restore
    return "$HARDFAIL"
  fi

  if _test_expected_to_pass; then
    # Can openssl work with this digest?
    cmd="openssl dgst $OPENSSL_ENGINE $OPENSSL_KEYFORM -$ALG $FILE"
    echo - "$cmd"
    if ! $cmd >/dev/null; then
      echo "${CYAN}$ALG ($key) test is skipped (openssl is unable to digest)$NORM"
      return "$SKIP"
    fi

    if [ "${key:0:7}" != pkcs11: ] && [ ! -e "$key" ]; then
      echo "${CYAN}$ALG ($key) test is skipped (key file not found)$NORM"
      return "$SKIP"
    fi

    # Can openssl sign with this digest and key?
    cmd="openssl dgst $OPENSSL_ENGINE $OPENSSL_KEYFORM -$ALG -sign $key -hex $FILE"
    echo - "$cmd"
    if ! $cmd >/dev/null; then
      echo "${CYAN}$ALG ($key) test is skipped (openssl is unable to sign)$NORM"
      return "$SKIP"
    fi
  fi

  # Insert keyid from cert into PREFIX in-place of marker `:K:'
  if [[ $PREFIX =~ :K: ]]; then
    keyid=$(_keyid_from_cert "$key")
    if [ $? -ne 0 ]; then
      color_red
      echo "Unable to determine keyid for $key"
      color_restore
      return "$HARDFAIL"
    fi
    [ "$VERBOSE" -gt 2 ] && echo "  Expected keyid: $keyid"
    PREFIX=${PREFIX/:K:/$keyid}
  fi

  # Perform signing by evmctl
  _evmctl_sign "$TYPE" "$key" "$ALG" "$FILE" "$OPTS" || return

  # First simple pattern match the signature.
  ADD_TEXT_FOR=$ALG \
    _test_xattr "$FILE" "$(_xattr "$TYPE")" "$PREFIX.*" || return

  # This is all we can do for v1 signatures.
  [[ "$OPTS" =~ --rsa ]] && return "$OK"

  # This is all we can do for evm.
  [[ "$TYPE" =~ evm ]] && return "$OK"

  # When using the SM2/3 algorithm, the openssl tool uses USERID for verify,
  # which is incompatible with calling API directly, so skip it.
  [[ "$ALG" == sm3 ]] && return "$OK"

  # Extract signature to a file
  _extract_xattr "$FILE" "$(_xattr "$TYPE")" "$FILE.sig2" "$PREFIX"

  # Verify extracted signature with openssl
  if [ "${key:0:7}" != pkcs11: ]; then
      verifykey=${key%.*}.pub
  else
      verifykey=${key}
  fi

  cmd="openssl dgst $OPENSSL_ENGINE $OPENSSL_KEYFORM -$ALG -verify ${verifykey} \
	-signature $FILE.sig2 $FILE"
  echo - "$cmd"
  if ! $cmd; then
    color_red_on_failure
    echo "Signature v2 verification with openssl is failed."
    color_restore
    rm "$FILE.sig2"
    return "$FAIL"
  fi

  rm "$FILE.sig2"
  return "$OK"
}

# Test verify operation
check_verify() {
  # Arguments are passed via global vars:
  # TYPE (ima or evm),
  # KEY,
  # ALG (hash algo),
  # OPTS (additional options for evmctl),
  # FILE (filename to verify).
  local "$@"

  # shellcheck disable=SC2086
  if ! openssl dgst $OPENSSL_ENGINE -"$ALG" /dev/null >/dev/null 2>&1; then
    echo $CYAN"$ALG ($KEY) test is skipped (openssl does not support $ALG)"$NORM
    return $SKIP
  fi

  # shellcheck disable=SC2086
  ADD_TEXT_FOR="$FILE ($KEY)" \
    _evmctl_run "$(_op "$TYPE")verify" --key "$KEY" --xattr-user $OPTS "$FILE"
}

# Test runners

# Perform sign and verify ima and evm testing
sign_verify() {
  local key=$1 alg=$2 prefix="$3" opts="$4"
  local file=$alg.txt

  # Set defaults:
  # Public key is different for v1 and v2 (where x509 cert is used).
  if [[ $opts =~ --rsa ]]; then
    KEY=test-$key.pub
  else
    KEY=test-$key.cer
  fi
  ALG=$alg
  PREFIX=$prefix
  OPTS=$opts
  FILE=$file

  TYPE=ima
  if expect_pass check_sign; then

    # Normal verify with proper key should pass
    expect_pass check_verify
    expect_pass check_verify OPTS="--sigfile"

    # Multiple files and some don't verify
    expect_fail check_verify FILE="/dev/null $file"

    rm "$FILE.sig"
  fi

  TYPE=evm
  # Avoid running blkid for evm tests which may require root
  # No generation on overlayfs:
  # ioctl(3, FS_IOC_GETVERSION, 0x7ffd8e0bd628) = -1 ENOTTY (Inappropriate ioctl for device)
  OPTS="$opts --uuid --generation 0"
  if expect_pass check_sign; then

    # Normal verify with proper key
    expect_pass check_verify

    # Verify with wrong key
    expect_fail check_verify KEY=rsa2048
  fi

  # Note: Leaving TYPE=evm and file is evm signed
}

# Test --keys
try_different_keys() {
  # This run after sign_verify which leaves
  # TYPE=evm and file is evm signed

  # v2 signing can work with multiple keys in --key option
  if [[ ! $OPTS =~ --rsa ]]; then

    # Have correct key in the key list
    expect_pass check_verify KEY="test-rsa2048.cer,$KEY"
    expect_pass check_verify KEY="/dev/null,$KEY,"
  fi

  # Try key that is not used for signing
  expect_fail check_verify KEY=rsa2048

  # Try completely wrong key files
  expect_fail check_verify KEY=/dev/null
  expect_fail check_verify KEY=/dev/zero
}

try_different_sigs() {
  # TYPE=evm and file is evm signed

  # Test --imasig
  if expect_pass check_sign OPTS="$OPTS --imasig"; then

    # Verify both evm and ima sigs
    expect_pass check_verify
    expect_pass check_verify TYPE=ima
  fi

  # Test --imahash
  if expect_pass check_sign OPTS="$OPTS --imahash"; then

    expect_pass check_verify

    # IMA hash is not verifiable by ima_verify
    expect_fail check_verify TYPE=ima
  fi

  # Test --portable (only supported for V2 signatures)
  if expect_pass check_sign OPTS="$OPTS --portable --imahash" PREFIX=0x05; then
     if [[ "$OPTS" =~ --rsa ]]; then
        expect_fail check_verify
     else
        expect_pass check_verify
     fi
  fi

  # Test -i (immutable)
  expect_pass check_sign OPTS="$OPTS -i" PREFIX=0x0303
  # Cannot be verified for now
}

# Single test args: type key hash signature-prefix "evmctl-options"
# sign_verify args:      key hash signature-prefix "evmctl-options"
# Only single test can be prefixed with expect_{fail,pass}
# `sign_verify' can not be prefixed with expect_{fail,pass} because
# it runs multiple tests inside. See more tests there.
# signature-prefix can contain `:K:' which will be resolved to keyid (v2 only)

## Test v1 signatures
# Signature v1 only supports sha1 and sha256 so any other should fail
if [ $SIGV1 -eq 0 ]; then
  __skip() { echo "IMA signature v1 tests are skipped: not supported"; return $SKIP; }
  expect_pass __skip
else
   expect_fail \
      check_sign TYPE=ima KEY=rsa1024 ALG=md5 PREFIX=0x0301 OPTS=--rsa

   sign_verify  rsa1024  sha1    0x0301 --rsa
   sign_verify  rsa1024  sha256  0x0301 --rsa
      try_different_keys
      try_different_sigs
fi

## Test v2 signatures with RSA PKCS#1
# List of allowed hashes much greater but not all are supported.
sign_verify  rsa1024  md5     0x030201:K:0080
sign_verify  rsa1024  sha1    0x030202:K:0080
sign_verify  rsa1024  sha224  0x030207:K:0080
expect_pass check_sign TYPE=ima KEY=rsa1024 ALG=sha256 PREFIX=0x030204aabbccdd0080 OPTS=--keyid=aabbccdd
expect_pass check_sign TYPE=ima KEY=rsa1024 ALG=sha256 PREFIX=0x030204:K:0080 OPTS=--keyid-from-cert=test-rsa1024.cer
expect_pass check_sign TYPE=ima KEY=rsa1024_skid ALG=sha256 PREFIX=0x030204123456780080
sign_verify  rsa1024  sha256  0x030204:K:0080
  try_different_keys
  try_different_sigs
sign_verify  rsa1024  sha384  0x030205:K:0080
sign_verify  rsa1024  sha512  0x030206:K:0080
sign_verify  rsa1024  rmd160  0x030203:K:0080

# Test v2 signatures with ECDSA
# Signature length is typically 0x34-0x38 bytes long, very rarely 0x33
sign_verify  prime192v1 sha1   0x030202:K:003[345678]
sign_verify  prime192v1 sha224 0x030207:K:003[345678]
sign_verify  prime192v1 sha256 0x030204:K:003[345678]
sign_verify  prime192v1 sha384 0x030205:K:003[345678]
sign_verify  prime192v1 sha512 0x030206:K:003[345678]

# Signature length is typically 0x44-0x48 bytes long, very rarely 0x43
sign_verify  prime256v1 sha1   0x030202:K:004[345678]
sign_verify  prime256v1 sha224 0x030207:K:004[345678]
sign_verify  prime256v1 sha256 0x030204:K:004[345678]
sign_verify  prime256v1 sha384 0x030205:K:004[345678]
sign_verify  prime256v1 sha512 0x030206:K:004[345678]

# If openssl 3.0 is installed, test the SM2/3 algorithm combination
ssl_major_version=$(openssl version | sed -n 's/^OpenSSL \([^\.]\).*/\1/p')
if [ "${ssl_major_version}" = 3 ]; then
   sign_verify  sm2    sm3    0x030211:K:004[345678]
else
	__skip() { echo "sm2/sm3 tests are skipped (ssl version)"; return $SKIP; }
  expect_pass __skip
fi

# Test v2 signatures with EC-RDSA
_enable_gost_engine
sign_verify  gost2012_256-A md_gost12_256 0x030212:K:0040
sign_verify  gost2012_256-B md_gost12_256 0x030212:K:0040
sign_verify  gost2012_256-C md_gost12_256 0x030212:K:0040
sign_verify  gost2012_512-A md_gost12_512 0x030213:K:0080
sign_verify  gost2012_512-B md_gost12_512 0x030213:K:0080
# Test if signing with wrong key length does not work.
expect_fail \
  check_sign TYPE=ima KEY=gost2012_512-B ALG=md_gost12_256 PREFIX=0x0302 OPTS=
expect_fail \
  check_sign TYPE=ima KEY=gost2012_256-B ALG=md_gost12_512 PREFIX=0x0302 OPTS=

# Test signing with key described by pkcs11 URI
_softhsm_setup "${WORKDIR}"
if [ -n "${PKCS11_KEYURI}" ]; then
  expect_pass check_sign FILE=pkcs11test TYPE=ima KEY=${PKCS11_KEYURI} ALG=sha256 PREFIX=0x030204aabbccdd0100 OPTS=--keyid=aabbccdd
  expect_pass check_sign FILE=pkcs11test TYPE=ima KEY=${PKCS11_KEYURI} ALG=sha1   PREFIX=0x030202aabbccdd0100 OPTS=--keyid=aabbccdd
else
  # to have a constant number of tests, skip these two tests
  __skip() { echo "pkcs11 test is skipped: could not setup softhsm"; return $SKIP; }
  expect_pass __skip
  expect_pass __skip
fi
_softhsm_teardown "${WORKDIR}"
