aboutsummaryrefslogtreecommitdiffstats
path: root/axon.go
diff options
context:
space:
mode:
Diffstat (limited to 'axon.go')
-rw-r--r--axon.go272
1 files changed, 272 insertions, 0 deletions
diff --git a/axon.go b/axon.go
new file mode 100644
index 0000000..51ec22d
--- /dev/null
+++ b/axon.go
@@ -0,0 +1,272 @@
1// Package axon implements the Axon protocol core: event signing, verification,
2// canonical payload construction, and related helpers.
3package axon
4
5import (
6 "crypto/ed25519"
7 "crypto/rand"
8 "crypto/sha256"
9 "encoding/binary"
10 "errors"
11 "fmt"
12 "sort"
13)
14
15// Event kind constants matching the registry in PROTOCOL.md.
16const (
17 KindProfile uint16 = 0 // Identity metadata
18 KindMessage uint16 = 1000 // Plain text note
19 KindDM uint16 = 2000 // Encrypted direct message
20 KindProgress uint16 = 3000 // Ephemeral progress/status indicator
21 KindJobRequest uint16 = 5000 // Request for agent work
22 KindJobResult uint16 = 6000 // Completed job output
23 KindJobFeedback uint16 = 7000 // In-progress status / error
24)
25
26// Tag is a named list of string values attached to an Event.
27type Tag struct {
28 Name string `msgpack:"name" json:"name"`
29 Values []string `msgpack:"values" json:"values"`
30}
31
32// Event is the core Axon data structure. All fields use their wire types.
33// id, pubkey and sig are raw 32/64-byte slices, not hex.
34// content is opaque bytes (msgpack bin type).
35type Event struct {
36 ID []byte `msgpack:"id"` // 32 bytes, SHA256 of canonical payload
37 PubKey []byte `msgpack:"pubkey"` // 32 bytes, Ed25519 public key
38 CreatedAt int64 `msgpack:"created_at"` // Unix timestamp
39 Kind uint16 `msgpack:"kind"`
40 Content []byte `msgpack:"content"` // opaque; msgpack bin type
41 Sig []byte `msgpack:"sig"` // 64 bytes, Ed25519 signature over id
42 Tags []Tag `msgpack:"tags"`
43}
44
45// KeyPair holds an Ed25519 private and public key.
46type KeyPair struct {
47 PrivKey ed25519.PrivateKey
48 PubKey ed25519.PublicKey
49}
50
51// NewKeyPair generates a fresh Ed25519 keypair.
52func NewKeyPair() (KeyPair, error) {
53 pub, priv, err := ed25519.GenerateKey(rand.Reader)
54 if err != nil {
55 return KeyPair{}, fmt.Errorf("axon: generate key: %w", err)
56 }
57 return KeyPair{PrivKey: priv, PubKey: pub}, nil
58}
59
60// NewKeyPairFromSeed derives a keypair from a 32-byte seed (the canonical
61// private key representation). Panics if seed is not 32 bytes.
62func NewKeyPairFromSeed(seed []byte) KeyPair {
63 if len(seed) != ed25519.SeedSize {
64 panic(fmt.Sprintf("axon: seed must be %d bytes, got %d", ed25519.SeedSize, len(seed)))
65 }
66 priv := ed25519.NewKeyFromSeed(seed)
67 return KeyPair{PrivKey: priv, PubKey: priv.Public().(ed25519.PublicKey)}
68}
69
70// CanonicalTags encodes tags into their canonical binary representation and
71// returns the raw bytes (before hashing). This is exposed so callers can
72// inspect the encoding in tests; normally you want CanonicalTagsHash.
73//
74// Encoding:
75//
76// uint16(num_tags)
77// for each tag (sorted by name, then first value for ties):
78// uint16(len(name)) || utf8(name)
79// uint16(num_values)
80// for each value:
81// uint32(len(value)) || utf8(value)
82//
83// Returns an error if two tags share the same name and first value (protocol error).
84func CanonicalTags(tags []Tag) ([]byte, error) {
85 // Sort a copy so we don't mutate the caller's slice.
86 sorted := make([]Tag, len(tags))
87 copy(sorted, tags)
88 sort.SliceStable(sorted, func(i, j int) bool {
89 if sorted[i].Name != sorted[j].Name {
90 return sorted[i].Name < sorted[j].Name
91 }
92 vi := ""
93 if len(sorted[i].Values) > 0 {
94 vi = sorted[i].Values[0]
95 }
96 vj := ""
97 if len(sorted[j].Values) > 0 {
98 vj = sorted[j].Values[0]
99 }
100 return vi < vj
101 })
102
103 // Detect duplicates (same name + same first value).
104 for i := 1; i < len(sorted); i++ {
105 prev, cur := sorted[i-1], sorted[i]
106 if prev.Name != cur.Name {
107 continue
108 }
109 prevFirst := ""
110 if len(prev.Values) > 0 {
111 prevFirst = prev.Values[0]
112 }
113 curFirst := ""
114 if len(cur.Values) > 0 {
115 curFirst = cur.Values[0]
116 }
117 if prevFirst == curFirst {
118 return nil, fmt.Errorf("axon: duplicate tag (name=%q first_value=%q)", cur.Name, curFirst)
119 }
120 }
121
122 // Estimate capacity to avoid repeated allocations.
123 buf := make([]byte, 0, 2+len(sorted)*16)
124 var hdr [4]byte
125
126 binary.BigEndian.PutUint16(hdr[:2], uint16(len(sorted)))
127 buf = append(buf, hdr[:2]...)
128
129 for _, tag := range sorted {
130 name := []byte(tag.Name)
131 binary.BigEndian.PutUint16(hdr[:2], uint16(len(name)))
132 buf = append(buf, hdr[:2]...)
133 buf = append(buf, name...)
134
135 binary.BigEndian.PutUint16(hdr[:2], uint16(len(tag.Values)))
136 buf = append(buf, hdr[:2]...)
137
138 for _, v := range tag.Values {
139 vb := []byte(v)
140 binary.BigEndian.PutUint32(hdr[:4], uint32(len(vb)))
141 buf = append(buf, hdr[:4]...)
142 buf = append(buf, vb...)
143 }
144 }
145
146 return buf, nil
147}
148
149// CanonicalTagsHash returns SHA256(canonical_tags encoding).
150func CanonicalTagsHash(tags []Tag) ([32]byte, error) {
151 enc, err := CanonicalTags(tags)
152 if err != nil {
153 return [32]byte{}, err
154 }
155 return sha256.Sum256(enc), nil
156}
157
158// CanonicalPayload constructs the deterministic byte payload that is hashed to
159// produce the event ID.
160//
161// Layout:
162//
163// [0:2] uint16 = 32 pubkey length (always 32)
164// [2:34] bytes pubkey
165// [34:42] uint64 created_at
166// [42:44] uint16 kind
167// [44:48] uint32 content length
168// [48:48+n] bytes content
169// [48+n:80+n] bytes SHA256(canonical_tags), 32 bytes
170func CanonicalPayload(pubkey []byte, createdAt int64, kind uint16, content []byte, tags []Tag) ([]byte, error) {
171 if len(pubkey) != 32 {
172 return nil, fmt.Errorf("axon: pubkey must be 32 bytes, got %d", len(pubkey))
173 }
174 if len(content) > 65536 {
175 return nil, errors.New("axon: content exceeds 65536 byte limit")
176 }
177
178 tagsHash, err := CanonicalTagsHash(tags)
179 if err != nil {
180 return nil, err
181 }
182
183 n := len(content)
184 // Total size: 2 + 32 + 8 + 2 + 4 + n + 32
185 payload := make([]byte, 80+n)
186
187 binary.BigEndian.PutUint16(payload[0:2], 32)
188 copy(payload[2:34], pubkey)
189 binary.BigEndian.PutUint64(payload[34:42], uint64(createdAt))
190 binary.BigEndian.PutUint16(payload[42:44], kind)
191 binary.BigEndian.PutUint32(payload[44:48], uint32(n))
192 copy(payload[48:48+n], content)
193 copy(payload[48+n:80+n], tagsHash[:])
194
195 return payload, nil
196}
197
198// EventID computes the canonical SHA256 event ID for the given fields.
199func EventID(pubkey []byte, createdAt int64, kind uint16, content []byte, tags []Tag) ([]byte, error) {
200 payload, err := CanonicalPayload(pubkey, createdAt, kind, content, tags)
201 if err != nil {
202 return nil, err
203 }
204 h := sha256.Sum256(payload)
205 return h[:], nil
206}
207
208// Sign fills in e.ID and e.Sig using kp. It also sets e.PubKey from kp.
209// The caller should populate all other fields (CreatedAt, Kind, Content, Tags)
210// before calling Sign.
211func Sign(e *Event, kp KeyPair) error {
212 e.PubKey = []byte(kp.PubKey)
213
214 id, err := EventID(e.PubKey, e.CreatedAt, e.Kind, e.Content, e.Tags)
215 if err != nil {
216 return err
217 }
218 e.ID = id
219 e.Sig = ed25519.Sign(kp.PrivKey, id)
220 return nil
221}
222
223// Verify checks that e.Sig is a valid Ed25519 signature of e.ID using e.PubKey,
224// and that e.ID matches the canonical payload derived from the event fields.
225// Returns nil if both checks pass.
226func Verify(e *Event) error {
227 if len(e.PubKey) != 32 {
228 return fmt.Errorf("axon: pubkey must be 32 bytes, got %d", len(e.PubKey))
229 }
230 if len(e.Sig) != ed25519.SignatureSize {
231 return fmt.Errorf("axon: sig must be %d bytes, got %d", ed25519.SignatureSize, len(e.Sig))
232 }
233
234 expectedID, err := EventID(e.PubKey, e.CreatedAt, e.Kind, e.Content, e.Tags)
235 if err != nil {
236 return fmt.Errorf("axon: compute expected id: %w", err)
237 }
238 if len(e.ID) != 32 {
239 return fmt.Errorf("axon: id must be 32 bytes, got %d", len(e.ID))
240 }
241 for i := range expectedID {
242 if expectedID[i] != e.ID[i] {
243 return errors.New("axon: event id does not match canonical payload")
244 }
245 }
246
247 pub := ed25519.PublicKey(e.PubKey)
248 if !ed25519.Verify(pub, e.ID, e.Sig) {
249 return errors.New("axon: invalid signature")
250 }
251 return nil
252}
253
254// SignChallenge signs the relay authentication challenge:
255//
256// sig = ed25519.Sign(privkey, SHA256(nonce || utf8(relay_url)))
257func SignChallenge(kp KeyPair, nonce []byte, relayURL string) []byte {
258 h := sha256.New()
259 h.Write(nonce)
260 h.Write([]byte(relayURL))
261 digest := h.Sum(nil)
262 return ed25519.Sign(kp.PrivKey, digest)
263}
264
265// VerifyChallenge verifies a challenge signature.
266func VerifyChallenge(pubkey []byte, nonce []byte, relayURL string, sig []byte) bool {
267 h := sha256.New()
268 h.Write(nonce)
269 h.Write([]byte(relayURL))
270 digest := h.Sum(nil)
271 return ed25519.Verify(ed25519.PublicKey(pubkey), digest, sig)
272}