-
Notifications
You must be signed in to change notification settings - Fork 3
/
coze.go
436 lines (390 loc) · 12.7 KB
/
coze.go
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
/*
Package coze, see the README at https://github.com/Cyphrme/Coze
This library exports some functions that may be helpful for other applications,
but should not be considered apart of the Coze specification API.
- ECDSAToLowSSig
- IsLowS
- ToLowS
- Marshal
- MarshalPretty
Other auxiliary types and functions, like B64, are reasonable exports for
package Coze, especially since compatible implementations are absent from the Go
standard library.
*/
package coze
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"math/big"
"golang.org/x/crypto/sha3"
)
// Coze is for signed Coze objects (cozies). See the Coze docs (README.md) for
// more on the construction of `coze`.
//
// Pay: The raw Payload.
// Key: Key used to sign the message. Must be pointer, otherwise
// json.Marshal will not marshal on zero type.
// See: https://github.com/golang/go/issues/11939.
// Can: "Canon" Pay's fields in order of appearance.
// Cad: "Canonical Digest" Pay's compactified form digest.
// Sig: Signature over `cad`.
// Czd: "Coze digest" with canon ["cad","sig"].
// Parsed: The standard Coze pay fields ["alg","iat","tmb","typ"] parsed
// from `Pay`. `Parsed` is populated by Meta() and is JSON ignored.
type Coze struct {
Pay json.RawMessage `json:"pay,omitempty"`
Key *Key `json:"key,omitempty"`
Can []string `json:"can,omitempty"`
Cad B64 `json:"cad,omitempty"`
Sig B64 `json:"sig,omitempty"`
Czd B64 `json:"czd,omitempty"`
Parsed *Pay `json:"-"`
}
// String implements fmt.Stringer. Without this method `pay` prints as bytes.
// On error, returns the error as a string.
func (cz Coze) String() string {
b, err := Marshal(cz)
if err != nil {
return err.Error()
}
return string(b)
}
// Meta calculates [can, cad, czd] and sets Coze.Parsed
// ["alg","iat","tmb","typ"] from Pay. Coze.Pay, Coze.Pay.Alg, and Coze.Sig must
// be set. Meta resets Parsed ("alg","iat","tmb","typ") to zero before
// populating Parsed from Pay. If needing to use for contextual cozies, use
// "MetaWithAlg".
//
// Meta does no cryptographic verification.
func (cz *Coze) Meta() (err error) {
if cz.Pay == nil || cz.Sig == nil {
return errors.New("Meta: pay and/or sig is nil")
}
// Reset coze.parsed to zero.
cz.Parsed = new(Pay)
return cz.MetaWithAlg("")
}
// MetaWithAlg is for contextual cozies that may be lacking `alg` in `pay`, but
// `alg` in otherwise known. MetaWithAlg recalculates [can, cad, czd] and sets
// Coze.Parsed ("alg","iat","tmb","typ") from Pay. Does not calculate `czd`
// if Coze.Sig is empty.
//
// Errors on
// 1. Invalid JSON.
// 2. No alg is given. (both coze.pay.alg and alg are empty).
// 3. Mismatched Pay.Alg and parameter alg if both are set.
//
// MetaWithAlg does no cryptographic verification.
func (cz *Coze) MetaWithAlg(alg SEAlg) (err error) {
// Set Parsed from Pay.
err = json.Unmarshal(cz.Pay, &cz.Parsed)
if err != nil {
return err
}
if alg == "" {
alg = cz.Parsed.Alg
}
if cz.Parsed.Alg != "" && alg != cz.Parsed.Alg {
return fmt.Errorf("MetaWithAlg: input alg %q and pay.alg %q are unequal", alg, cz.Parsed.Alg)
}
b, err := compact(cz.Pay)
if err != nil {
return err
}
cz.Cad, err = Hash(alg.Hash(), b)
if err != nil {
return err
}
cz.Can, err = Canon(b)
if err != nil {
return err
}
cz.Czd = []byte{} // Zero `czd` in case Coze.Sig is not set.
if len(cz.Sig) != 0 {
cz.Czd, err = GenCzd(alg.Hash(), cz.Cad, cz.Sig)
return err
}
return nil
}
// UnmarshalJSON unmarshals checks for duplicates and unmarshals `coze`.
// See notes on Pay.UnmarshalJSON.
func (cz *Coze) UnmarshalJSON(b []byte) error {
err := checkDuplicate(json.NewDecoder(bytes.NewReader(b)))
if err != nil {
return err
}
type coze2 Coze // Break infinite unmarshal loop
cz2 := new(coze2)
cz2.Parsed = cz.Parsed
cz2.Key = cz.Key
err = json.Unmarshal(b, cz2)
if err != nil {
return err
}
*cz = *(*Coze)(cz2)
return nil
}
// CzdCanon is the canon for a `czd`.
var CzdCanon = []string{"cad", "sig"}
const maxSafeInteger = 9007199254740991
// GenCzd generates and returns `czd`.
func GenCzd(hash HshAlg, cad B64, sig B64) (czd B64, err error) {
return Hash(hash, []byte(fmt.Sprintf(`{"cad":%q,"sig":%q}`, cad, sig)))
}
// Pay contains the standard Coze pay fields as well as custom struct given by
// third party applications. This allows embedding third party structs into Pay
// for creating custom cozies (see example ExampleKey_SignPay).
//
// The JSON tags on [Alg, Iat, Tmb, Typ, Rvk, Struct] are ineffective due to the
// custom MarshalJSON(), however they are present for documentation.
//
// `Struct` will be marshaled when not empty. The custom marshaller promotes
// fields inside `Struct` to be top level fields inside of `pay`. The tag
// `json:"-"` is ignored by the custom marshaller, and is set to "-" so that the
// default marshaller does not include it.
//
// iat and rvk are type int64 and not uint64 to follow the advised type for
// third party time fields.
type Pay struct {
Alg SEAlg `json:"alg,omitempty"` // e.g. "ES256"
Iat int64 `json:"iat,omitempty"` // e.g. 1623132000
Tmb B64 `json:"tmb,omitempty"` // e.g. "cLj8vsYtMBwYkzoFVZHBZo6SNL8wSdCIjCKAwXNuhOk"
Typ string `json:"typ,omitempty"` // e.g. "cyphr.me/msg/create"
// Rvk is only for revoke messages.
Rvk int64 `json:"rvk,omitempty"` // e.g. 1623132000
// Custom arbitrary struct given by application.
Struct any `json:"-"`
}
// Coze returns a new coze with only Pay populated.
func (p *Pay) Coze() (coze *Coze, err error) {
coze = new(Coze)
coze.Pay, err = Marshal(p)
return coze, err
}
// String implements fmt.Stringer.
// On error, returns the error as a string.
func (p Pay) String() string {
b, err := p.MarshalJSON()
if err != nil {
return err.Error()
}
return string(b)
}
// MarshalJSON promotes the embedded field "Struct" to top level JSON.
// Solution from Jonathan Hall:
// https://jhall.io/posts/go-json-tricks-embedded-marshaler
func (p *Pay) MarshalJSON() ([]byte, error) {
type pay2 Pay // Break infinite Marshal loop
pay, err := Marshal((*pay2)(p))
if err != nil {
return nil, err
}
if p.Struct == nil {
return pay, nil
}
s, err := json.Marshal(p.Struct)
if err != nil {
return nil, err
}
// Concatenate the two:
s[0] = ','
return append(pay[:len(pay)-1], s...), nil
}
// UnmarshalJSON unmarshals both Pay and if given custom Pay.Struct. Throws an
// error on duplicate. (Duplicate related, see
// https://github.com/golang/go/issues/48298)
func (p *Pay) UnmarshalJSON(b []byte) error {
err := checkDuplicate(json.NewDecoder(bytes.NewReader(b)))
if err != nil {
return err
}
type pay2 Pay // Break infinite unmarshal loop
p2 := new(pay2)
err = json.Unmarshal(b, p2)
if err != nil {
return err
}
if p.Struct != nil { // Inner custom application struct.
str := p.Struct
err = json.Unmarshal(b, str)
if err != nil {
return err
}
p2.Struct = str
}
if p2.Iat > maxSafeInteger || p2.Rvk > maxSafeInteger || p2.Iat < 0 || p2.Rvk < 0 {
return fmt.Errorf("Pay.UnmarshalJSON: values for iat and rvk must be between 0 and 2^53 - 1")
}
*p = *(*Pay)(p2)
return nil
}
// PadInts creates a big-endian byte slice with given size that is the left
// padded concatenation of two input integers. Parameter `size` must be even.
// From Go's packages, X, Y, R, and S are type big.Int of varying size. Before
// encoding to fixed size string, left padding of bytes is needed.
//
// Algorithm notes: EdDSA is little-endian while ECDSA is big-endian. EdDSA
// should not be used with this function.
//
// For ECDSA, Coze's `x` and `sig` is left padded concatenation of X || Y and R
// || S respectively.
//
// Note: ES512's signature size is 132 bytes (and not 128, 131, or 130.25),
// because R and S are each respectively rounded up and padded to 528 and for a
// total signature size of 1056 bits.
// See https://datatracker.ietf.org/doc/html/rfc4754#section-7
func PadInts(r, s *big.Int, size int) (out B64) {
if !(size%2 == 0) {
panic("size must be even.")
}
out = make([]byte, size)
half := size / 2
rb := r.Bytes()
copy(out[half-len(rb):], rb)
sb := s.Bytes()
copy(out[half+(half-(len(sb))):], sb)
return out
}
// Marshaler is a UTF-8 marshaller for Go structs. Go's `json.Marshal`
// removes the valid characters "&". "<", ">". See note on Marshal. (Note that
// Marshaler follows the Go "-er" convention for interfaces while marshaller is
// spelled with two l's)
type Marshaler interface {
CozeMarshal() ([]byte, error)
}
// Marshal is a JSON friendly marshaller. json.Marshal preemptively replaces
// the valid JSON and UTF-8 characters "&". "<", ">" with the "slash u" unicode
// escapes (e.g. \u0026) in the name of HTML friendliness, for example:
// https://play.golang.org/p/o2hiX0c62oN. As JSON is not HTML, preemptive HTML
// escaping is incorrect. (The JSON spec calls for no such measures and other
// industry encoders do no such preemptive escaping.) Where JSON may include
// these legitimate characters, like sanitized arbitrary user data, json.Marshal
// should not be used. The Go team is aware that the existing implementation has
// this and other concerns but has not yet released a new version. Joe Tsai is
// working on fixes in a yet-to-be-released Tailscale's "JSONv2" package, which
// we hope to use upon release.
// https://pkg.go.dev/github.com/go-json-experiment/json. The package also
// enumerates other JSON best practices that may be of concern for anyone
// concerned with the aforementioned issue.
// https://github.com/go-json-experiment/json#behavior-changes. See
// https://github.com/Cyphrme/Coze/issues/15 for other JSON encoding concerns.
//
// Go structs already require unique fields, so unlike coze.UnmarshalJSON or
// pay.UnmarshalJSON, marshaling will not sanitize for duplicates.
func Marshal(i any) ([]byte, error) {
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
encoder.SetEscapeHTML(false)
err := encoder.Encode(i)
if err != nil {
return nil, err
}
return bytes.TrimRight(buffer.Bytes(), "\n"), nil
}
// MarshalPretty uses 4 spaces for each level. Spaces are used instead of tabs
// because some applications display tabs as 8 spaces, which is excessive.
func MarshalPretty(i any) ([]byte, error) {
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
encoder.SetEscapeHTML(false)
err := encoder.Encode(i)
if err != nil {
return nil, err
}
return bytes.TrimRight(buffer.Bytes(), "\n"), nil
}
// Hash hashes msg and returns the digest. Returns nil on error. Errors on
// invalid HshAlg or if the resulting digest is empty (as a sanity check).
//
// For algorithms that support arbitrary sized digests, Hash only returns a
// static size. (SHAKE128 returns 32 bytes and SHAKE256 returns 64 bytes.)
func Hash(h HshAlg, msg []byte) (digest B64, err error) {
switch h {
case SHAKE128:
digest = make([]byte, 32)
sha3.ShakeSum128(digest, msg)
case SHAKE256:
digest = make([]byte, 64)
sha3.ShakeSum256(digest, msg)
default:
hash := h.goHash()
if hash == nil {
return nil, fmt.Errorf("Hash: invalid HshAlg %q", h)
}
_, err = hash.Write(msg)
if err != nil {
return nil, err
}
digest = hash.Sum(nil)
}
if len(digest) == 0 { // sanity check
return nil, fmt.Errorf("Hash: digest is empty; given HshAlg %q", h)
}
return digest, nil
}
// IsRevoke returns true if the given Key is marked as revoked.
func (p *Pay) IsRevoke() bool {
return isRevoke(p.Rvk)
}
func isRevoke(rvk int64) bool {
// rvk is not allowed to be larger than 2^53 -1. This library assumes that
// Unmarshal will error on rvk's that do not meet the specification
// requirements, so no error is needed here.
if rvk > maxSafeInteger {
return false
}
return rvk > 0
}
// ErrJSONDuplicate allows applications to check for JSON duplicate error.
type ErrJSONDuplicate error
// checkDuplicate checks for JSON duplicates. See notes on Marshal and the
// README FAQ on duplicate fields.
func checkDuplicate(d *json.Decoder) error {
t, err := d.Token()
if err != nil {
return err
}
// Is it a delimiter?
delim, ok := t.(json.Delim)
if !ok {
return nil // scaler type, nothing to do
}
switch delim {
case '{':
keys := make(map[string]bool)
for d.More() {
t, err := d.Token() // Get field key.
if err != nil {
return err
}
key := t.(string)
if keys[key] { // Check for duplicates.
return ErrJSONDuplicate(fmt.Errorf("Coze: JSON duplicate field %q", key))
}
keys[key] = true
// Recursive, Check value in case value is object.
err = checkDuplicate(d)
if err != nil {
return err
}
}
// consume trailing }
if _, err := d.Token(); err != nil {
return err
}
case '[':
for d.More() {
if err := checkDuplicate(d); err != nil {
return err
}
}
// consume trailing ]
if _, err := d.Token(); err != nil {
return err
}
}
return nil
}