146 lines
4.5 KiB
Go
146 lines
4.5 KiB
Go
package wire
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"sort"
|
|
)
|
|
|
|
// marshal is encoding/json with HTML escaping off. The default escapes <, > and &, which would make
|
|
// a body containing those characters hash differently here than it does in Node or Swift.
|
|
func marshal(value any) ([]byte, error) {
|
|
buf := bytes.NewBuffer(nil)
|
|
encoder := json.NewEncoder(buf)
|
|
encoder.SetEscapeHTML(false)
|
|
if err := encoder.Encode(value); err != nil {
|
|
return nil, err
|
|
}
|
|
return bytes.TrimRight(buf.Bytes(), "\n"), nil
|
|
}
|
|
|
|
func unmarshal(raw []byte, value any) error {
|
|
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
decoder.UseNumber()
|
|
if err := decoder.Decode(value); err != nil {
|
|
return err
|
|
}
|
|
// Trailing content would mean the caller is looking at a different document than the one that
|
|
// was decoded, which for a signed field is a correctness bug rather than a style choice.
|
|
var extra any
|
|
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
|
|
return errors.New("wire: trailing data after JSON value")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CanonicalJSON renders a JSON value in the one form this protocol hashes.
|
|
//
|
|
// The rules are deliberately small, and every implementation (Go agent, Node relay, Swift app)
|
|
// must follow exactly these:
|
|
//
|
|
// 1. Object keys are sorted by UTF-8 byte order and emitted without whitespace.
|
|
// 2. Arrays keep their order.
|
|
// 3. Strings use the same escapes as Go's encoder with HTML escaping disabled: \" \\ \n \r \t,
|
|
// \u00XX for other control characters, and literal UTF-8 for everything else.
|
|
// 4. Numbers must be integers in canonical form: an optional minus, no leading zero, no fraction,
|
|
// no exponent, no plus. This is stricter than JSON allows and on purpose. Any wider grammar has
|
|
// values that are the same number but different bytes (1 and 1.0, 1e2 and 100), and "which
|
|
// spelling did the phone hash" is not a question this protocol is willing to leave open. The
|
|
// APIs behind it take integers, strings, booleans, arrays and objects, so nothing is lost.
|
|
// 5. null, true and false are literal.
|
|
//
|
|
// An empty or absent body canonicalises to the empty byte string, not to the four characters
|
|
// "null", so "no body" and "empty body" cannot be confused.
|
|
func CanonicalJSON(raw []byte) ([]byte, error) {
|
|
trimmed := bytes.TrimSpace(raw)
|
|
if len(trimmed) == 0 {
|
|
return nil, nil
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(trimmed))
|
|
decoder.UseNumber()
|
|
var value any
|
|
if err := decoder.Decode(&value); err != nil {
|
|
return nil, fmt.Errorf("wire: body is not valid JSON: %w", err)
|
|
}
|
|
var extra any
|
|
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
|
|
return nil, errors.New("wire: body has trailing data after the JSON value")
|
|
}
|
|
buf := bytes.NewBuffer(make([]byte, 0, len(trimmed)))
|
|
if err := writeCanonical(buf, value); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// canonicalNumber is the only number spelling this protocol will hash. See rule 4 above.
|
|
//
|
|
// Negative zero is excluded even though it is an integer: no implementation can be relied on to
|
|
// round-trip "-0" as text rather than as the number 0, and nothing in these payloads needs it.
|
|
var canonicalNumber = regexp.MustCompile(`^-?[1-9][0-9]*$|^-?[1-9][0-9]*$|^0$`)
|
|
|
|
func writeCanonical(buf *bytes.Buffer, value any) error {
|
|
switch typed := value.(type) {
|
|
case nil:
|
|
buf.WriteString("null")
|
|
case bool:
|
|
if typed {
|
|
buf.WriteString("true")
|
|
} else {
|
|
buf.WriteString("false")
|
|
}
|
|
case json.Number:
|
|
literal := typed.String()
|
|
if !canonicalNumber.MatchString(literal) || literal == "-0" {
|
|
return fmt.Errorf("wire: number %q is not a canonical integer", literal)
|
|
}
|
|
buf.WriteString(literal)
|
|
case string:
|
|
encoded, err := marshal(typed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
buf.Write(encoded)
|
|
case []any:
|
|
buf.WriteByte('[')
|
|
for index, item := range typed {
|
|
if index > 0 {
|
|
buf.WriteByte(',')
|
|
}
|
|
if err := writeCanonical(buf, item); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
buf.WriteByte(']')
|
|
case map[string]any:
|
|
keys := make([]string, 0, len(typed))
|
|
for key := range typed {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
buf.WriteByte('{')
|
|
for index, key := range keys {
|
|
if index > 0 {
|
|
buf.WriteByte(',')
|
|
}
|
|
encodedKey, err := marshal(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
buf.Write(encodedKey)
|
|
buf.WriteByte(':')
|
|
if err := writeCanonical(buf, typed[key]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
buf.WriteByte('}')
|
|
default:
|
|
return fmt.Errorf("wire: cannot canonicalise %T", value)
|
|
}
|
|
return nil
|
|
}
|