Files
msa-agent/internal/wire/json.go
T

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
}