-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpattern.go
More file actions
1808 lines (1625 loc) · 69.4 KB
/
Copy pathpattern.go
File metadata and controls
1808 lines (1625 loc) · 69.4 KB
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
package clatter
import "fmt"
// Token represents a single operation in a Noise handshake pattern message.
// Matches Rust Clatter's Token enum exactly.
type Token uint8
const (
TokenE Token = iota // Generate/receive ephemeral key
TokenS // Send/receive static key
TokenEE // DH: ee
TokenES // DH: es
TokenSE // DH: se
TokenSS // DH: ss
TokenPsk // Mix in pre-shared key
TokenEkem // KEM: ephemeral encapsulate/decapsulate
TokenSkem // KEM: static encapsulate/decapsulate
)
// tokenString returns the display name for a token (used in pattern naming).
func tokenString(t Token) string {
switch t {
case TokenE:
return "e"
case TokenS:
return "s"
case TokenEE:
return "ee"
case TokenES:
return "es"
case TokenSE:
return "se"
case TokenSS:
return "ss"
case TokenPsk:
return "psk"
case TokenEkem:
return "ekem"
case TokenSkem:
return "skem"
default:
return "?"
}
}
// PatternType indicates the cryptographic category of a handshake pattern.
// Auto-detected from tokens at construction time.
type PatternType uint8
const (
PatternTypeDH PatternType = iota // NQ: classical DH only
PatternTypeKEM // PQ: KEM only
PatternTypeHybrid // Hybrid: DH + KEM
)
// HandshakePattern defines the structure of a Noise handshake.
// Value type with fixed arrays (not slices) to avoid heap allocation.
// hasPSK is cached at construction from a token scan.
type HandshakePattern struct {
name string // e.g., "NN", "XX", "IK", "pqXX", "hybridXX"
// Message patterns: each entry is a list of tokens for one message.
// Fixed max: 5 messages (no Noise pattern exceeds this), max 10 tokens per message.
initiatorMsgs [5]patternMessage
responderMsgs [5]patternMessage
numInitiator int
numResponder int
// Pre-message patterns (static keys known ahead of time)
preInitiator [2]Token // max 2 pre-message tokens (e, s)
preResponder [2]Token
numPreInit int
numPreResp int
patternType PatternType // auto-detected from tokens
hasPSK bool // cached from token scan
isOneWay bool // derived at construction: true iff the responder writes no message (one-way patterns N, K, X)
}
// patternMessage holds tokens for a single handshake message.
// Fixed array avoids heap allocation. Max 10 tokens covers the largest
// hybrid PSK patterns (e.g., hybridKXpsk2 responder: 9 tokens).
type patternMessage struct {
tokens [10]Token
count int
}
// Name returns the pattern name (e.g., "NN", "XX", "pqIK").
func (p *HandshakePattern) Name() string {
return p.name
}
// Type returns the pattern type (DH, KEM, or Hybrid).
func (p *HandshakePattern) Type() PatternType {
return p.patternType
}
// HasPSK returns true if the pattern uses pre-shared keys.
func (p *HandshakePattern) HasPSK() bool {
return p.hasPSK
}
// IsOneWay reports whether this is a one-way pattern (the built-in N, K, X and
// their PSK variants). The value is derived from structure at construction - a
// pattern is one-way exactly when its responder writes no handshake message
// (Noise section 7.4) - so it can never disagree with the message counts that
// drive the runtime status machine and the transport one-way send/receive gating.
func (p *HandshakePattern) IsOneWay() bool {
return p.isOneWay
}
// NumInitiatorMessages returns the number of initiator messages.
func (p *HandshakePattern) NumInitiatorMessages() int {
return p.numInitiator
}
// NumResponderMessages returns the number of responder messages.
func (p *HandshakePattern) NumResponderMessages() int {
return p.numResponder
}
// InitiatorMessage returns a copy of the tokens for the nth initiator message (0-indexed).
// Returns a copy to prevent external mutation of the pattern's internal state.
func (p *HandshakePattern) InitiatorMessage(n int) []Token {
if n < 0 || n >= p.numInitiator {
return nil
}
msg := &p.initiatorMsgs[n]
out := make([]Token, msg.count)
copy(out, msg.tokens[:msg.count])
return out
}
// ResponderMessage returns a copy of the tokens for the nth responder message (0-indexed).
// Returns a copy to prevent external mutation of the pattern's internal state.
func (p *HandshakePattern) ResponderMessage(n int) []Token {
if n < 0 || n >= p.numResponder {
return nil
}
msg := &p.responderMsgs[n]
out := make([]Token, msg.count)
copy(out, msg.tokens[:msg.count])
return out
}
// PreInitiator returns a copy of pre-message tokens for the initiator.
func (p *HandshakePattern) PreInitiator() []Token {
out := make([]Token, p.numPreInit)
copy(out, p.preInitiator[:p.numPreInit])
return out
}
// PreResponder returns a copy of pre-message tokens for the responder.
func (p *HandshakePattern) PreResponder() []Token {
out := make([]Token, p.numPreResp)
copy(out, p.preResponder[:p.numPreResp])
return out
}
// TotalMessages returns the total number of messages in the handshake
// (initiator messages + responder messages, interleaved).
func (p *HandshakePattern) TotalMessages() int {
return p.numInitiator + p.numResponder
}
// NewPattern creates a HandshakePattern with validation.
// Returns a wrapped ErrInvalidPattern for invalid patterns. Use mustNewPattern
// for predefined patterns.
//
// Validates, in order: message/token/pre-message count limits; message-shape
// arity (strict initiator-first alternation - at least one message, and the
// responder writes either as many messages as the initiator or one fewer per
// Noise section 7.4), checked first because it is pure structure and every
// later wire-order validator assumes a well-shaped, initiator-first pattern;
// token domain (a pre-message carries only Token::E or Token::S per Noise
// section 7.1, and a message body carries only a defined token value), checked
// before any downstream logic because detectPatternType and scanForPSK switch
// on known tokens and silently ignore an unknown one; that the pattern performs
// at least one DH or KEM key agreement (a handshake with no ee/es/se/ss/ekem/skem
// negotiates no shared secret); PSK position
// rules (first or last in a message); PQ token ordering (Ekem before Skem within
// a message); that each party emits Token::E at most once and sends its static
// (Token::S) at most once, pre-messages included (Noise section 7.3); that no
// classical-DH calculation (ee/es/se/ss) is performed more than once per handshake
// (Noise section 7.3 - the KEM tokens ekem/skem are exempt, since PQNoise repeats
// skem by design); that no party consumes an ephemeral (DH ee/es/se or a KEM
// ekem-decap that reads its local ephemeral) before emitting its Token::E; and that
// no token reads a remote static (DH es/se/ss or a KEM skem-encap) before that
// static's owner has transmitted it via Token::S or pre-shared it via a pre-message
// - a pattern that violates either precedence rule cannot complete a handshake;
// that no party calls ENCRYPT() after a static-key DH without also performing the
// local-ephemeral DH that gives it forward secrecy (Noise section 7.3); and that
// no party calls ENCRYPT() after a static-key KEM encapsulation (skem) when an
// ephemeral-KEM (ekem) that would protect it was already achievable - the
// post-quantum analogue of the same forward-secrecy requirement, which the DH
// rule leaves to this check because a Skem's forward secrecy comes from an ekem,
// not a DH. Auto-detects pattern type from tokens (DH, KEM, or Hybrid) and caches
// hasPSK from a token scan.
func NewPattern(name string, initiatorMsgs, responderMsgs [][]Token,
preInit, preResp []Token) (*HandshakePattern, error) {
if len(initiatorMsgs) > 5 || len(responderMsgs) > 5 {
return nil, fmt.Errorf("%w: too many messages", ErrInvalidPattern)
}
if len(preInit) > 2 || len(preResp) > 2 {
return nil, fmt.Errorf("%w: too many pre-message tokens", ErrInvalidPattern)
}
// One-way is DERIVED from structure, never caller-asserted: a one-way Noise
// pattern is exactly one whose responder writes no handshake message (Noise
// section 7.4). Deriving the bit (rather than taking a parameter) makes it
// impossible to disagree with the message counts that drive the runtime
// status machine and the transport one-way gating - a mislabel cannot be
// expressed through the API. validatePatternArity (below) guarantees the only
// surviving numResponder==0 patterns have exactly one initiator message, so
// the derived bit is always the spec-correct single-message one-way pattern.
p := &HandshakePattern{
name: name,
numInitiator: len(initiatorMsgs),
numResponder: len(responderMsgs),
numPreInit: len(preInit),
numPreResp: len(preResp),
isOneWay: len(responderMsgs) == 0,
}
for i, msg := range initiatorMsgs {
if len(msg) > 10 {
return nil, fmt.Errorf("%w: message %d has too many tokens", ErrInvalidPattern, i)
}
p.initiatorMsgs[i].count = len(msg)
copy(p.initiatorMsgs[i].tokens[:], msg)
}
for i, msg := range responderMsgs {
if len(msg) > 10 {
return nil, fmt.Errorf("%w: responder message %d has too many tokens", ErrInvalidPattern, i)
}
p.responderMsgs[i].count = len(msg)
copy(p.responderMsgs[i].tokens[:], msg)
}
copy(p.preInitiator[:], preInit)
copy(p.preResponder[:], preResp)
// Reject malformed message-shape arity (zero-message, responder-first,
// excessive one-way messages, message-count skew) FIRST: it is pure
// structure, gives the clearest error, and every wire-order validator below
// assumes a well-shaped, strictly-alternating initiator-first pattern.
if err := validatePatternArity(p); err != nil {
return nil, err
}
// Reject undefined token values and non-e/s pre-message tokens before any
// downstream logic: detectPatternType and scanForPSK switch on known tokens
// with no default and would silently misclassify an unknown one.
if err := validateTokenDomain(p); err != nil {
return nil, err
}
// Reject a pattern that performs no DH or KEM key agreement at all: it
// negotiates no shared secret (a degenerate non-handshake). Runs right after
// the token domain is known to be clean and before the PSK/PQ token-semantic
// validators (it is independent of detectPatternType, whose default branch
// would otherwise mislabel such a pattern as DH).
if err := validateHasCryptographicCalc(p); err != nil {
return nil, err
}
// Auto-detect pattern type from tokens
p.patternType = detectPatternType(p)
// Scan for PSK tokens
p.hasPSK = scanForPSK(p)
// Validate PSK and PQ rules
if err := validatePSKRules(p); err != nil {
return nil, err
}
if err := validatePQTokenOrder(p); err != nil {
return nil, err
}
if err := validateEphemeralTokenRules(p); err != nil {
return nil, err
}
if err := validateStaticTokenRules(p); err != nil {
return nil, err
}
if err := validateDHCalcAtMostOnce(p); err != nil {
return nil, err
}
if err := validateEphemeralEmittedBeforeUse(p); err != nil {
return nil, err
}
if err := validateStaticAvailableBeforeUse(p); err != nil {
return nil, err
}
if err := validateStaticDHComplementBeforeEncrypt(p); err != nil {
return nil, err
}
if err := validateStaticKEMSealHasComplement(p); err != nil {
return nil, err
}
return p, nil
}
// hsRole identifies one of the two handshake parties. It indexes the per-role
// arrays in the construction-time validators and renders itself in their errors.
type hsRole int
const (
roleInitiator hsRole = iota
roleResponder
)
// String renders the role name used in pattern-validation error messages.
func (r hsRole) String() string {
if r == roleInitiator {
return "initiator"
}
return "responder"
}
// other returns the opposite role (the reader of a message the receiver writes).
func (r hsRole) other() hsRole {
if r == roleInitiator {
return roleResponder
}
return roleInitiator
}
// walkInterleaved calls visit once per handshake message in true wire order:
// initiator first, strict Send/Receive alternation, handling unequal initiator/
// responder message counts (the toggle skips a side that has run out). It passes
// the writing role, the 0-based position in the interleaved sequence, and the
// message tokens. visit may return an error to stop the walk early, which is
// returned to the caller.
//
// This is the SINGLE source of truth for the handshake message ordering - the
// same order the runtime state machine drives (see determineInitialStatus /
// updateStatus / getNextMessage). Every construction-time validator that reasons
// in wire order shares it so the load-bearing alternation invariant is
// transcribed exactly once: validatePatternMaxMsgLen, validateEphemeralEmittedBeforeUse,
// validateStaticAvailableBeforeUse, validateStaticDHComplementBeforeEncrypt, and
// validateStaticKEMSealHasComplement.
func (p *HandshakePattern) walkInterleaved(visit func(writer hsRole, msgIdx int, tokens []Token) error) error {
initIdx, respIdx, msgIdx := 0, 0, 0
initiatorTurn := true
for initIdx < p.numInitiator || respIdx < p.numResponder {
var writer hsRole
var tokens []Token
if initiatorTurn && initIdx < p.numInitiator {
msg := &p.initiatorMsgs[initIdx]
writer, tokens = roleInitiator, msg.tokens[:msg.count]
initIdx++
} else if !initiatorTurn && respIdx < p.numResponder {
msg := &p.responderMsgs[respIdx]
writer, tokens = roleResponder, msg.tokens[:msg.count]
respIdx++
} else {
// One side is exhausted while the other still has messages: skip
// this turn without consuming a message.
initiatorTurn = !initiatorTurn
continue
}
if err := visit(writer, msgIdx, tokens); err != nil {
return err
}
msgIdx++
initiatorTurn = !initiatorTurn
}
return nil
}
// patternContainsToken reports whether want appears in tokens.
func patternContainsToken(tokens []Token, want Token) bool {
for _, t := range tokens {
if t == want {
return true
}
}
return false
}
// validateEphemeralEmittedBeforeUse rejects patterns in which a party consumes
// an ephemeral (does a DH or KEM-decap that reads a local ephemeral) before that
// party has emitted its Token::E. Such a pattern can never complete a handshake:
// the consuming operation reads a key that does not yet exist.
//
// validateEphemeralTokenRules only caps Token::E count at <=1 per party; it does
// not check that emission precedes use. This walks the handshake in true
// interleaved order (via walkInterleaved) and, per ephemeral-consuming token,
// verifies every party whose LOCAL ephemeral the token reads has already emitted
// Token::E.
//
// Token -> local-ephemeral consumers:
// - ee: {initiator, responder} (doDH(hs.e, hs.re) on both sides).
// - es: {initiator} (uses the initiator's local e; responder uses hs.s).
// - se: {responder} (uses the responder's local e; initiator uses hs.s).
// - ekem: {reader} (readTokenEkem decaps with the reader's local hs.e/hs.kemE).
//
// skem/ss/s touch only statics, so they carry no ephemeral precondition. A
// hybrid Token::E emits BOTH the DH (hs.e) and KEM (hs.kemE) local ephemeral, so
// one emitted bool per party covers both. Token::E flips the writer's emitted
// bit AFTER the consume check, so a message like {ee, e} is correctly rejected
// (the ee consumes before the e in the same message emits).
//
// This LOCAL-emission rule is complete: it also subsumes every "remote ephemeral
// not yet received" defect, because every ephemeral-consuming token reads SOME
// party's local ephemeral, and the remote side's availability is implied by that
// party's checked local emission.
func validateEphemeralEmittedBeforeUse(p *HandshakePattern) error {
var emitted [2]bool
emitted[roleInitiator] = patternContainsToken(p.preInitiator[:p.numPreInit], TokenE)
emitted[roleResponder] = patternContainsToken(p.preResponder[:p.numPreResp], TokenE)
return p.walkInterleaved(func(writer hsRole, msgIdx int, tokens []Token) error {
reader := writer.other()
// requireEmitted rejects token t (in this message) if party has not yet
// emitted its Token::E. Closes over writer/msgIdx for a fully localized
// error, matching validatePatternMaxMsgLen's "<role> message <n>" form.
requireEmitted := func(t Token, party hsRole) error {
if emitted[party] {
return nil
}
return fmt.Errorf("%w: token %q in %s message %d consumes the %s ephemeral before it is emitted (Token::E must precede use)",
ErrInvalidPattern, tokenString(t), writer, msgIdx, party)
}
for _, t := range tokens {
var err error
switch t {
case TokenEE:
if err = requireEmitted(t, roleInitiator); err == nil {
err = requireEmitted(t, roleResponder)
}
case TokenES:
err = requireEmitted(t, roleInitiator)
case TokenSE:
err = requireEmitted(t, roleResponder)
case TokenEkem:
err = requireEmitted(t, reader)
}
if err != nil {
return err
}
if t == TokenE {
emitted[writer] = true
}
}
return nil
})
}
// validateStaticAvailableBeforeUse rejects patterns in which a token uses a
// REMOTE static (a DH or a KEM encapsulation that operates on the other party's
// static)
// before that static's owner has made it available. Per the Noise spec (section
// 7.3, rule 1: "Parties can only perform DH between private keys and public keys
// they possess"), a remote static is possessed only after its owner has
// transmitted it via Token::S or pre-shared it via a pre-message. A pattern that
// reads it earlier can never complete: the consuming party holds no such key.
//
// This is the static analogue of validateEphemeralEmittedBeforeUse and shares
// walkInterleaved with it. The asymmetry between the two is intrinsic to Noise:
// an ephemeral is CREATED mid-handshake (Token::E), so the validator tracks the
// owner's LOCAL emission; a static is the owner's own pre-existing key, so the
// only precedence dimension a pattern can express is whether the owner has
// TRANSMITTED it to the peer before a remote-reading token. A party's use of its
// OWN local static (Token::S emit, es-as-responder, se-as-initiator, the local
// leg of ss, a skem decapsulation) carries no pattern-level precondition - the
// local static is supplied at construction via WithStaticKey, invisible to
// NewPattern; its absence is a runtime ErrMissingKey, not a malformed pattern.
//
// Token -> remote-static reader (owner whose transmission is required):
// - es: the responder's static (es = DH(initiator e, responder s); role-keyed).
// - se: the initiator's static (se = DH(initiator s, responder e); role-keyed).
// - ss: BOTH statics.
// - skem: the reader's static (skem encapsulates to the peer; the encapsulating
// WRITER needs the reader = writer.other() to have transmitted it).
//
// Token::S flips the writer's transmitted bit AFTER the consume check at that
// token's position, so a message like {s, ss} or XX's {e, ee, s, es} - the static
// sent and then immediately read within one message - is accepted just-in-time.
func validateStaticAvailableBeforeUse(p *HandshakePattern) error {
var transmitted [2]bool
transmitted[roleInitiator] = patternContainsToken(p.preInitiator[:p.numPreInit], TokenS)
transmitted[roleResponder] = patternContainsToken(p.preResponder[:p.numPreResp], TokenS)
return p.walkInterleaved(func(writer hsRole, msgIdx int, tokens []Token) error {
// requireTransmitted rejects token t (in this message) if owner has not
// yet transmitted or pre-shared its static. Closes over writer/msgIdx for
// a localized error matching the ephemeral-precedence validator's form.
requireTransmitted := func(t Token, owner hsRole) error {
if transmitted[owner] {
return nil
}
return fmt.Errorf("%w: token %q in %s message %d uses the %s static before it is transmitted or pre-shared (Token::S or a pre-message static must precede use)",
ErrInvalidPattern, tokenString(t), writer, msgIdx, owner)
}
for _, t := range tokens {
var err error
switch t {
case TokenES:
err = requireTransmitted(t, roleResponder)
case TokenSE:
err = requireTransmitted(t, roleInitiator)
case TokenSS:
if err = requireTransmitted(t, roleInitiator); err == nil {
err = requireTransmitted(t, roleResponder)
}
case TokenSkem:
err = requireTransmitted(t, writer.other())
}
if err != nil {
return err
}
if t == TokenS {
transmitted[writer] = true
}
}
return nil
})
}
// errStaticDHForwardSecrecy builds the forward-secrecy rejection for a
// static-key DH that reaches an ENCRYPT() with no ephemeral-DH complement. The
// message is localized to the writing role and message index, matching the
// precedence validators' form.
func errStaticDHForwardSecrecy(writer hsRole, msgIdx int, trigger, complement Token) error {
return fmt.Errorf("%w: %s message %d encrypts after the %q static-key DH without the %q ephemeral DH that protects it (forward secrecy, Noise section 7.3)",
ErrInvalidPattern, writer, msgIdx, tokenString(trigger), tokenString(complement))
}
// validateStaticDHComplementBeforeEncrypt enforces the forward-secrecy
// requirement of Noise section 7.3: "After performing a DH between a remote
// public key (either static or ephemeral) and the local static key, the local
// party must not call ENCRYPT() unless it has also performed a DH between its
// local ephemeral key and the remote public key." A payload (or static-key
// transmission) encrypted after a static-key DH that has no ephemeral DH over
// the same remote key lacks forward secrecy and is rejected at construction.
//
// The walk (walkInterleaved) accumulates the mixed DH set in true wire order -
// both parties mix every DH token at the same transcript position, so a single
// bool per token is the shared chaining-key state. At each point the WRITER
// calls ENCRYPT() - a Token::S it transmits (writeTokenS EncryptAndHash's the
// static public key) and the always-present end-of-message payload - every
// static-key DH already mixed must have its local-ephemeral complement mixed
// too. The static-key DH operands are role-keyed (processWriteToken /
// processReadToken):
//
// - initiator's local-static DHs: se (s . remote-e) needs ee; ss (s . remote-s)
// needs es (the initiator's e . remote-s).
// - responder's local-static DHs: es (s . remote-e) needs ee; ss needs se (the
// responder's e . remote-s).
//
// Token::Skem is deliberately NOT an encrypt point here: a Skem ciphertext's
// forward secrecy comes from a KEM ephemeral (Ekem), not a DH, so a hybrid
// pattern whose Skem precedes its ee (e.g. hybridKK, hybridIK) must not be
// rejected by this rule - validateStaticKEMSealHasComplement owns the Skem seal.
// The two validators are independent and compose; a missing DH complement is not
// cured by an Ekem, nor vice versa (forward secrecy is per-primitive).
//
// All 90 predefined patterns satisfy this rule by construction; it rejects only
// malformed custom patterns built via NewPattern. It assumes the at-most-once DH
// rule (validateDHCalcAtMostOnce) already holds, so one bool per DH token
// suffices, and runs last in the NewPattern chain.
func validateStaticDHComplementBeforeEncrypt(p *HandshakePattern) error {
var mixedEE, mixedES, mixedSE, mixedSS bool
requireComplement := func(writer hsRole, msgIdx int) error {
if writer == roleInitiator {
if mixedSE && !mixedEE {
return errStaticDHForwardSecrecy(writer, msgIdx, TokenSE, TokenEE)
}
if mixedSS && !mixedES {
return errStaticDHForwardSecrecy(writer, msgIdx, TokenSS, TokenES)
}
return nil
}
if mixedES && !mixedEE {
return errStaticDHForwardSecrecy(writer, msgIdx, TokenES, TokenEE)
}
if mixedSS && !mixedSE {
return errStaticDHForwardSecrecy(writer, msgIdx, TokenSS, TokenSE)
}
return nil
}
return p.walkInterleaved(func(writer hsRole, msgIdx int, tokens []Token) error {
for _, t := range tokens {
switch t {
case TokenS:
// Token::S transmits the static, EncryptAndHash'd: an ENCRYPT()
// point. The DH tokens later in this same message have not mixed
// yet (they follow S in the stream), matching the runtime order.
if err := requireComplement(writer, msgIdx); err != nil {
return err
}
case TokenEE:
mixedEE = true
case TokenES:
mixedES = true
case TokenSE:
mixedSE = true
case TokenSS:
mixedSS = true
}
}
// End-of-message payload is always EncryptAndHash'd: an ENCRYPT() point.
return requireComplement(writer, msgIdx)
})
}
// errStaticKEMSealForwardSecrecy builds the forward-secrecy rejection for a
// static-KEM seal that reaches an ENCRYPT() while an ephemeral-KEM that would
// protect it was achievable but omitted.
func errStaticKEMSealForwardSecrecy(writer hsRole, msgIdx int) error {
return fmt.Errorf("%w: %s message %d encrypts after a static-key KEM encapsulation without the %q ephemeral KEM that protects it (forward secrecy, Noise section 7.3)",
ErrInvalidPattern, writer, msgIdx, tokenString(TokenEkem))
}
// validateStaticKEMSealHasComplement enforces the post-quantum/hybrid analogue of
// the Noise section 7.3 forward-secrecy requirement. A static-KEM operation
// (Token::Skem) binds a shared secret to a long-lived static key with no
// ephemeral contribution: the writer encapsulates to the remote static
// (writeTokenSkem) and the remote decapsulates with its local static
// (readTokenSkem), so BOTH parties fold the same non-forward-secret secret. The
// forward-secrecy complement is any ephemeral-KEM operation (Token::Ekem), which
// encapsulates to a key that is discarded after the handshake.
//
// Unlike classical DH - where the initiator can supply its own ephemeral DH
// non-interactively at time zero - a KEM ephemeral needs a round trip: an Ekem
// can only encapsulate to an ephemeral the remote has already transmitted via
// Token::E. So the obligation attaches only once an Ekem was actually
// constructible. At the start of each message, if the writing party's remote has
// already emitted Token::E in a PRIOR message, an Ekem-encapsulation was
// insertable, and the requirement latches on monotonically from then on (it also
// covers the reply-suppliable case: a later party can encapsulate to an
// ephemeral an earlier party emitted). This legitimately permits the one-way
// "encapsulate to a known static at time zero" case - pqNK's initiator message,
// the N-pattern analogue - whose forward secrecy is impossible by necessity, not
// by omission.
//
// At each point the WRITER calls ENCRYPT() - a Token::S, a Token::Skem (the Skem
// ciphertext is itself EncryptAndHash'd), and the end-of-message payload - the
// pattern is rejected when a static-KEM secret has been mixed for the writer, no
// ephemeral-KEM has been mixed anywhere in the transcript, and an Ekem was
// constructible by now. Token::E emission is recorded at end-of-message so a
// within-message own-E does not retroactively excuse that message's own seals
// (the reply Ekem can only arrive later). A Token::Skem whose symmetric state is
// not yet keyed copies its ciphertext verbatim rather than AEAD-sealing it, but
// every such first-message Skem has its obligation un-attached (no prior remote
// Token::E), so the conservative treatment of Skem as an encrypt point is
// benign for all 90 built-ins.
//
// All 90 predefined patterns satisfy this rule (the post-quantum K-patterns seal
// their Skem in the first message, where no prior remote Token::E exists); it
// rejects only custom patterns that seal a static-KEM without an achievable
// ephemeral-KEM complement. This is per-primitive (strict hybrid): a hybrid Skem
// is NOT excused by a classical ephemeral DH (ee) in the transcript, because a
// classical ephemeral provides no forward secrecy against a quantum adversary -
// the exact threat the KEM layer exists to defend - so its Skem leg requires its
// own ephemeral KEM. (Symmetrically, validateStaticDHComplementBeforeEncrypt is
// not excused by an ekem.) The two validators are independent - that rule never
// treats a Skem as an encrypt point, this one owns the Skem seal - and compose
// without either excusing the other.
func validateStaticKEMSealHasComplement(p *HandshakePattern) error {
var emittedE [2]bool
emittedE[roleInitiator] = patternContainsToken(p.preInitiator[:p.numPreInit], TokenE)
emittedE[roleResponder] = patternContainsToken(p.preResponder[:p.numPreResp], TokenE)
var skemMixed [2]bool
var anyEkemMixed bool
var ekemConstructible bool
return p.walkInterleaved(func(writer hsRole, msgIdx int, tokens []Token) error {
// An Ekem-encapsulation was insertable by now iff the writer's remote had
// already emitted Token::E in a PRIOR message. Latch monotonically.
if emittedE[writer.other()] {
ekemConstructible = true
}
seal := func() error {
if skemMixed[writer] && !anyEkemMixed && ekemConstructible {
return errStaticKEMSealForwardSecrecy(writer, msgIdx)
}
return nil
}
for _, t := range tokens {
switch t {
case TokenS:
if err := seal(); err != nil {
return err
}
case TokenSkem:
// Encapsulation by the writer, decapsulation by the remote: both
// fold the same static-bound secret. Record before the seal check -
// the Skem ciphertext is itself an ENCRYPT() point.
skemMixed[writer] = true
skemMixed[writer.other()] = true
if err := seal(); err != nil {
return err
}
case TokenEkem:
anyEkemMixed = true
}
}
if err := seal(); err != nil {
return err
}
// Record Token::E emission at end-of-message (see the doc comment).
for _, t := range tokens {
if t == TokenE {
emittedE[writer] = true
}
}
return nil
})
}
// mustNewPattern creates a pattern, panicking on invalid input.
// Used for predefined patterns (like template.Must).
func mustNewPattern(name string, initiatorMsgs, responderMsgs [][]Token,
preInit, preResp []Token) *HandshakePattern {
p, err := NewPattern(name, initiatorMsgs, responderMsgs, preInit, preResp)
if err != nil {
panic(fmt.Sprintf("invalid predefined pattern %q: %v", name, err))
}
return p
}
// detectPatternType determines whether a pattern is DH, KEM, or Hybrid.
// Both DH and KEM tokens = HYBRID. Only KEM = KEM. Only DH = DH.
func detectPatternType(p *HandshakePattern) PatternType {
hasDH := false
hasKEM := false
scanTokens := func(tokens []Token) {
for _, t := range tokens {
switch t {
case TokenEE, TokenES, TokenSE, TokenSS:
hasDH = true
case TokenEkem, TokenSkem:
hasKEM = true
}
}
}
for i := 0; i < p.numInitiator; i++ {
scanTokens(p.initiatorMsgs[i].tokens[:p.initiatorMsgs[i].count])
}
for i := 0; i < p.numResponder; i++ {
scanTokens(p.responderMsgs[i].tokens[:p.responderMsgs[i].count])
}
if hasDH && hasKEM {
return PatternTypeHybrid
}
if hasKEM {
return PatternTypeKEM
}
return PatternTypeDH
}
// scanForPSK returns true if any message contains a PSK token.
// Result is cached at construction time.
func scanForPSK(p *HandshakePattern) bool {
for i := 0; i < p.numInitiator; i++ {
for j := 0; j < p.initiatorMsgs[i].count; j++ {
if p.initiatorMsgs[i].tokens[j] == TokenPsk {
return true
}
}
}
for i := 0; i < p.numResponder; i++ {
for j := 0; j < p.responderMsgs[i].count; j++ {
if p.responderMsgs[i].tokens[j] == TokenPsk {
return true
}
}
}
return false
}
// validateTokenDomain rejects tokens that are not allowed in their position: a
// pre-message may carry only Token::E or Token::S (Noise section 7.1 - a
// pre-message declares a pre-shared ephemeral or static public key), and a
// message body may carry only a defined token value (TokenE..TokenSkem). Both
// are construction-time fail-early checks for the same class the runtime
// otherwise catches late (processPreMessages / processWriteToken /
// processReadToken). It runs before detectPatternType and scanForPSK (the first
// token-semantic validators) because they switch on known tokens with no default
// and would otherwise misclassify an unknown one. Token position and order WITHIN a valid domain are
// enforced by validatePSKRules / validatePQTokenOrder, not duplicated here. The
// body check is an explicit token list rather than a numeric bound so a token
// added to the enum in future fails closed until it is handled. All 90
// predefined patterns satisfy both rules; this rejects only malformed custom
// patterns built via NewPattern.
//
// NewPattern is the only constructor of a HandshakePattern (its fields are
// unexported), so gating here covers every pattern the rest of the package ever
// sees, including the no-default token switches in detectPatternType, scanForPSK
// and the per-message overhead helpers. Any future pattern factory (for example
// a deserializer) must route through here too, or those switches reopen.
func validateTokenDomain(p *HandshakePattern) error {
// Pre-messages may declare only a pre-shared ephemeral or static public key.
checkPre := func(tokens []Token, who hsRole) error {
for _, t := range tokens {
if t != TokenE && t != TokenS {
return fmt.Errorf("%w: %s pre-message token value %d is not allowed (pre-message tokens may be only e or s)", ErrInvalidPattern, who, t)
}
}
return nil
}
if err := checkPre(p.preInitiator[:p.numPreInit], roleInitiator); err != nil {
return err
}
if err := checkPre(p.preResponder[:p.numPreResp], roleResponder); err != nil {
return err
}
// Message bodies may carry only a defined token value. Walk them in true wire
// order (walkInterleaved, the shared single source of truth for message
// ordering) so the reported message index matches every other validator.
return p.walkInterleaved(func(writer hsRole, msgIdx int, tokens []Token) error {
for _, t := range tokens {
switch t {
case TokenE, TokenS, TokenEE, TokenES, TokenSE, TokenSS, TokenPsk, TokenEkem, TokenSkem:
// defined token value; position and order are checked by
// validatePSKRules / validatePQTokenOrder
default:
return fmt.Errorf("%w: %s message %d carries unknown token value %d", ErrInvalidPattern, writer, msgIdx, t)
}
}
return nil
})
}
// validatePatternArity rejects a pattern whose message counts cannot form a
// well-shaped Noise handshake. A Noise handshake strictly alternates messages
// starting with the initiator (the initiator writes the first message, Noise
// section 7.1), so the only valid shapes have the responder writing either as
// many messages as the initiator (the last message is the responder's) or one
// fewer (the last message is the initiator's): numResponder <= numInitiator <=
// numResponder+1, with at least one message total. This single invariant
// subsumes every message-count malformation: zero messages (no handshake);
// responder-first / responder-heavy (numInitiator < numResponder, which the
// runtime status machine - a strict Send/Receive toggle - cannot drive without
// overflowing a message index); a one-way pattern with more than one message
// (numResponder==0 forces numInitiator==1, the single-message form Noise
// section 7.4 requires); and message-count skew (numInitiator > numResponder+1,
// two consecutive same-party messages with no reply between, breaking strict
// alternation). All 90 predefined patterns satisfy the invariant; this rejects
// only malformed custom patterns built via NewPattern. It runs first because it
// is pure structure and every later wire-order validator (which interleaves the
// two message arrays via walkInterleaved) assumes a well-shaped pattern.
func validatePatternArity(p *HandshakePattern) error {
ni, nr := p.numInitiator, p.numResponder
if ni+nr == 0 {
return fmt.Errorf("%w: pattern has no handshake messages", ErrInvalidPattern)
}
if ni < nr {
return fmt.Errorf("%w: responder writes %d messages but the initiator only %d; a Noise handshake alternates messages starting with the initiator (Noise section 7.1)",
ErrInvalidPattern, nr, ni)
}
if ni > nr+1 {
if nr == 0 {
return fmt.Errorf("%w: one-way pattern (the responder writes no message) sends %d initiator messages; a one-way pattern is a single message (Noise section 7.4)",
ErrInvalidPattern, ni)
}
return fmt.Errorf("%w: pattern has %d initiator and %d responder messages; Noise messages strictly alternate initiator-first, so the initiator may lead by at most one (Noise section 7.4)",
ErrInvalidPattern, ni, nr)
}
return nil
}
// validateHasCryptographicCalc rejects a pattern that performs no DH or KEM key
// agreement: with no ee/es/se/ss (DH) and no ekem/skem (KEM) token, the parties
// negotiate no shared secret, so the transport keys would derive entirely from
// public key material - a degenerate non-handshake (nyquist calls it "valid but
// nonsensical"). A PSK is deliberately NOT counted: a PSK authenticates but
// agrees no fresh secret, and Noise defines no pure-PSK pattern (its minimal PSK
// patterns always carry a DH/KEM). The count reuses tokenCountForParty (which
// includes pre-messages, but validateTokenDomain has already restricted those to
// e/s, so they contribute zero). All 90 predefined patterns perform at least one
// key agreement; this rejects only malformed custom patterns built via NewPattern.
func validateHasCryptographicCalc(p *HandshakePattern) error {
for _, tok := range [...]Token{TokenEE, TokenES, TokenSE, TokenSS, TokenEkem, TokenSkem} {
if tokenCountForParty(p, tok, true)+tokenCountForParty(p, tok, false) > 0 {
return nil
}
}
return fmt.Errorf("%w: pattern performs no DH or KEM key agreement (no ee/es/se/ss/ekem/skem token); it negotiates no shared secret", ErrInvalidPattern)
}
// validatePSKRules validates PSK position rules per Noise spec.
// PSK validation scans ACROSS messages (psk_sent persists).
// The own_randomness_applied check (PSK before first S) is enforced at
// RUNTIME in the handshake state machine, not here at construction.
// This function validates structural placement only.
//
// Rule: PSK token can only appear at position 0 or after all other tokens in a message.
//
// This is intentionally stricter than nyquist's IsValid, which performs no PSK
// position check at all: the Noise pskN modifier only ever emits a psk at the
// start of a message (psk0) or at its end, so a mid-message psk is not
// expressible by any standard pattern and is rejected here as malformed.
func validatePSKRules(p *HandshakePattern) error {
if !p.hasPSK {
return nil
}
// Validate PSK position: must be first or last in each message
validatePositions := func(msgs [5]patternMessage, count int) error {
for i := 0; i < count; i++ {
msg := &msgs[i]
for j := 0; j < msg.count; j++ {
if msg.tokens[j] == TokenPsk {
if j != 0 && j != msg.count-1 {
return fmt.Errorf("%w: PSK must be first or last token in message",
ErrInvalidPattern)
}
}
}
}
return nil
}
if err := validatePositions(p.initiatorMsgs, p.numInitiator); err != nil {
return err
}
return validatePositions(p.responderMsgs, p.numResponder)
}
// validatePQTokenOrder validates PQ-specific token ordering rules.
// PQ order validation is PER message (reset per message).
//
// In PQ patterns, Ekem and Skem can appear in messages independently.
// Ekem encapsulates to the remote's E (from a prior message).
// Skem encapsulates to the remote's S (from a prior message or pre-message).
// Within a single message, if both appear, Ekem must come before Skem.
func validatePQTokenOrder(p *HandshakePattern) error {
if p.patternType == PatternTypeDH {
return nil // NQ patterns have no KEM tokens
}
validateMsg := func(tokens []Token) error {
skemSeen := false
for _, t := range tokens {
switch t {
case TokenEkem:
if skemSeen {
return fmt.Errorf("%w: Ekem must come before Skem in same message", ErrInvalidPattern)
}
case TokenSkem:
skemSeen = true
}
}
return nil
}
for i := 0; i < p.numInitiator; i++ {
msg := &p.initiatorMsgs[i]
if err := validateMsg(msg.tokens[:msg.count]); err != nil {
return err
}
}
for i := 0; i < p.numResponder; i++ {
msg := &p.responderMsgs[i]
if err := validateMsg(msg.tokens[:msg.count]); err != nil {
return err
}
}
return nil
}
// tokenCountForParty returns how many times tok appears across the given party's
// pre-message and its own messages (the initiator writes initiatorMsgs, the
// responder writes responderMsgs).
func tokenCountForParty(p *HandshakePattern, tok Token, initiator bool) int {
count := func(tokens []Token) int {
n := 0
for _, t := range tokens {
if t == tok {
n++
}
}
return n
}
if initiator {
n := count(p.preInitiator[:p.numPreInit])