- internal/{address,identity,protocol,tce,transport,verify} -> pkg/ so
external Go projects can import the verified core; invariant tests
updated for the new paths
- Config.TrustProxy: key rate limiting by X-Forwarded-For when the relay
sits behind a reverse proxy (off by default, header never trusted
otherwise)
- examples/service + examples/approve: complete passwordless login round
trip (mint request -> wallet approves -> local verify), run live in CI
- docs/SERVICE-GUIDE.md: the integration recipe
242 lines
5.7 KiB
Go
242 lines
5.7 KiB
Go
package tce
|
|
|
|
import "math/big"
|
|
|
|
// Number canonicalization, implementing PROTOCOL.md section 5.1.
|
|
//
|
|
// Numbers are carried as decimal text rather than as binary floating point. A
|
|
// JSON number is an arbitrary-precision decimal literal, so 1, 1.0 and 1e0 are
|
|
// one value written three ways. Converting through an IEEE-754 double would
|
|
// lose precision above 2^53 and would make the signed bytes depend on the
|
|
// implementation's parsing and rounding, which is exactly the ambiguity the
|
|
// canonical encoding exists to remove.
|
|
//
|
|
// The canonical form is plain decimal with no exponent: an optional minus
|
|
// sign, digits without a leading zero, and an optional fractional part without
|
|
// trailing zeros. Negative zero is not representable and canonicalizes to "0".
|
|
|
|
// CanonicalNumber reduces a JSON number token to its unique canonical form.
|
|
//
|
|
// The input must be the exact source token as it appeared in the document.
|
|
// The output is ASCII and is what gets encoded under value tag 0x04.
|
|
func CanonicalNumber(token string) (string, error) {
|
|
if len(token) == 0 || len(token) > MaxNumberSource {
|
|
return "", ErrNumberFormat
|
|
}
|
|
|
|
intPart, fracPart, expPart, negative, err := splitNumber(token)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// The exponent digit count is bounded before conversion so that a token
|
|
// such as 1e999999999 is refused rather than driving a huge shift.
|
|
if len(expPart) > 4 {
|
|
return "", ErrNumberRange
|
|
}
|
|
exp := 0
|
|
expNeg := false
|
|
if len(expPart) > 0 {
|
|
i := 0
|
|
if expPart[0] == '+' || expPart[0] == '-' {
|
|
expNeg = expPart[0] == '-'
|
|
i = 1
|
|
}
|
|
digits := expPart[i:]
|
|
if len(digits) == 0 || len(digits) > 4 {
|
|
return "", ErrNumberRange
|
|
}
|
|
for _, c := range []byte(digits) {
|
|
exp = exp*10 + int(c-'0')
|
|
}
|
|
if expNeg {
|
|
exp = -exp
|
|
}
|
|
}
|
|
|
|
// value = sign * mantissa * 10^scale
|
|
digits := intPart + fracPart
|
|
scale := exp - len(fracPart)
|
|
|
|
mantissa, ok := new(big.Int).SetString(digits, 10)
|
|
if !ok {
|
|
return "", ErrNumberFormat
|
|
}
|
|
|
|
if mantissa.Sign() == 0 {
|
|
// Maps -0, 0.0 and 0e10 all to "0".
|
|
return "0", nil
|
|
}
|
|
|
|
// Strip factors of ten that exist only to pad the fraction.
|
|
ten := big.NewInt(10)
|
|
qr := new(big.Int)
|
|
rem := new(big.Int)
|
|
for scale < 0 {
|
|
qr.QuoRem(mantissa, ten, rem)
|
|
if rem.Sign() != 0 {
|
|
break
|
|
}
|
|
mantissa.Set(qr)
|
|
scale++
|
|
}
|
|
|
|
// Bound the work before materialising the decimal form: a positive scale
|
|
// appends that many zeros, so it must be checked before the string is
|
|
// built rather than after.
|
|
ds := mantissa.String()
|
|
if ds[0] == '-' {
|
|
ds = ds[1:]
|
|
}
|
|
if scale > 0 && len(ds)+scale > MaxNumberIntDigs {
|
|
return "", ErrNumberRange
|
|
}
|
|
if scale < 0 && -scale > MaxNumberFracDig+len(ds) {
|
|
return "", ErrNumberRange
|
|
}
|
|
|
|
var intDigits, fracDigits string
|
|
if scale >= 0 {
|
|
intDigits = ds + zeros(scale)
|
|
} else {
|
|
point := len(ds) + scale
|
|
if point <= 0 {
|
|
intDigits = "0"
|
|
fracDigits = zeros(-point) + ds
|
|
} else {
|
|
intDigits = ds[:point]
|
|
fracDigits = ds[point:]
|
|
}
|
|
}
|
|
|
|
if len(trimLeadingZeros(intDigits)) > MaxNumberIntDigs {
|
|
return "", ErrNumberRange
|
|
}
|
|
if len(fracDigits) > MaxNumberFracDig {
|
|
return "", ErrNumberRange
|
|
}
|
|
|
|
n := len(intDigits)
|
|
if negative {
|
|
n++
|
|
}
|
|
if len(fracDigits) > 0 {
|
|
n += 1 + len(fracDigits)
|
|
}
|
|
if n > MaxNumberToken {
|
|
return "", ErrNumberRange
|
|
}
|
|
|
|
out := make([]byte, 0, n)
|
|
if negative {
|
|
out = append(out, '-')
|
|
}
|
|
out = append(out, intDigits...)
|
|
if len(fracDigits) > 0 {
|
|
out = append(out, '.')
|
|
out = append(out, fracDigits...)
|
|
}
|
|
return string(out), nil
|
|
}
|
|
|
|
// splitNumber validates the JSON number grammar from RFC 8259 and returns its
|
|
// parts:
|
|
//
|
|
// -? ( 0 | [1-9][0-9]* ) ( "." [0-9]+ )? ( [eE] [+-]? [0-9]+ )?
|
|
//
|
|
// The grammar is checked by hand rather than with a regular expression so
|
|
// that the accepted language is visible and no regexp engine behaviour is
|
|
// involved in deciding what gets signed.
|
|
func splitNumber(s string) (intPart, fracPart, expPart string, negative bool, err error) {
|
|
i := 0
|
|
if i < len(s) && s[i] == '-' {
|
|
negative = true
|
|
i++
|
|
}
|
|
|
|
// Integer part: a single 0, or a non-zero digit followed by digits.
|
|
start := i
|
|
if i >= len(s) {
|
|
return "", "", "", false, ErrNumberFormat
|
|
}
|
|
if s[i] == '0' {
|
|
i++
|
|
} else if s[i] >= '1' && s[i] <= '9' {
|
|
for i < len(s) && isDigit(s[i]) {
|
|
i++
|
|
}
|
|
} else {
|
|
return "", "", "", false, ErrNumberFormat
|
|
}
|
|
intPart = s[start:i]
|
|
|
|
// Leading zeros are rejected by the grammar above, which is what makes
|
|
// "01" invalid rather than a second spelling of 1.
|
|
if len(intPart) > 1 && intPart[0] == '0' {
|
|
return "", "", "", false, ErrNumberFormat
|
|
}
|
|
|
|
// Optional fraction.
|
|
if i < len(s) && s[i] == '.' {
|
|
i++
|
|
fs := i
|
|
for i < len(s) && isDigit(s[i]) {
|
|
i++
|
|
}
|
|
if i == fs {
|
|
return "", "", "", false, ErrNumberFormat
|
|
}
|
|
fracPart = s[fs:i]
|
|
}
|
|
|
|
// Optional exponent.
|
|
if i < len(s) && (s[i] == 'e' || s[i] == 'E') {
|
|
i++
|
|
es := i
|
|
if i < len(s) && (s[i] == '+' || s[i] == '-') {
|
|
i++
|
|
}
|
|
ds := i
|
|
for i < len(s) && isDigit(s[i]) {
|
|
i++
|
|
}
|
|
if i == ds {
|
|
return "", "", "", false, ErrNumberFormat
|
|
}
|
|
expPart = s[es:i]
|
|
}
|
|
|
|
if i != len(s) {
|
|
return "", "", "", false, ErrNumberFormat
|
|
}
|
|
return intPart, fracPart, expPart, negative, nil
|
|
}
|
|
|
|
func isDigit(c byte) bool { return c >= '0' && c <= '9' }
|
|
|
|
func zeros(n int) string {
|
|
if n <= 0 {
|
|
return ""
|
|
}
|
|
b := make([]byte, n)
|
|
for i := range b {
|
|
b[i] = '0'
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func trimLeadingZeros(s string) string {
|
|
i := 0
|
|
for i < len(s)-1 && s[i] == '0' {
|
|
i++
|
|
}
|
|
return s[i:]
|
|
}
|
|
|
|
// IsCanonicalNumber reports whether token is already in canonical form. The
|
|
// decoder uses this to reject any other spelling rather than silently
|
|
// accepting it.
|
|
func IsCanonicalNumber(token string) bool {
|
|
c, err := CanonicalNumber(token)
|
|
return err == nil && c == token
|
|
}
|