carlos / contra Public

Clone
git clone https://amadan.net/carlos/contra

Plain git — no account needed to clone.

Download

Download this file

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
43static 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. */
49static 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. */
63static 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
75static 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
80static 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 */
105static 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
131static 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
138static 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 */
171static 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
186struct 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 */
199static 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 */
246static 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 */
282static 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 */
306static 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
346static 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
373static 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 */
381static 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. */
394static 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
400static 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
406static 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 */
421static 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 */
439static 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. */
458static 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. */
478static 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 */
493static 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
513static int need_comma = 0;
514static const char *indent = " ";
515
516static 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
528static 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 */
542static 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
625struct 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. */
641static 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
647static 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
655static 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
671int 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