package attesto import ( "bytes" "crypto/ed25519" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "math" "sort" "strconv" "strings" "time" ) var ProofstreamDomains = map[string]string{ "anchor": "attesto.v2.anchor", "bundle": "attesto.v2.bundle", "checkpoint": "attesto.v2.checkpoint", "consistency": "attesto.v2.consistency", "event": "attesto.v2.event", "fork": "attesto.v2.fork", "ivc": "attesto.v2.ivc", "receipt": "attesto.v2.receipt", "stream": "attesto.v2.stream", "window": "attesto.v2.window", "witness": "attesto.v2.witness", "witness_policy": "attesto.v2.witness_policy", } func CanonicalJSON(value any) ([]byte, error) { normalized, err := normalizeCanonical(value) if err != nil { return nil, err } var buf bytes.Buffer encoder := json.NewEncoder(&buf) encoder.SetEscapeHTML(false) if err := encoder.Encode(normalized); err != nil { return nil, err } return bytes.TrimSuffix(buf.Bytes(), []byte("\n")), nil } func CanonicalJSONHex(value any) (string, error) { raw, err := CanonicalJSON(value) if err != nil { return "", err } return hex.EncodeToString(raw), nil } func DomainHashHex(domain string, value any) (string, error) { raw, err := CanonicalJSON(value) if err != nil { return "", err } h := sha256.New() h.Write([]byte(domain)) h.Write([]byte{0}) h.Write(raw) return hex.EncodeToString(h.Sum(nil)), nil } func SHA256Hex(value []byte) string { sum := sha256.Sum256(value) return hex.EncodeToString(sum[:]) } // maxSafeInteger mirrors JavaScript's Number.MAX_SAFE_INTEGER (2^53 - 1). // Integers beyond this lose precision when a JS verifier parses them, so they // would re-serialize to different canonical bytes across languages. const maxSafeInteger = int64(1)<<53 - 1 // UnsafeNumberError reports a committed payload/metadata number whose // canonical-JSON bytes diverge across Python, Go, and JavaScript (a non-integer, // or an integer outside +/-(2^53 - 1)). Path is the JSON path to the offender. type UnsafeNumberError struct { Path string Message string } func (e *UnsafeNumberError) Error() string { return e.Message } func nonIntegerError(path string) *UnsafeNumberError { return &UnsafeNumberError{ Path: path, Message: fmt.Sprintf( "non-integer numbers are not permitted in committed payloads (%s); "+ "encode decimals as strings", path), } } func unsafeIntegerError(path string) *UnsafeNumberError { return &UnsafeNumberError{ Path: path, Message: fmt.Sprintf( "integers beyond +/-2^53-1 are not permitted in committed payloads (%s); "+ "encode large integers as strings", path), } } func assertSafeFloat(v float64, path string) error { if math.IsInf(v, 0) || math.IsNaN(v) || math.Trunc(v) != v { return nonIntegerError(path) } if math.Abs(v) > float64(maxSafeInteger) { return unsafeIntegerError(path) } return nil } func assertSafeInt64(v int64, path string) error { if v > maxSafeInteger || v < -maxSafeInteger { return unsafeIntegerError(path) } return nil } func assertSafeNumberString(s, path string) error { if strings.ContainsAny(s, ".eE") { f, err := strconv.ParseFloat(s, 64) if err != nil { return nonIntegerError(path) } return assertSafeFloat(f, path) } n, err := strconv.ParseInt(s, 10, 64) if err != nil { // Outside int64 range is necessarily outside the safe integer range. return unsafeIntegerError(path) } return assertSafeInt64(n, path) } // AssertCommitmentSafeNumbers rejects numbers whose canonical-JSON bytes diverge // across Python, Go, and JavaScript, so a commitment computed here matches the // backend's byte-for-byte. Mirrors assert_commitment_safe_numbers in the backend: // non-integer numbers are rejected outright; integers must be within +/-(2^53 - 1). // Booleans are exempt. Returns an *UnsafeNumberError on the first offending value. func AssertCommitmentSafeNumbers(value any, path string) error { switch v := value.(type) { case nil, bool, string: return nil case json.Number: return assertSafeNumberString(v.String(), path) case float64: return assertSafeFloat(v, path) case float32: return assertSafeFloat(float64(v), path) case int: return assertSafeInt64(int64(v), path) case int8: return assertSafeInt64(int64(v), path) case int16: return assertSafeInt64(int64(v), path) case int32: return assertSafeInt64(int64(v), path) case int64: return assertSafeInt64(v, path) case uint: return assertSafeUint64(uint64(v), path) case uint8: return assertSafeInt64(int64(v), path) case uint16: return assertSafeInt64(int64(v), path) case uint32: return assertSafeInt64(int64(v), path) case uint64: return assertSafeUint64(v, path) case []any: for i, item := range v { if err := AssertCommitmentSafeNumbers(item, fmt.Sprintf("%s[%d]", path, i)); err != nil { return err } } return nil case []M: for i, item := range v { if err := AssertCommitmentSafeNumbers(item, fmt.Sprintf("%s[%d]", path, i)); err != nil { return err } } return nil case map[string]any: return assertSafeMap(v, path) case M: return assertSafeMap(map[string]any(v), path) default: // Structs and other types: normalize through JSON exactly as the // canonical encoder does, then re-check the plain representation. raw, err := json.Marshal(v) if err != nil { return err } var decoded any dec := json.NewDecoder(bytes.NewReader(raw)) dec.UseNumber() if err := dec.Decode(&decoded); err != nil { return err } return AssertCommitmentSafeNumbers(decoded, path) } } func assertSafeUint64(v uint64, path string) error { if v > uint64(maxSafeInteger) { return unsafeIntegerError(path) } return nil } func assertSafeMap(m map[string]any, path string) error { for key, item := range m { if err := AssertCommitmentSafeNumbers(item, fmt.Sprintf("%s.%s", path, key)); err != nil { return err } } return nil } // PayloadCommitment returns a deterministic commitment to an event payload, // byte-identical to the server's stored payload_commitment. Call // AssertCommitmentSafeNumbers first if the payload is not yet known to be safe. func PayloadCommitment(payload any) (map[string]string, error) { raw, err := CanonicalJSON(payload) if err != nil { return nil, err } return map[string]string{ "hash_alg": "sha256", "canonical_payload_hash": SHA256Hex(raw), }, nil } // MetadataCommitment returns a deterministic commitment to event metadata, // byte-identical to the server's stored metadata_commitment. func MetadataCommitment(metadata any) (map[string]string, error) { raw, err := CanonicalJSON(metadata) if err != nil { return nil, err } return map[string]string{ "hash_alg": "sha256", "canonical_metadata_hash": SHA256Hex(raw), }, nil } func storedCommitmentHash(event map[string]any, commitmentKey, hashKey string) string { containers := []any{event, event["envelope"], event["envelope_json"]} for _, container := range containers { obj, ok := container.(map[string]any) if !ok { continue } commitment, ok := obj[commitmentKey].(map[string]any) if !ok { continue } if stored, ok := commitment[hashKey].(string); ok { return stored } } return "" } // VerifyPayloadCommitment recomputes the payload commitment locally and compares // it to the value stored on an event (payload_commitment.canonical_payload_hash, // whether the event is given flat or under envelope / envelope_json). Returns // false when the stored commitment is absent or differs. func VerifyPayloadCommitment(payload any, event map[string]any) (bool, error) { stored := storedCommitmentHash(event, "payload_commitment", "canonical_payload_hash") if stored == "" { return false, nil } commitment, err := PayloadCommitment(payload) if err != nil { return false, err } return commitment["canonical_payload_hash"] == stored, nil } // VerifyMetadataCommitment recomputes the metadata commitment locally and // compares it to the value stored on an event. func VerifyMetadataCommitment(metadata any, event map[string]any) (bool, error) { stored := storedCommitmentHash(event, "metadata_commitment", "canonical_metadata_hash") if stored == "" { return false, nil } commitment, err := MetadataCommitment(metadata) if err != nil { return false, err } return commitment["canonical_metadata_hash"] == stored, nil } func windowNodeHash(left, right string) (string, error) { return DomainHashHex(ProofstreamDomains["window"], map[string]any{ "kind": "node", "left_hash": left, "right_hash": right, }) } func checkpointNodeHash(left, right string) (string, error) { return DomainHashHex(ProofstreamDomains["checkpoint"], map[string]any{ "kind": "node", "left_hash": left, "right_hash": right, }) } // InclusionStep is one node of a window inclusion proof. type InclusionStep struct { Side string `json:"side"` Hash string `json:"hash"` } // VerifyInclusionProof folds a window inclusion proof from a leaf up to the // window root. Mirrors verify_inclusion_proof in the backend windows.py: a left // sibling hashes as node(sibling, current), a right sibling as node(current, sibling). func VerifyInclusionProof(leafHash string, proof []InclusionStep, rootHash string) (bool, error) { current := leafHash for _, step := range proof { var err error switch step.Side { case "left": if step.Hash == "" { return false, nil } current, err = windowNodeHash(step.Hash, current) case "right": if step.Hash == "" { return false, nil } current, err = windowNodeHash(current, step.Hash) default: return false, nil } if err != nil { return false, err } } return current == rootHash, nil } // VerifyCheckpointRoot recomputes a checkpoint root from its window hashes and // compares. Mirrors checkpoint_root_hash in the backend checkpoints.py: an odd // node at any level is promoted unchanged (never duplicated/hashed with itself). func VerifyCheckpointRoot(windowHashes []string, expectedRoot string) (bool, error) { if len(windowHashes) == 0 { return false, nil } level := append([]string{}, windowHashes...) for len(level) > 1 { next := make([]string, 0, (len(level)+1)/2) for offset := 0; offset < len(level); offset += 2 { if offset+1 >= len(level) { next = append(next, level[offset]) // promote, do not duplicate } else { h, err := checkpointNodeHash(level[offset], level[offset+1]) if err != nil { return false, err } next = append(next, h) } } level = next } return level[0] == expectedRoot, nil } // VerifyCheckpointExtension checks that current continues previous: contiguous // sequence (current.from_seq_no == previous.to_seq_no + 1) and back-link // (current.previous_checkpoint_hash == previous.checkpoint_hash). func VerifyCheckpointExtension(previous, current map[string]any) VerifyReport { problems := make([]string, 0) if asFloat(current["from_seq_no"]) != asFloat(previous["to_seq_no"])+1 { problems = append(problems, "checkpoint does not extend previous (sequence gap)") } if !sameString(current["previous_checkpoint_hash"], previous["checkpoint_hash"]) { problems = append(problems, "checkpoint previous_checkpoint_hash does not match previous") } return VerifyReport{Kind: "checkpoint-extension", OK: len(problems) == 0, Problems: problems} } // VerifyCompleteness proves no events were omitted in [fromSeqNo, toSeqNo]: the // sequence numbers must be gap-free and every event's prev_event_hash must equal // the previous event's event_hash (the per-stream hash chain). func VerifyCompleteness(events []map[string]any, fromSeqNo, toSeqNo int) VerifyReport { problems := make([]string, 0) ordered := append([]map[string]any{}, events...) sort.Slice(ordered, func(i, j int) bool { return asFloat(ordered[i]["seq_no"]) < asFloat(ordered[j]["seq_no"]) }) gapFree := len(ordered) == toSeqNo-fromSeqNo+1 if gapFree { for i, event := range ordered { if int(asFloat(event["seq_no"])) != fromSeqNo+i { gapFree = false break } } } if !gapFree { problems = append(problems, "sequence range is not gap-free") } else { for i := 1; i < len(ordered); i++ { if !sameString(ordered[i]["prev_event_hash"], ordered[i-1]["event_hash"]) { problems = append(problems, fmt.Sprintf( "event chain broken at seq_no %d", int(asFloat(ordered[i]["seq_no"])))) break } } } return VerifyReport{Kind: "completeness", OK: len(problems) == 0, Problems: problems} } func asFloat(v any) float64 { switch n := v.(type) { case float64: return n case int: return float64(n) case int64: return float64(n) case json.Number: f, _ := n.Float64() return f } return 0 } func sameString(a, b any) bool { as, aok := a.(string) bs, bok := b.(string) if aok && bok { return as == bs } return a == nil && b == nil } func SignConnectorWebhookPayload(secret string, body []byte, timestamp int64) (string, string) { if timestamp == 0 { timestamp = time.Now().Unix() } ts := fmt.Sprintf("%d", timestamp) mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(ts)) mac.Write([]byte(".")) mac.Write(body) return ts, hex.EncodeToString(mac.Sum(nil)) } func SignedConnectorWebhookHeaders(secret string, body []byte, timestamp int64) map[string]string { ts, sig := SignConnectorWebhookPayload(secret, body, timestamp) return map[string]string{ "X-Attesto-Connector-Timestamp": ts, "X-Attesto-Connector-Signature": sig, } } func VerifyReceiptOffline(receipt SignedReceipt, publicKeyHex string) VerifyReport { problems := make([]string, 0) hash, err := DomainHashHex(ProofstreamDomains["receipt"], receipt.Payload) if err != nil { problems = append(problems, "receipt payload is not canonical-json compatible") } else if hash != receipt.ReceiptHash { problems = append(problems, "receipt hash mismatch") } if strings.ToLower(receipt.Signature.Alg) != "ed25519" { problems = append(problems, "unsupported receipt signature algorithm") } signatureHex := receipt.Signature.SignatureHex if signatureHex == "" { problems = append(problems, "receipt signature missing") } if err == nil && signatureHex != "" { publicKey, keyErr := hex.DecodeString(strings.TrimPrefix(strings.ToLower(publicKeyHex), "0x")) signature, sigErr := hex.DecodeString(strings.TrimPrefix(strings.ToLower(signatureHex), "0x")) payloadBytes, payloadErr := CanonicalJSON(receipt.Payload) if keyErr != nil || len(publicKey) != ed25519.PublicKeySize { problems = append(problems, "invalid public key") } else if sigErr != nil || len(signature) != ed25519.SignatureSize { problems = append(problems, "invalid receipt signature") } else if payloadErr != nil { problems = append(problems, "receipt payload is not canonical-json compatible") } else { message := make([]byte, 0, len(ProofstreamDomains["receipt"])+1+len(payloadBytes)) message = append(message, []byte(ProofstreamDomains["receipt"])...) message = append(message, 0) message = append(message, payloadBytes...) if !ed25519.Verify(ed25519.PublicKey(publicKey), message, signature) { problems = append(problems, "receipt signature mismatch") } } } ok := len(problems) == 0 result := "failed" if ok { result = "accepted" } eventHash, _ := receipt.Payload["event_hash"].(string) if eventHash == "" { eventHash, _ = receipt.Payload["eventHash"].(string) } return VerifyReport{ Kind: VerifyReceipt, OK: ok, ReceiptHash: hash, EventHash: eventHash, Problems: problems, Protocol: ProofstreamProtocol, ProtocolVersion: ProtocolVersionAlpha, Subject: subjectFromPayload(receipt.Payload), Result: result, Checks: []VerificationCheck{{ Name: "receipt", Result: result, Details: M{"problems": problems}, }}, } } func normalizeCanonical(value any) (any, error) { switch v := value.(type) { case nil, string, bool: return v, nil case time.Time: return v.UTC().Format("2006-01-02T15:04:05.000Z"), nil case []byte: return hex.EncodeToString(v), nil case int: return v, nil case int8: return int64(v), nil case int16: return int64(v), nil case int32: return int64(v), nil case int64: return v, nil case uint: return v, nil case uint8: return uint64(v), nil case uint16: return uint64(v), nil case uint32: return uint64(v), nil case uint64: return v, nil case float32: if math.IsInf(float64(v), 0) || math.IsNaN(float64(v)) { return nil, errors.New("canonical JSON cannot encode non-finite numbers") } return v, nil case float64: if math.IsInf(v, 0) || math.IsNaN(v) { return nil, errors.New("canonical JSON cannot encode non-finite numbers") } return v, nil case []any: out := make([]any, len(v)) for i, item := range v { normalized, err := normalizeCanonical(item) if err != nil { return nil, err } out[i] = normalized } return out, nil case []M: out := make([]any, len(v)) for i, item := range v { normalized, err := normalizeCanonical(item) if err != nil { return nil, err } out[i] = normalized } return out, nil case map[string]any: return normalizeMap(v) case M: return normalizeMap(map[string]any(v)) default: raw, err := json.Marshal(v) if err != nil { return nil, err } var decoded any if err := json.Unmarshal(raw, &decoded); err != nil { return nil, err } return normalizeCanonical(decoded) } } func normalizeMap(in map[string]any) (map[string]any, error) { out := make(map[string]any, len(in)) keys := make([]string, 0, len(in)) for key := range in { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { normalized, err := normalizeCanonical(in[key]) if err != nil { return nil, err } out[key] = normalized } return out, nil } func subjectFromPayload(payload M) M { subject := M{} for _, key := range []string{"tenant_id", "system_id", "stream_id", "stream_event_id", "seq_no", "from_seq_no", "to_seq_no", "checkpoint_id", "from_checkpoint_id", "to_checkpoint_id"} { if value, ok := payload[key]; ok { subject[key] = value } } return subject }