sdk(P1.10): embedded parity self-test + attesto doctor
A trimmed (~1.7 KB) copy of the cross-language parity vectors now ships inside
each package (Python package-data JSON, Go go:embed, TS generated module). On
the first hashing operation per process each SDK recomputes the commitment
hash, the receipt domain-hash, and an inclusion fold against the vendored
vectors and fails closed (AttestoSelfTestError / ErrSelfTest) on any mismatch
— a corrupted install or diverging runtime can never silently produce wrong
evidence. Result is cached (including failure); cost <5 ms once. Corrupting a
vendored vector is test-asserted to fail closed in all three languages. The
frozen canonical primitives are untouched; the gate lives in the commitment/
verify entry points built on top of them.
attesto doctor: Go CLI subcommand and Python attesto.doctor(), producing a
deterministic {"ok", "checks"} report — vendored self-test, head-store
writability, number-policy dry-run on a sample payload, Ed25519 availability
(Python), and with credentials: reachability, protocol-header acceptance, and
clock skew vs the server Date header (warn >30 s; webhooks break at 300 s).
package_artifact_policy allows exactly attesto/_selftest_vectors.json in the
wheel (verified: built wheel contains it, policy green). READMEs updated.
This completes the last Phase-1 build item.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -129,6 +129,15 @@ client, _ := attesto.NewClient(apiKey, attesto.WithHeadStore(attesto.NewFileHead
|
|||||||
client, _ = attesto.NewClient(apiKey, attesto.WithHeadStore(nil))
|
client, _ = attesto.NewClient(apiKey, attesto.WithHeadStore(nil))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Built-in self-test and doctor
|
||||||
|
|
||||||
|
On the first hashing operation per process the SDK verifies itself against an
|
||||||
|
embedded copy of the cross-language parity vectors and fails closed with
|
||||||
|
`ErrSelfTest` on any divergence. `attesto doctor` (CLI) prints a deterministic
|
||||||
|
JSON report: self-test, head-store writability, number-policy dry-run
|
||||||
|
(`--sample-payload file.json`), and — with credentials configured —
|
||||||
|
reachability and protocol acceptance.
|
||||||
|
|
||||||
## Iterating long listings
|
## Iterating long listings
|
||||||
|
|
||||||
Paginated `List*` methods have `Iter*` twins that walk limit/offset pages
|
Paginated `List*` methods have `Iter*` twins that walk limit/offset pages
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -142,6 +143,8 @@ func (a *app) dispatch(ctx context.Context, args []string) error {
|
|||||||
return a.localVault(ctx, args[1:])
|
return a.localVault(ctx, args[1:])
|
||||||
case "marketplace":
|
case "marketplace":
|
||||||
return a.marketplace(ctx, args[1:])
|
return a.marketplace(ctx, args[1:])
|
||||||
|
case "doctor":
|
||||||
|
return a.doctor(ctx, args[1:])
|
||||||
case "readiness":
|
case "readiness":
|
||||||
return a.readiness(args[1:])
|
return a.readiness(args[1:])
|
||||||
default:
|
default:
|
||||||
@@ -411,6 +414,88 @@ func (a *app) verifyableObject(ctx context.Context, group string, args []string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// doctor diagnoses an SDK/CLI install: vendored parity self-test, head-store
|
||||||
|
// writability, number-policy dry-run on a sample payload, and (when an API key
|
||||||
|
// is configured) reachability, protocol-header acceptance, and clock skew vs
|
||||||
|
// the server Date header. Deterministic JSON report: {"ok": bool, "checks": {...}}.
|
||||||
|
func (a *app) doctor(ctx context.Context, args []string) error {
|
||||||
|
fs := flag.NewFlagSet("doctor", flag.ContinueOnError)
|
||||||
|
fs.SetOutput(a.err)
|
||||||
|
samplePayload := fs.String("sample-payload", "", "JSON file with a sample payload for the number-policy dry-run")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
checks := map[string]map[string]any{}
|
||||||
|
pass := func(name string, extra map[string]any) {
|
||||||
|
if extra == nil {
|
||||||
|
extra = map[string]any{}
|
||||||
|
}
|
||||||
|
extra["ok"] = true
|
||||||
|
checks[name] = extra
|
||||||
|
}
|
||||||
|
fail := func(name string, err error) {
|
||||||
|
checks[name] = map[string]any{"ok": false, "error": err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := attesto.EnsureSelfTest(); err != nil {
|
||||||
|
fail("self_test", err)
|
||||||
|
} else {
|
||||||
|
pass("self_test", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
headStore := attesto.NewFileHeadStore("")
|
||||||
|
headStore.Set("__doctor__", 1, strings.Repeat("0", 64))
|
||||||
|
if seq, hash, ok := headStore.Get("__doctor__"); ok && seq == 1 && hash == strings.Repeat("0", 64) {
|
||||||
|
pass("head_store", nil)
|
||||||
|
} else {
|
||||||
|
fail("head_store", errors.New("head store readback failed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if *samplePayload != "" {
|
||||||
|
raw, err := os.ReadFile(*samplePayload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(raw))
|
||||||
|
decoder.UseNumber()
|
||||||
|
var payload any
|
||||||
|
if err := decoder.Decode(&payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := attesto.AssertCommitmentSafeNumbers(payload, "$"); err != nil {
|
||||||
|
fail("number_policy", err)
|
||||||
|
} else {
|
||||||
|
pass("number_policy", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if client, err := a.systemClient(); err == nil {
|
||||||
|
start := time.Now()
|
||||||
|
head, headErr := client.GetStreamHead(ctx, "__doctor-probe__")
|
||||||
|
_ = head
|
||||||
|
status := 0
|
||||||
|
var apiErr *attesto.APIError
|
||||||
|
if errors.As(headErr, &apiErr) {
|
||||||
|
status = apiErr.StatusCode
|
||||||
|
}
|
||||||
|
// Any HTTP-level answer (including 404 for the probe id) proves
|
||||||
|
// reachability + auth handling; transport errors do not.
|
||||||
|
reachable := headErr == nil || status > 0
|
||||||
|
checks["api_reachable"] = map[string]any{
|
||||||
|
"ok": reachable, "status": status, "latency_ms": time.Since(start).Milliseconds(),
|
||||||
|
}
|
||||||
|
checks["protocol_accepted"] = map[string]any{"ok": status != http.StatusUpgradeRequired, "status": status}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := true
|
||||||
|
for _, check := range checks {
|
||||||
|
if v, has := check["ok"].(bool); has && !v {
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a.write(map[string]any{"ok": ok, "checks": checks})
|
||||||
|
}
|
||||||
|
|
||||||
func (a *app) anchors(ctx context.Context, args []string) error {
|
func (a *app) anchors(ctx context.Context, args []string) error {
|
||||||
// `anchors verify <anchor_epoch_id> --rpc-url <url>` chains an API fetch
|
// `anchors verify <anchor_epoch_id> --rpc-url <url>` chains an API fetch
|
||||||
// with the on-chain check (eth_call getCommitment + tx receipt) against a
|
// with the on-chain check (eth_call getCommitment + tx receipt) against a
|
||||||
|
|||||||
@@ -226,6 +226,9 @@ func assertSafeMap(m map[string]any, path string) error {
|
|||||||
// byte-identical to the server's stored payload_commitment. Call
|
// byte-identical to the server's stored payload_commitment. Call
|
||||||
// AssertCommitmentSafeNumbers first if the payload is not yet known to be safe.
|
// AssertCommitmentSafeNumbers first if the payload is not yet known to be safe.
|
||||||
func PayloadCommitment(payload any) (map[string]string, error) {
|
func PayloadCommitment(payload any) (map[string]string, error) {
|
||||||
|
if err := EnsureSelfTest(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
raw, err := CanonicalJSON(payload)
|
raw, err := CanonicalJSON(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -503,6 +506,9 @@ func SignedConnectorWebhookHeaders(secret string, body []byte, timestamp int64)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func VerifyReceiptOffline(receipt SignedReceipt, publicKeyHex string) VerifyReport {
|
func VerifyReceiptOffline(receipt SignedReceipt, publicKeyHex string) VerifyReport {
|
||||||
|
if err := EnsureSelfTest(); err != nil {
|
||||||
|
return VerifyReport{Kind: VerifyReceipt, OK: false, Problems: []string{err.Error()}}
|
||||||
|
}
|
||||||
problems := make([]string, 0)
|
problems := make([]string, 0)
|
||||||
hash, err := DomainHashHex(ProofstreamDomains["receipt"], receipt.Payload)
|
hash, err := DomainHashHex(ProofstreamDomains["receipt"], receipt.Payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
72
selftest.go
Normal file
72
selftest.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package attesto
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// [P1.10] Trimmed parity vectors vendored into the package; regenerated from
|
||||||
|
// golden-vectors/sdk-parity (do not hand-edit).
|
||||||
|
//
|
||||||
|
//go:embed selftest_vectors.json
|
||||||
|
var selftestVectors []byte
|
||||||
|
|
||||||
|
// ErrSelfTest wraps a failed vendored parity self-test: this install's hashing
|
||||||
|
// diverges from the pinned cross-language vectors (corrupted package or broken
|
||||||
|
// runtime). The SDK fails closed rather than produce wrong evidence.
|
||||||
|
var ErrSelfTest = errors.New("attesto self-test failed")
|
||||||
|
|
||||||
|
var (
|
||||||
|
selftestOnce sync.Once
|
||||||
|
selftestErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
func runSelfTest(raw []byte) error {
|
||||||
|
var vectors struct {
|
||||||
|
Commitment struct {
|
||||||
|
Payload map[string]any `json:"payload"`
|
||||||
|
CanonicalPayloadHash string `json:"canonical_payload_hash"`
|
||||||
|
} `json:"commitment"`
|
||||||
|
Receipt struct {
|
||||||
|
Payload map[string]any `json:"payload"`
|
||||||
|
ReceiptHash string `json:"receipt_hash"`
|
||||||
|
} `json:"receipt"`
|
||||||
|
Inclusion struct {
|
||||||
|
LeafHash string `json:"leaf_hash"`
|
||||||
|
Proof []InclusionStep `json:"proof"`
|
||||||
|
RootHash string `json:"root_hash"`
|
||||||
|
} `json:"inclusion"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &vectors); err != nil {
|
||||||
|
return fmt.Errorf("%w: vendored vectors unreadable: %v", ErrSelfTest, err)
|
||||||
|
}
|
||||||
|
canonical, err := CanonicalJSON(vectors.Commitment.Payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSelfTest, err)
|
||||||
|
}
|
||||||
|
if SHA256Hex(canonical) != vectors.Commitment.CanonicalPayloadHash {
|
||||||
|
return fmt.Errorf("%w: commitment hash diverged from vendored vector", ErrSelfTest)
|
||||||
|
}
|
||||||
|
receiptHash, err := DomainHashHex(ProofstreamDomains["receipt"], vectors.Receipt.Payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSelfTest, err)
|
||||||
|
}
|
||||||
|
if receiptHash != vectors.Receipt.ReceiptHash {
|
||||||
|
return fmt.Errorf("%w: receipt domain-hash diverged from vendored vector", ErrSelfTest)
|
||||||
|
}
|
||||||
|
ok, err := VerifyInclusionProof(
|
||||||
|
vectors.Inclusion.LeafHash, vectors.Inclusion.Proof, vectors.Inclusion.RootHash)
|
||||||
|
if err != nil || !ok {
|
||||||
|
return fmt.Errorf("%w: inclusion fold diverged from vendored vector", ErrSelfTest)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureSelfTest runs the vendored parity self-test once per process (cached).
|
||||||
|
func EnsureSelfTest() error {
|
||||||
|
selftestOnce.Do(func() { selftestErr = runSelfTest(selftestVectors) })
|
||||||
|
return selftestErr
|
||||||
|
}
|
||||||
26
selftest_test.go
Normal file
26
selftest_test.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package attesto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSelfTestPassesOnVendoredVectors(t *testing.T) {
|
||||||
|
if err := EnsureSelfTest(); err != nil {
|
||||||
|
t.Fatalf("self-test failed on shipped vectors: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorruptedVendoredVectorFailsClosed(t *testing.T) {
|
||||||
|
corrupted := bytes.Replace(
|
||||||
|
selftestVectors,
|
||||||
|
[]byte(`"canonical_payload_hash": "`),
|
||||||
|
[]byte(`"canonical_payload_hash": "0`),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
err := runSelfTest(corrupted)
|
||||||
|
if !errors.Is(err, ErrSelfTest) {
|
||||||
|
t.Fatalf("expected ErrSelfTest, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
58
selftest_vectors.json
Normal file
58
selftest_vectors.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"note": "Trimmed parity self-test set vendored into each SDK package; regenerated from golden-vectors/sdk-parity (do not hand-edit).",
|
||||||
|
"commitment": {
|
||||||
|
"payload": {
|
||||||
|
"b": 2,
|
||||||
|
"a": 1,
|
||||||
|
"nested": {
|
||||||
|
"z": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"flag": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"canonical_payload_hash": "8b2252e6632801faa71c8eb4a597bfeaba385face3ddef30b7eb636787902304"
|
||||||
|
},
|
||||||
|
"receipt": {
|
||||||
|
"payload": {
|
||||||
|
"event_hash": "cd7753e1bc862729d4de6d2e9d9c0942830e557cf539ebcbf00bf9b4cd6a62a9",
|
||||||
|
"event_id": "evt_golden_2",
|
||||||
|
"issued_at": "2026-06-05T20:59:04.000Z",
|
||||||
|
"prev_event_hash": "731f8a0a3cc6ea2ca00bb26dca8f721da34e1328750f2e16a32f7bf7b36d7264",
|
||||||
|
"protocol": "ATTESTO-PROOFSTREAM-001",
|
||||||
|
"protocol_version": "0.1-alpha",
|
||||||
|
"seq_no": 2,
|
||||||
|
"signer": {
|
||||||
|
"alg": "ed25519",
|
||||||
|
"key_epoch": "golden-key-2026-06",
|
||||||
|
"kid": "golden-key-2026-06"
|
||||||
|
},
|
||||||
|
"stream_event_id": "sev_golden_2",
|
||||||
|
"stream_head_hash": "140142e46eb7cbe17db99c717666cb5a038a858feaff7cc26b78d10c8868da51",
|
||||||
|
"stream_id": "str_golden",
|
||||||
|
"system_id": "sys_golden",
|
||||||
|
"tenant_id": "tnt_golden"
|
||||||
|
},
|
||||||
|
"receipt_hash": "4f72f15fa4d07c9a83baa908f6168e27beeb1d4c9984f70e52f67ff6c380ccb2"
|
||||||
|
},
|
||||||
|
"inclusion": {
|
||||||
|
"leaf_hash": "7973a148f766058042177a3507c5baa4757c6159c91049cbc42f60f138715072",
|
||||||
|
"proof": [
|
||||||
|
{
|
||||||
|
"side": "right",
|
||||||
|
"hash": "2faac607f33011ec3bdf305e01e36d1257bb226868cec5df4bfecbb8a552b48b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"side": "right",
|
||||||
|
"hash": "9ff585ea4fd08983d242fd3b73144e800b07a4ae86324d4e0f0fb2f70933b26f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"side": "right",
|
||||||
|
"hash": "6977f0ace2809f289002e6600967de240acc3dcdcf62aa4e078c56c10f57d381"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"root_hash": "37a6a0d69e0951df3827205cfb8440c84d5d60c729b4c00a0d9460361923a18b"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user