|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Regenerate the NameConstraints ancestor-walk test PEMs. |
| 3 | +# |
| 4 | +# Chains produced: |
| 5 | +# |
| 6 | +# Negative (leaf violates grandparent's URI permit): |
| 7 | +# 00-root -> 01-uri-permit-ca (permits URI:.example.com) -> |
| 8 | +# 02-benign-sub-ca -> 03-leaf (URI:https://attacker.com/leaf) |
| 9 | +# Emitted as concatenated bundle 03-leaf-chain.cert.pem; only the |
| 10 | +# leaf is verified by the test, but the bundle order keeps the file |
| 11 | +# self-documenting. |
| 12 | +# |
| 13 | +# Positive (chain that should verify successfully): |
| 14 | +# 00-root -> 01-uri-permit-ca -> 02-benign-sub-ca -> |
| 15 | +# 03-valid-leaf (URI:https://benign.example.com/) |
| 16 | +# |
| 17 | +# SKID disambiguation (same DN as 01-uri-permit-ca, different key, no NC): |
| 18 | +# self-signed 00-uri-permit-ca-permissive |
| 19 | +# Sits in the CM as a same-DN distractor; the ancestor walk must use |
| 20 | +# AKID->SKID lookup (not a name-only walk) to pick the real signer. |
| 21 | +# |
| 22 | +# NotBefore is the current wall-clock time and NotAfter is +1000d, set by |
| 23 | +# openssl's -days 1000 flag. Serial numbers come from openssl's default |
| 24 | +# random generator. Re-running this script rewrites the PEMs with new |
| 25 | +# serials and shifted validity windows; the tests only assert verify |
| 26 | +# behavior, not specific cert bytes. |
| 27 | + |
| 28 | +set -euo pipefail |
| 29 | + |
| 30 | +DIR="$(cd "$(dirname "$0")" && pwd)" |
| 31 | +WORK="$(mktemp -d)" |
| 32 | +trap 'rm -rf "$WORK"' EXIT |
| 33 | + |
| 34 | +cd "$WORK" |
| 35 | + |
| 36 | +CURVE="prime256v1" |
| 37 | + |
| 38 | +# ---- helpers ---- |
| 39 | + |
| 40 | +mkkey() { |
| 41 | + # $1 = key file |
| 42 | + openssl ecparam -name "$CURVE" -genkey -noout -out "$1" |
| 43 | +} |
| 44 | + |
| 45 | +mkroot() { |
| 46 | + # $1 = key, $2 = cert out, $3 = subject CN, $4 = ext-file |
| 47 | + openssl req -new -x509 -key "$1" -out "$2" \ |
| 48 | + -subj "/C=US/O=NC Tests/CN=$3" \ |
| 49 | + -config "$4" -extensions v3_ca \ |
| 50 | + -set_serial "0x$(openssl rand -hex 20)" \ |
| 51 | + -days 1000 -sha256 |
| 52 | +} |
| 53 | + |
| 54 | +# Issue a child cert from $issuer_key / $issuer_cert using ext-file $4. |
| 55 | +# $1 child-key $2 child-csr-subject-CN $3 out-cert $4 ext-file $5 ext-section |
| 56 | +mkchild() { |
| 57 | + local child_key=$1 cn=$2 out=$3 extfile=$4 extsec=$5 |
| 58 | + openssl req -new -key "$child_key" -out child.csr \ |
| 59 | + -subj "/C=US/O=NC Tests/CN=$cn" -config "$extfile" |
| 60 | + openssl x509 -req -in child.csr \ |
| 61 | + -CA "$issuer_cert" -CAkey "$issuer_key" \ |
| 62 | + -set_serial "0x$(openssl rand -hex 20)" \ |
| 63 | + -out "$out" -days 1000 -sha256 \ |
| 64 | + -extfile "$extfile" -extensions "$extsec" |
| 65 | + rm -f child.csr |
| 66 | +} |
| 67 | + |
| 68 | +# ---- ext configs ---- |
| 69 | + |
| 70 | +cat > root.cnf <<'EOF' |
| 71 | +[req] |
| 72 | +distinguished_name = dn |
| 73 | +prompt = no |
| 74 | +[dn] |
| 75 | +[v3_ca] |
| 76 | +basicConstraints = critical, CA:TRUE |
| 77 | +keyUsage = critical, digitalSignature, keyCertSign, cRLSign |
| 78 | +subjectKeyIdentifier = hash |
| 79 | +EOF |
| 80 | + |
| 81 | +cat > uri-permit-ca.cnf <<'EOF' |
| 82 | +[req] |
| 83 | +distinguished_name = dn |
| 84 | +prompt = no |
| 85 | +[dn] |
| 86 | +[v3_uri_permit] |
| 87 | +basicConstraints = critical, CA:TRUE |
| 88 | +keyUsage = critical, digitalSignature, keyCertSign, cRLSign |
| 89 | +subjectKeyIdentifier = hash |
| 90 | +authorityKeyIdentifier = keyid |
| 91 | +nameConstraints = critical, permitted;URI:.example.com |
| 92 | +EOF |
| 93 | + |
| 94 | +cat > sub-ca-nonc.cnf <<'EOF' |
| 95 | +[req] |
| 96 | +distinguished_name = dn |
| 97 | +prompt = no |
| 98 | +[dn] |
| 99 | +[v3_sub_ca] |
| 100 | +basicConstraints = critical, CA:TRUE |
| 101 | +keyUsage = critical, digitalSignature, keyCertSign, cRLSign |
| 102 | +subjectKeyIdentifier = hash |
| 103 | +authorityKeyIdentifier = keyid |
| 104 | +EOF |
| 105 | + |
| 106 | +cat > leaf-attacker.cnf <<'EOF' |
| 107 | +[req] |
| 108 | +distinguished_name = dn |
| 109 | +prompt = no |
| 110 | +[dn] |
| 111 | +[v3_leaf_attacker] |
| 112 | +basicConstraints = critical, CA:FALSE |
| 113 | +keyUsage = critical, digitalSignature, keyEncipherment |
| 114 | +extendedKeyUsage = serverAuth |
| 115 | +subjectKeyIdentifier = hash |
| 116 | +authorityKeyIdentifier = keyid |
| 117 | +subjectAltName = critical, URI:https://attacker.com/leaf |
| 118 | +EOF |
| 119 | + |
| 120 | +cat > leaf-valid.cnf <<'EOF' |
| 121 | +[req] |
| 122 | +distinguished_name = dn |
| 123 | +prompt = no |
| 124 | +[dn] |
| 125 | +[v3_leaf_valid] |
| 126 | +basicConstraints = critical, CA:FALSE |
| 127 | +keyUsage = critical, digitalSignature, keyEncipherment |
| 128 | +extendedKeyUsage = serverAuth |
| 129 | +subjectKeyIdentifier = hash |
| 130 | +authorityKeyIdentifier = keyid |
| 131 | +subjectAltName = critical, URI:https://benign.example.com/ |
| 132 | +EOF |
| 133 | + |
| 134 | +# ---- root ---- |
| 135 | + |
| 136 | +mkkey root.key |
| 137 | +mkroot root.key 00-root.cert.pem "NC Test Root" root.cnf |
| 138 | + |
| 139 | +# ---- 01 uri-permit-ca (permits URI:.example.com), issued by root ---- |
| 140 | + |
| 141 | +mkkey uri-permit-ca.key |
| 142 | +issuer_cert=00-root.cert.pem; issuer_key=root.key |
| 143 | +mkchild uri-permit-ca.key "URI Permit CA" 01-uri-permit-ca.cert.pem \ |
| 144 | + uri-permit-ca.cnf v3_uri_permit |
| 145 | + |
| 146 | +# ---- 01 permissive sibling (same DN as 01-uri-permit-ca, different key, |
| 147 | +# no NC). Self-signed so it isn't part of any chain of trust; sits |
| 148 | +# in the CM purely as a same-DN distractor for the ancestor walk's |
| 149 | +# AKID->SKID disambiguation test. |
| 150 | +# |
| 151 | +# The wolfSSL CM hash-buckets signers by SKID into CA_TABLE_SIZE=11 rows. |
| 152 | +# A name-only lookup (the walk's fallback when AKID disambiguation is |
| 153 | +# broken) iterates rows 0..10 and returns the first match. For the |
| 154 | +# regression test to FAIL when disambiguation is broken, the permissive |
| 155 | +# sibling must land in a row strictly less than the strict variant's so |
| 156 | +# the name-only lookup surfaces it. Iterate key generation until that |
| 157 | +# holds. ---- |
| 158 | + |
| 159 | +# Compute the CM row index for a cert's SKID: |
| 160 | +# row = (first 4 SKID bytes as big-endian word) mod 11 |
| 161 | +# Mirrors HashSigner() in src/ssl_certman.c. |
| 162 | +skid_bucket() { |
| 163 | + local hex |
| 164 | + hex=$(openssl x509 -in "$1" -noout -ext subjectKeyIdentifier \ |
| 165 | + | grep -E '^[[:space:]]+[A-Fa-f0-9]' | head -1 \ |
| 166 | + | tr -d ' :' | head -c 8) |
| 167 | + echo $(( 0x$hex % 11 )) |
| 168 | +} |
| 169 | + |
| 170 | +strict_bucket=$(skid_bucket 01-uri-permit-ca.cert.pem) |
| 171 | +if (( strict_bucket == 0 )); then |
| 172 | + echo "ERROR: strict CA hashed to row 0; cannot place permissive sibling lower." >&2 |
| 173 | + echo " Rerun the script to rotate the strict CA's SKID." >&2 |
| 174 | + exit 1 |
| 175 | +fi |
| 176 | + |
| 177 | +attempts=0 |
| 178 | +perm_bucket=$strict_bucket |
| 179 | +while (( attempts < 200 )); do |
| 180 | + attempts=$(( attempts + 1 )) |
| 181 | + mkkey uri-permit-ca-permissive.key |
| 182 | + mkroot uri-permit-ca-permissive.key 00-uri-permit-ca-permissive.cert.pem \ |
| 183 | + "URI Permit CA" root.cnf |
| 184 | + perm_bucket=$(skid_bucket 00-uri-permit-ca-permissive.cert.pem) |
| 185 | + if (( perm_bucket < strict_bucket )); then |
| 186 | + break |
| 187 | + fi |
| 188 | +done |
| 189 | +if (( perm_bucket >= strict_bucket )); then |
| 190 | + echo "ERROR: failed to land permissive sibling below row $strict_bucket after $attempts tries." >&2 |
| 191 | + exit 1 |
| 192 | +fi |
| 193 | +echo "Permissive sibling: row $perm_bucket < strict row $strict_bucket (after $attempts tries)" |
| 194 | + |
| 195 | +# ---- 02 benign-sub-ca (no NC, no URI SAN), issued by uri-permit-ca ---- |
| 196 | + |
| 197 | +mkkey benign-sub-ca.key |
| 198 | +issuer_cert=01-uri-permit-ca.cert.pem; issuer_key=uri-permit-ca.key |
| 199 | +mkchild benign-sub-ca.key "Benign Sub CA" 02-benign-sub-ca.cert.pem \ |
| 200 | + sub-ca-nonc.cnf v3_sub_ca |
| 201 | + |
| 202 | +# ---- 03 leaf (URI:attacker.com/leaf -- violates grandparent's permit) ---- |
| 203 | + |
| 204 | +mkkey leaf-attacker.key |
| 205 | +issuer_cert=02-benign-sub-ca.cert.pem; issuer_key=benign-sub-ca.key |
| 206 | +mkchild leaf-attacker.key "NC Test Attacker Leaf" 03-leaf.cert.pem \ |
| 207 | + leaf-attacker.cnf v3_leaf_attacker |
| 208 | + |
| 209 | +# ---- 03 valid-leaf (URI:benign.example.com -- inside permit) ---- |
| 210 | + |
| 211 | +mkkey leaf-valid.key |
| 212 | +issuer_cert=02-benign-sub-ca.cert.pem; issuer_key=benign-sub-ca.key |
| 213 | +mkchild leaf-valid.key "NC Test Valid Leaf" 03-valid-leaf.cert.pem \ |
| 214 | + leaf-valid.cnf v3_leaf_valid |
| 215 | + |
| 216 | +# ---- copy into destination ---- |
| 217 | + |
| 218 | +cp -f 00-root.cert.pem 01-uri-permit-ca.cert.pem \ |
| 219 | + 00-uri-permit-ca-permissive.cert.pem \ |
| 220 | + 02-benign-sub-ca.cert.pem 03-valid-leaf.cert.pem \ |
| 221 | + "$DIR/" |
| 222 | + |
| 223 | +# Concatenated bundle (attacker leaf + benign-sub-ca + uri-permit-ca). |
| 224 | +# CertManagerVerify reads only the first PEM block (the leaf); the |
| 225 | +# trailing CAs keep the file self-documenting. Order: leaf first, then |
| 226 | +# ascending issuers. |
| 227 | +cat 03-leaf.cert.pem 02-benign-sub-ca.cert.pem 01-uri-permit-ca.cert.pem \ |
| 228 | + > "$DIR/03-leaf-chain.cert.pem" |
| 229 | + |
| 230 | +echo "Generated chain in $DIR/" |
| 231 | +ls -la "$DIR/" |
0 commit comments