#!/bin/bash

# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# Abort on error.
set -e

# Load common constants and variables.
. "$(dirname "$0")/common.sh"

# Given a kernel boot param string which includes ...dm="dmstuff"...
# this returns the dmstuff by itself.
get_dmparams() {
    echo "$1" | sed 's/^.*\ dm="\([^"]*\)".*/\1/'
}

# Given a kernel boot param string which includes ...dm="stuff"...
# this returns the param string with the dm="..." section removed.
# Useful in conjunction with get_dmparams to divide and process
# the two sections of parameters in seperate passes
kparams_remove_dm() {
    echo "$1" | sed 's/dm="[^"]*"//'
}

# Given a dm param string which includes dynamic values, return the
# same string with these values replaced by a magic string placeholder.
# This same magic placeholder is used in the config file, for comparison
# purposes.
dmparams_mangle() {
  local dmparams=$1
  # First handle new key-value style verity parameters.
  dmparams=$(echo "$dmparams" |
    sed -e 's/root_hexdigest=[0-9a-fA-F]*/root_hexdigest=MAGIC_HASH/' |
    sed -e 's/salt=[0-9a-fA-F]*/salt=MAGIC_SALT'/)
  # If we didn't substitute the MAGIC_HASH yet, these are the old
  # verity parameter format.
  if [[ $dmparams != *MAGIC_HASH* ]]; then
    dmparams=$(echo $dmparams | sed 's/sha1 [0-9a-fA-F]*/sha1 MAGIC_HASH/')
  fi
  # If we have bootcache enabled, replace its copy of the root_hexdigest
  # with MAGIC_HASH. The parameter is positional.
  if [[ $dmparams == *bootcache* ]]; then
    dmparams=$(echo $dmparams |
      sed -r 's:(bootcache (PARTUUID=)?%U(/PARTNROFF=|\+)1 [0-9]+) [0-9a-fA-F]+:\1 MAGIC_HASH:')
  fi
  echo $dmparams
}

# This escapes any non-alphanum character, since many such characters
# are regex metacharacters.
escape_regexmetas() {
    echo "$1" | sed 's/\([^a-zA-Z0-9]\)/\\\1/g'
}

usage() {
    echo "Usage $PROG image [config]"
}

main() {
    # We want to catch all the discrepancies, not just the first one.
    # So, any time we find one, we set testfail=1 and continue.
    # When finished we will use testfail to determine our exit value.
    local testfail=0
    # A buffer to include useful information that we dump when things fail.
    local output
    # Copy of a string before it has been through sed
    local pre_sed
    # Where the disk image is mounted.
    local loopdev

    if [[ $# -ne 1 ]] && [[ $# -ne 2 ]]; then
        usage
        exit 1
    fi

    local image="$1"

    # A byte that should not appear in the command line to use as a sed
    # marker when doing regular expression replacements.
    local M=$'\001'

    # Default config location: same name/directory as this script,
    # with a .config file extension, ie ensure_secure_kernelparams.config.
    local configfile="$(dirname "$0")/${0/%.sh/.config}"
    # Or, maybe a config was provided on the command line.
    if [[ $# -eq 2 ]]; then
        configfile="$2"
    fi
    # Either way, load test-expectations data from config.
    . "$configfile" || return 1

    # Set up the image on a loopback device so it's faster to access.
    local loopdev
    loopdev=$(loopback_partscan "${image}")

    # TODO(jimhebert): Perform the kernel security tests on both the kernel
    #                  partitions. Here, we just run it on kernel partition 4
    #                  which is the install kernel on the recovery image.
    #                  crosbug.com/24274
    local loop_kern="${loopdev}p4"
    local loop_rootfs="${loopdev}p3"
    local rootfs=$(make_temp_dir)
    sudo mount -o ro "${loop_rootfs}" "${rootfs}"

    # Pick the right set of test-expectation data to use.
    local boardvar=$(get_boardvar_from_lsb_release "${rootfs}")
    eval "required_kparams=(\"\${required_kparams_${boardvar}[@]}\")"
    eval "required_kparams_regex=(\"\${required_kparams_regex_${boardvar}[@]}\")"
    eval "optional_kparams=(\"\${optional_kparams_${boardvar}[@]}\")"
    eval "optional_kparams_regex=(\"\${optional_kparams_regex_${boardvar}[@]}\")"
    eval "required_dmparams=(\"\${required_dmparams_${boardvar}[@]}\")"
    eval "required_dmparams_regex=(\"\${required_dmparams_regex_${boardvar}[@]}\")"
    output+="required_kparams=(\n"
    output+="$(printf "\t'%s'\n" "${required_kparams[@]}")\n)\n"
    output+="required_kparams_regex=(\n"
    output+="$(printf "\t'%s'\n" "${required_kparams_regex[@]}")\n)\n"
    output+="optional_kparams=(\n"
    output+="$(printf "\t'%s'\n" "${optional_kparams[@]}")\n)\n"
    output+="optional_kparams_regex=(\n"
    output+="$(printf "\t'%s'\n" "${optional_kparams_regex[@]}")\n)\n"
    output+="required_dmparams=(\n"
    output+="$(printf "\t'%s'\n" "${required_dmparams[@]}")\n)\n"
    output+="required_dmparams_regex=(\n"
    output+="$(printf "\t'%s'\n" "${required_dmparams_regex[@]}")\n)\n"

    # Divide the dm params from the rest and process seperately.
    local kparams=$(sudo dump_kernel_config "${loop_kern}")
    local dmparams=$(get_dmparams "$kparams")
    local kparams_nodm=$(kparams_remove_dm "$kparams")

    output+="\nkparams='${kparams}'\n"
    output+="\ndmparams='${dmparams}'\n"
    output+="\nkparams_nodm='${kparams_nodm}'\n"

    mangled_dmparams=$(dmparams_mangle "${dmparams}")
    output+="\nmangled_dmparams='${mangled_dmparams}'\n"
    # Special-case handling of the dm= param:
    testfail=1
    for expected_dmparams in "${required_dmparams[@]}"; do
      # Filter out all dynamic parameters.
      if [ "$mangled_dmparams" = "$expected_dmparams" ]; then
        testfail=0
        break
      fi
    done

    for expected_dmparams in "${required_dmparams_regex[@]}"; do
      if [[ -z $(echo "${mangled_dmparams}" | \
           sed "s${M}^${expected_dmparams}\$${M}${M}") ]]; then
        testfail=0
        break
      fi
    done

    if [ $testfail -eq 1 ]; then
        echo "Kernel dm= parameter does not match any expected values!"
        echo "Actual:   $dmparams"
        echo "Expected: ${required_dmparams[*]}"
        echo "Expected (regex): ${required_dmparams_regex[*]}"
    fi

    # Ensure all other required params are present.
    for param in "${required_kparams[@]}"; do
        if [[ "$kparams_nodm" != *$param* ]]; then
            echo "Kernel parameters missing required value: $param"
            testfail=1
        else
            # Remove matched params as we go. If all goes well, kparams_nodm
            # will be nothing left but whitespace by the end.
            param=$(escape_regexmetas "$param")
            kparams_nodm=$(echo " ${kparams_nodm} " |
                           sed "s${M} ${param} ${M} ${M}")
        fi
    done

    # Ensure all other required regex params are present.
    for param in "${required_kparams_regex[@]}"; do
      pre_sed=" ${kparams_nodm} "
      kparams_nodm=$(echo "${pre_sed}" | sed "s${M} ${param} ${M} ${M}")
      if [[ "${pre_sed}" == "${kparams_nodm}" ]]; then
        echo "Kernel parameters missing required value: ${param}"
        testfail=1
      fi
    done

    # Check-off each of the allowed-but-optional params that were present.
    for param in "${optional_kparams[@]}"; do
        param=$(escape_regexmetas "$param")
        kparams_nodm=$(echo " ${kparams_nodm} " |
                       sed "s${M} ${param} ${M} ${M}")
    done

    # Check-off each of the allowed-but-optional params that were present.
    for param in "${optional_kparams_regex[@]}"; do
        kparams_nodm=$(echo " ${kparams_nodm} " |
                       sed "s${M} ${param} ${M} ${M}")
    done

    # This section enforces the default-deny for any unexpected params
    # not already processed by one of the above loops.
    if [[ ! -z ${kparams_nodm// /} ]]; then
        echo "Unexpected kernel parameters found:"
        echo " $(echo "${kparams_nodm}" | sed -r 's:  +: :g')"
        testfail=1
    fi

    if [[ ${testfail} -eq 1 ]]; then
        echo "Debug output:"
        printf '%b\n' "${output}"
        echo "(actual error will be at the top of output)"
    fi

    exit $testfail
}

main $@
