| 1 | /* |
| 2 | * gen — produce vectors/contra-v1.json from PROTOCOL.md, using OpenSSL. |
| 3 | * |
| 4 | * Why this is C and not Go: discussion #6 forbids generating the vectors |
| 5 | * from an implementation written in the same session and then testing that |
| 6 | * implementation against them, because it proves nothing. So the generator |
| 7 | * is a second, independent implementation — OpenSSL 3.x, which nobody here |
| 8 | * wrote — and the Go package under test never contributes a byte to this |
| 9 | * file. |
| 10 | * |
| 11 | * Before it generates anything, main() re-computes the published third-party |
| 12 | * vectors in `external_anchors` and aborts if any disagrees. That is what |
| 13 | * stops "independent" from meaning "differently wrong": the PHC Argon2id |
| 14 | * reference outputs, RFC 5869 HKDF and RFC 4231 HMAC all have to come back |
| 15 | * right first. |
| 16 | * |
| 17 | * Every fixture input is itself derived from a published label — |
| 18 | * |
| 19 | * fixture(label, n) = SHA-256("contra/v1/vectors/" || label)[0..n] |
| 20 | * |
| 21 | * so the whole file regenerates from nothing but this source, and no value |
| 22 | * in it can be mistaken for a real key. AGENTS.md decision 8: fixtures are |
| 23 | * generated, never real. |
| 24 | * |
| 25 | * Build: make Run: ./gen > ../contra-v1.json |
| 26 | */ |
| 27 | #include <stdarg.h> |
| 28 | #include <stdint.h> |
| 29 | #include <stdio.h> |
| 30 | #include <stdlib.h> |
| 31 | #include <string.h> |
| 32 | |
| 33 | #include <openssl/bn.h> |
| 34 | #include <openssl/core_names.h> |
| 35 | #include <openssl/ec.h> |
| 36 | #include <openssl/evp.h> |
| 37 | #include <openssl/kdf.h> |
| 38 | #include <openssl/obj_mac.h> |
| 39 | #include <openssl/params.h> |
| 40 | |
| 41 | /* ---------------------------------------------------------------- basics */ |
| 42 | |
| 43 | static void die(const char *what) { |
| 44 | fprintf(stderr, "gen: %s\n", what); |
| 45 | exit(1); |
| 46 | } |
| 47 | |
| 48 | /* Rotating hex buffers, so several hex() calls can share one printf. */ |
| 49 | static const char *hex(const unsigned char *b, size_t n) { |
| 50 | static char pool[16][600]; |
| 51 | static int slot = 0; |
| 52 | char *out = pool[slot = (slot + 1) % 16]; |
| 53 | size_t i; |
| 54 | |
| 55 | if (n * 2 + 1 > sizeof pool[0]) die("hex buffer too small"); |
| 56 | for (i = 0; i < n; i++) sprintf(out + i * 2, "%02x", b[i]); |
| 57 | out[n * 2] = 0; |
| 58 | return out; |
| 59 | } |
| 60 | |
| 61 | /* The inverse, for transcribing published anchors as the RFC prints them |
| 62 | * rather than as a wall of 0x-prefixed bytes nobody can proofread. */ |
| 63 | static int hex2bin(const char *s, unsigned char *out, size_t n) { |
| 64 | size_t i; |
| 65 | |
| 66 | if (strlen(s) != n * 2) return 0; |
| 67 | for (i = 0; i < n; i++) { |
| 68 | unsigned v; |
| 69 | if (sscanf(s + i * 2, "%2x", &v) != 1) return 0; |
| 70 | out[i] = (unsigned char)v; |
| 71 | } |
| 72 | return 1; |
| 73 | } |
| 74 | |
| 75 | static void sha256(const unsigned char *m, size_t n, unsigned char out[32]) { |
| 76 | unsigned int len = 32; |
| 77 | if (!EVP_Digest(m, n, out, &len, EVP_sha256(), NULL)) die("sha256"); |
| 78 | } |
| 79 | |
| 80 | static void hmac_sha256(const unsigned char *key, size_t keylen, |
| 81 | const unsigned char *msg, size_t msglen, |
| 82 | unsigned char out[32]) { |
| 83 | EVP_MAC *mac = EVP_MAC_fetch(NULL, "HMAC", NULL); |
| 84 | EVP_MAC_CTX *ctx; |
| 85 | OSSL_PARAM p[2]; |
| 86 | size_t outlen = 0; |
| 87 | |
| 88 | if (!mac) die("fetch HMAC"); |
| 89 | ctx = EVP_MAC_CTX_new(mac); |
| 90 | if (!ctx) die("HMAC ctx"); |
| 91 | p[0] = OSSL_PARAM_construct_utf8_string(OSSL_MAC_PARAM_DIGEST, "SHA256", 0); |
| 92 | p[1] = OSSL_PARAM_construct_end(); |
| 93 | if (!EVP_MAC_init(ctx, key, keylen, p)) die("HMAC init"); |
| 94 | if (!EVP_MAC_update(ctx, msg, msglen)) die("HMAC update"); |
| 95 | if (!EVP_MAC_final(ctx, out, &outlen, 32)) die("HMAC final"); |
| 96 | EVP_MAC_CTX_free(ctx); |
| 97 | EVP_MAC_free(mac); |
| 98 | } |
| 99 | |
| 100 | /* |
| 101 | * HKDF-SHA-256, RFC 5869 extract-then-expand — the one mode this protocol |
| 102 | * uses (discussion #19). A NULL salt is a zero-length salt, which RFC 5869 |
| 103 | * §2.2 turns into HashLen zero bytes. |
| 104 | */ |
| 105 | static void hkdf(const unsigned char *ikm, size_t ikmlen, |
| 106 | const unsigned char *salt, size_t saltlen, |
| 107 | const unsigned char *info, size_t infolen, |
| 108 | unsigned char *out, size_t outlen) { |
| 109 | EVP_KDF *kdf = EVP_KDF_fetch(NULL, "HKDF", NULL); |
| 110 | EVP_KDF_CTX *ctx; |
| 111 | OSSL_PARAM p[6]; |
| 112 | int i = 0; |
| 113 | static const unsigned char empty[1] = {0}; |
| 114 | |
| 115 | if (!kdf) die("fetch HKDF"); |
| 116 | ctx = EVP_KDF_CTX_new(kdf); |
| 117 | if (!ctx) die("HKDF ctx"); |
| 118 | p[i++] = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, "SHA256", 0); |
| 119 | p[i++] = OSSL_PARAM_construct_octet_string( |
| 120 | OSSL_KDF_PARAM_KEY, (void *)(ikmlen ? ikm : empty), ikmlen); |
| 121 | p[i++] = OSSL_PARAM_construct_octet_string( |
| 122 | OSSL_KDF_PARAM_SALT, (void *)(saltlen ? salt : empty), saltlen); |
| 123 | p[i++] = OSSL_PARAM_construct_octet_string( |
| 124 | OSSL_KDF_PARAM_INFO, (void *)(infolen ? info : empty), infolen); |
| 125 | p[i] = OSSL_PARAM_construct_end(); |
| 126 | if (EVP_KDF_derive(ctx, out, outlen, p) <= 0) die("HKDF derive"); |
| 127 | EVP_KDF_CTX_free(ctx); |
| 128 | EVP_KDF_free(kdf); |
| 129 | } |
| 130 | |
| 131 | static void hkdf_label(const unsigned char *ikm, size_t ikmlen, |
| 132 | const unsigned char *salt, size_t saltlen, |
| 133 | const char *label, unsigned char *out, size_t outlen) { |
| 134 | hkdf(ikm, ikmlen, salt, saltlen, (const unsigned char *)label, strlen(label), |
| 135 | out, outlen); |
| 136 | } |
| 137 | |
| 138 | static void argon2id(const unsigned char *pass, size_t passlen, |
| 139 | const unsigned char *salt, size_t saltlen, uint32_t m_kib, |
| 140 | uint32_t t, uint32_t lanes, unsigned char *out, |
| 141 | size_t outlen) { |
| 142 | EVP_KDF *kdf = EVP_KDF_fetch(NULL, "ARGON2ID", NULL); |
| 143 | EVP_KDF_CTX *ctx; |
| 144 | OSSL_PARAM p[7]; |
| 145 | uint32_t threads = 1; |
| 146 | int i = 0; |
| 147 | |
| 148 | if (!kdf) die("fetch ARGON2ID (needs OpenSSL 3.2+)"); |
| 149 | ctx = EVP_KDF_CTX_new(kdf); |
| 150 | if (!ctx) die("argon2 ctx"); |
| 151 | p[i++] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_PASSWORD, |
| 152 | (void *)pass, passlen); |
| 153 | p[i++] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, |
| 154 | (void *)salt, saltlen); |
| 155 | p[i++] = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ITER, &t); |
| 156 | p[i++] = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_MEMCOST, &m_kib); |
| 157 | p[i++] = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_LANES, &lanes); |
| 158 | p[i++] = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_THREADS, &threads); |
| 159 | p[i] = OSSL_PARAM_construct_end(); |
| 160 | if (EVP_KDF_derive(ctx, out, outlen, p) <= 0) die("argon2 derive"); |
| 161 | EVP_KDF_CTX_free(ctx); |
| 162 | EVP_KDF_free(kdf); |
| 163 | } |
| 164 | |
| 165 | /* --------------------------------------------------------------- fixtures */ |
| 166 | |
| 167 | /* |
| 168 | * fixture(label, n) — every synthetic input in the file, derived from its own |
| 169 | * published label so the file regenerates from this source alone. |
| 170 | */ |
| 171 | static void fixture(const char *label, unsigned char *out, size_t n) { |
| 172 | char buf[128]; |
| 173 | unsigned char d[32]; |
| 174 | |
| 175 | if (n > 32) die("fixture longer than SHA-256"); |
| 176 | if (snprintf(buf, sizeof buf, "contra/v1/vectors/%s", label) >= (int)sizeof buf) |
| 177 | die("fixture label too long"); |
| 178 | sha256((const unsigned char *)buf, strlen(buf), d); |
| 179 | memcpy(out, d, n); |
| 180 | } |
| 181 | |
| 182 | /* ------------------------------------------------------ P-256 from a seed */ |
| 183 | |
| 184 | #define EC_LABEL "contra/v1/ec-p256" |
| 185 | |
| 186 | struct p256 { |
| 187 | unsigned char cand[8][32]; /* candidates tried, in order */ |
| 188 | int ncand; /* how many, including the accepted one */ |
| 189 | unsigned char d[32]; /* the accepted scalar */ |
| 190 | unsigned char pub[65]; /* SEC1 uncompressed 0x04 || X || Y */ |
| 191 | }; |
| 192 | |
| 193 | /* |
| 194 | * Rejection sampling exactly as pinned in discussion #19: expand the seed |
| 195 | * under "contra/v1/ec-p256" || byte(i), reject anything outside [1, n-1], |
| 196 | * and step i. Rejection has probability ~2^-32, so `p256_rejecting_seed` in |
| 197 | * the vector file is the only reason this loop is ever tested. |
| 198 | */ |
| 199 | static void p256_from_seed(const unsigned char seed[32], struct p256 *r) { |
| 200 | EC_GROUP *g = EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1); |
| 201 | const BIGNUM *n; |
| 202 | EC_POINT *pt; |
| 203 | BIGNUM *d; |
| 204 | unsigned char info[sizeof EC_LABEL]; /* label bytes + counter, no NUL */ |
| 205 | int i; |
| 206 | |
| 207 | if (!g) die("P-256 group"); |
| 208 | n = EC_GROUP_get0_order(g); |
| 209 | memcpy(info, EC_LABEL, sizeof EC_LABEL - 1); |
| 210 | memset(r, 0, sizeof *r); |
| 211 | |
| 212 | for (i = 0; i < 8; i++) { |
| 213 | info[sizeof EC_LABEL - 1] = (unsigned char)i; |
| 214 | hkdf(seed, 32, NULL, 0, info, sizeof info, r->cand[i], 32); |
| 215 | r->ncand = i + 1; |
| 216 | |
| 217 | d = BN_bin2bn(r->cand[i], 32, NULL); |
| 218 | if (!d) die("BN_bin2bn"); |
| 219 | if (!BN_is_zero(d) && BN_cmp(d, n) < 0) { |
| 220 | memcpy(r->d, r->cand[i], 32); |
| 221 | pt = EC_POINT_new(g); |
| 222 | if (!pt || !EC_POINT_mul(g, pt, d, NULL, NULL, NULL)) |
| 223 | die("EC_POINT_mul"); |
| 224 | if (EC_POINT_point2oct(g, pt, POINT_CONVERSION_UNCOMPRESSED, r->pub, |
| 225 | sizeof r->pub, NULL) != sizeof r->pub) |
| 226 | die("point2oct"); |
| 227 | EC_POINT_free(pt); |
| 228 | BN_free(d); |
| 229 | EC_GROUP_free(g); |
| 230 | return; |
| 231 | } |
| 232 | BN_free(d); |
| 233 | } |
| 234 | die("P-256 derivation exhausted 8 candidates"); |
| 235 | } |
| 236 | |
| 237 | /* ------------------------------------------------- sealing to a public key */ |
| 238 | |
| 239 | #define ECIES_LABEL "contra/v1/ecies" |
| 240 | |
| 241 | /* |
| 242 | * The ECDH primitive: the X coordinate of d*P, big-endian at the full field |
| 243 | * width. Y is discarded, per SEC 1 3.3.1 — which is why the anchor below is |
| 244 | * checked against RFC 5903 rather than trusted. |
| 245 | */ |
| 246 | static void ecdh_shared(const unsigned char d[32], const unsigned char peer[65], |
| 247 | unsigned char out[32]) { |
| 248 | EC_GROUP *g = EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1); |
| 249 | EC_POINT *p, *r; |
| 250 | BIGNUM *s, *x; |
| 251 | |
| 252 | if (!g) die("P-256 group"); |
| 253 | p = EC_POINT_new(g); |
| 254 | r = EC_POINT_new(g); |
| 255 | x = BN_new(); |
| 256 | s = BN_bin2bn(d, 32, NULL); |
| 257 | if (!p || !r || !x || !s) die("ecdh alloc"); |
| 258 | if (!EC_POINT_oct2point(g, p, peer, 65, NULL)) die("peer point"); |
| 259 | if (!EC_POINT_mul(g, r, NULL, p, s, NULL)) die("ecdh mul"); |
| 260 | if (EC_POINT_is_at_infinity(g, r)) die("ecdh degenerate"); |
| 261 | if (!EC_POINT_get_affine_coordinates(g, r, x, NULL, NULL)) die("ecdh x"); |
| 262 | if (BN_bn2binpad(x, out, 32) != 32) die("ecdh x width"); |
| 263 | |
| 264 | BN_free(s); |
| 265 | BN_free(x); |
| 266 | EC_POINT_free(r); |
| 267 | EC_POINT_free(p); |
| 268 | EC_GROUP_free(g); |
| 269 | } |
| 270 | |
| 271 | /* |
| 272 | * The AEAD key for a message sealed to a static public key. The sender mints |
| 273 | * a fresh ephemeral pair per message, so the key is new even though the |
| 274 | * recipient's is not. |
| 275 | * |
| 276 | * Both public points go in the HKDF salt, not just the shared secret. The |
| 277 | * shared secret alone names a *relationship*, not a message: two senders who |
| 278 | * somehow reached the same secret would derive the same key, and a recipient |
| 279 | * could be talked into believing a message came from the wrong ephemeral key. |
| 280 | * Binding the pair is the standard fix and costs nothing. |
| 281 | */ |
| 282 | static void ecies_key(const unsigned char eph_pub[65], |
| 283 | const unsigned char recipient_pub[65], |
| 284 | const unsigned char shared[32], unsigned char out[32]) { |
| 285 | unsigned char salt[130]; |
| 286 | |
| 287 | memcpy(salt, eph_pub, 65); |
| 288 | memcpy(salt + 65, recipient_pub, 65); |
| 289 | hkdf(shared, 32, salt, sizeof salt, (const unsigned char *)ECIES_LABEL, |
| 290 | sizeof ECIES_LABEL - 1, out, 32); |
| 291 | } |
| 292 | |
| 293 | /* --------------------------------------------------------- AEAD, with AAD */ |
| 294 | |
| 295 | /* |
| 296 | * The canonical AAD, pinned in discussion #19. Length-prefixed so the tuple |
| 297 | * is injective — plain concatenation lets different tuples produce identical |
| 298 | * AAD, which is the exact transplant the binding exists to stop — and |
| 299 | * domain-tagged so it cannot be replayed into another protocol. |
| 300 | * |
| 301 | * The scope id is the account id for account-scoped objects and the vault id |
| 302 | * for vault-scoped ones, which is how PROTOCOL.md's two spellings of the |
| 303 | * binding — "(account_id, object_type, object_id, version)" and "(vault_id, |
| 304 | * item_id, version)" — turn out to be the same rule. |
| 305 | */ |
| 306 | static size_t build_aad(const unsigned char *scope_id, size_t scope_len, |
| 307 | const char *object_type, const unsigned char *object_id, |
| 308 | size_t object_id_len, uint64_t version, |
| 309 | unsigned char *out, size_t cap) { |
| 310 | size_t n = 0, tlen = strlen(object_type); |
| 311 | int i; |
| 312 | |
| 313 | #define PUT(p, l) \ |
| 314 | do { \ |
| 315 | if (n + (l) > cap) die("AAD buffer"); \ |
| 316 | memcpy(out + n, (p), (l)); \ |
| 317 | n += (l); \ |
| 318 | } while (0) |
| 319 | #define PUT_U32(v) \ |
| 320 | do { \ |
| 321 | unsigned char b4[4]; \ |
| 322 | b4[0] = (unsigned char)((v) >> 24); \ |
| 323 | b4[1] = (unsigned char)((v) >> 16); \ |
| 324 | b4[2] = (unsigned char)((v) >> 8); \ |
| 325 | b4[3] = (unsigned char)(v); \ |
| 326 | PUT(b4, 4); \ |
| 327 | } while (0) |
| 328 | |
| 329 | PUT("contra/v1/aad", 13); |
| 330 | PUT("\0", 1); |
| 331 | PUT_U32((uint32_t)scope_len); |
| 332 | PUT(scope_id, scope_len); |
| 333 | PUT_U32((uint32_t)tlen); |
| 334 | PUT(object_type, tlen); |
| 335 | PUT_U32((uint32_t)object_id_len); |
| 336 | PUT(object_id, object_id_len); |
| 337 | for (i = 7; i >= 0; i--) { |
| 338 | unsigned char b = (unsigned char)(version >> (8 * i)); |
| 339 | PUT(&b, 1); |
| 340 | } |
| 341 | #undef PUT_U32 |
| 342 | #undef PUT |
| 343 | return n; |
| 344 | } |
| 345 | |
| 346 | static void aes256gcm(const unsigned char key[32], const unsigned char nonce[12], |
| 347 | const unsigned char *aad, size_t aadlen, |
| 348 | const unsigned char *pt, size_t ptlen, unsigned char *ct, |
| 349 | unsigned char tag[16]) { |
| 350 | EVP_CIPHER_CTX *c = EVP_CIPHER_CTX_new(); |
| 351 | unsigned char scratch[16]; |
| 352 | int len = 0; |
| 353 | |
| 354 | if (!c) die("gcm ctx"); |
| 355 | if (!EVP_EncryptInit_ex(c, EVP_aes_256_gcm(), NULL, NULL, NULL)) |
| 356 | die("gcm init"); |
| 357 | if (!EVP_CIPHER_CTX_ctrl(c, EVP_CTRL_AEAD_SET_IVLEN, 12, NULL)) |
| 358 | die("gcm ivlen"); |
| 359 | if (!EVP_EncryptInit_ex(c, NULL, NULL, key, nonce)) die("gcm key"); |
| 360 | if (aadlen && !EVP_EncryptUpdate(c, NULL, &len, aad, (int)aadlen)) |
| 361 | die("gcm aad"); |
| 362 | if (ptlen && !EVP_EncryptUpdate(c, ct, &len, pt, (int)ptlen)) |
| 363 | die("gcm update"); |
| 364 | /* GCM is a stream cipher, so Final never emits bytes; give it somewhere |
| 365 | * to write anyway rather than offsetting a possibly-NULL ct. */ |
| 366 | if (!EVP_EncryptFinal_ex(c, scratch, &len)) die("gcm final"); |
| 367 | if (!EVP_CIPHER_CTX_ctrl(c, EVP_CTRL_AEAD_GET_TAG, 16, tag)) die("gcm tag"); |
| 368 | EVP_CIPHER_CTX_free(c); |
| 369 | } |
| 370 | |
| 371 | /* ------------------------------------------------------- Crockford Base32 */ |
| 372 | |
| 373 | static const char CROCKFORD[33] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; |
| 374 | |
| 375 | /* |
| 376 | * Byte-oriented, MSB-first, left-aligned (discussion #19) — 16 bytes become |
| 377 | * 26 symbols with two zero pad bits in the last one. Byte packing rather than |
| 378 | * Crockford's big-integer form because the JS console has a 300-line module |
| 379 | * cap and no business carrying a bignum. |
| 380 | */ |
| 381 | static void crockford_encode(const unsigned char *in, size_t nbytes, char *out) { |
| 382 | size_t nsym = (nbytes * 8 + 4) / 5, i; |
| 383 | |
| 384 | for (i = 0; i < nsym; i++) { |
| 385 | size_t bit = i * 5, byte = bit / 8, off = bit % 8; |
| 386 | unsigned v = (unsigned)in[byte] << 8; |
| 387 | if (byte + 1 < nbytes) v |= in[byte + 1]; |
| 388 | out[i] = CROCKFORD[(v >> (11 - off)) & 0x1f]; |
| 389 | } |
| 390 | out[nsym] = 0; |
| 391 | } |
| 392 | |
| 393 | /* The 20 bits of SHA-256(payload) that become the 4 checksum symbols. */ |
| 394 | static uint32_t checksum20(const unsigned char *payload, size_t n) { |
| 395 | unsigned char d[32]; |
| 396 | sha256(payload, n, d); |
| 397 | return ((uint32_t)d[0] << 12) | ((uint32_t)d[1] << 4) | (uint32_t)(d[2] >> 4); |
| 398 | } |
| 399 | |
| 400 | static void checksum_symbols(uint32_t c20, char out[5]) { |
| 401 | int i; |
| 402 | for (i = 0; i < 4; i++) out[i] = CROCKFORD[(c20 >> (15 - 5 * i)) & 0x1f]; |
| 403 | out[4] = 0; |
| 404 | } |
| 405 | |
| 406 | static int crockford_value(char c) { |
| 407 | const char *p; |
| 408 | if (c >= 'a' && c <= 'z') c = (char)(c - 'a' + 'A'); |
| 409 | if (c == 'O') c = '0'; |
| 410 | if (c == 'I' || c == 'L') c = '1'; |
| 411 | p = strchr(CROCKFORD, c); |
| 412 | return (p && c) ? (int)(p - CROCKFORD) : -1; |
| 413 | } |
| 414 | |
| 415 | /* |
| 416 | * Normalisation: strip hyphens and whitespace, uppercase, O->0, I->1, L->1. |
| 417 | * The tag survives it unchanged, so normalise first and match the tag after. |
| 418 | * Returns 0 if any character is outside the alphabet — `U` is the one people |
| 419 | * ask about, and it is an error, not a repair candidate. |
| 420 | */ |
| 421 | static int crockford_normalize(const char *in, char *out, size_t cap) { |
| 422 | size_t n = 0; |
| 423 | |
| 424 | for (; *in; in++) { |
| 425 | if (*in == '-' || *in == ' ' || *in == '\t' || *in == '\n') continue; |
| 426 | if (crockford_value(*in) < 0) return 0; |
| 427 | if (n + 1 >= cap) return 0; |
| 428 | out[n++] = CROCKFORD[crockford_value(*in)]; |
| 429 | } |
| 430 | out[n] = 0; |
| 431 | return 1; |
| 432 | } |
| 433 | |
| 434 | /* |
| 435 | * Strict decode of 26 symbols back to 16 bytes. A non-zero pad in the last |
| 436 | * symbol is rejected, so every value has exactly one encoding — without that |
| 437 | * two strings decode to one key and the checksum stops meaning what it says. |
| 438 | */ |
| 439 | static int crockford_decode16(const char *sym, unsigned char out[16]) { |
| 440 | unsigned acc = 0; |
| 441 | int nbits = 0, i, o = 0; |
| 442 | |
| 443 | for (i = 0; i < 26; i++) { |
| 444 | int v = crockford_value(sym[i]); |
| 445 | if (v < 0) return 0; |
| 446 | acc = (acc << 5) | (unsigned)v; |
| 447 | nbits += 5; |
| 448 | if (nbits >= 8) { |
| 449 | nbits -= 8; |
| 450 | if (o >= 16) return 0; |
| 451 | out[o++] = (unsigned char)((acc >> nbits) & 0xff); |
| 452 | } |
| 453 | } |
| 454 | return o == 16 && (acc & ((1u << nbits) - 1)) == 0; |
| 455 | } |
| 456 | |
| 457 | /* tag(2) || data(26) || checksum(4), grouped five at a time for printing. */ |
| 458 | static void printed_form(const char *tag, const unsigned char payload[16], |
| 459 | char normalized[33], char printed[40]) { |
| 460 | char data[27], ck[5]; |
| 461 | int i, o = 0; |
| 462 | |
| 463 | crockford_encode(payload, 16, data); |
| 464 | checksum_symbols(checksum20(payload, 16), ck); |
| 465 | snprintf(normalized, 33, "%s%s%s", tag, data, ck); |
| 466 | |
| 467 | memcpy(printed, tag, 2); |
| 468 | o = 2; |
| 469 | for (i = 0; i < 30; i++) { |
| 470 | if (i % 5 == 0) printed[o++] = '-'; |
| 471 | printed[o++] = normalized[2 + i]; |
| 472 | } |
| 473 | printed[o] = 0; |
| 474 | } |
| 475 | |
| 476 | /* A normalised 32-character code is valid when it decodes and its own |
| 477 | * checksum symbols match. Nothing here looks at the tag. */ |
| 478 | static int code_valid(const char *norm, unsigned char payload[16]) { |
| 479 | char want[5]; |
| 480 | if (strlen(norm) != 32) return 0; |
| 481 | if (!crockford_decode16(norm + 2, payload)) return 0; |
| 482 | checksum_symbols(checksum20(payload, 16), want); |
| 483 | return memcmp(norm + 28, want, 4) == 0; |
| 484 | } |
| 485 | |
| 486 | /* |
| 487 | * Repair: try every single-character substitution over the 30 payload |
| 488 | * positions and return every survivor, in position-then-alphabet order. It |
| 489 | * deliberately does not pick one — a 20-bit checksum collides about once in |
| 490 | * 1100 attempts, and silently choosing would be worse than handing the caller |
| 491 | * two candidates to try against the KCV. |
| 492 | */ |
| 493 | static int code_repair(const char *norm, char out[][33], int cap) { |
| 494 | int n = 0, pos, s; |
| 495 | |
| 496 | for (pos = 2; pos < 32; pos++) { |
| 497 | for (s = 0; s < 32; s++) { |
| 498 | char cand[33]; |
| 499 | unsigned char payload[16]; |
| 500 | if (CROCKFORD[s] == norm[pos]) continue; |
| 501 | memcpy(cand, norm, 33); |
| 502 | cand[pos] = CROCKFORD[s]; |
| 503 | if (!code_valid(cand, payload)) continue; |
| 504 | if (n < cap) memcpy(out[n], cand, 33); |
| 505 | n++; |
| 506 | } |
| 507 | } |
| 508 | return n; |
| 509 | } |
| 510 | |
| 511 | /* -------------------------------------------------------- JSON emit sugar */ |
| 512 | |
| 513 | static int need_comma = 0; |
| 514 | static const char *indent = " "; |
| 515 | |
| 516 | static void field(const char *k, const char *fmt, ...) { |
| 517 | va_list ap; |
| 518 | if (need_comma) printf(",\n"); |
| 519 | printf("%s\"%s\": ", indent, k); |
| 520 | va_start(ap, fmt); |
| 521 | vprintf(fmt, ap); |
| 522 | va_end(ap); |
| 523 | need_comma = 1; |
| 524 | } |
| 525 | |
| 526 | /* ------------------------------------------------- external anchor checks */ |
| 527 | |
| 528 | static void expect(const char *what, const unsigned char *got, size_t n, |
| 529 | const char *want_hex) { |
| 530 | if (strcmp(hex(got, n), want_hex) != 0) { |
| 531 | fprintf(stderr, "gen: anchor %s mismatch\n got %s\n want %s\n", what, |
| 532 | hex(got, n), want_hex); |
| 533 | exit(1); |
| 534 | } |
| 535 | } |
| 536 | |
| 537 | /* |
| 538 | * Check OpenSSL against published third-party vectors before it is allowed to |
| 539 | * generate ours. Without this, "independent implementation" only guarantees |
| 540 | * the two implementations are not the same — not that either is right. |
| 541 | */ |
| 542 | static void check_anchors(void) { |
| 543 | unsigned char out[42]; |
| 544 | static const unsigned char rfc5869_ikm[22] = { |
| 545 | 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, |
| 546 | 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b}; |
| 547 | static const unsigned char rfc5869_salt[13] = {0x00, 0x01, 0x02, 0x03, 0x04, |
| 548 | 0x05, 0x06, 0x07, 0x08, 0x09, |
| 549 | 0x0a, 0x0b, 0x0c}; |
| 550 | static const unsigned char rfc5869_info[10] = {0xf0, 0xf1, 0xf2, 0xf3, 0xf4, |
| 551 | 0xf5, 0xf6, 0xf7, 0xf8, 0xf9}; |
| 552 | static const unsigned char rfc4231_key[20] = { |
| 553 | 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, |
| 554 | 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b}; |
| 555 | |
| 556 | /* PHC argon2 reference, Argon2id v=0x13, t=2 m=65536 p=1. */ |
| 557 | argon2id((const unsigned char *)"password", 8, |
| 558 | (const unsigned char *)"somesalt", 8, 65536, 2, 1, out, 32); |
| 559 | expect("PHC Argon2id t=2 m=65536 p=1", out, 32, |
| 560 | "09316115d5cf24ed5a15a31a3ba326e5cf32edc24702987c02b6566f61913cf7"); |
| 561 | |
| 562 | /* Same, at t=3 — the parameters PROTOCOL.md actually pins. */ |
| 563 | argon2id((const unsigned char *)"password", 8, |
| 564 | (const unsigned char *)"somesalt", 8, 65536, 3, 1, out, 32); |
| 565 | |
| 566 | /* RFC 5869 test case 1. */ |
| 567 | hkdf(rfc5869_ikm, sizeof rfc5869_ikm, rfc5869_salt, sizeof rfc5869_salt, |
| 568 | rfc5869_info, sizeof rfc5869_info, out, 42); |
| 569 | expect("RFC 5869 case 1", out, 42, |
| 570 | "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf" |
| 571 | "34007208d5b887185865"); |
| 572 | |
| 573 | /* RFC 5869 test case 3: zero-length salt and info — the shape every |
| 574 | * unsalted CONTRA label uses. */ |
| 575 | hkdf(rfc5869_ikm, sizeof rfc5869_ikm, NULL, 0, NULL, 0, out, 42); |
| 576 | expect("RFC 5869 case 3", out, 42, |
| 577 | "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d" |
| 578 | "9d201395faa4b61a96c8"); |
| 579 | |
| 580 | /* RFC 4231 test case 1, HMAC-SHA-256. */ |
| 581 | hmac_sha256(rfc4231_key, sizeof rfc4231_key, (const unsigned char *)"Hi There", |
| 582 | 8, out); |
| 583 | expect("RFC 4231 case 1", out, 32, |
| 584 | "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"); |
| 585 | |
| 586 | /* NIST CAVP AES-256-GCM, all-zero key/IV, empty plaintext and AAD. */ |
| 587 | { |
| 588 | unsigned char key[32] = {0}, iv[12] = {0}, tag[16]; |
| 589 | aes256gcm(key, iv, NULL, 0, NULL, 0, NULL, tag); |
| 590 | expect("NIST AES-256-GCM empty", tag, 16, |
| 591 | "530f8afbc74536b9a963b4f1c4cb738b"); |
| 592 | } |
| 593 | |
| 594 | /* |
| 595 | * RFC 5903 section 8.1 — ECDH over P-256. The initiator's private scalar |
| 596 | * i against the responder's public point gr, giving the shared X the RFC |
| 597 | * prints as "g^ir". Discarding Y is the whole of the primitive, so this |
| 598 | * anchor is what stops a plausible-looking wrong answer. |
| 599 | */ |
| 600 | { |
| 601 | static const unsigned char rfc5903_i[32] = { |
| 602 | 0xc8, 0x8f, 0x01, 0xf5, 0x10, 0xd9, 0xac, 0x3f, 0x70, 0xa2, 0x92, |
| 603 | 0xda, 0xa2, 0x31, 0x6d, 0xe5, 0x44, 0xe9, 0xaa, 0xb8, 0xaf, 0xe8, |
| 604 | 0x40, 0x49, 0xc6, 0x2a, 0x9c, 0x57, 0x86, 0x2d, 0x14, 0x33, |
| 605 | }; |
| 606 | unsigned char gr[65] = {0x04}; |
| 607 | unsigned char shared[32]; |
| 608 | |
| 609 | if (!hex2bin("d12dfb5289c8d4f81208b70270398c342296970a0bccb74c736fc75" |
| 610 | "54494bf63", |
| 611 | gr + 1, 32) || |
| 612 | !hex2bin("56fbf3ca366cc23e8157854c13c58d6aac23f046ada30f8353e74f3" |
| 613 | "3039872ab", |
| 614 | gr + 33, 32)) |
| 615 | die("rfc5903 responder point"); |
| 616 | ecdh_shared(rfc5903_i, gr, shared); |
| 617 | expect("RFC 5903 ECDH P-256", shared, 32, |
| 618 | "d6840f6b42f6edafd13116e0e12565202fef8e9ece7dce03812464d04b94" |
| 619 | "42de"); |
| 620 | } |
| 621 | } |
| 622 | |
| 623 | /* ------------------------------------------------------------------- main */ |
| 624 | |
| 625 | struct wrap { |
| 626 | const char *name; |
| 627 | const char *object_type; |
| 628 | unsigned char kek[32]; |
| 629 | unsigned char kcv[16]; |
| 630 | unsigned char wrap_id[16]; |
| 631 | unsigned char nonce[12]; |
| 632 | unsigned char aad[256]; |
| 633 | size_t aadlen; |
| 634 | unsigned char ct[32]; |
| 635 | unsigned char tag[16]; |
| 636 | }; |
| 637 | |
| 638 | /* KCV = HMAC-SHA-256(KEK, "contra/v1/kcv")[..16] — AES-GCM is not |
| 639 | * key-committing, so this is the commitment check, and it is also what tells |
| 640 | * "wrong key" from "corrupted blob" on every unlock path. */ |
| 641 | static void kcv_of(const unsigned char kek[32], unsigned char out[16]) { |
| 642 | unsigned char full[32]; |
| 643 | hmac_sha256(kek, 32, (const unsigned char *)"contra/v1/kcv", 13, full); |
| 644 | memcpy(out, full, 16); |
| 645 | } |
| 646 | |
| 647 | static void seal_wrap(struct wrap *w, const unsigned char account_id[16], |
| 648 | const unsigned char rk[32]) { |
| 649 | kcv_of(w->kek, w->kcv); |
| 650 | w->aadlen = build_aad(account_id, 16, w->object_type, w->wrap_id, 16, 1, |
| 651 | w->aad, sizeof w->aad); |
| 652 | aes256gcm(w->kek, w->nonce, w->aad, w->aadlen, rk, 32, w->ct, w->tag); |
| 653 | } |
| 654 | |
| 655 | static void emit_wrap(const struct wrap *w) { |
| 656 | printf(" {\n"); |
| 657 | need_comma = 0; |
| 658 | field("name", "\"%s\"", w->name); |
| 659 | field("object_type", "\"%s\"", w->object_type); |
| 660 | field("kek_hex", "\"%s\"", hex(w->kek, 32)); |
| 661 | field("kcv_hex", "\"%s\"", hex(w->kcv, 16)); |
| 662 | field("wrap_id_hex", "\"%s\"", hex(w->wrap_id, 16)); |
| 663 | field("version", "1"); |
| 664 | field("nonce_hex", "\"%s\"", hex(w->nonce, 12)); |
| 665 | field("aad_hex", "\"%s\"", hex(w->aad, w->aadlen)); |
| 666 | field("ciphertext_hex", "\"%s\"", hex(w->ct, 32)); |
| 667 | field("tag_hex", "\"%s\"", hex(w->tag, 16)); |
| 668 | printf("\n }"); |
| 669 | } |
| 670 | |
| 671 | int main(int argc, char **argv) { |
| 672 | /* The witness k from seedsearch, passed in by the Makefile. */ |
| 673 | unsigned long long reject_k = (argc > 1) ? strtoull(argv[1], NULL, 10) : 0; |
| 674 | |
| 675 | const char *passphrase = "contra-golden-vector-passphrase-do-not-use-8f3a1c"; |
| 676 | unsigned char salt_pw[16], account_key[16], recovery_code[16]; |
| 677 | unsigned char prf_output[32], cred_salt[32], rk[32], k_vault[32]; |
| 678 | unsigned char account_id[16], vault_id[16], item_id[16]; |
| 679 | unsigned char s_pw[32], seed_login[32], seed_rc[32]; |
| 680 | unsigned char item_nonce[12], item_ct[256], item_tag[16], item_aad[256]; |
| 681 | unsigned char reject_seed[32]; |
| 682 | unsigned char ho_seed_r[32], ho_seed_e[32], ho_shared[32], ho_key[32]; |
| 683 | unsigned char ho_nonce[12], ho_aad[256], ho_ct[16], ho_tag[16]; |
| 684 | size_t ho_aadlen; |
| 685 | size_t item_aadlen, item_ptlen; |
| 686 | struct p256 lk_pw, lk_rc, lk_reject, ho_recipient, ho_ephemeral; |
| 687 | struct wrap w_pw, w_rc, w_prf; |
| 688 | char ak_norm[33], ak_print[40], rc_norm[33], rc_print[40]; |
| 689 | char typo[40]; |
| 690 | const char *item_pt = |
| 691 | "{\"title\":\"Generated vector item\",\"username\":\"vector@example." |
| 692 | "invalid\",\"secret\":\"not-a-real-value\"}"; |
| 693 | int i; |
| 694 | |
| 695 | check_anchors(); |
| 696 | |
| 697 | fixture("salt_pw", salt_pw, 16); |
| 698 | fixture("account_key", account_key, 16); |
| 699 | fixture("recovery_code", recovery_code, 16); |
| 700 | fixture("prf_output", prf_output, 32); |
| 701 | fixture("cred_salt", cred_salt, 32); |
| 702 | fixture("rk", rk, 32); |
| 703 | fixture("k_vault", k_vault, 32); |
| 704 | fixture("account_id", account_id, 16); |
| 705 | fixture("vault_id", vault_id, 16); |
| 706 | fixture("item_id", item_id, 16); |
| 707 | fixture("nonce/item", item_nonce, 12); |
| 708 | fixture("handoff/recipient_seed", ho_seed_r, 32); |
| 709 | fixture("handoff/ephemeral_seed", ho_seed_e, 32); |
| 710 | fixture("nonce/handoff", ho_nonce, 12); |
| 711 | |
| 712 | /* The passphrase branch. One expensive Argon2id call, then two peer |
| 713 | * subkeys under distinct labels — holding one says nothing about the |
| 714 | * other without inverting HMAC-SHA-256. */ |
| 715 | argon2id((const unsigned char *)passphrase, strlen(passphrase), salt_pw, 16, |
| 716 | 65536, 3, 1, s_pw, 32); |
| 717 | hkdf_label(s_pw, 32, NULL, 0, "contra/v1/login", seed_login, 32); |
| 718 | p256_from_seed(seed_login, &lk_pw); |
| 719 | |
| 720 | w_pw.name = "W_pw"; |
| 721 | w_pw.object_type = "wrap.pw"; |
| 722 | hkdf_label(s_pw, 32, account_key, 16, "contra/v1/kek", w_pw.kek, 32); |
| 723 | fixture("wrap_id/pw", w_pw.wrap_id, 16); |
| 724 | fixture("nonce/wrap_pw", w_pw.nonce, 12); |
| 725 | seal_wrap(&w_pw, account_id, rk); |
| 726 | |
| 727 | /* The recovery branch. HKDF, not Argon2id: the input is already |
| 728 | * full-entropy, and this path must be the least-rotted code we own. */ |
| 729 | hkdf_label(recovery_code, 16, NULL, 0, "contra/v1/rc-login", seed_rc, 32); |
| 730 | p256_from_seed(seed_rc, &lk_rc); |
| 731 | |
| 732 | w_rc.name = "W_rc"; |
| 733 | w_rc.object_type = "wrap.rc"; |
| 734 | hkdf_label(recovery_code, 16, NULL, 0, "contra/v1/rc-kek", w_rc.kek, 32); |
| 735 | fixture("wrap_id/rc", w_rc.wrap_id, 16); |
| 736 | fixture("nonce/wrap_rc", w_rc.nonce, 12); |
| 737 | seal_wrap(&w_rc, account_id, rk); |
| 738 | |
| 739 | /* The passkey branch — a third peer wrap, never a shortcut around RK. */ |
| 740 | w_prf.name = "W_prf"; |
| 741 | w_prf.object_type = "wrap.prf"; |
| 742 | hkdf_label(prf_output, 32, cred_salt, 32, "contra/v1/prf-kek", w_prf.kek, 32); |
| 743 | fixture("wrap_id/prf", w_prf.wrap_id, 16); |
| 744 | fixture("nonce/wrap_prf", w_prf.nonce, 12); |
| 745 | seal_wrap(&w_prf, account_id, rk); |
| 746 | |
| 747 | /* An item, sealed under the vault key and scoped to the vault. */ |
| 748 | item_ptlen = strlen(item_pt); |
| 749 | item_aadlen = build_aad(vault_id, 16, "item", item_id, 16, 1, item_aad, |
| 750 | sizeof item_aad); |
| 751 | aes256gcm(k_vault, item_nonce, item_aad, item_aadlen, |
| 752 | (const unsigned char *)item_pt, item_ptlen, item_ct, item_tag); |
| 753 | |
| 754 | /* |
| 755 | * The handoff: an Account Key sealed to a fresh device's ephemeral public |
| 756 | * key, which is the one place in the protocol where a secret travels |
| 757 | * through the server. Both keypairs come from seeds here only so the |
| 758 | * vector is reproducible — in the app both are freshly random. |
| 759 | * |
| 760 | * The object id in the AAD is the ephemeral public key itself, so the |
| 761 | * envelope is bound to the device it was sealed for and cannot be |
| 762 | * replayed at a second one. |
| 763 | */ |
| 764 | p256_from_seed(ho_seed_r, &ho_recipient); |
| 765 | p256_from_seed(ho_seed_e, &ho_ephemeral); |
| 766 | ecdh_shared(ho_ephemeral.d, ho_recipient.pub, ho_shared); |
| 767 | ecies_key(ho_ephemeral.pub, ho_recipient.pub, ho_shared, ho_key); |
| 768 | ho_aadlen = build_aad(account_id, 16, "handoff", ho_recipient.pub, 65, 1, |
| 769 | ho_aad, sizeof ho_aad); |
| 770 | aes256gcm(ho_key, ho_nonce, ho_aad, ho_aadlen, account_key, 16, ho_ct, |
| 771 | ho_tag); |
| 772 | |
| 773 | /* The receiving device derives the same key from the other side. */ |
| 774 | { |
| 775 | unsigned char back[32]; |
| 776 | ecdh_shared(ho_recipient.d, ho_ephemeral.pub, back); |
| 777 | if (memcmp(back, ho_shared, 32) != 0) die("ECDH is not symmetric"); |
| 778 | } |
| 779 | |
| 780 | /* The seed that forces the rejection branch. */ |
| 781 | memcpy(reject_seed, "contra/v1/vector-search!", 24); |
| 782 | for (i = 0; i < 8; i++) |
| 783 | reject_seed[24 + i] = (unsigned char)(reject_k >> (56 - 8 * i)); |
| 784 | p256_from_seed(reject_seed, &lk_reject); |
| 785 | if (reject_k && lk_reject.ncand < 2) |
| 786 | die("witness k does not reject at i=0 — rerun seedsearch"); |
| 787 | |
| 788 | printed_form("A1", account_key, ak_norm, ak_print); |
| 789 | printed_form("C1", recovery_code, rc_norm, rc_print); |
| 790 | |
| 791 | /* ------------------------------------------------------------ output */ |
| 792 | |
| 793 | printf("{\n"); |
| 794 | printf(" \"format\": \"contra-golden-vectors\",\n"); |
| 795 | printf(" \"version\": 1,\n"); |
| 796 | printf(" \"spec\": \"PROTOCOL.md; wire formats pinned in discussion 19\",\n"); |
| 797 | printf(" \"generator\": \"vectors/gen/gen.c against %s\",\n", |
| 798 | OPENSSL_VERSION_TEXT); |
| 799 | printf(" \"note\": \"Every input here is generated: fixture(label) = " |
| 800 | "SHA-256(\\\"contra/v1/vectors/\\\" || label). Nothing in this file " |
| 801 | "is a real secret, and nothing in it may be reused as one.\",\n"); |
| 802 | printf(" \"binding\": \"The Go, JS and Swift crypto suites must all pass " |
| 803 | "this file. Changing a value here is changing the protocol.\",\n"); |
| 804 | |
| 805 | printf(" \"external_anchors\": {\n"); |
| 806 | printf(" \"note\": \"Published third-party vectors. gen.c recomputes " |
| 807 | "each one and aborts on mismatch before generating anything " |
| 808 | "below.\",\n"); |
| 809 | printf(" \"sources\": [\"PHC argon2 reference vectors\", \"RFC 5869 " |
| 810 | "cases 1 and 3\", \"RFC 4231 case 1\", \"NIST CAVP AES-256-GCM\"]\n"); |
| 811 | printf(" },\n"); |
| 812 | |
| 813 | printf(" \"argon2id\": {\n"); |
| 814 | printf(" \"params\": {\"m_kib\": 65536, \"t\": 3, \"p\": 1, " |
| 815 | "\"tag_len\": 32},\n"); |
| 816 | printf(" \"floor\": {\"m_kib\": 65536, \"t\": 3, \"note\": \"clients " |
| 817 | "refuse anything weaker a server offers\"},\n"); |
| 818 | printf(" \"cases\": [\n"); |
| 819 | printf(" {\n"); |
| 820 | need_comma = 0; |
| 821 | field("name", "\"S_pw\""); |
| 822 | field("passphrase_utf8", "\"%s\"", passphrase); |
| 823 | field("passphrase_hex", "\"%s\"", |
| 824 | hex((const unsigned char *)passphrase, strlen(passphrase))); |
| 825 | field("salt_hex", "\"%s\"", hex(salt_pw, 16)); |
| 826 | field("out_hex", "\"%s\"", hex(s_pw, 32)); |
| 827 | printf("\n }\n ]\n },\n"); |
| 828 | |
| 829 | printf(" \"hkdf\": {\n"); |
| 830 | printf(" \"construction\": \"RFC 5869 extract-then-expand, SHA-256; an " |
| 831 | "absent salt is zero-length; info is the label's ASCII bytes with " |
| 832 | "no NUL\",\n"); |
| 833 | printf(" \"cases\": [\n"); |
| 834 | { |
| 835 | struct { |
| 836 | const char *name, *label; |
| 837 | const unsigned char *ikm; |
| 838 | size_t ikmlen; |
| 839 | const unsigned char *salt; |
| 840 | size_t saltlen; |
| 841 | const unsigned char *out; |
| 842 | } cases[] = { |
| 843 | {"seed_login", "contra/v1/login", s_pw, 32, NULL, 0, seed_login}, |
| 844 | {"KEK_pw", "contra/v1/kek", s_pw, 32, account_key, 16, w_pw.kek}, |
| 845 | {"seed_rc_login", "contra/v1/rc-login", recovery_code, 16, NULL, 0, |
| 846 | seed_rc}, |
| 847 | {"KEK_rc", "contra/v1/rc-kek", recovery_code, 16, NULL, 0, w_rc.kek}, |
| 848 | {"KEK_prf", "contra/v1/prf-kek", prf_output, 32, cred_salt, 32, |
| 849 | w_prf.kek}, |
| 850 | }; |
| 851 | for (i = 0; i < (int)(sizeof cases / sizeof cases[0]); i++) { |
| 852 | printf(" {\n"); |
| 853 | need_comma = 0; |
| 854 | field("name", "\"%s\"", cases[i].name); |
| 855 | field("info", "\"%s\"", cases[i].label); |
| 856 | field("ikm_hex", "\"%s\"", hex(cases[i].ikm, cases[i].ikmlen)); |
| 857 | field("salt_hex", "\"%s\"", |
| 858 | cases[i].saltlen ? hex(cases[i].salt, cases[i].saltlen) : ""); |
| 859 | field("l", "32"); |
| 860 | field("okm_hex", "\"%s\"", hex(cases[i].out, 32)); |
| 861 | printf("\n }%s\n", |
| 862 | i + 1 < (int)(sizeof cases / sizeof cases[0]) ? "," : ""); |
| 863 | } |
| 864 | } |
| 865 | printf(" ]\n },\n"); |
| 866 | |
| 867 | printf(" \"kcv\": {\n"); |
| 868 | printf(" \"construction\": \"HMAC-SHA-256(KEK, \\\"contra/v1/kcv\\\")" |
| 869 | "[..16]\",\n"); |
| 870 | printf(" \"cases\": [\n"); |
| 871 | printf(" {\"name\": \"KCV_pw\", \"kek_hex\": \"%s\", \"kcv_hex\": " |
| 872 | "\"%s\"},\n", |
| 873 | hex(w_pw.kek, 32), hex(w_pw.kcv, 16)); |
| 874 | printf(" {\"name\": \"KCV_rc\", \"kek_hex\": \"%s\", \"kcv_hex\": " |
| 875 | "\"%s\"},\n", |
| 876 | hex(w_rc.kek, 32), hex(w_rc.kcv, 16)); |
| 877 | printf(" {\"name\": \"KCV_prf\", \"kek_hex\": \"%s\", \"kcv_hex\": " |
| 878 | "\"%s\"}\n", |
| 879 | hex(w_prf.kek, 32), hex(w_prf.kcv, 16)); |
| 880 | printf(" ]\n },\n"); |
| 881 | |
| 882 | printf(" \"p256_from_seed\": {\n"); |
| 883 | printf(" \"construction\": \"candidate_i = HKDF-SHA-256(ikm=seed, " |
| 884 | "salt=\\\"\\\", info=\\\"%s\\\" || byte(i)); accept when 1 <= " |
| 885 | "OS2IP(candidate) <= n-1, else i += 1\",\n", |
| 886 | EC_LABEL); |
| 887 | printf(" \"public_key_encoding\": \"SEC1 uncompressed, 0x04 || X || " |
| 888 | "Y\",\n"); |
| 889 | printf(" \"cases\": [\n"); |
| 890 | { |
| 891 | struct { |
| 892 | const char *name; |
| 893 | const unsigned char *seed; |
| 894 | const struct p256 *r; |
| 895 | } cases[] = { |
| 896 | {"LK_pw", seed_login, &lk_pw}, |
| 897 | {"LK_rc", seed_rc, &lk_rc}, |
| 898 | {"rejects_at_i0", reject_seed, &lk_reject}, |
| 899 | }; |
| 900 | int c; |
| 901 | for (c = 0; c < 3; c++) { |
| 902 | printf(" {\n"); |
| 903 | need_comma = 0; |
| 904 | field("name", "\"%s\"", cases[c].name); |
| 905 | field("seed_hex", "\"%s\"", hex(cases[c].seed, 32)); |
| 906 | field("candidates_tried", "%d", cases[c].r->ncand); |
| 907 | printf(",\n \"candidates\": ["); |
| 908 | for (i = 0; i < cases[c].r->ncand; i++) |
| 909 | printf("%s\"%s\"", i ? ", " : "", hex(cases[c].r->cand[i], 32)); |
| 910 | printf("]"); |
| 911 | need_comma = 1; |
| 912 | field("d_hex", "\"%s\"", hex(cases[c].r->d, 32)); |
| 913 | field("public_uncompressed_hex", "\"%s\"", hex(cases[c].r->pub, 65)); |
| 914 | printf("\n }%s\n", c < 2 ? "," : ""); |
| 915 | } |
| 916 | } |
| 917 | printf(" ]\n },\n"); |
| 918 | printf(" \"p256_rejecting_seed\": {\n"); |
| 919 | printf(" \"note\": \"A rejection has probability about 2^-32, so this " |
| 920 | "seed was searched for. Without it the rejection branch is dead " |
| 921 | "code in all three languages.\",\n"); |
| 922 | printf(" \"seed_recipe\": \"\\\"contra/v1/vector-search!\\\" || " |
| 923 | "u64be(k)\",\n"); |
| 924 | printf(" \"k\": %llu,\n", reject_k); |
| 925 | printf(" \"seed_hex\": \"%s\"\n", hex(reject_seed, 32)); |
| 926 | printf(" },\n"); |
| 927 | |
| 928 | printf(" \"crockford\": {\n"); |
| 929 | printf(" \"alphabet\": \"%s\",\n", CROCKFORD); |
| 930 | printf(" \"packing\": \"MSB-first over the raw bytes, left-aligned; 16 " |
| 931 | "bytes -> 26 symbols with two zero pad bits. Decoding rejects a " |
| 932 | "non-zero pad, so every value has exactly one encoding.\",\n"); |
| 933 | printf(" \"normalize\": \"strip '-' and whitespace, uppercase, O->0, " |
| 934 | "I->1, L->1; U is not in the alphabet and is an error\",\n"); |
| 935 | printf(" \"encode_cases\": [\n"); |
| 936 | { |
| 937 | unsigned char z[16], f[16]; |
| 938 | char s[27]; |
| 939 | memset(z, 0x00, 16); |
| 940 | memset(f, 0xff, 16); |
| 941 | crockford_encode(z, 16, s); |
| 942 | printf(" {\"bytes_hex\": \"%s\", \"symbols\": \"%s\"},\n", |
| 943 | hex(z, 16), s); |
| 944 | crockford_encode(f, 16, s); |
| 945 | printf(" {\"bytes_hex\": \"%s\", \"symbols\": \"%s\"},\n", |
| 946 | hex(f, 16), s); |
| 947 | crockford_encode(account_key, 16, s); |
| 948 | printf(" {\"bytes_hex\": \"%s\", \"symbols\": \"%s\"},\n", |
| 949 | hex(account_key, 16), s); |
| 950 | crockford_encode(recovery_code, 16, s); |
| 951 | printf(" {\"bytes_hex\": \"%s\", \"symbols\": \"%s\"}\n", |
| 952 | hex(recovery_code, 16), s); |
| 953 | } |
| 954 | printf(" ],\n"); |
| 955 | printf(" \"normalize_cases\": [\n"); |
| 956 | { |
| 957 | static const char *inputs[] = {"c1-oi1l-0O1I", " A1 oL Li ", |
| 958 | "c1-abc-def", "C1-UUUU", "c1?abc"}; |
| 959 | char norm[64]; |
| 960 | int k; |
| 961 | for (k = 0; k < 5; k++) { |
| 962 | int ok = crockford_normalize(inputs[k], norm, sizeof norm); |
| 963 | printf(" {\"input\": \"%s\", ", inputs[k]); |
| 964 | if (ok) |
| 965 | printf("\"normalized\": \"%s\"}", norm); |
| 966 | else |
| 967 | printf("\"error\": \"character outside the Crockford " |
| 968 | "alphabet\"}"); |
| 969 | printf("%s\n", k < 4 ? "," : ""); |
| 970 | } |
| 971 | } |
| 972 | printf(" ],\n"); |
| 973 | printf(" \"decode_cases\": [\n"); |
| 974 | { |
| 975 | char sym[27]; |
| 976 | unsigned char rt[16]; |
| 977 | char bad[27]; |
| 978 | crockford_encode(recovery_code, 16, sym); |
| 979 | printf(" {\"symbols\": \"%s\", \"bytes_hex\": \"%s\"},\n", sym, |
| 980 | crockford_decode16(sym, rt) ? hex(rt, 16) : "DECODE FAILED"); |
| 981 | /* Same value, last symbol nudged so the two pad bits are no longer |
| 982 | * zero. Strict decoding must refuse it. */ |
| 983 | memcpy(bad, sym, sizeof bad); |
| 984 | bad[25] = CROCKFORD[(crockford_value(bad[25]) + 1) & 0x1f]; |
| 985 | printf(" {\"symbols\": \"%s\", \"error\": \"non-zero pad bits in " |
| 986 | "the final symbol\", \"rejected\": %s}\n", |
| 987 | bad, crockford_decode16(bad, rt) ? "false" : "true"); |
| 988 | } |
| 989 | printf(" ]\n },\n"); |
| 990 | |
| 991 | printf(" \"printed_code\": {\n"); |
| 992 | printf(" \"form\": \"tag(2) || data(26) || checksum(4), printed as " |
| 993 | "TAG-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX\",\n"); |
| 994 | printf(" \"checksum\": \"first 20 bits, MSB-first, of " |
| 995 | "SHA-256(payload_bytes) -- over the 16 raw bytes, not the " |
| 996 | "string\",\n"); |
| 997 | printf(" \"cases\": [\n"); |
| 998 | printf(" {\"name\": \"account_key\", \"tag\": \"A1\", " |
| 999 | "\"payload_hex\": \"%s\", \"checksum20\": %u, \"normalized\": " |
| 1000 | "\"%s\", \"printed\": \"%s\"},\n", |
| 1001 | hex(account_key, 16), checksum20(account_key, 16), ak_norm, ak_print); |
| 1002 | printf(" {\"name\": \"recovery_code\", \"tag\": \"C1\", " |
| 1003 | "\"payload_hex\": \"%s\", \"checksum20\": %u, \"normalized\": " |
| 1004 | "\"%s\", \"printed\": \"%s\"}\n", |
| 1005 | hex(recovery_code, 16), checksum20(recovery_code, 16), rc_norm, |
| 1006 | rc_print); |
| 1007 | printf(" ],\n"); |
| 1008 | |
| 1009 | /* One substituted character, for the self-healing repair path. The |
| 1010 | * candidate list is what a client tries against the KCV; the repair |
| 1011 | * never picks one on its own, because 20 bits collides. */ |
| 1012 | { |
| 1013 | int pos = 7; /* a data position, well inside the printed groups */ |
| 1014 | char cands[8][33]; |
| 1015 | unsigned char payload[16]; |
| 1016 | int ncand, k; |
| 1017 | |
| 1018 | strcpy(typo, rc_norm); |
| 1019 | typo[pos] = CROCKFORD[(crockford_value(typo[pos]) + 9) & 0x1f]; |
| 1020 | if (code_valid(typo, payload)) die("typo case still validates"); |
| 1021 | ncand = code_repair(typo, cands, 8); |
| 1022 | |
| 1023 | printf(" \"repair\": {\n"); |
| 1024 | printf(" \"note\": \"substitute one character, then brute-force " |
| 1025 | "all 30 payload positions x 32 symbols against the checksum; " |
| 1026 | "return every survivor in position-then-alphabet order for the " |
| 1027 | "caller to try against the KCV\",\n"); |
| 1028 | printf(" \"correct_normalized\": \"%s\",\n", rc_norm); |
| 1029 | printf(" \"typo_normalized\": \"%s\",\n", typo); |
| 1030 | printf(" \"typo_position\": %d,\n", pos); |
| 1031 | printf(" \"candidates\": ["); |
| 1032 | for (k = 0; k < ncand && k < 8; k++) |
| 1033 | printf("%s\"%s\"", k ? ", " : "", cands[k]); |
| 1034 | printf("],\n"); |
| 1035 | printf(" \"candidate_count\": %d,\n", ncand); |
| 1036 | { |
| 1037 | int found = 0; |
| 1038 | for (k = 0; k < ncand && k < 8; k++) |
| 1039 | if (strcmp(cands[k], rc_norm) == 0) found = 1; |
| 1040 | if (!found) die("repair lost the correct code"); |
| 1041 | printf(" \"correct_is_a_candidate\": true\n"); |
| 1042 | } |
| 1043 | printf(" }\n },\n"); |
| 1044 | } |
| 1045 | |
| 1046 | printf(" \"aead\": {\n"); |
| 1047 | printf(" \"algorithm\": \"AES-256-GCM, 96-bit nonce, 128-bit tag\",\n"); |
| 1048 | printf(" \"aad\": \"\\\"contra/v1/aad\\\" || 0x00 || " |
| 1049 | "u32be(len(scope_id)) || scope_id || u32be(len(object_type)) || " |
| 1050 | "object_type || u32be(len(object_id)) || object_id || " |
| 1051 | "u64be(version)\",\n"); |
| 1052 | printf(" \"scope_id\": \"the account id for account-scoped objects, the " |
| 1053 | "vault id for vault-scoped ones\",\n"); |
| 1054 | printf(" \"account_id_hex\": \"%s\",\n", hex(account_id, 16)); |
| 1055 | printf(" \"vault_id_hex\": \"%s\",\n", hex(vault_id, 16)); |
| 1056 | printf(" \"rk_hex\": \"%s\",\n", hex(rk, 32)); |
| 1057 | printf(" \"wraps\": [\n"); |
| 1058 | emit_wrap(&w_pw); |
| 1059 | printf(",\n"); |
| 1060 | emit_wrap(&w_rc); |
| 1061 | printf(",\n"); |
| 1062 | emit_wrap(&w_prf); |
| 1063 | printf("\n ],\n"); |
| 1064 | printf(" \"item\": {\n"); |
| 1065 | need_comma = 0; |
| 1066 | field("scope", "\"vault\""); |
| 1067 | field("vault_id_hex", "\"%s\"", hex(vault_id, 16)); |
| 1068 | field("object_type", "\"item\""); |
| 1069 | field("item_id_hex", "\"%s\"", hex(item_id, 16)); |
| 1070 | field("version", "1"); |
| 1071 | field("k_vault_hex", "\"%s\"", hex(k_vault, 32)); |
| 1072 | field("nonce_hex", "\"%s\"", hex(item_nonce, 12)); |
| 1073 | field("aad_hex", "\"%s\"", hex(item_aad, item_aadlen)); |
| 1074 | field("plaintext_utf8", "\"%s\"", |
| 1075 | "{\\\"title\\\":\\\"Generated vector item\\\",\\\"username\\\":" |
| 1076 | "\\\"vector@example.invalid\\\",\\\"secret\\\":" |
| 1077 | "\\\"not-a-real-value\\\"}"); |
| 1078 | field("plaintext_hex", "\"%s\"", |
| 1079 | hex((const unsigned char *)item_pt, item_ptlen)); |
| 1080 | field("ciphertext_hex", "\"%s\"", hex(item_ct, item_ptlen)); |
| 1081 | field("tag_hex", "\"%s\"", hex(item_tag, 16)); |
| 1082 | printf("\n }\n },\n"); |
| 1083 | |
| 1084 | printf(" \"handoff\": {\n"); |
| 1085 | printf(" \"note\": \"PROTOCOL.md rung 1: an enrolled device seals the " |
| 1086 | "Account Key to a fresh device's ephemeral public key, and the " |
| 1087 | "server relays a blob it cannot open\",\n"); |
| 1088 | printf(" \"shared_secret\": \"X(d*P), big-endian at the field width; Y " |
| 1089 | "discarded, per SEC 1 3.3.1\",\n"); |
| 1090 | printf(" \"key\": \"HKDF-SHA-256(ikm=shared_secret, " |
| 1091 | "salt=ephemeral_public || recipient_public, " |
| 1092 | "info=\\\"contra/v1/ecies\\\", L=32)\",\n"); |
| 1093 | printf(" \"envelope\": \"ephemeral_public(65) || nonce(12) || " |
| 1094 | "ciphertext || tag(16)\",\n"); |
| 1095 | need_comma = 0; |
| 1096 | indent = " "; |
| 1097 | field("recipient_seed_hex", "\"%s\"", hex(ho_seed_r, 32)); |
| 1098 | field("recipient_scalar_hex", "\"%s\"", hex(ho_recipient.d, 32)); |
| 1099 | field("recipient_public_hex", "\"%s\"", hex(ho_recipient.pub, 65)); |
| 1100 | field("ephemeral_seed_hex", "\"%s\"", hex(ho_seed_e, 32)); |
| 1101 | field("ephemeral_scalar_hex", "\"%s\"", hex(ho_ephemeral.d, 32)); |
| 1102 | field("ephemeral_public_hex", "\"%s\"", hex(ho_ephemeral.pub, 65)); |
| 1103 | field("shared_secret_hex", "\"%s\"", hex(ho_shared, 32)); |
| 1104 | field("key_hex", "\"%s\"", hex(ho_key, 32)); |
| 1105 | field("account_id_hex", "\"%s\"", hex(account_id, 16)); |
| 1106 | field("object_type", "\"handoff\""); |
| 1107 | field("version", "1"); |
| 1108 | field("nonce_hex", "\"%s\"", hex(ho_nonce, 12)); |
| 1109 | field("aad_hex", "\"%s\"", hex(ho_aad, ho_aadlen)); |
| 1110 | field("plaintext_hex", "\"%s\"", hex(account_key, 16)); |
| 1111 | field("ciphertext_hex", "\"%s\"", hex(ho_ct, 16)); |
| 1112 | field("tag_hex", "\"%s\"", hex(ho_tag, 16)); |
| 1113 | printf("\n },\n"); |
| 1114 | |
| 1115 | printf(" \"account_chain\": {\n"); |
| 1116 | printf(" \"note\": \"the whole of PROTOCOL.md's 'The shape', end to end, " |
| 1117 | "so a new implementation can bisect where it diverges\",\n"); |
| 1118 | need_comma = 0; |
| 1119 | indent = " "; |
| 1120 | field("passphrase_utf8", "\"%s\"", passphrase); |
| 1121 | field("salt_pw_hex", "\"%s\"", hex(salt_pw, 16)); |
| 1122 | field("account_key_hex", "\"%s\"", hex(account_key, 16)); |
| 1123 | field("recovery_code_hex", "\"%s\"", hex(recovery_code, 16)); |
| 1124 | field("prf_output_hex", "\"%s\"", hex(prf_output, 32)); |
| 1125 | field("cred_salt_hex", "\"%s\"", hex(cred_salt, 32)); |
| 1126 | field("s_pw_hex", "\"%s\"", hex(s_pw, 32)); |
| 1127 | field("seed_login_hex", "\"%s\"", hex(seed_login, 32)); |
| 1128 | field("lk_pw_public_hex", "\"%s\"", hex(lk_pw.pub, 65)); |
| 1129 | field("seed_rc_login_hex", "\"%s\"", hex(seed_rc, 32)); |
| 1130 | field("lk_rc_public_hex", "\"%s\"", hex(lk_rc.pub, 65)); |
| 1131 | field("kek_pw_hex", "\"%s\"", hex(w_pw.kek, 32)); |
| 1132 | field("kek_rc_hex", "\"%s\"", hex(w_rc.kek, 32)); |
| 1133 | field("kek_prf_hex", "\"%s\"", hex(w_prf.kek, 32)); |
| 1134 | field("rk_hex", "\"%s\"", hex(rk, 32)); |
| 1135 | printf("\n }\n}\n"); |
| 1136 | |
| 1137 | return 0; |
| 1138 | } |
| 1139 | |