aboutsummaryrefslogtreecommitdiffstats
path: root/axon_test.go
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-03-08 22:07:14 -0700
committerbndw <ben@bdw.to>2026-03-08 22:07:14 -0700
commit3ff2bc0530bb98da139a5f68202c8e119f9d4775 (patch)
treebcca197dee7f13823ac17d2b5ec1b62b94b897a8 /axon_test.go
parent53b10eab74d83522dd90af697773e32279469b30 (diff)
feat: implement Phase 1 Axon protocol core package
Adds the foundational Go package implementing the full Axon protocol signing and crypto spec per PROTOCOL.md: - Event/Tag structs and all kind constants (KindProfile through KindJobFeedback) - Byte-exact canonical_payload construction per the PROTOCOL.md layout table - Tag sorting and canonical_tags SHA256 hash (duplicate detection included) - Ed25519 sign/verify, challenge sign/verify - X25519 key conversion from Ed25519 keypair (RFC 8032 §5.1.5 clamping + birational Edwards→Montgomery map for pubkeys) - ChaCha20-Poly1305 encrypt/decrypt for DMs (nonce prepended) - MessagePack encode/decode for events and wire messages Test vectors written first in testdata/vectors.json covering canonical_tags, canonical_payload, event_id, and signature verification — all deterministic known-input → known-output pairs for cross-language validation in Phase 4. 13 tests, all passing.
Diffstat (limited to 'axon_test.go')
-rw-r--r--axon_test.go550
1 files changed, 550 insertions, 0 deletions
diff --git a/axon_test.go b/axon_test.go
new file mode 100644
index 0000000..b7f5bd6
--- /dev/null
+++ b/axon_test.go
@@ -0,0 +1,550 @@
1package axon_test
2
3import (
4 "crypto/ed25519"
5 "encoding/hex"
6 "encoding/json"
7 "os"
8 "path/filepath"
9 "testing"
10 "time"
11
12 "axon"
13)
14
15// --------------------------------------------------------------------------
16// Helpers
17// --------------------------------------------------------------------------
18
19func mustDecodeHex(t *testing.T, s string) []byte {
20 t.Helper()
21 b, err := hex.DecodeString(s)
22 if err != nil {
23 t.Fatalf("hex decode %q: %v", s, err)
24 }
25 return b
26}
27
28func hexOf(b []byte) string { return hex.EncodeToString(b) }
29
30// knownSeed returns a fixed 32-byte seed so tests are fully deterministic.
31func knownSeed(n byte) []byte {
32 seed := make([]byte, 32)
33 for i := range seed {
34 seed[i] = n + byte(i)
35 }
36 return seed
37}
38
39// --------------------------------------------------------------------------
40// Test: canonical_tags
41// --------------------------------------------------------------------------
42
43func TestCanonicalTags(t *testing.T) {
44 vectors := loadVectors(t)
45 tags := vectors.TagsInput
46
47 enc, err := axon.CanonicalTags(tags)
48 if err != nil {
49 t.Fatalf("CanonicalTags: %v", err)
50 }
51 if len(enc) == 0 {
52 t.Fatal("CanonicalTags returned empty bytes")
53 }
54
55 h, err := axon.CanonicalTagsHash(tags)
56 if err != nil {
57 t.Fatalf("CanonicalTagsHash: %v", err)
58 }
59 if h == [32]byte{} {
60 t.Fatal("CanonicalTagsHash returned zero hash")
61 }
62
63 // Verify sort order: tags with the same name are sorted by first value.
64 // "e"/"reply-id" < "e"/"root-id" lexicographically. "e" < "p".
65 // So expected order: e/reply-id, e/root-id, p/alice
66 expectHash := mustDecodeHex(t, vectors.CanonicalTagsHash)
67 if hexOf(h[:]) != hexOf(expectHash) {
68 t.Errorf("CanonicalTagsHash mismatch:\n got %s\n want %s", hexOf(h[:]), hexOf(expectHash))
69 }
70
71 expectEnc := mustDecodeHex(t, vectors.CanonicalTagsHex)
72 if hexOf(enc) != hexOf(expectEnc) {
73 t.Errorf("canonical_tags bytes mismatch:\n got %s\n want %s", hexOf(enc), hexOf(expectEnc))
74 }
75}
76
77func TestCanonicalTagsDuplicateError(t *testing.T) {
78 tags := []axon.Tag{
79 {Name: "e", Values: []string{"same-id", "root"}},
80 {Name: "e", Values: []string{"same-id", "reply"}},
81 }
82 _, err := axon.CanonicalTags(tags)
83 if err == nil {
84 t.Fatal("expected error for duplicate (name, first_value) pair")
85 }
86}
87
88func TestCanonicalTagsEmpty(t *testing.T) {
89 enc, err := axon.CanonicalTags(nil)
90 if err != nil {
91 t.Fatalf("CanonicalTags(nil): %v", err)
92 }
93 // Should encode as uint16(0) = 2 zero bytes.
94 if len(enc) != 2 || enc[0] != 0 || enc[1] != 0 {
95 t.Errorf("empty tags encoding: got %x, want 0000", enc)
96 }
97}
98
99// --------------------------------------------------------------------------
100// Test: canonical_payload
101// --------------------------------------------------------------------------
102
103func TestCanonicalPayload(t *testing.T) {
104 vectors := loadVectors(t)
105 kp := axon.NewKeyPairFromSeed(mustDecodeHex(t, vectors.SeedHex))
106 content := mustDecodeHex(t, vectors.ContentHex)
107
108 payload, err := axon.CanonicalPayload(kp.PubKey, vectors.CreatedAt, vectors.Kind, content, vectors.TagsInput)
109 if err != nil {
110 t.Fatalf("CanonicalPayload: %v", err)
111 }
112
113 // Validate structure: first 2 bytes must be 0x00 0x20 (= 32).
114 if payload[0] != 0 || payload[1] != 32 {
115 t.Errorf("payload[0:2] = %x, want 0020", payload[0:2])
116 }
117 // Bytes [2:34] = pubkey.
118 if hexOf(payload[2:34]) != hexOf(kp.PubKey) {
119 t.Errorf("pubkey in payload mismatch")
120 }
121 // Total length: 2 + 32 + 8 + 2 + 4 + len(content) + 32
122 want := 2 + 32 + 8 + 2 + 4 + len(content) + 32
123 if len(payload) != want {
124 t.Errorf("payload length = %d, want %d", len(payload), want)
125 }
126
127 expectPayload := mustDecodeHex(t, vectors.CanonicalPayload)
128 if hexOf(payload) != hexOf(expectPayload) {
129 t.Errorf("canonical_payload mismatch:\n got %s\n want %s", hexOf(payload), hexOf(expectPayload))
130 }
131}
132
133// --------------------------------------------------------------------------
134// Test: event id
135// --------------------------------------------------------------------------
136
137func TestEventID(t *testing.T) {
138 vectors := loadVectors(t)
139 kp := axon.NewKeyPairFromSeed(mustDecodeHex(t, vectors.SeedHex))
140 content := mustDecodeHex(t, vectors.ContentHex)
141
142 id, err := axon.EventID(kp.PubKey, vectors.CreatedAt, vectors.Kind, content, vectors.TagsInput)
143 if err != nil {
144 t.Fatalf("EventID: %v", err)
145 }
146 if len(id) != 32 {
147 t.Fatalf("EventID returned %d bytes, want 32", len(id))
148 }
149
150 expectID := mustDecodeHex(t, vectors.EventID)
151 if hexOf(id) != hexOf(expectID) {
152 t.Errorf("event id mismatch:\n got %s\n want %s", hexOf(id), hexOf(expectID))
153 }
154}
155
156// --------------------------------------------------------------------------
157// Test: sign and verify
158// --------------------------------------------------------------------------
159
160func TestSignVerify(t *testing.T) {
161 kp := axon.NewKeyPairFromSeed(knownSeed(0x01))
162
163 e := &axon.Event{
164 CreatedAt: 1700000000,
165 Kind: axon.KindMessage,
166 Content: []byte("hello axon"),
167 Tags: []axon.Tag{{Name: "p", Values: []string{"alice"}}},
168 }
169 if err := axon.Sign(e, kp); err != nil {
170 t.Fatalf("Sign: %v", err)
171 }
172
173 if len(e.ID) != 32 {
174 t.Errorf("ID is %d bytes, want 32", len(e.ID))
175 }
176 if len(e.Sig) != ed25519.SignatureSize {
177 t.Errorf("Sig is %d bytes, want 64", len(e.Sig))
178 }
179
180 if err := axon.Verify(e); err != nil {
181 t.Fatalf("Verify: %v", err)
182 }
183
184 // Tampering with content must invalidate.
185 e2 := *e
186 e2.Content = []byte("tampered")
187 if err := axon.Verify(&e2); err == nil {
188 t.Fatal("Verify should fail after content tamper")
189 }
190
191 // Tampering with signature must invalidate.
192 e3 := *e
193 sig := make([]byte, len(e.Sig))
194 copy(sig, e.Sig)
195 sig[0] ^= 0xff
196 e3.Sig = sig
197 if err := axon.Verify(&e3); err == nil {
198 t.Fatal("Verify should fail after sig tamper")
199 }
200}
201
202func TestVerifyWrongKey(t *testing.T) {
203 kp1 := axon.NewKeyPairFromSeed(knownSeed(0x01))
204 kp2 := axon.NewKeyPairFromSeed(knownSeed(0x02))
205
206 e := &axon.Event{
207 CreatedAt: time.Now().Unix(),
208 Kind: axon.KindMessage,
209 Content: []byte("test"),
210 }
211 if err := axon.Sign(e, kp1); err != nil {
212 t.Fatalf("Sign: %v", err)
213 }
214
215 // Replace pubkey with kp2's pubkey; should fail.
216 e.PubKey = kp2.PubKey
217 if err := axon.Verify(e); err == nil {
218 t.Fatal("Verify should fail with wrong pubkey")
219 }
220}
221
222// --------------------------------------------------------------------------
223// Test: challenge sign/verify
224// --------------------------------------------------------------------------
225
226func TestChallengeSignVerify(t *testing.T) {
227 kp := axon.NewKeyPairFromSeed(knownSeed(0x03))
228 nonce := make([]byte, 32)
229 for i := range nonce {
230 nonce[i] = byte(i)
231 }
232 relayURL := "wss://relay.example.com"
233
234 sig := axon.SignChallenge(kp, nonce, relayURL)
235 if !axon.VerifyChallenge(kp.PubKey, nonce, relayURL, sig) {
236 t.Fatal("VerifyChallenge failed for valid signature")
237 }
238 if axon.VerifyChallenge(kp.PubKey, nonce, "wss://other.relay.com", sig) {
239 t.Fatal("VerifyChallenge should fail for different relay URL")
240 }
241}
242
243// --------------------------------------------------------------------------
244// Test: X25519 key conversion
245// --------------------------------------------------------------------------
246
247func TestX25519Conversion(t *testing.T) {
248 kp := axon.NewKeyPairFromSeed(knownSeed(0x01))
249
250 // Converting same Ed25519 keypair from both sides should yield same shared secret.
251 kp2 := axon.NewKeyPairFromSeed(knownSeed(0x02))
252
253 shared1, err := axon.DHSharedSecret(kp.PrivKey, kp2.PubKey)
254 if err != nil {
255 t.Fatalf("DHSharedSecret(kp->kp2): %v", err)
256 }
257 shared2, err := axon.DHSharedSecret(kp2.PrivKey, kp.PubKey)
258 if err != nil {
259 t.Fatalf("DHSharedSecret(kp2->kp): %v", err)
260 }
261 if shared1 != shared2 {
262 t.Errorf("DH not commutative:\n kp->kp2: %s\n kp2->kp: %s",
263 hexOf(shared1[:]), hexOf(shared2[:]))
264 }
265 if shared1 == [32]byte{} {
266 t.Fatal("shared secret is all zeros")
267 }
268}
269
270// --------------------------------------------------------------------------
271// Test: DM encrypt/decrypt round-trip
272// --------------------------------------------------------------------------
273
274func TestDMEncryptDecrypt(t *testing.T) {
275 alice := axon.NewKeyPairFromSeed(knownSeed(0x0a))
276 bob := axon.NewKeyPairFromSeed(knownSeed(0x0b))
277
278 plaintext := []byte("secret message from alice to bob")
279
280 ct, err := axon.EncryptDM(alice.PrivKey, bob.PubKey, plaintext)
281 if err != nil {
282 t.Fatalf("EncryptDM: %v", err)
283 }
284
285 pt, err := axon.DecryptDM(bob.PrivKey, alice.PubKey, ct)
286 if err != nil {
287 t.Fatalf("DecryptDM: %v", err)
288 }
289 if string(pt) != string(plaintext) {
290 t.Errorf("decrypt mismatch: got %q, want %q", pt, plaintext)
291 }
292
293 // Tamper with ciphertext — authentication should fail.
294 ct2 := make([]byte, len(ct))
295 copy(ct2, ct)
296 ct2[len(ct2)-1] ^= 0xff
297 if _, err := axon.DecryptDM(bob.PrivKey, alice.PubKey, ct2); err == nil {
298 t.Fatal("DecryptDM should fail on tampered ciphertext")
299 }
300
301 // Decrypt with wrong key should fail.
302 carol := axon.NewKeyPairFromSeed(knownSeed(0x0c))
303 if _, err := axon.DecryptDM(carol.PrivKey, alice.PubKey, ct); err == nil {
304 t.Fatal("DecryptDM should fail with wrong recipient key")
305 }
306}
307
308// --------------------------------------------------------------------------
309// Test: MessagePack encode/decode
310// --------------------------------------------------------------------------
311
312func TestMsgPackRoundTrip(t *testing.T) {
313 kp := axon.NewKeyPairFromSeed(knownSeed(0x05))
314 e := &axon.Event{
315 CreatedAt: 1700000000,
316 Kind: axon.KindMessage,
317 Content: []byte("msgpack test"),
318 Tags: []axon.Tag{
319 {Name: "p", Values: []string{"recipient"}},
320 {Name: "e", Values: []string{"ref-id", "root"}},
321 },
322 }
323 if err := axon.Sign(e, kp); err != nil {
324 t.Fatalf("Sign: %v", err)
325 }
326
327 data, err := axon.MarshalEvent(e)
328 if err != nil {
329 t.Fatalf("MarshalEvent: %v", err)
330 }
331 if len(data) == 0 {
332 t.Fatal("MarshalEvent returned empty bytes")
333 }
334
335 e2, err := axon.UnmarshalEvent(data)
336 if err != nil {
337 t.Fatalf("UnmarshalEvent: %v", err)
338 }
339
340 // Verify round-tripped event is still valid.
341 if err := axon.Verify(e2); err != nil {
342 t.Fatalf("Verify after msgpack round-trip: %v", err)
343 }
344
345 if hexOf(e.ID) != hexOf(e2.ID) {
346 t.Errorf("ID mismatch after round-trip")
347 }
348 if hexOf(e.Sig) != hexOf(e2.Sig) {
349 t.Errorf("Sig mismatch after round-trip")
350 }
351 if string(e.Content) != string(e2.Content) {
352 t.Errorf("Content mismatch after round-trip: got %q, want %q", e2.Content, e.Content)
353 }
354}
355
356// --------------------------------------------------------------------------
357// Test vector generation and validation
358// --------------------------------------------------------------------------
359
360// Vectors is the schema for testdata/vectors.json.
361type Vectors struct {
362 // canonical_tags vector
363 TagsInput []axon.Tag `json:"tags_input"`
364 CanonicalTagsHex string `json:"canonical_tags_hex"`
365 CanonicalTagsHash string `json:"canonical_tags_hash"`
366
367 // canonical_payload vector
368 SeedHex string `json:"seed_hex"`
369 PubKeyHex string `json:"pubkey_hex"`
370 CreatedAt int64 `json:"created_at"`
371 Kind uint16 `json:"kind"`
372 ContentHex string `json:"content_hex"`
373 CanonicalPayload string `json:"canonical_payload"`
374
375 // event id
376 EventID string `json:"event_id"`
377
378 // signature (sign with known seed; Ed25519 is deterministic so sig is stable)
379 SigHex string `json:"sig_hex"`
380}
381
382func vectorsPath() string {
383 return filepath.Join("testdata", "vectors.json")
384}
385
386func loadVectors(t *testing.T) Vectors {
387 t.Helper()
388 data, err := os.ReadFile(vectorsPath())
389 if err != nil {
390 t.Fatalf("load vectors: %v (run TestGenerateVectors first)", err)
391 }
392 var v Vectors
393 if err := json.Unmarshal(data, &v); err != nil {
394 t.Fatalf("parse vectors: %v", err)
395 }
396 return v
397}
398
399// TestGenerateVectors generates testdata/vectors.json from known inputs.
400// Run once with -run TestGenerateVectors to produce the file, then the other
401// tests validate against it. The file is committed to the repo.
402func TestGenerateVectors(t *testing.T) {
403 seed := knownSeed(0x01)
404 kp := axon.NewKeyPairFromSeed(seed)
405
406 tags := []axon.Tag{
407 {Name: "p", Values: []string{"alice"}},
408 {Name: "e", Values: []string{"root-id", "root"}},
409 {Name: "e", Values: []string{"reply-id", "reply"}},
410 }
411 content := []byte("hello axon")
412 createdAt := int64(1700000000)
413 kind := axon.KindMessage
414
415 // canonical_tags
416 tagsEnc, err := axon.CanonicalTags(tags)
417 if err != nil {
418 t.Fatalf("CanonicalTags: %v", err)
419 }
420 tagsHash, err := axon.CanonicalTagsHash(tags)
421 if err != nil {
422 t.Fatalf("CanonicalTagsHash: %v", err)
423 }
424
425 // canonical_payload
426 payload, err := axon.CanonicalPayload(kp.PubKey, createdAt, kind, content, tags)
427 if err != nil {
428 t.Fatalf("CanonicalPayload: %v", err)
429 }
430
431 // event id
432 id, err := axon.EventID(kp.PubKey, createdAt, kind, content, tags)
433 if err != nil {
434 t.Fatalf("EventID: %v", err)
435 }
436
437 // sign
438 e := &axon.Event{
439 CreatedAt: createdAt,
440 Kind: kind,
441 Content: content,
442 Tags: tags,
443 }
444 if err := axon.Sign(e, kp); err != nil {
445 t.Fatalf("Sign: %v", err)
446 }
447 // Verify the event so we know the sig is valid.
448 if err := axon.Verify(e); err != nil {
449 t.Fatalf("Verify generated event: %v", err)
450 }
451
452 v := Vectors{
453 TagsInput: tags,
454 CanonicalTagsHex: hexOf(tagsEnc),
455 CanonicalTagsHash: hexOf(tagsHash[:]),
456
457 SeedHex: hexOf(seed),
458 PubKeyHex: hexOf(kp.PubKey),
459 CreatedAt: createdAt,
460 Kind: kind,
461 ContentHex: hexOf(content),
462 CanonicalPayload: hexOf(payload),
463
464 EventID: hexOf(id),
465 SigHex: hexOf(e.Sig),
466 }
467
468 data, err := json.MarshalIndent(v, "", " ")
469 if err != nil {
470 t.Fatalf("marshal vectors: %v", err)
471 }
472
473 if err := os.MkdirAll("testdata", 0o755); err != nil {
474 t.Fatalf("mkdir testdata: %v", err)
475 }
476 if err := os.WriteFile(vectorsPath(), data, 0o644); err != nil {
477 t.Fatalf("write vectors: %v", err)
478 }
479
480 t.Logf("vectors written to %s", vectorsPath())
481 t.Logf("canonical_tags_hash: %s", hexOf(tagsHash[:]))
482 t.Logf("event_id: %s", hexOf(id))
483}
484
485// TestVectors validates all vectors in testdata/vectors.json. This is the
486// primary cross-language compatibility check.
487func TestVectors(t *testing.T) {
488 v := loadVectors(t)
489
490 seed := mustDecodeHex(t, v.SeedHex)
491 kp := axon.NewKeyPairFromSeed(seed)
492
493 if hexOf(kp.PubKey) != v.PubKeyHex {
494 t.Errorf("pubkey mismatch:\n got %s\n want %s", hexOf(kp.PubKey), v.PubKeyHex)
495 }
496
497 // canonical_tags
498 tagsEnc, err := axon.CanonicalTags(v.TagsInput)
499 if err != nil {
500 t.Fatalf("CanonicalTags: %v", err)
501 }
502 if hexOf(tagsEnc) != v.CanonicalTagsHex {
503 t.Errorf("canonical_tags bytes mismatch:\n got %s\n want %s",
504 hexOf(tagsEnc), v.CanonicalTagsHex)
505 }
506
507 tagsHash, err := axon.CanonicalTagsHash(v.TagsInput)
508 if err != nil {
509 t.Fatalf("CanonicalTagsHash: %v", err)
510 }
511 if hexOf(tagsHash[:]) != v.CanonicalTagsHash {
512 t.Errorf("canonical_tags_hash mismatch:\n got %s\n want %s",
513 hexOf(tagsHash[:]), v.CanonicalTagsHash)
514 }
515
516 // canonical_payload
517 content := mustDecodeHex(t, v.ContentHex)
518 payload, err := axon.CanonicalPayload(kp.PubKey, v.CreatedAt, v.Kind, content, v.TagsInput)
519 if err != nil {
520 t.Fatalf("CanonicalPayload: %v", err)
521 }
522 if hexOf(payload) != v.CanonicalPayload {
523 t.Errorf("canonical_payload mismatch:\n got %s\n want %s",
524 hexOf(payload), v.CanonicalPayload)
525 }
526
527 // event id
528 id, err := axon.EventID(kp.PubKey, v.CreatedAt, v.Kind, content, v.TagsInput)
529 if err != nil {
530 t.Fatalf("EventID: %v", err)
531 }
532 if hexOf(id) != v.EventID {
533 t.Errorf("event_id mismatch:\n got %s\n want %s", hexOf(id), v.EventID)
534 }
535
536 // verify the stored signature
537 sig := mustDecodeHex(t, v.SigHex)
538 e := &axon.Event{
539 ID: id,
540 PubKey: kp.PubKey,
541 CreatedAt: v.CreatedAt,
542 Kind: v.Kind,
543 Content: content,
544 Sig: sig,
545 Tags: v.TagsInput,
546 }
547 if err := axon.Verify(e); err != nil {
548 t.Errorf("Verify stored vector event: %v", err)
549 }
550}