v2.0.0: claims model - signed MMUR2 votes in a truth DB, merged view in a rebuildable cache DB; POST /api/claims, claims and merge routes, unsigned writes answer 410
Build and Push Docker Image / build (push) Successful in 5m1s
Build and Push Docker Image / build (push) Successful in 5m1s
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
package main
|
||||
|
||||
// Decoding of the retired MMUR1 user-data record (meta-watch record/canonical.rs
|
||||
// at meta-core v1.2.0) and the private-tier envelope (record/crypto.rs).
|
||||
//
|
||||
// record = canonical CBOR array(7) [cid, uid, key, value, version, ts, sig]
|
||||
// bytes_to_sign = "MMUR1\n" || array(6) [cid, uid, key, value, version, ts]
|
||||
// sig = DER ECDSA-secp256k1 over SHA-256(bytes_to_sign)
|
||||
//
|
||||
// value is CBOR null / bool / int / float / text / bytes (bytes = a private-tier
|
||||
// XChaCha20-Poly1305 envelope whose plaintext is the CBOR of the typed value).
|
||||
// The signed bytes are rebuilt from the record's own raw element bytes (array
|
||||
// header 0x87 → 0x86, sig element cut off), so the old signature is checked
|
||||
// against exactly what was signed, whatever float width the encoder chose.
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
const mmur1Domain = "MMUR1\n"
|
||||
|
||||
type cborKind uint8
|
||||
|
||||
const (
|
||||
cNull cborKind = iota
|
||||
cBool
|
||||
cInt
|
||||
cFloat
|
||||
cText
|
||||
cBytes
|
||||
)
|
||||
|
||||
// cborItem is one decoded scalar plus its raw encoding.
|
||||
type cborItem struct {
|
||||
kind cborKind
|
||||
b bool
|
||||
i int64
|
||||
f float64
|
||||
s string
|
||||
bytes []byte
|
||||
raw []byte
|
||||
}
|
||||
|
||||
// wireText is the MMUR2 text form of a typed value — meta-watch's
|
||||
// Value::to_wire_text: bools "true"/"false", ints in decimal, floats as Rust's
|
||||
// f64 Display (shortest round-trip, never an exponent), text verbatim.
|
||||
func (it cborItem) wireText() (string, bool) {
|
||||
switch it.kind {
|
||||
case cBool:
|
||||
if it.b {
|
||||
return "true", true
|
||||
}
|
||||
return "false", true
|
||||
case cInt:
|
||||
return strconv.FormatInt(it.i, 10), true
|
||||
case cFloat:
|
||||
return strconv.FormatFloat(it.f, 'f', -1, 64), true
|
||||
case cText:
|
||||
return it.s, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
type mmur1Record struct {
|
||||
CID, UID, Key string
|
||||
Value cborItem
|
||||
Version uint64
|
||||
TS int64
|
||||
Sig []byte
|
||||
Signed []byte // the exact MMUR1 bytes the signature covers
|
||||
}
|
||||
|
||||
var errCBOR = errors.New("malformed MMUR1 CBOR")
|
||||
|
||||
// readHead parses a CBOR initial byte + argument (definite lengths only).
|
||||
func readHead(b []byte, off int) (major byte, ai byte, n uint64, next int, err error) {
|
||||
if off >= len(b) {
|
||||
return 0, 0, 0, 0, errCBOR
|
||||
}
|
||||
ib := b[off]
|
||||
major, ai = ib>>5, ib&31
|
||||
off++
|
||||
var size int
|
||||
switch {
|
||||
case ai < 24:
|
||||
return major, ai, uint64(ai), off, nil
|
||||
case ai == 24:
|
||||
size = 1
|
||||
case ai == 25:
|
||||
size = 2
|
||||
case ai == 26:
|
||||
size = 4
|
||||
case ai == 27:
|
||||
size = 8
|
||||
default:
|
||||
return 0, 0, 0, 0, fmt.Errorf("%w: indefinite or reserved length", errCBOR)
|
||||
}
|
||||
if off+size > len(b) {
|
||||
return 0, 0, 0, 0, errCBOR
|
||||
}
|
||||
for _, x := range b[off : off+size] {
|
||||
n = n<<8 | uint64(x)
|
||||
}
|
||||
return major, ai, n, off + size, nil
|
||||
}
|
||||
|
||||
// readItem decodes one scalar item at off.
|
||||
func readItem(b []byte, off int) (cborItem, int, error) {
|
||||
start := off
|
||||
major, ai, n, off, err := readHead(b, off)
|
||||
if err != nil {
|
||||
return cborItem{}, 0, err
|
||||
}
|
||||
var it cborItem
|
||||
switch major {
|
||||
case 0:
|
||||
if n > math.MaxInt64 {
|
||||
return it, 0, fmt.Errorf("%w: int out of range", errCBOR)
|
||||
}
|
||||
it = cborItem{kind: cInt, i: int64(n)}
|
||||
case 1:
|
||||
if n > math.MaxInt64 {
|
||||
return it, 0, fmt.Errorf("%w: int out of range", errCBOR)
|
||||
}
|
||||
it = cborItem{kind: cInt, i: -1 - int64(n)}
|
||||
case 2, 3:
|
||||
if n > uint64(len(b)-off) {
|
||||
return it, 0, errCBOR
|
||||
}
|
||||
end := off + int(n)
|
||||
if major == 2 {
|
||||
it = cborItem{kind: cBytes, bytes: append([]byte(nil), b[off:end]...)}
|
||||
} else {
|
||||
it = cborItem{kind: cText, s: string(b[off:end])}
|
||||
}
|
||||
off = end
|
||||
case 7:
|
||||
switch ai {
|
||||
case 20, 21:
|
||||
it = cborItem{kind: cBool, b: ai == 21}
|
||||
case 22:
|
||||
it = cborItem{kind: cNull}
|
||||
case 25:
|
||||
it = cborItem{kind: cFloat, f: halfToFloat(uint16(n))}
|
||||
case 26:
|
||||
it = cborItem{kind: cFloat, f: float64(math.Float32frombits(uint32(n)))}
|
||||
case 27:
|
||||
it = cborItem{kind: cFloat, f: math.Float64frombits(n)}
|
||||
default:
|
||||
return it, 0, fmt.Errorf("%w: unsupported simple value %d", errCBOR, ai)
|
||||
}
|
||||
default:
|
||||
return it, 0, fmt.Errorf("%w: unsupported major type %d", errCBOR, major)
|
||||
}
|
||||
it.raw = b[start:off]
|
||||
return it, off, nil
|
||||
}
|
||||
|
||||
func halfToFloat(h uint16) float64 {
|
||||
sign := 1.0
|
||||
if h&0x8000 != 0 {
|
||||
sign = -1
|
||||
}
|
||||
exp := int(h>>10) & 0x1f
|
||||
frac := float64(h & 0x3ff)
|
||||
switch exp {
|
||||
case 0:
|
||||
return sign * math.Ldexp(frac, -24)
|
||||
case 31:
|
||||
if frac == 0 {
|
||||
return math.Inf(int(sign))
|
||||
}
|
||||
return math.NaN()
|
||||
}
|
||||
return sign * math.Ldexp(frac+1024, exp-25)
|
||||
}
|
||||
|
||||
// decodeMMUR1 parses a stored record and rebuilds its signed bytes.
|
||||
func decodeMMUR1(b []byte) (*mmur1Record, error) {
|
||||
if len(b) == 0 || b[0] != 0x87 {
|
||||
return nil, fmt.Errorf("%w: want array(7)", errCBOR)
|
||||
}
|
||||
off := 1
|
||||
items := make([]cborItem, 7)
|
||||
sigStart := 0
|
||||
for i := range items {
|
||||
if i == 6 {
|
||||
sigStart = off
|
||||
}
|
||||
it, next, err := readItem(b, off)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items[i], off = it, next
|
||||
}
|
||||
if off != len(b) {
|
||||
return nil, fmt.Errorf("%w: trailing bytes", errCBOR)
|
||||
}
|
||||
for i, want := range []cborKind{cText, cText, cText} {
|
||||
if items[i].kind != want {
|
||||
return nil, fmt.Errorf("%w: cid/uid/key must be text", errCBOR)
|
||||
}
|
||||
}
|
||||
if items[4].kind != cInt || items[4].i < 0 {
|
||||
return nil, fmt.Errorf("%w: version must be an unsigned int", errCBOR)
|
||||
}
|
||||
if items[5].kind != cInt {
|
||||
return nil, fmt.Errorf("%w: ts must be an int", errCBOR)
|
||||
}
|
||||
if items[6].kind != cBytes {
|
||||
return nil, fmt.Errorf("%w: sig must be bytes", errCBOR)
|
||||
}
|
||||
signed := make([]byte, 0, len(mmur1Domain)+sigStart)
|
||||
signed = append(signed, mmur1Domain...)
|
||||
signed = append(signed, 0x86)
|
||||
signed = append(signed, b[1:sigStart]...)
|
||||
return &mmur1Record{
|
||||
CID: items[0].s, UID: items[1].s, Key: items[2].s,
|
||||
Value: items[3],
|
||||
Version: uint64(items[4].i),
|
||||
TS: items[5].i,
|
||||
Sig: items[6].bytes,
|
||||
Signed: signed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// envelopeAAD is cid ‖ 0x1F ‖ uid ‖ 0x1F ‖ key ‖ 0x1F ‖ n_be(8) — n was the
|
||||
// MMUR1 version and is the MMUR2 ts.
|
||||
func envelopeAAD(cid, uid, key string, n uint64) []byte {
|
||||
b := make([]byte, 0, len(cid)+len(uid)+len(key)+11)
|
||||
b = append(b, cid...)
|
||||
b = append(b, 0x1f)
|
||||
b = append(b, uid...)
|
||||
b = append(b, 0x1f)
|
||||
b = append(b, key...)
|
||||
b = append(b, 0x1f)
|
||||
return binary.BigEndian.AppendUint64(b, n)
|
||||
}
|
||||
|
||||
// openEnvelope decrypts nonce(24) ‖ ciphertext+tag.
|
||||
func openEnvelope(aeadKey, env, aad []byte) ([]byte, error) {
|
||||
a, err := chacha20poly1305.NewX(aeadKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(env) < chacha20poly1305.NonceSizeX+a.Overhead() {
|
||||
return nil, errors.New("envelope too short")
|
||||
}
|
||||
return a.Open(nil, env[:chacha20poly1305.NonceSizeX], env[chacha20poly1305.NonceSizeX:], aad)
|
||||
}
|
||||
|
||||
// sealEnvelope encrypts plain under a fresh random nonce.
|
||||
func sealEnvelope(aeadKey, plain, aad []byte, nonce []byte) ([]byte, error) {
|
||||
a, err := chacha20poly1305.NewX(aeadKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]byte, 0, len(nonce)+len(plain)+a.Overhead())
|
||||
out = append(out, nonce...)
|
||||
return a.Seal(out, nonce, plain, aad), nil
|
||||
}
|
||||
Reference in New Issue
Block a user