Add SDK parity and Go CLI release readiness
This commit is contained in:
254
proofstream.go
Normal file
254
proofstream.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package attesto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"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[:])
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user