package attesto // [P3.4] Portable receipt export — a self-contained `*.attesto.json`. // Mirrors attesto.export (Python) and export.ts (TypeScript); the // receipt-export.json parity corpus is normative for all three. import ( "encoding/json" "fmt" "os" "time" ) const ( ExportFormat = "attesto-receipt-export" ExportFormatVersion = 1 ) // ReceiptExport is the portable envelope. Receipt is kept as raw JSON so the // export carries the receipt verbatim, exactly as the API returned it. type ReceiptExport struct { Format string `json:"format"` FormatVersion int `json:"format_version"` ExportedAt string `json:"exported_at"` StreamID any `json:"stream_id,omitempty"` SeqNo any `json:"seq_no,omitempty"` EventHash any `json:"event_hash,omitempty"` WitnessPublicKeyHex string `json:"witness_public_key_hex,omitempty"` Receipt json.RawMessage `json:"receipt"` Payload M `json:"payload,omitempty"` PayloadCommitment M `json:"payload_commitment,omitempty"` } func exportPick(source M, keys ...string) any { for _, key := range keys { if value, ok := source[key]; ok { return value } } return nil } // ExportReceiptFile builds a portable export from a receipt's raw JSON (as // returned by the API) and optionally writes it to path (empty = no write). func ExportReceiptFile(receiptJSON []byte, path string, witnessPublicKeyHex string) (ReceiptExport, error) { var parsed struct { Payload M `json:"payload"` } if err := json.Unmarshal(receiptJSON, &parsed); err != nil { return ReceiptExport{}, fmt.Errorf("receipt is not valid JSON: %w", err) } export := ReceiptExport{ Format: ExportFormat, FormatVersion: ExportFormatVersion, ExportedAt: time.Now().UTC().Format("2006-01-02T15:04:05.000Z"), StreamID: exportPick(parsed.Payload, "stream_id", "streamId"), SeqNo: exportPick(parsed.Payload, "seq_no", "seqNo"), EventHash: exportPick(parsed.Payload, "event_hash", "eventHash"), WitnessPublicKeyHex: witnessPublicKeyHex, Receipt: json.RawMessage(receiptJSON), } if path != "" { raw, err := json.MarshalIndent(export, "", " ") if err != nil { return ReceiptExport{}, err } if err := os.WriteFile(path, append(raw, '\n'), 0o644); err != nil { return ReceiptExport{}, err } } return export, nil } // VerifyReceiptExport verifies a portable export offline. publicKeyHex == "" // falls back to the embedded hint (self-contained mode — proves internal // consistency against the key the file itself names). func VerifyReceiptExport(exportJSON []byte, publicKeyHex string) VerifyReport { var export struct { Format string `json:"format"` FormatVersion int `json:"format_version"` StreamID any `json:"stream_id"` SeqNo any `json:"seq_no"` EventHash any `json:"event_hash"` WitnessPublicKeyHex string `json:"witness_public_key_hex"` Receipt json.RawMessage `json:"receipt"` Payload M `json:"payload"` PayloadCommitment M `json:"payload_commitment"` } kind := VerifyKind("receipt-export") if publicKeyHex == "" { kind = "receipt-export-selfcontained" } fail := func(problems ...string) VerifyReport { return VerifyReport{Kind: kind, OK: false, Problems: problems} } if err := json.Unmarshal(exportJSON, &export); err != nil { return fail("export is not valid JSON: " + err.Error()) } var problems []string if export.Format != ExportFormat { problems = append(problems, "not an attesto receipt export (format field)") } if export.FormatVersion != ExportFormatVersion { problems = append(problems, fmt.Sprintf("unsupported export format_version: %d", export.FormatVersion)) } if len(export.Receipt) == 0 { problems = append(problems, "export carries no receipt object") } if len(problems) > 0 { return fail(problems...) } key := publicKeyHex if key == "" { key = export.WitnessPublicKeyHex } if key == "" { return fail("no public key supplied and no embedded hint") } var receipt SignedReceipt if err := json.Unmarshal(export.Receipt, &receipt); err != nil { return fail("receipt is not valid JSON: " + err.Error()) } report := VerifyReceiptOffline(receipt, key) problems = append(problems, report.Problems...) linkage := []struct { name string outer any keys []string }{ {"stream_id", export.StreamID, []string{"stream_id", "streamId"}}, {"seq_no", export.SeqNo, []string{"seq_no", "seqNo"}}, {"event_hash", export.EventHash, []string{"event_hash", "eventHash"}}, } for _, link := range linkage { inner := exportPick(receipt.Payload, link.keys...) if link.outer == nil || inner == nil { continue } // JSON numbers decode as float64 on both sides, so == is sound here. if fmt.Sprint(link.outer) != fmt.Sprint(inner) { problems = append(problems, "export linkage mismatch: "+link.name) } } if export.Payload != nil && export.PayloadCommitment != nil { ok, err := VerifyPayloadCommitment(export.Payload, M{"payload_commitment": export.PayloadCommitment}) if err != nil || !ok { problems = append(problems, "embedded payload does not match payload_commitment") } } return VerifyReport{ Kind: kind, OK: len(problems) == 0, ReceiptHash: report.ReceiptHash, EventHash: report.EventHash, Problems: problems, } }