agentknock 0.1.0

Developer secrets on your phone, provided only to approved commands.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
# Agentknock v1 cryptosystem

## Abstract

Agentknock lets a client and a device exchange protected application messages
through an untrusted relay. This document specifies the cryptography for
pairing the endpoints, protecting request-response-completion exchanges, and
rotating the pairing pre-shared key (PSK).

The construction uses HPKE base mode during initial pairing, an out-of-band
short authentication string (SAS) to authenticate that pairing, and HPKE PSK
mode after pairing. Responses use a key and nonce derived from the request HPKE
context. Completions use the second record in that same HPKE context.

This document fixes every cryptographic algorithm, input length,
domain-separation label, context value, and message-order invariant required
for an interoperable implementation of `agentknock-v1`.

## 1. Scope

Protected application plaintexts are opaque byte strings. Their schemas,
serialization, and semantics are outside this document. The cryptosystem
protects the exact bytes supplied to it.

Transport behavior, application behavior, and platform-specific storage are
also outside this document except where they impose a cryptographic state or
uniqueness requirement.

The transport supplies the identifiers and cryptographic objects named below as
untrusted inputs. Values in the clear initial pairing exchange are authenticated
only by the later SAS confirmation. No trust is placed in the transport for
confidentiality, integrity, or endpoint authentication.

## 2. Roles, terminology, and threat model

### 2.1. Roles and terminology

The cryptosystem has three roles:

- The **client** initiates pairing and all later protected exchanges.
- The **device** owns a stable X25519 key pair, receives requests, and returns
  responses.
- The **relay** transports messages between them.

Pairing creates a **binding**: a long-lived association between one device
identity and one client identity that holds their shared `client_psk`. A
pending binding can be used only to finish pairing; an active binding can be
used for ordinary paired exchanges.

### 2.2. Construction overview

Protocol messages shown below travel through the untrusted relay; the arrows
indicate only their logical direction. SAS confirmation is the exception and
occurs out of band.

```text
Initial pairing and activation

Client  -> Device   clear pairing request
Client  <- Device   clear pairing response
Client  -> Device   base-mode pairing completion
Client <-> Device   out-of-band SAS confirmation
Client  -> Device   PSK-mode activation request
Client  <- Device   protected activation response
Client  -> Device   protected activation completion

Ordinary paired exchange

Client  -> Device   paired request
Client  <- Device   paired response
Client  -> Device   optional paired completion
```

The initial base-mode exchange establishes `client_psk`; SAS confirmation
authenticates the binding. The activation exchange confirms possession of that
PSK at both endpoints. Later exchanges use PSK mode. Rotation derives a
successor PSK and carries the corresponding encapsulated key in every new
paired request until the rotation is confirmed.

### 2.3. Threat model

The relay is a fully active adversary. It can observe, delay, drop, replay,
reorder, modify, and fabricate messages. It can always deny service and perform
traffic analysis. Before SAS verification, it can also replace the clear
pairing response, including the device public key.

The device is the trusted endpoint. Its implementation, random-number
generator, long-term private key, and cryptographic state are assumed to behave
as specified. Compromise of the device is outside the protection offered by
this cryptosystem.

For the confidentiality and authenticity of one honest exchange, the client
process, its random-number generator, and its live exchange state are assumed
to behave as specified for the duration of that exchange. Durable client state
has a weaker trust assumption: it can be disclosed, copied, lost, or rolled
back. The consequences of those events are part of the security analysis; the
cryptosystem does not turn general-purpose client storage into a trusted
keystore.

The device treats all client input as adversarial, including input produced by
a client that possesses a valid PSK. Such a client can act with the authority
of its own binding and can deny service to that binding. It must not be able to
make the device encrypt different plaintexts under the same key and nonce,
process more than one accepted record for one protocol slot, roll back
cryptographic state, expose the device private key, or alter another client
binding. The uniqueness and state-transition rules in this document are
device-side requirements even when the client is malicious.

## 3. Notation

The following notation is used:

| Notation | Meaning |
| --- | --- |
| `ASCII(s)` | The ASCII bytes of string `s`, with no terminator. |
| `zero(n)` | A byte string of `n` zero bytes. |
| `random(n)` | `n` bytes generated by a cryptographically secure random-number generator. |
| <code>a &#124;&#124; b</code> | Concatenation of byte strings `a` and `b`. |
| `empty` | The zero-length byte string. |
| `OS2IP-BE(x)` | The unsigned integer represented by `x` in big-endian byte order. |

This document uses:

```text
HKDF-SHA256(salt, IKM, info, L) =
    HKDF-Expand(
        HKDF-Extract(salt, IKM),
        info,
        L
    )
```

The argument order above is normative. `salt` is the HKDF salt and `IKM` is
the input keying material.

HPKE operations use the notation from RFC 9180. In particular:

```text
(enc, context) = SetupBaseS(pkR, info)
context         = SetupBaseR(enc, skR, info)

(enc, context) = SetupPSKS(pkR, info, psk, psk_id)
context         = SetupPSKR(enc, skR, info, psk, psk_id)

ciphertext = context.Seal(aad, plaintext)
plaintext  = context.Open(aad, ciphertext)
secret     = context.Export(exporter_context, length)
```

HPKE record sequence numbers start at zero as specified by RFC 9180.

## 4. Cryptographic suite

Agentknock v1 uses the following fixed HPKE suite:

| Component | RFC 9180 identifier | Selection | Relevant sizes |
| --- | ---: | --- | --- |
| KEM | `0x0020` | DHKEM(X25519, HKDF-SHA256) | 32-byte public, private, and encapsulated keys |
| KDF | `0x0001` | HKDF-SHA256 | `Nh = 32` bytes |
| AEAD | `0x0003` | ChaCha20Poly1305 | 32-byte key, 12-byte nonce, 16-byte tag |

Initial pairing uses HPKE base mode. Every post-pairing exchange and PSK
rotation uses HPKE PSK mode.

All HPKE records and the separately protected response use `aad = empty`. The
full 16-byte ChaCha20Poly1305 authentication tag must be retained.

All random values and HPKE ephemeral key pairs generated in production must
use a cryptographically secure random-number generator.

## 5. Common protocol elements

### 5.1. Protocol version

The protocol version string is:

```text
version = "agentknock-v1"
```

Its ASCII encoding is 13 bytes. Where the protocol version is included in HPKE
`info`, it is padded to exactly 16 bytes:

```text
version_info = ASCII(version) || zero(3)
```

In hexadecimal:

```text
version_info = 6167656e746b6e6f636b2d7631000000
```

No other label in this document has a NUL terminator or NUL padding.

The clear `version` component selects this cryptosystem. The corresponding
`version_info` value cryptographically binds that selection to each HPKE
context.

### 5.2. ULID identifiers

`device_id`, `client_id`, and ordinary `request_id` values are the 16-byte
binary values represented by ULIDs. Cryptographic operations use these bytes,
not a textual ULID representation.

The device generates one stable `device_id` for its device identity. The
client generates one `client_id` for each pairing. Each ordinary paired
exchange uses a fresh `request_id`. A generated ULID uses the current Unix time
in milliseconds in its 48-bit timestamp field and 80 independently generated
CSPRNG bits in its randomness field. A monotonic ULID generator must not be
used: multiple identifiers in one millisecond still receive independent random
fields rather than values obtained by incrementing an earlier ULID. Every
generated ULID must be distinct within the applicable device, pairing, or
request scope.

ULIDs serve both as unique identifiers and as carriers of a claimed creation
time. Once an identifier is bound to a cryptographic context, the timestamp
bits have integrity along with the rest of the identifier. This does not prove
that a malicious client supplied the correct time. Each receiver that accepts
previously unseen identifiers must define a ULID freshness policy and apply it
to every previously unseen `client_id` or `request_id` before accepting that
identifier. The policy may use the timestamp together with local state and
local time. Its duration, clock-skew allowance, and acceptance algorithm are
implementation-defined. Neither the timestamp nor the random field is treated
as secret.

For the initial pairing exchange, its `request_id` and the new `client_id` are
the same ULID. The all-zero 16-byte request identifier is reserved exclusively
for PSK rotation and must not be used for an ordinary exchange.

### 5.3. Cryptographic message boundary

Sections 7 through 9 define each cryptographic message as an abstract tuple of
byte strings and identifiers. Tuple notation does not prescribe concatenation
or a wire serialization. The surrounding protocol serializes each component
without changing its byte value.

Clear tuple members are not authenticated merely because they travel beside a
ciphertext. A value that requires confidentiality or integrity must be inside
a protected plaintext or explicitly bound by the cryptographic construction.
The [Agentknock v1 client-device protocol](client-device-protocol.md) defines
the wire representation of these tuples.

### 5.4. Fixed labels

Every label is the exact ASCII byte string shown:

| Purpose | Bytes |
| --- | --- |
| Base derivation salt | `ASCII("agentknock-v1")` |
| Address derivation | `ASCII("agentknock-v1 address")` |
| Pairing commitment | `ASCII("agentknock-v1 commitment")` |
| Client PSK export | `ASCII("agentknock-v1 psk")` |
| SAS derivation prefix | `ASCII("agentknock-v1 sas")` |
| Response secret export | `ASCII("agentknock-v1 response")` |
| Response key derivation | `ASCII("key")` |
| Response nonce derivation | `ASCII("nonce")` |

### 5.5. HPKE context binding

Every HPKE `info` value in this cryptosystem is exactly 48 bytes:

| Context | HPKE `info` |
| --- | --- |
| Initial pairing | <code>version_info &#124;&#124; device_id &#124;&#124; client_id</code> |
| Paired exchange | <code>version_info &#124;&#124; device_id &#124;&#124; request_id</code> |
| PSK rotation | <code>version_info &#124;&#124; device_id &#124;&#124; zero(16)</code> |

All components have fixed lengths, so no separators or length prefixes are
used. Each setup call below repeats its complete `info` expression. Every
PSK-mode setup uses the 16-byte `client_id` as its `psk_id`. The client identity
is therefore bound through `psk_id`; it is not duplicated in `info`.

### 5.6. Record acceptance and redelivery

The surrounding protocol associates each request, response, and completion
with a record slot selected by its message kind and transport-supplied
identifiers. Initial pairing uses `client_id = request_id`; a paired exchange
uses `(client_id, request_id)`.

Transport handling is separate from cryptographic acceptance. Receiving,
storing, discarding, or acknowledging a serialized message at the transport
layer does not authenticate it or change trusted endpoint state. The transport
may end an exchange after an invalid message or supply another candidate; this
document does not prescribe that behavior.

A receiver applies these rules in order:

1. It looks up retained state before processing the candidate. If the slot is
   already cryptographically accepted or has a terminal disposition, the
   receiver handles the delivery from retained state. It does not decode,
   authenticate, or compare the later candidate, and it does not repeat an
   application action, response generation, or cryptographic state transition.
2. Otherwise, the receiver performs all applicable decoding, validation, and
   authentication. A protected record is accepted only after it authenticates
   successfully. Failure causes no trusted state transition and exposes no
   plaintext. Section 7 defines the acceptance checks for the clear pairing
   messages.
3. Lookup and acceptance are serialized. Once a record is accepted, any later
   candidate is handled from the resulting retained state.

A sender creates at most one cryptographic record for a slot. Redelivery uses
the exact previously generated cryptographic values and ciphertext bytes; it
does not repeat HPKE setup, sealing, or response encryption under the same
identifiers. Redelivery can serialize the tuple again, but its decoded
cryptographic components must be identical.

A receiver retains a slot's idempotency state while a live operation depends
on it and while its freshness policy could accept the slot's identifiers as
previously unseen. It may discard that state only when, under the policy's
trusted time and state assumptions, the same identifiers cannot later be
accepted as a new operation. The policy, retention method, and those
assumptions are implementation-defined.

Idempotency state may consist only of a terminal marker. It need not retain
plaintext, ciphertext, or an exchange context after those values are no longer
needed for redelivery.

## 6. Pairing-address derivation

A pairing address consists of one or more nonempty lowercase ASCII words
separated by single hyphens:

```text
[a-z]+(?:-[a-z]+)*
```

No trimming, case conversion, Unicode normalization, or other transformation
is performed. `address_bytes` is the exact ASCII, and therefore UTF-8, encoding
of the accepted address.

The relay-visible address identifier is:

```text
address_id = HKDF-SHA256(
    salt = ASCII("agentknock-v1"),
    IKM  = address_bytes,
    info = ASCII("agentknock-v1 address"),
    L    = 16
)
```

`address_id` allows an observer to test address guesses offline. HKDF-SHA256 is
not a password-hardening function, so the secrecy of the pairing address is
limited by its entropy.

Neither the address nor `address_id` is included in the initial pairing HPKE
`info`, the SAS, or the active pairing state defined by this cryptosystem. The
address is not used by paired exchanges after the initial pairing request has
been routed.

## 7. Initial pairing

### 7.1. Inputs and clear messages

The device has a stable X25519 key pair:

```text
(skD, pkD)
```

`pkD` is the 32-byte X25519 public-key serialization defined by RFC 9180 and
RFC 7748.

Before it sends an initial pairing request, the client must generate:

```text
client_id     = a fresh ULID
request_id    = client_id
client_secret = random(32)
```

The initial `client_id`, which is also the transport-supplied `request_id`,
identifies one logical pairing attempt. The client retains one fixed
`client_secret` for that attempt. It must not reuse `client_id` for another
attempt or replace `client_secret` within this one.

Before receiving the device response, the client commits to its random
contribution:

```text
commitment = HKDF-SHA256(
    salt = ASCII("agentknock-v1"),
    IKM  = client_secret,
    info = ASCII("agentknock-v1 commitment"),
    L    = 32
)
```

This commit-before-peer-contribution pattern parallels the ZRTP HVI commitment
in RFC 6189, ยง4.4.1.1. Both prevent an active attacker from seeing both honest
contributions before choosing its own contribution to grind for a matching
short authentication string. Agentknock commits only to `client_secret` and
reveals it in the first HPKE record; it does not use the ZRTP message format or
HVI calculation.

The clear initial request is the abstract tuple:

```text
pairing_request = (version, commitment)
```

The initial request occupies a clear request slot under Section 5.6. An empty
slot is accepted only if `client_id` passes the freshness policy, `version` is
supported, and `commitment` is exactly 32 bytes. Once accepted, later
deliveries use its retained state.

For a new accepted pairing attempt, the device generates:

```text
device_random = random(32)
```

and returns the abstract tuple:

```text
pairing_response = (device_id, pkD, device_random)
```

The response is not authenticated at this point. As one serialized, crash-safe
transition, the device accepts the request and durably fixes its `commitment`,
`device_random`, and complete response tuple. While the response tuple is
retained, later deliveries return that tuple and never create another device
contribution. After those values are discarded, the device must not generate
another response for the slot.

### 7.2. Base-mode exchange and client PSK

`application_plaintext` is the opaque application message sent during pairing.
Its encoding and contents are outside this document.

After validating the response encodings and lengths, the client creates an
HPKE base-mode sender context and seals two records in this fixed order:

```text
(enc, pairing_context) = SetupBaseS(
    pkR  = pkD,
    info = version_info || device_id || client_id
)

secret_ciphertext = pairing_context.Seal(
    aad       = empty,
    plaintext = client_secret
)

application_ciphertext = pairing_context.Seal(
    aad       = empty,
    plaintext = application_plaintext
)

client_psk = pairing_context.Export(
    exporter_context = ASCII("agentknock-v1 psk"),
    length           = 32
)
```

`secret_ciphertext` is HPKE record sequence number 0.
`application_ciphertext` is HPKE record sequence number 1. This base-mode
context has no later completion record.

The client sends the abstract tuple:

```text
pairing_completion = (
    enc,
    secret_ciphertext,
    application_ciphertext
)
```

The pairing completion occupies a completion slot under Section 5.6. It can be
accepted only for a retained attempt awaiting this message and only after the
device completes the following validation. It reconstructs the receiver
context and opens both records in order:

```text
pairing_context = SetupBaseR(
    enc  = enc,
    skR  = skD,
    info = version_info || device_id || client_id
)

client_secret = pairing_context.Open(
    aad        = empty,
    ciphertext = secret_ciphertext
)

application_plaintext = pairing_context.Open(
    aad        = empty,
    ciphertext = application_ciphertext
)
```

The device must reject the pairing completion if either record fails to open
or if `client_secret` is not exactly 32 bytes. After both records have opened,
it derives:

```text
recovered_commitment = HKDF-SHA256(
    salt = ASCII("agentknock-v1"),
    IKM  = client_secret,
    info = ASCII("agentknock-v1 commitment"),
    L    = 32
)
```

The device compares `recovered_commitment` with the commitment retained from
the initial request. The comparison must be constant-time. If they differ, the
device rejects the pairing completion without processing
`application_plaintext`, deriving or storing a client PSK, or presenting a
SAS. Any failure leaves the pairing attempt unchanged.

Only after the commitment matches does the device export:

```text
client_psk = pairing_context.Export(
    exporter_context = ASCII("agentknock-v1 psk"),
    length           = 32
)
```

Only after both records authenticate and the commitment matches may the device
fix this message as the one accepted `pairing_completion` for the attempt and
parse or display `application_plaintext`. Until the user accepts the SAS, that
plaintext remains attacker-controlled, untrusted metadata. It must not cause a
security-sensitive side effect or become trusted durable client metadata. An
implementation may retain it as explicitly untrusted pending state for display
on the SAS confirmation interface. Acceptance of the completion and creation of
the pending binding must be one serialized, crash-safe transition.

### 7.3. Short authentication string

Both endpoints calculate:

```text
sas_bytes = HKDF-SHA256(
    salt = device_random,
    IKM  = client_secret,
    info = ASCII("agentknock-v1 sas")
        || device_id
        || client_id
        || pkD,
    L    = 8
)

sas_integer = OS2IP-BE(sas_bytes) mod 1_000_000_000_000
```

`pkD` is the exact 32-byte public-key value in `pairing_response`.

The integer is rendered as exactly 12 zero-padded decimal digits in three
groups of four:

```text
dddd dddd dddd
```

The user must confirm the full 12-digit SAS through an out-of-band interaction
before either endpoint treats the pairing as authenticated. The initial
base-mode exchange alone does not authenticate the intended device key because
the relay can replace the clear pairing response.

### 7.4. Pairing state and activation

A device-side pairing attempt moves through these states:

| State | Entry and permitted transition | Occupies a pending slot |
| --- | --- | --- |
| Awaiting pairing completion | The initial request and response are fixed. A valid `pairing_completion` advances the attempt. | Yes |
| Awaiting SAS decision | The completion, pending binding, SAS, and untrusted application metadata are fixed. The user may accept or explicitly reject the attempt. | Yes |
| Awaiting activation | The user accepted the SAS. The pending binding may authenticate only the activation exchange. | Yes |
| Active | An authenticated activation request atomically activates the binding and fixes its accepted response. | No |
| Rejected | The user explicitly rejected a non-active attempt. The binding is not activated. | No |

Every transition in this table is serialized and crash-safe.
Malformed, replaced, abandoned, or timed-out client traffic does not change
these states or release a pending slot. The device enforces a small, finite,
implementation-defined limit on the three pending states. When the limit is
reached, it rejects new attempts without altering existing state. State needed
for idempotency remains subject to Section 5.6 after a slot is released.

Activation uses the paired exchange in Section 8. After SAS acceptance, the
client sends the PSK-mode activation request. The device authenticates it with
the pending binding, then atomically activates the binding and fixes the
accepted response before releasing that response. This transition releases the
pending slot.

The client activates only after authenticating the accepted response. It then
sends the authenticated completion from Section 8.3. The completion reports
client-side activation but does not gate or change device activation. The
request, response, and completion plaintext schemas are outside this document.

### 7.5. Client transcript retention

For one `client_id`, the client fixes one `client_secret`, commitment,
`(device_id, pkD, device_random)` response tuple, base-mode sender context,
`enc`, `secret_ciphertext`, `application_ciphertext`, and exported
`client_psk`. The response tuple becomes fixed only after its component
lengths, X25519 input, and HPKE setup validate; later deliveries follow
Section 5.6.

The client seals exactly twice in the pairing context, in the order defined in
Section 7.2, and does not repeat setup or sealing. Until the completion has been
safely handed off and the SAS calculated, it retains enough state to reproduce
that completion and SAS. Losing that state requires a new attempt with a fresh
`client_id`.

After safe handoff and SAS calculation, the client may erase the base-mode
context, `client_secret`, `device_random`, `enc`, and both ciphertexts. It
retains the pending binding required for activation.

## 8. Paired request-response-completion exchange

Each paired operation uses one fresh nonzero `request_id` and one HPKE context.
It has one request, at most one terminal response, and at most one completion.
A successful activation exchange in Section 7.4 uses all three, although device
activation does not depend on receiving its completion.

### 8.1. Request

The client creates an HPKE PSK-mode sender context using the active binding, or
using a pending binding solely for the activation exchange in Section 7.4:

```text
(enc, request_context) = SetupPSKS(
    pkR    = pkD,
    info   = version_info || device_id || request_id,
    psk    = client_psk,
    psk_id = client_id
)

request_ciphertext = request_context.Seal(
    aad       = empty,
    plaintext = request_plaintext
)
```

`request_ciphertext` is HPKE record sequence number 0. The abstract request
message is:

```text
paired_request = (version, enc, request_ciphertext, rotation_enc?)
```

`rotation_enc` is absent unless a PSK rotation is pending. Section 9 defines
its construction.

The device applies Section 5.6. Before accepting a new request, it applies its
freshness policy, selects the binding using `client_id`, checks the exact outer
version, and runs:

```text
request_context = SetupPSKR(
    enc    = enc,
    skR    = skD,
    info   = version_info || device_id || request_id,
    psk    = client_psk,
    psk_id = client_id
)

request_plaintext = request_context.Open(
    aad        = empty,
    ciphertext = request_ciphertext
)
```

A successful open and creation of the operation state form one serialized,
crash-safe transition.

Here `client_psk` is normally the current binding PSK. Section 9.2 defines the
ordered selection of the current PSK, an eligible previous PSK, or a rotation
candidate.

The protocol version, device identity, and request identity are bound through
HPKE `info`. The client identity is bound through `psk_id` and possession of
the corresponding PSK.

### 8.2. Response

The exporter-plus-random-value construction in this section follows the
bidirectional response construction described in HPKE-bis, Section 9.8,
specialized to the fixed Agentknock v1 suite and labels. The complete algorithm
below defines the Agentknock construction.

The response is not an HPKE record in the reverse direction. After the device
successfully opens request sequence number 0, the device invokes the HPKE
exporter once:

```text
response_secret = request_context.Export(
    exporter_context = ASCII("agentknock-v1 response"),
    length           = 32
)
```

Each endpoint must obtain this one 32-byte exporter output and use the two HKDF
derivations below. It must not make separate HPKE exporter calls with the
response key and nonce lengths.

It then generates a public random value whose length is the KDF hash length
`Nh = 32`, and derives a response key and the actual AEAD nonce:

```text
response_random = random(32)
response_salt   = enc || response_random

response_key = HKDF-SHA256(
    salt = response_salt,
    IKM  = response_secret,
    info = ASCII("key"),
    L    = 32
)

response_aead_nonce = HKDF-SHA256(
    salt = response_salt,
    IKM  = response_secret,
    info = ASCII("nonce"),
    L    = 12
)

response_ciphertext = ChaCha20Poly1305.Seal(
    key       = response_key,
    nonce     = response_aead_nonce,
    aad       = empty,
    plaintext = response_plaintext
)
```

`response_ciphertext` is the encrypted bytes followed by the full 16-byte
authentication tag, as defined by RFC 8439.

The abstract response message is:

```text
paired_response = (response_random, response_ciphertext)
```

`response_random` is public HKDF input, not the ChaCha20Poly1305 nonce. It must
be exactly 32 bytes.

The client applies Section 5.6. Before accepting a response, it derives the
same `response_secret`, `response_key`, and `response_aead_nonce` and attempts
to open the ciphertext. Opening a response does not advance the HPKE record
sequence.

For a given request, the device must create at most one terminal
`response_plaintext` and one `paired_response`. Before releasing the first
copy, it must durably fix `response_random` and `response_ciphertext` as request
state. Those values are sufficient to reproduce the exact encrypted response;
this cryptosystem does not require durable retention of `response_plaintext`.
Application state may separately retain a terminal result when needed. A later
response for the slot uses the fixed encrypted values. After the device
discards those values, it must not generate another response for the slot,
including after a crash.

### 8.3. Completion

After sealing the request, the client may seal one completion in the original
sender context:

```text
completion_ciphertext = request_context.Seal(
    aad       = empty,
    plaintext = completion_plaintext
)
```

`completion_ciphertext` is HPKE record sequence number 1. The abstract
completion message is:

```text
paired_completion = (completion_ciphertext)
```

The completion does not repeat `version`, `enc`, `device_id`, `client_id`, or
`request_id`; they are supplied by the original request and its context.

The device applies Section 5.6. Before accepting a completion, it reconstructs
the receiver context from the accepted request, opens request sequence number
0, and then opens the completion as sequence number 1. Acceptance and any state
transition caused by the completion plaintext form one serialized, crash-safe
transition. Response derivation and encryption do not consume an HPKE record
sequence number.

A completion may report an application-level abort even if no response was
obtained. Cryptographically, it can be generated only after the request was
sealed. Its application meaning is outside this document.

### 8.4. Message-order and uniqueness requirements

A new operation uses a fresh nonzero `request_id` and a fresh HPKE sender
context. The client seals the request exactly once at sequence number 0. If the
operation has a completion, it seals that record exactly once at sequence
number 1 and only after the request. Response derivation does not consume an
HPKE sequence number.

If the client loses the sender context before an exchange finishes, it abandons
that exchange and uses a new `request_id`; the request tuple alone cannot
reconstruct a valid completion. Record acceptance, redelivery, and state
retention follow Section 5.6.

## 9. PSK rotation

PSK rotation creates a successor PSK without a dedicated application message.
It uses a normal PSK-mode HPKE context only for export and carries that
context's encapsulated key in later requests. It does not use the RFC 9180
Export-Only AEAD identifier.

### 9.1. Client rotation

The client may rotate only an active binding and only when no rotation is
already pending. Let the current PSK be `old_psk`. The client performs a fresh
HPKE PSK-mode sender setup:

```text
(rotation_enc, rotation_context) = SetupPSKS(
    pkR    = pkD,
    info   = version_info || device_id || zero(16),
    psk    = old_psk,
    psk_id = client_id
)

new_psk = rotation_context.Export(
    exporter_context = ASCII("agentknock-v1 psk"),
    length           = 32
)
```

The client must not seal a plaintext in `rotation_context`.

The abstract optional request value `rotation_enc` is an HPKE encapsulated key,
not `new_psk`.

As one serialized, crash-safe state transition, the client must replace
`old_psk` with `new_psk` and retain the pending `rotation_enc`. After this
transition, every new paired request uses `new_psk` and includes the same
`rotation_enc` until confirmation. The client must not have more than one
unconfirmed rotation.

### 9.2. Device rotation processing

Rotation processing applies only to an active binding. When a pending binding
is used for the activation exchange, the device ignores `rotation_enc` and
attempts to open the request only with the pending `client_psk`. It must not
derive or adopt a rotation candidate for that binding.

For a candidate in an empty request slot, the device processes rotation in this
order:

1. It first tries to open the ordinary request using its current stored PSK.
   If this succeeds, it ignores the optional rotation value.
2. If step 1 fails and the immediately previous PSK is still within the bounded
   overlap described below, the device tries that PSK. If this succeeds, it
   authenticates an ordinary request from that older generation and ignores
   the optional rotation value. An older PSK must not authorize a rotation or
   become current again.
3. If the ordinary opens fail and `rotation_enc` is absent, the device rejects
   the request. Otherwise, it reconstructs a normal PSK-mode context used only
   for export:

   ```text
   rotation_context = SetupPSKR(
       enc    = rotation_enc,
       skR    = skD,
       info   = version_info || device_id || zero(16),
       psk    = current_psk,
       psk_id = client_id
   )

   candidate_psk = rotation_context.Export(
       exporter_context = ASCII("agentknock-v1 psk"),
       length           = 32
   )
   ```

4. It tries to open the ordinary request using `candidate_psk` in the normal
   request context.
5. It accepts the rotation only if that ordinary request authenticates
   successfully. A bare, malformed, or substituted `rotation_enc` must not
   change device state.
6. Acceptance of the request and replacement of the current PSK with
   `candidate_psk` must be one serialized, crash-safe transition and must occur
   before the terminal response is released.

If the device already adopted `new_psk`, step 1 succeeds and the still-present
`rotation_enc` is harmless. This permits recovery from a lost rotation
confirmation.

`rotation_enc` is not direct AEAD associated data. Its derivation is instead
authenticated indirectly: the device commits `candidate_psk` only after the
ordinary request opens under that candidate. Removing or replacing the value
can make a request fail, but cannot cause adoption of an attacker-chosen PSK.

The device must retain the exact PSK generation or reconstructed HPKE receiver
state for every request it has accepted until that request no longer needs a
response or completion. A later rotation must not change the context used by an
already accepted request.

The device retains the immediately previous PSK for a bounded,
implementation-defined overlap so step 2 can accept delayed ordinary requests.
The previous PSK cannot authorize rotation, become current, or extend its
overlap through use. This necessarily preserves its authority for the same
period; cryptography cannot distinguish a delayed request from a newly created
one.

Only one previous generation is retained. Its overlap ends at the fixed
deadline or upon a later rotation, whichever comes first.

The one-unconfirmed-rotation limit applies to each client state, not to the
device. A separate copy with the current PSK can authenticate a later rotation;
accepting it replaces the previous generation and can strand another copy on
an older pending lineage.

The trigger and application semantics for invalidating or removing an active
binding are outside this document. Rotation overlap exists only while the
binding remains active: after invalidation, neither the current nor previous
PSK can authenticate a new request. Previously accepted records remain governed
by their retained exchange and idempotency state, and invalidation cannot
retract ciphertext already released. Whether an accepted but unresolved
operation produces a response is application policy.

### 9.3. Rotation confirmation

Successfully opening an authenticated response to a request that carried the
pending `rotation_enc` confirms that the device derived the same `new_psk`.
The client then removes that pending `rotation_enc`, but only if it still
matches the value carried by that request.

The application result inside the response does not affect cryptographic
confirmation. Relay delivery, a clear error, or an undecryptable response is
not confirmation.

Each endpoint should erase a retired PSK when practical, but only after no live
request state or permitted overlap depends on it. A client may erase the raw
`old_psk` after its atomic transition if every live request retains a
self-contained sender context. Rotation scheduling and the overlap duration are
product policy, not cryptographic constants.

## 10. Cryptographic state

The cryptosystem requires the following state:

| State | Required contents |
| --- | --- |
| Device identity | Stable `device_id`, secret `skD`, and matching `pkD` |
| Device pairing attempt | Its Section 7.4 state and the fixed values needed to return the pairing response, validate the completion, calculate the SAS, and activate or reject the binding |
| Device binding | `client_id`, current `client_psk`, status, and any previous PSK with its overlap deadline |
| Client pairing attempt | The live transcript needed through safe handoff and SAS calculation, followed by the pending binding needed for activation |
| Client binding | `device_id`, `pkD`, `client_id`, current `client_psk`, status, and any pending `rotation_enc` |
| Client paired exchange | `request_id`, fixed request and sender context, accepted response, and any fixed completion |
| Device paired exchange | `request_id`, accepted request and receiver context, its PSK generation, processing state, any fixed response, and accepted completion |
| Idempotency | Accepted identifiers and enough disposition or response state to handle redelivery under Section 5.6 |

Secret keys and PSKs require confidentiality and integrity. Public keys,
identifiers, and pending-rotation state require integrity. Transitions described
as serialized and crash-safe must not expose partially updated state after
concurrency or a crash. This does not prescribe a storage format or platform
mechanism.

Pending-slot accounting and idempotency retention are separate. Explicit
rejection or successful device activation releases a pairing's pending slot,
but Section 5.6 can require its idempotency state to remain longer.

Request-local contexts, response secrets, derived response keys and nonces,
retired PSKs, and protected plaintext should be erased when no live request or
allowed rotation overlap requires them. An endpoint must not reuse an abandoned
request context for a new identifier or a new application operation.

## 11. Validation and failure handling

An implementation must fail closed on any of the following:

- an unsupported protocol version;
- a malformed or noncanonical identifier;
- an identifier, random value, key, encapsulation, or nonce of the wrong
  length;
- HPKE setup, open, seal, or export failure;
- ChaCha20Poly1305 authentication failure;
- a record presented in the wrong sequence before cryptographic acceptance.

These checks apply before cryptographic acceptance. Later transport delivery
for an accepted slot follows Section 5.6 without validating the new candidate.

Every X25519 public key and encapsulated key must decode to exactly 32 bytes.
When processing an X25519 input u-coordinate, the implementation follows RFC
7748, Section 5: it masks the most significant bit of the final byte, accepts
non-canonical values, and processes the resulting integer modulo
`2^255 - 19`. It must not reject an input merely because the encoding is
non-canonical. If an X25519 operation used by DHKEM produces an all-zero shared
secret, the implementation must detect that result without leaking secret
information and abort, as required for X25519 input validation by HPKE-bis,
Section 7.1.4.

When Section 9.2 ignores `rotation_enc`, the device does not validate it. If
rotation fallback is needed, `rotation_enc` must be an exactly 32-byte HPKE
encapsulated key.

Unauthenticated data must not be interpreted as an authenticated endpoint
decision. In particular, a clear error fabricated by the relay is not proof
that the device rejected a request and must not, by itself, authorize release
of protected data, activate a pairing, confirm a rotation, or otherwise change
trusted cryptographic state.

On authentication failure, an implementation must not expose the failed
plaintext or use any partially derived application value. Error reporting and
recovery guidance are outside this document.

## 12. Security considerations

### 12.1. Properties after verified pairing

Subject to the assumptions of HPKE, X25519, HKDF-SHA256, ChaCha20Poly1305, and
secure endpoint state:

- A paired request is confidential to the holder of `skD` and is
  authenticated as coming from a holder of `client_psk` for `client_id`.
- A response is confidential and authenticated to the client through the
  exporter secret of the authenticated request context. Against the relay, this
  proves possession of that context, not which endpoint performed the
  encryption.
- A completion is confidential and authenticated as HPKE record sequence
  number 1 in the original request context.
- `version`, `device_id`, and `request_id` are bound through HPKE `info`, while
  `client_id` is bound through `psk_id`.
- Modification or cross-context substitution of a protected record causes
  authentication failure and no trusted state transition. After acceptance,
  later candidates for that slot are ignored and cannot replace the accepted
  record or repeat its effects.

HPKE PSK mode proves possession of a shared secret; it does not create a
digital signature or non-repudiable client identity. Either endpoint that
possesses the relevant secrets can construct protocol messages consistent with
its role. In particular, responses and completions are not signatures or
third-party proof of which endpoint generated them.

### 12.2. Initial pairing and SAS

The initial request and device response are clear and unauthenticated. A relay
can replace `device_id`, `pkD`, or `device_random`, causing the client to
encrypt both pairing records to an attacker-selected key. The attacker can open
both `client_secret` and `application_plaintext`, and can supply different
application metadata on its separate leg toward the real device, before a
later SAS mismatch is detected. SAS verification does not retroactively give
these records confidentiality, and application metadata remains untrusted
before SAS acceptance. HPKE base mode alone does not authenticate the intended
device; SAS confirmation is mandatory.

The client commits to `client_secret` before learning the device contribution,
and the device fixes its response before learning `client_secret`. Commitment
verification binds the revealed secret to the initial request; the SAS binds
both random contributions, both identifiers, and `pkD`. A relay impersonating
the device must fix its substituted key and random value before the completion
reveals `client_secret`. The commitment then prevents it from presenting a
different client contribution to the real device. It therefore cannot choose
either substituted contribution after learning the corresponding honest
contribution.

For full confirmation of the 12-digit SAS, one independent incorrect value
matches with probability approximately `10^-12` (about 39.9 bits); the modulo
bias is negligible. Partial confirmation or repeated attempts reduce this
security. With at most `n` pending attempts, the relay's best chance from those
candidates is at most approximately `n * 10^-12`. The finite limit in Section
7.4 bounds this amplification but not denial of service.

The address-derived values do not replace SAS verification. A low-entropy or
public pairing address can be recovered by offline enumeration. Even a
high-entropy address does not make the clear device response authenticated to
the client.

### 12.3. Replay and state rollback

Authenticated encryption does not invalidate a ciphertext replay. Section 5.6
maps each slot to one operation and handles later candidates from retained
state.
A modified protected record fails authentication and causes no trusted state
transition.

Senders must still generate only one record per slot. Cloned or rolled-back
HPKE state could otherwise seal different plaintexts at one sequence number. A
fixed response must not be replaced; changing its plaintext while retaining
`response_random` would reuse the derived key and nonce. Sections 7.5, 8.2, and
8.4 prohibit these cases.

Rollback or cloning of endpoint state can fork a PSK lineage. Atomic writes
prevent partial local transitions but cannot distinguish two independently
running copies of the same valid state. A holder of a copied current PSK can
impersonate the client and race a competing rotation. PSK rotation is routine
key evolution, not a mechanism for recovering from a known compromise.

### 12.4. Key compromise and forward secrecy

Compromise of the current `client_psk` permits future client impersonation and
rotation attempts until the device state changes. The PSK alone does not
decrypt previously recorded HPKE exchanges without the corresponding device
private key or sender ephemeral state.

Disclosure of a live request context, its exporter secret, or equivalent
record-key material is stronger than disclosure of `client_psk` alone. Access
to the request record's sequence-zero key and nonce before device acceptance
can permit substitution of that request. Access to the still-live context can
permit construction of a context-consistent response or completion.

Compromise of `skD` has a wider retrospective effect. If an attacker recorded
the initial base-mode pairing exchange, `skD` lets it reconstruct that context
and its exported `client_psk`. Recorded rotation encapsulations then reveal the
successor PSKs in sequence, allowing recorded paired traffic to be opened.
Agentknock v1 therefore does not provide forward secrecy against later
compromise of the long-term device private key when the pairing transcript was
recorded.

Agentknock v1 is not post-quantum secure because its fixed HPKE suite uses
X25519. The construction can also be instantiated with a post-quantum or hybrid
HPKE suite by substituting that suite and adjusting every algorithm-dependent
length. Such an instantiation is a separate protocol version, not an extension
or negotiated variant of `agentknock-v1`.

A malicious client can exercise its own binding and disclose its own secrets as
described in Section 2, but that does not relax the device-side safety
requirements. Compromise of the device, weak random-number generation, and
side-channel leakage are outside the protection of this cryptosystem.

### 12.5. Relay visibility and availability

The relay can observe at least the values carried outside encryption,
including routing identifiers, `version`, the pairing commitment, `device_id`,
`pkD`, `device_random`, HPKE encapsulated keys, whether a rotation is pending,
the public response random value, ciphertext lengths, timing, and traffic
relationships. ULID timestamps are also visible wherever the identifiers are
visible.

The cryptosystem does not hide message length or traffic patterns. Padding is
not defined. The relay can suppress all traffic or fabricate unauthenticated
errors, so no availability guarantee is possible.

## 13. Test vectors

Unless stated otherwise, every byte string in this section is hexadecimal.
Production implementations must generate fresh randomness; the `ikmE` values
below are deterministic inputs to the DHKEM `DeriveKeyPair` procedure and are
only for reproducible tests.

`client_secret` and every `ikmE` value below are independent protocol inputs and
must be generated independently. None is derived from another.

### 13.1. Pairing address

```text
address = yup-its-free

address_id =
  9e6f33bf47382846903dffa0962ea313
```

### 13.2. Common pairing inputs and SAS

```text
device_id =
  01989d5eb75c0d87d560923ecec4b411

client_id =
  01989d609abcaaa12f3d120dc5b3587d

device private key =
  42424242424242424242424242424242
  42424242424242424242424242424242

pkD =
  132c442be010fbd57e72603328aa76e7
  1fccc1503aae219327d14d9c9993f472

client_secret =
  606162636465666768696a6b6c6d6e6f
  707172737475767778797a7b7c7d7e7f

commitment =
  8d455348112298bcfa39d7d7000e2ac5
  e9a6e211f2cf3739c8e8f565dcc7b2ae

device_random =
  a0a1a2a3a4a5a6a7a8a9aaabacadaeaf
  b0b1b2b3b4b5b6b7b8b9babbbcbdbebf

SAS HKDF info =
  6167656e746b6e6f636b2d7631207361
  7301989d5eb75c0d87d560923ecec4b4
  1101989d609abcaaa12f3d120dc5b358
  7d132c442be010fbd57e72603328aa76
  e71fccc1503aae219327d14d9c9993f4
  72

sas_bytes = f8ec5b7b6afd9de4
sas_integer = 1543953892
displayed SAS = 0015 4395 3892
```

### 13.3. Initial pairing and PSK export

```text
version_info =
  6167656e746b6e6f636b2d7631000000

pairing HPKE info = version_info || device_id || client_id =
  6167656e746b6e6f636b2d7631000000
  01989d5eb75c0d87d560923ecec4b411
  01989d609abcaaa12f3d120dc5b3587d

ikmE =
  000102030405060708090a0b0c0d0e0f
  101112131415161718191a1b1c1d1e1f

enc =
  b1f1b840de7a3241b02748cf9b05b74d
  c8c5e8451298738817bd76aa8ebe8c2b

secret_ciphertext =
  1fc4998009b2f274e37a7805cebc7368
  af8cc1d37f3a08458224071eeb96da80
  7a97f1c9a67a4b18ad924f0182d182b7

application_plaintext, as UTF-8 = application
application_plaintext = 6170706c69636174696f6e

application_ciphertext =
  0c0c8a15fd6ec95a39002878958e409c
  e5a9c765f1ebc774290d45

client_psk =
  b208e67b262c76f1bffb13acdabf3467
  4a1e41deb1bb4fff9dbdb8a31e218bd8
```

The string `application` is a test-only opaque application plaintext. It does
not define an application message schema.

### 13.4. Paired request, response, and completion

This vector uses `client_psk` from Section 13.3.

```text
request_id =
  01563e3ab5d3d6764c61efb99302bd5d

paired-exchange HPKE info = version_info || device_id || request_id =
  6167656e746b6e6f636b2d7631000000
  01989d5eb75c0d87d560923ecec4b411
  01563e3ab5d3d6764c61efb99302bd5d

psk_id =
  01989d609abcaaa12f3d120dc5b3587d

ikmE =
  202122232425262728292a2b2c2d2e2f
  303132333435363738393a3b3c3d3e3f

request_plaintext, as UTF-8 = request
request_plaintext = 72657175657374

enc =
  693658254630f73ad8da78fb331bf976
  cd42f90e0e9c9e83f40c51072a6f7417

request_ciphertext =
  2ea6b00a2a368e88fa4e7c8a9812871d
  762e1ca79690e7

response_secret =
  7c74ea99133595bf8bde52676eddba3b
  adb86495e024ccdab714cceba5d69791

response_random =
  c0c1c2c3c4c5c6c7c8c9cacbcccdcecf
  d0d1d2d3d4d5d6d7d8d9dadbdcdddedf

response_salt = enc || response_random =
  693658254630f73ad8da78fb331bf976
  cd42f90e0e9c9e83f40c51072a6f7417
  c0c1c2c3c4c5c6c7c8c9cacbcccdcecf
  d0d1d2d3d4d5d6d7d8d9dadbdcdddedf

response_key =
  ba62e9d298f8cd6440991d2e728b8599
  98d31d4f3102b215b86420ac35c32059

response_aead_nonce =
  2f514dfcfff4606cfcefb725

response_plaintext, as UTF-8 = response
response_plaintext = 726573706f6e7365

response_ciphertext =
  e3ad78eea1e4e69741089c0ecffa88b8
  a820547ae46b2029

completion_plaintext, as UTF-8 = completion
completion_plaintext = 636f6d706c6574696f6e

completion_ciphertext =
  d04b4a1ccbb6b9330f8c6a1a392ddb4b
  bf4b9d92ffae0dc1b08a
```

The plaintexts `request`, `response`, and `completion` are test-only opaque
values; they are not application methods.

### 13.5. PSK rotation

This vector rotates `client_psk` from Section 13.3.

```text
rotation HPKE info = version_info || device_id || zero(16) =
  6167656e746b6e6f636b2d7631000000
  01989d5eb75c0d87d560923ecec4b411
  00000000000000000000000000000000

psk_id =
  01989d609abcaaa12f3d120dc5b3587d

ikmE =
  404142434445464748494a4b4c4d4e4f
  505152535455565758595a5b5c5d5e5f

rotation_enc =
  b259f6ee92dcba0111850b13b3f6dccc
  827726f9b08235ab62922b6b3f3f2a19

new_psk =
  b0ca03f5aea63fe0810516db0bd966c1
  e5ae73744d3bbfdb3a9139571b1ddb9b
```

## 14. References

- [RFC 5869: HMAC-based Extract-and-Expand Key Derivation Function]https://www.rfc-editor.org/rfc/rfc5869.html
- [RFC 6189, ยง4.4.1.1: ZRTP Hash Commitment in Diffie-Hellman Mode]https://www.rfc-editor.org/rfc/rfc6189.html#section-4.4.1.1
- [RFC 7748: Elliptic Curves for Security]https://www.rfc-editor.org/rfc/rfc7748.html
- [RFC 8439: ChaCha20 and Poly1305 for IETF Protocols]https://www.rfc-editor.org/rfc/rfc8439.html
- [RFC 9180: Hybrid Public Key Encryption]https://www.rfc-editor.org/rfc/rfc9180.html
- [HPKE-bis: Hybrid Public Key Encryption, draft-ietf-hpke-hpke-04 (work in progress)]https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-04
- [ULID canonical specification, revision `d0c7170`]https://github.com/ulid/spec/blob/d0c7170df4517939e70129b4d6462cc162f2d5bf/README.md