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 }