niko_trust/internal/tce/number.go
Niko Marmeladkov 9d66003689
Initial commit: signed-object trust relay, verifier, and docs
- server: relay storing signed objects (PUT/GET), per-IP rate limiting,
  per-subject quota (1000), one-response-per-request, pagination,
  /v1/healthz /v1/readyz /v1/metrics
- verify: signature-verifying trust evaluator; every object is checked via
  env.Verify(), approvals via VerifyApprovalResponse, revocations via
  VerifyRevocationOf; k-of-n approval quorum
- docs: TRUST-MODEL.md and API.md describing issuer-anchored signatures and
  the endpoint/status-code contract
- tests: server, verify, and ratelimit packages
2026-08-12 22:36:49 +03:00

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
}