#!/bin/bash
# Provision a client certificate onto a YubiKey PIV slot end to end:
# generate the key on the card, build a CSR, get it signed by the test API,
# and import the signed certificate back onto the card.
#
# Usage: ./make-yubikey-cert.sh <name> <leafkeytype> <catype> [slot] [--yes]
#
#   leafkeytype : rsa | ecdsa | mldsa | mldsa65 | mldsa87
#   catype      : rsa | ecdsa | mldsa | mldsa65 | mldsa87  (which CA signs it)
#   slot        : PIV slot, default 9a (client authentication)
#
#   ./make-yubikey-cert.sh ep2 rsa rsa          # the classical baseline
#   ./make-yubikey-cert.sh ep2 rsa mldsa        # RSA key under the ML-DSA-44 CA
#   ./make-yubikey-cert.sh ep2 mldsa65 mldsa65  # ML-DSA-65 key under ML-DSA-65 CA
#   ./make-yubikey-cert.sh ep2 mldsa87 mldsa87  # ML-DSA-87 key under ML-DSA-87 CA
#
# Produces <name>-<leafkeytype>-ca-<catype>{-pubkey.pem,.csr,.crt}. There is no
# .key file: the private key is generated on the card and cannot leave it. That
# also means test-soft-certs.sh will SKIP these -- use a browser to test them.
#
# Needs ykman and OpenSSL 3.5+ (the latter only for the ML-DSA cases).

set -u

NAME="${1:-}"
KEYTYPE="${2:-}"
CATYPE="${3:-}"
SLOT="${4:-9a}"
ASSUME_YES="${5:-}"

if [ -z "$NAME" ] || [ -z "$KEYTYPE" ] || [ -z "$CATYPE" ]; then
    echo "Usage: $0 <name> <leafkeytype> <catype> [slot] [--yes]"
    echo "       leafkeytype: rsa | ecdsa | mldsa | mldsa65 | mldsa87"
    echo "       catype:      rsa | ecdsa | mldsa | mldsa65 | mldsa87"
    exit 1
fi

# Allow --yes in the slot position when the slot is left at its default.
if [ "$SLOT" = "--yes" ]; then
    ASSUME_YES="--yes"
    SLOT="9a"
fi

HOST=sctest.yksi.me
OPENSSL="${OPENSSL_BIN:-openssl}"

# ykman's algorithm names, as accepted by `ykman piv keys generate --algorithm`.
# These are lowercase: rsa1024 rsa2048 rsa3072 rsa4096 eccp256 eccp384 ed25519
# x25519 ml-dsa44 ml-dsa65 ml-dsa87 ml-kem512 ml-kem768 ml-kem1024
# Override any of them with YKMAN_ALG=<name> if your build differs.
declare -A YKALG=(
    [rsa]="${YKMAN_ALG:-rsa2048}"
    [ecdsa]="${YKMAN_ALG:-eccp256}"
    [mldsa]="${YKMAN_ALG:-ml-dsa44}"
    [mldsa65]="${YKMAN_ALG:-ml-dsa65}"
    [mldsa87]="${YKMAN_ALG:-ml-dsa87}"
)

declare -A CA_FILE=(
    [rsa]=YK-RSA-Root-CA.crt
    [ecdsa]=YK-ECDSA-Root-CA.crt
    [mldsa]=YK-ML-DSA-44-Root-CA.crt
    [mldsa65]=YK-ML-DSA-65-Root-CA.crt
    [mldsa87]=YK-ML-DSA-87-Root-CA.crt
)

if [ -z "${YKALG[$KEYTYPE]:-}" ]; then
    echo "ERROR: unknown leaf key type '$KEYTYPE' (use rsa, ecdsa, mldsa, mldsa65 or mldsa87)"
    exit 1
fi
if [ -z "${CA_FILE[$CATYPE]:-}" ]; then
    echo "ERROR: unknown CA type '$CATYPE' (use rsa, ecdsa, mldsa, mldsa65 or mldsa87)"
    exit 1
fi

command -v ykman >/dev/null 2>&1 || { echo "ERROR: ykman not found on PATH"; exit 1; }

SLUG="$NAME-$KEYTYPE-ca-$CATYPE"
PUBKEY="$SLUG-pubkey.pem"
CSR="$SLUG.csr"
CRT="$SLUG.crt"

# ---------------------------------------------------------------------------

echo "Current YubiKey PIV state:"
ykman piv info || { echo "ERROR: cannot talk to a YubiKey"; exit 1; }

cat <<EOF

About to provision:
  Slot            : $SLOT
  Leaf key        : ${YKALG[$KEYTYPE]}  (generated ON the card)
  Signed by CA    : $CATYPE
  Subject         : CN=$SLUG
  Output files    : $PUBKEY, $CSR, $CRT

WARNING: generating a key in slot $SLOT PERMANENTLY DESTROYS the key currently
in that slot. There is no way to recover it. Any certificate already issued
against that key becomes unusable.

EOF

if [ "$ASSUME_YES" != "--yes" ]; then
    read -r -p "Type the slot number ($SLOT) to continue, anything else to abort: " confirm
    if [ "$confirm" != "$SLOT" ]; then
        echo "Aborted. Nothing was changed on the card."
        exit 1
    fi
fi

# 1. Generate the key pair on the card. ykman prompts for the management key.
echo ""
echo "[1/5] Generating ${YKALG[$KEYTYPE]} key in slot $SLOT..."
if ! ykman piv keys generate --algorithm "${YKALG[$KEYTYPE]}" "$SLOT" "$PUBKEY"; then
    echo ""
    echo "FAILED: key generation with algorithm '${YKALG[$KEYTYPE]}'."
    echo "        If ykman rejected the name, check what your build accepts:"
    echo "          ykman piv keys generate --help"
    echo "        then re-run with YKMAN_ALG=<name> $0 ..."
    echo "        If the name was valid, the firmware may not support it."
    exit 1
fi

# 2. Build the CSR. The card signs it, so this will prompt for the PIN.
#    CN carries the full slug so permutations stay distinguishable in the
#    Windows/browser certificate picker.
echo ""
echo "[2/5] Building CSR (card will ask for your PIN)..."
if ! ykman piv certificates request --subject "CN=$SLUG" "$SLOT" "$PUBKEY" "$CSR"; then
    echo "FAILED: CSR generation"
    exit 1
fi

# 3. Get it signed.
echo ""
echo "[3/5] Submitting CSR to the $CATYPE CA..."
resp=$(curl -sk -F "testerName=$NAME" -F "caType=$CATYPE" \
            -F "csr=@$CSR" "https://$HOST/api/cert/request")

if command -v jq >/dev/null 2>&1; then
    cert=$(printf '%s' "$resp" | jq -r '.certificate // empty')
    err=$(printf '%s' "$resp" | jq -r '.error // empty')
elif command -v python3 >/dev/null 2>&1; then
    cert=$(printf '%s' "$resp" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("certificate",""),end="")')
    err=$(printf '%s' "$resp" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("error",""),end="")')
else
    echo "FAILED: need jq or python3 to parse the API response"
    exit 1
fi

if [ -z "$cert" ]; then
    echo "FAILED: API returned no certificate"
    echo "  ${err:-$resp}" | head -3
    exit 1
fi

printf '%s' "$cert" > "$CRT"

# 4. Confirm the signed cert belongs to the key we just made on the card.
#    Compare against the exported public key -- the private key cannot leave.
echo ""
echo "[4/5] Verifying the signed certificate matches the on-card key..."
certpub=$("$OPENSSL" x509 -in "$CRT" -noout -pubkey 2>/dev/null | "$OPENSSL" sha256)
cardpub=$("$OPENSSL" pkey -pubin -in "$PUBKEY" -pubout 2>/dev/null | "$OPENSSL" sha256)

if [ -z "$certpub" ] || [ "$certpub" != "$cardpub" ]; then
    echo "FAILED: signed certificate does not match the key in slot $SLOT."
    echo "        NOT importing. The card is unchanged since step 1."
    exit 1
fi
echo "  match confirmed"
"$OPENSSL" x509 -in "$CRT" -noout -subject -issuer | sed 's/^/  /'

# 5. Import onto the card.
echo ""
echo "[5/5] Importing certificate into slot $SLOT..."
if ! ykman piv certificates import "$SLOT" "$CRT"; then
    echo "FAILED: certificate import"
    exit 1
fi

echo ""
echo "Done. Slot $SLOT now holds:"
ykman piv certificates export "$SLOT" - 2>/dev/null \
    | "$OPENSSL" x509 -noout -subject -issuer | sed 's/^/  /'

cat <<EOF

Next steps for browser testing:

  1. Trust the CA. Download ${CA_FILE[$CATYPE]} and import it into the
     Windows "Trusted Root Certification Authorities" store (certmgr.msc),
     or into Firefox under Settings > Certificates > Authorities.

  2. Confirm Windows sees the card's private key:
       certutil -scinfo -silent

  3. Load https://sctest.yksi.me:PORT and pick the CN=$SLUG certificate.
     Ports that accept a $CATYPE-CA client cert:
EOF

case "$CATYPE" in
    rsa)     echo "       8443 (hybrid KX)   8448 (classical KX)" ;;
    ecdsa)   echo "       8445 (hybrid KX)" ;;
    mldsa)   echo "       8444   8446   8447  (all hybrid KX)" ;;
    mldsa65) echo "       8450 (hybrid KX)" ;;
    mldsa87) echo "       8451 (hybrid KX)" ;;
esac

cat <<EOF

  If the browser never prompts and you get SSL_ERROR_RX_CERTIFICATE_REQUIRED_ALERT,
  it sent no certificate at all. In Firefox set security.osclientcerts.autoload
  to true in about:config and restart; Edge and Chrome use the Windows store
  directly and are the faster way to isolate that.
EOF
