#!/bin/bash
# Copyright 2011 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# Common key generation functions.

SCRIPT_DIR="$(dirname "$(readlink -f -- "$0")")"
PROG=$(basename "$0")
CROS_LOG_PREFIX="${PROG}: "

# Prints an informational message.
info() {
  echo "${CROS_LOG_PREFIX}INFO: $*" >&2
}

# Prints a warning message.
warn() {
  echo "${CROS_LOG_PREFIX}WARNING: $*" >&2
}

# Prints an error message.
error() {
  echo "${CROS_LOG_PREFIX}ERROR: $*" >&2
}

# Print an error message and then exit the script.
die() {
  error "$@"
  exit 1
}

# Algorithm ID mappings:
RSA1024_SHA1_ALGOID=0
RSA1024_SHA256_ALGOID=1
RSA1024_SHA512_ALGOID=2
RSA2048_SHA1_ALGOID=3
RSA2048_SHA256_ALGOID=4
RSA2048_SHA512_ALGOID=5
RSA4096_SHA1_ALGOID=6
RSA4096_SHA256_ALGOID=7
RSA4096_SHA512_ALGOID=8
RSA8192_SHA1_ALGOID=9
RSA8192_SHA256_ALGOID=10
RSA8192_SHA512_ALGOID=11
alg_to_keylen() {
  echo $(( 1 << (10 + ($1 / 3)) ))
}

# Default algorithms.
ROOT_KEY_ALGOID=${RSA4096_SHA512_ALGOID}
RECOVERY_KEY_ALGOID=${RSA4096_SHA512_ALGOID}

FIRMWARE_DATAKEY_ALGOID=${RSA4096_SHA256_ALGOID}
DEV_FIRMWARE_DATAKEY_ALGOID=${RSA4096_SHA256_ALGOID}

RECOVERY_KERNEL_ALGOID=${RSA4096_SHA512_ALGOID}
MINIOS_KERNEL_ALGOID=${RSA4096_SHA512_ALGOID}
INSTALLER_KERNEL_ALGOID=${RSA4096_SHA512_ALGOID}
KERNEL_SUBKEY_ALGOID=${RSA4096_SHA256_ALGOID}
KERNEL_DATAKEY_ALGOID=${RSA2048_SHA256_ALGOID}

# AP RO Verification.
ARV_ROOT_ALGOID=${RSA4096_SHA256_ALGOID}
ARV_PLATFORM_ALGOID=${RSA4096_SHA256_ALGOID}
ARV_ROOT_NAME_BASE="arv_root"
# Presumably the script is run from the top of the PreMP keys directory
# tree, place AP RO verification root key there.
ARV_ROOT_DIR="ApRoV1Signing-PreMP"

# Keyblock modes determine which boot modes a signing key is valid for use
# in verification.
#    !DEV 0x1      DEV 0x2
#    !REC 0x4      REC 0x8
# !MINIOS 0x10  MINIOS 0x20
# Note that firmware keyblock modes are not used.  Consider deprecating.

# Only allow RW firmware in non-recovery + non-miniOS.
FIRMWARE_KEYBLOCK_MODE=$((0x1 | 0x2 | 0x4 | 0x10))
# Only allow in dev mode + non-recovery + non-miniOS.
DEV_FIRMWARE_KEYBLOCK_MODE=$((0x2 | 0x4 | 0x10))
# Only allow in recovery mode + non-miniOS.
RECOVERY_KERNEL_KEYBLOCK_MODE=$((0x1 | 0x2 | 0x8 | 0x10))
# Only allow in recovery mode + miniOS.
MINIOS_KERNEL_KEYBLOCK_MODE=$((0x1 | 0x2 | 0x8 | 0x20))
# Only allow in non-recovery + non-miniOS.
KERNEL_KEYBLOCK_MODE=$((0x1 | 0x2 | 0x4 | 0x10))
# Only allow in dev + recovery + non-miniOS.
INSTALLER_KERNEL_KEYBLOCK_MODE=$((0x2 | 0x8 | 0x10))
# Only allow in non-recovery + non-miniOS, does not mean much for AP RO keys.
ARV_KEYBLOCK_MODE=$((0x1 | 0x2 | 0x4 | 0x10))


# Emit .vbpubk and .vbprivk using given basename and algorithm
# NOTE: This function also appears in ../../utility/dev_make_keypair. Making
# the two implementations the same would require some common.sh, which is more
# likely to cause problems than just keeping an eye out for any differences. If
# you feel the need to change this file, check the history of that other file
# to see what may need updating here too.
make_pair() {
  local base=$1
  local alg=$2
  local key_version=${3:-1}
  local len=$(alg_to_keylen $alg)

  echo "creating $base keypair (version = $key_version)..."

  # make the RSA keypair
  openssl genrsa -F4 -out "${base}_${len}.pem" $len
  # create a self-signed certificate
  openssl req -batch -new -x509 -key "${base}_${len}.pem" \
    -out "${base}_${len}.crt"
  # generate pre-processed RSA public key
  dumpRSAPublicKey -cert "${base}_${len}.crt" > "${base}_${len}.keyb"

  # wrap the public key
  vbutil_key \
    --pack "${base}.vbpubk" \
    --key "${base}_${len}.keyb" \
    --version  "${key_version}" \
    --algorithm $alg

  # wrap the private key
  vbutil_key \
    --pack "${base}.vbprivk" \
    --key "${base}_${len}.pem" \
    --algorithm $alg

  # remove intermediate files
  rm -f "${base}_${len}.pem" "${base}_${len}.crt" "${base}_${len}.keyb"
}

# Used to generate keys for signing update payloads.
make_au_payload_key() {
  local dir=$1
  local priv="${dir}/update_key.pem"
  local pub="${dir}/update-payload-key-pub.pem"
  openssl genrsa -out "${priv}" 2048
  openssl rsa -pubout -in "${priv}" -out "${pub}"
}

# Emit a .keyblock containing flags and a public key, signed by a private key
# flags are the bitwise OR of these (passed in decimal, though)
#   0x01  Developer switch off
#   0x02  Developer switch on
#   0x04  Not recovery mode
#   0x08  Recovery mode
#   0x10  Not miniOS mode
#   0x20  miniOS mode
make_keyblock() {
  local base=$1
  local flags=$2
  local pubkey=$3
  # (Local) path to the key we're using to sign the keyblock.
  # This is required, since the public key (as specified with --signpubkey)
  # must always be local.
  local signkey_path=$4
  # Remote URI to the key we're using to sign the keyblock.
  # Optional, if not set we'll look for the private key in signkey_path.
  local signkey_uri=$5

  local signkey_priv="${signkey_path}.vbprivk"
  # If the URI is set, the private key is remote.
  if [[ -n "${signkey_uri}" ]]; then
    signkey_priv="${signkey_uri}"
  fi

  echo "creating $base keyblock..."

  # create it
  vbutil_keyblock \
    --pack "${base}.keyblock" \
    --flags $flags \
    --datapubkey "${pubkey}.vbpubk" \
    --signprivate "${signkey_priv}"

  # verify it
  vbutil_keyblock \
    --unpack "${base}.keyblock" \
    --signpubkey "${signkey_path}.vbpubk"
}

# File to read current versions from.
VERSION_FILE="key.versions"

# ARGS: <VERSION_TYPE> [VERSION_FILE]
get_version() {
  local key="$1"
  local file="${2:-${VERSION_FILE}}"
  awk -F= -vkey="${key}" '$1 == key { print $NF }' "${file}"
}

# Loads the current versions prints them to stdout and sets the global version
# variables: CURR_FIRMKEY_VER CURR_FIRM_VER CURR_KERNKEY_VER CURR_KERN_VER
load_current_versions() {
  local key_dir=$1
  local VERSION_FILE="${key_dir}/${VERSION_FILE}"
  if [[ ! -f ${VERSION_FILE} ]]; then
    return 1
  fi
  CURR_FIRMKEY_VER=$(get_version "firmware_key_version")
  # Firmware version is the kernel subkey version.
  CURR_FIRM_VER=$(get_version "firmware_version")
  # Kernel data key version is the kernel key version.
  CURR_KERNKEY_VER=$(get_version "kernel_key_version")
  CURR_KERN_VER=$(get_version "kernel_version")

  cat <<EOF
Current Firmware key version: ${CURR_FIRMKEY_VER}
Current Firmware version: ${CURR_FIRM_VER}
Current Kernel key version: ${CURR_KERNKEY_VER}
Current Kernel version: ${CURR_KERN_VER}
EOF
}

# Make backups of existing kernel subkeys and keyblocks that will be revved.
# Backup format:
# for keyblocks: <keyblock_name>.v<datakey version>.v<subkey version>.keyblock
# Args: SUBKEY_VERSION DATAKEY_VERSION
backup_existing_kernel_keyblock() {
  if [[ ! -e kernel.keyblock ]]; then
    return
  fi
  mv --no-clobber kernel.{keyblock,"v$2.v$1.keyblock"}
}

# Make backups of existing kernel subkeys and keyblocks that will be revved.
# Backup format:
# for keys: <key_name>.v<version>.vb{pub|priv}k
# for keyblocks: <keyblock_name>.v<datakey version>.v<subkey version>.keyblock
# Args: SUBKEY_VERSION DATAKEY_VERSION
backup_existing_kernel_subkeys() {
  local subkey_ver=$1
  local datakey_ver=$2
  # --no-clobber to prevent accidentally overwriting existing
  # backups.
  mv --no-clobber kernel_subkey.{vbprivk,"v${subkey_ver}.vbprivk"}
  mv --no-clobber kernel_subkey.{vbpubk,"v${subkey_ver}.vbpubk"}
  backup_existing_kernel_keyblock ${subkey_ver} ${datakey_ver}
}

# Make backups of existing kernel data keys and keyblocks that will be revved.
# Backup format:
# for keys: <key_name>.v<version>.vb{pub|priv}k
# for keyblocks: <keyblock_name>.v<datakey version>.v<subkey version>.keyblock
# Args: SUBKEY_VERSION DATAKEY_VERSION
backup_existing_kernel_data_keys() {
  local subkey_ver=$1
  local datakey_ver=$2
  # --no-clobber to prevent accidentally overwriting existing
  # backups.
  mv --no-clobber kernel_data_key.{vbprivk,"v${datakey_ver}.vbprivk"}
  mv --no-clobber kernel_data_key.{vbpubk,"v${datakey_ver}.vbpubk"}
  backup_existing_kernel_keyblock ${subkey_ver} ${datakey_ver}
}

# Make backups of existing firmware keys and keyblocks that will be revved.
# Backup format:
# for keys: <key_name>.v<version>.vb{pub|priv}k
# for keyblocks: <keyblock_name>.v<datakey version>.v<subkey version>.keyblock
# Args: SUBKEY_VERSION DATAKEY_VERSION
backup_existing_firmware_keys() {
  local subkey_ver=$1
  local datakey_ver=$2
  mv --no-clobber firmware_data_key.{vbprivk,"v${subkey_ver}.vbprivk"}
  mv --no-clobber firmware_data_key.{vbpubk,"v${subkey_ver}.vbpubk"}
  mv --no-clobber firmware.{keyblock,"v${datakey_ver}.v${subkey_ver}.keyblock"}
}


# Write new key version file with the updated key versions.
# Args: FIRMWARE_KEY_VERSION FIRMWARE_VERSION KERNEL_KEY_VERSION
#       KERNEL_VERSION
write_updated_version_file() {
  local firmware_key_version=$1
  local firmware_version=$2
  local kernel_key_version=$3
  local kernel_version=$4

  cat > ${VERSION_FILE} <<EOF
firmware_key_version=${firmware_key_version}
firmware_version=${firmware_version}
kernel_key_version=${kernel_key_version}
kernel_version=${kernel_version}
EOF
}

# Returns the incremented version number of the passed in key from the version
# file.  The options are "firmware_key_version", "firmware_version",
# "kernel_key_version", or "kernel_version".
# ARGS: KEY_DIR <key_name>
increment_version() {
  local key_dir=$1
  local VERSION_FILE="${key_dir}/${VERSION_FILE}"
  local old_version=$(get_version $2)
  local new_version=$(( ${old_version} + 1 ))

  if [[ ${new_version} -gt 0xffff ]]; then
    echo "Version overflow!" >&2
    return 1
  fi
  echo ${new_version}
}

# Create a new ed25519 key pair given a base name. For example, if the
# base is "dir/foo", this will create "dir/foo.priv.pem" and
# "dir/foo.pub.pem".
# Args: BASE
generate_ed25519_key() {
  local base="$1"

  # Generate ed25519 private and public key.
  openssl genpkey -algorithm Ed25519 -out "${base}.priv.pem"
  openssl pkey -in "${base}.priv.pem" -pubout -text_pub -out "${base}.pub.pem"
}
