diff --git a/run-tests.sh b/run-tests.sh index 7b4b225..e8b7fe4 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -41,6 +41,13 @@ reset_dirs() { SIMPLE_CA_DIR="" } +assert_file() { + if [[ ! -f "$1" ]]; then + echo "ERROR: expected file not found: $1" >&2 + exit 1 + fi +} + verify_cert() { local CERT_PATH="$1" if ! openssl verify -CAfile "$CA_DIR/ca_bundle.pem" "$CERT_PATH" 2>/dev/null; then @@ -50,6 +57,18 @@ verify_cert() { echo "Verified: $CERT_PATH" } +assert_eku() { + local CERT_PATH="$1" + local EKU="$2" + local TEXT + TEXT="$(openssl x509 -in "$CERT_PATH" -noout -text 2>/dev/null)" + if ! echo "$TEXT" | grep -q "$EKU"; then + echo "ERROR: EKU '$EKU' not found in $CERT_PATH" >&2 + exit 1 + fi + echo "EKU OK: $EKU" +} + # --------------------------------------------------------------------------- # Standalone CA — certs issued by root CA go into CA_DIR # --------------------------------------------------------------------------- @@ -58,12 +77,12 @@ echo echo "--- [shell] Standalone CA ---" reset_dirs make_ca --ca-dir "$CA_DIR" "Test CA" 2>/dev/null -[[ -f "$CA_DIR/ca_cert.pem" ]] || { echo "ERROR: ca_cert.pem not created" >&2; exit 1; } -[[ -f "$CA_DIR/ca_bundle.pem" ]] || { echo "ERROR: ca_bundle.pem not created" >&2; exit 1; } +assert_file "$CA_DIR/ca_cert.pem" +assert_file "$CA_DIR/ca_bundle.pem" verify_cert "$CA_DIR/ca_cert.pem" make_cert "test" "test.example.com" "127.0.0.1" 2>/dev/null -[[ -f "$CA_DIR/test_cert.pem" ]] || { echo "ERROR: test_cert.pem not created in CA_DIR" >&2; exit 1; } +assert_file "$CA_DIR/test_cert.pem" verify_cert "$CA_DIR/test_cert.pem" # --------------------------------------------------------------------------- @@ -77,18 +96,35 @@ make_ca --ca-dir "$CA_DIR" "Test Root CA" 2>/dev/null verify_cert "$CA_DIR/ca_cert.pem" make_ca --issuing-ca "issuing_ca" "Issuing CA" 2>/dev/null -[[ -f "$CA_DIR/issuing_ca/ca_cert.pem" ]] || { echo "ERROR: issuing_ca/ca_cert.pem not created" >&2; exit 1; } +assert_file "$CA_DIR/issuing_ca/ca_cert.pem" verify_cert "$CA_DIR/issuing_ca/ca_cert.pem" make_cert --issuing-ca "issuing_ca" "test" "test.example.com" "127.0.0.1" 2>/dev/null -[[ -f "$CA_DIR/issuing_ca/test_cert.pem" ]] || { echo "ERROR: issuing_ca/test_cert.pem not created" >&2; exit 1; } +assert_file "$CA_DIR/issuing_ca/test_cert.pem" verify_cert "$CA_DIR/issuing_ca/test_cert.pem" +assert_eku "$CA_DIR/issuing_ca/test_cert.pem" "TLS Web Server Authentication" +assert_eku "$CA_DIR/issuing_ca/test_cert.pem" "TLS Web Client Authentication" make_pfx --issuing-ca "issuing_ca" --password "s3cr3t" "$CA_DIR/issuing_ca/test_cert.pem" 2>/dev/null -[[ -f "$CA_DIR/issuing_ca/test.pfx" ]] || { echo "ERROR: issuing_ca/test.pfx not created" >&2; exit 1; } -openssl pkcs12 -in "$CA_DIR/issuing_ca/test.pfx" -noout -info -password pass:"s3cr3t" 2>/dev/null \ - || { echo "ERROR: PFX verification failed" >&2; exit 1; } +assert_file "$CA_DIR/issuing_ca/test.pfx" +if ! openssl pkcs12 -in "$CA_DIR/issuing_ca/test.pfx" -noout -info -password pass:"s3cr3t" 2>/dev/null; then + echo "ERROR: PFX verification failed" >&2 + exit 1 +fi echo "PFX: OK" +# --------------------------------------------------------------------------- +# User certificate +# --------------------------------------------------------------------------- + +echo +echo "--- [shell] User certificate ---" +make_cert --issuing-ca "issuing_ca" --type user "Alice Example" "alice@example.com" 2>/dev/null +assert_file "$CA_DIR/issuing_ca/Alice Example_cert.pem" +verify_cert "$CA_DIR/issuing_ca/Alice Example_cert.pem" +assert_eku "$CA_DIR/issuing_ca/Alice Example_cert.pem" "TLS Web Client Authentication" +assert_eku "$CA_DIR/issuing_ca/Alice Example_cert.pem" "E-mail Protection" +assert_eku "$CA_DIR/issuing_ca/Alice Example_cert.pem" "Code Signing" + echo echo "All shell tests passed." diff --git a/simple-ca.sh b/simple-ca.sh index ec9743b..99abe3a 100755 --- a/simple-ca.sh +++ b/simple-ca.sh @@ -57,8 +57,9 @@ _require_ca_dir() { fi } -_is_ip() { [[ "$1" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; } -_is_dns() { [[ "$1" =~ ^[a-z0-9-]+(\.[a-z0-9-]+)*$ ]]; } +_is_ip() { [[ "$1" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; } +_is_dns() { [[ "$1" =~ ^[a-z0-9-]+(\.[a-z0-9-]+)*$ ]]; } +_is_email() { [[ "$1" =~ ^[^@]+@[^@]+\.[^@]+$ ]]; } make_ca() { local CA_DAYS=3650 @@ -187,6 +188,7 @@ make_cert() { local ISSUING_CA="" local CERT_DAYS=365 local CERT_DIR="" + local CERT_TYPE="server" while [[ $# -gt 0 ]]; do case "$1" in @@ -218,6 +220,16 @@ make_cert() { return 1 fi CERT_DAYS="$2"; shift 2 ;; + --type) + if [[ -z "$2" ]]; then + echo "ERROR: --type requires a value." >&2 + return 1 + fi + if [[ "$2" != "server" && "$2" != "user" ]]; then + echo "ERROR: --type must be 'server' or 'user'." >&2 + return 1 + fi + CERT_TYPE="$2"; shift 2 ;; *) break ;; esac done @@ -233,7 +245,7 @@ make_cert() { echo "ERROR: Subject name is required." >&2 return 1 fi - if ! _is_dns "$CERT_SUBJECT_NAME"; then + if [[ "$CERT_TYPE" == "server" ]] && ! _is_dns "$CERT_SUBJECT_NAME"; then echo "ERROR: Invalid subject name '$CERT_SUBJECT_NAME'. Must be a valid DNS name." >&2 return 1 fi @@ -256,11 +268,17 @@ make_cert() { fi local CERT_NAME="${CERT_SUBJECT_NAME%%.*}" - local SANS=("DNS:${CERT_SUBJECT_NAME}") + local SANS=() + + if [[ "$CERT_TYPE" == "server" ]]; then + SANS=("DNS:${CERT_SUBJECT_NAME}") + fi while [[ $# -gt 0 ]]; do if _is_ip "$1"; then SANS+=("IP:$1") + elif _is_email "$1"; then + SANS+=("email:$1") elif _is_dns "$1"; then SANS+=("DNS:$1") else @@ -270,7 +288,11 @@ make_cert() { shift done - echo "Generating server certificate for '$CERT_SUBJECT_NAME' with SANs:" + if [[ "$CERT_TYPE" == "server" ]]; then + echo "Generating server certificate for '$CERT_SUBJECT_NAME' with SANs:" + else + echo "Generating user certificate for '$CERT_SUBJECT_NAME':" + fi for san in "${SANS[@]}"; do echo " - $san"; done if [[ -f "$CERT_DIR/${CERT_NAME}_cert.pem" && -f "$CERT_DIR/${CERT_NAME}_key.pem" ]]; then @@ -278,28 +300,47 @@ make_cert() { return 0 fi - local SANS_EXT="subjectAltName=$(IFS=,; echo "${SANS[*]}")" + local REQ_ARGS=( + -newkey rsa:4096 + -keyout "$CERT_DIR/${CERT_NAME}_key.pem" + -noenc + -subj "/CN=${CERT_SUBJECT_NAME}" + -addext "basicConstraints=critical,CA:FALSE" + ) - echo "Generating server certificate and key..." - if ! openssl req \ - -newkey rsa:4096 \ - -keyout "$CERT_DIR/${CERT_NAME}_key.pem" \ - -noenc \ - -subj "/CN=${CERT_SUBJECT_NAME}" \ - -addext "basicConstraints=critical,CA:FALSE" \ - -addext "keyUsage=critical,digitalSignature,keyEncipherment" \ - -addext "extendedKeyUsage=serverAuth,clientAuth" \ - -addext "$SANS_EXT" \ - ${AIA_URL:+-addext "authorityInfoAccess=caIssuers;URI:${AIA_URL}"} \ - | openssl x509 \ - -req \ - -CA "$SIGNING_CERT" \ - -CAkey "$SIGNING_KEY" \ - -copy_extensions copyall \ - -days "$CERT_DAYS" \ - -text \ - -out "$CERT_DIR/${CERT_NAME}_cert.pem"; then - echo "ERROR: Failed to generate server certificate and key." >&2 + local X509_ARGS=( + -req + -CA "$SIGNING_CERT" + -CAkey "$SIGNING_KEY" + -copy_extensions copyall + -days "$CERT_DAYS" + -text + -out "$CERT_DIR/${CERT_NAME}_cert.pem" + ) + + if [[ "$CERT_TYPE" == "server" ]]; then + REQ_ARGS+=( + -addext "keyUsage=critical,digitalSignature,keyEncipherment" + -addext "extendedKeyUsage=serverAuth,clientAuth" + -addext "subjectAltName=$(IFS=,; echo "${SANS[*]}")" + ) + else + REQ_ARGS+=( + -addext "keyUsage=critical,digitalSignature,nonRepudiation" + -addext "extendedKeyUsage=clientAuth,emailProtection,codeSigning" + ) + if [[ ${#SANS[@]} -gt 0 ]]; then + REQ_ARGS+=(-addext "subjectAltName=$(IFS=,; echo "${SANS[*]}")") + fi + fi + + if [[ -n "$AIA_URL" ]]; then + REQ_ARGS+=(-addext "authorityInfoAccess=caIssuers;URI:${AIA_URL}") + fi + + echo "Generating certificate and key..." + if ! openssl req "${REQ_ARGS[@]}" | openssl x509 "${X509_ARGS[@]}"; then + echo "ERROR: Failed to generate certificate and key." >&2 return 1 fi }