/* * ARION AER-1 verifier — independent Swift implementation (single file) * * Ported faithfully from verify.js (ARION's independent Node.js verifier), * itself an independent reimplementation of the AER-1 receipt/chain/workflow * verification rules in draft-zambo-aer1-09+ written from the draft text. * * Implements: * - Section 3/4/5/6 : core receipt checks (members, types, provenance, * strict base64 + strict UTF-8 canonical bytes, * output commitment recomputation) * - Reference-producer profile (--profile): canonical payload must be a * JSON object carrying an "inputs" member * - Disinterested anchor tier (--anchor, or auto when the anchor uses the * tier vocabulary log/leaf_hash/anchored_at/proof) * - Section 7.1/7.2 : -07/-08 hash-chained job timelines (canonical-JSON * entry digest binding seq, job_id, close, id, tool, * provenance_class, output_hash) * - Section 7.4 : --chain06 = historical -06 construction (digest = * SHA-256 of the decoded canonical_bytes alone) * - Section 7.3 : bare final-entry-digest commitment + -09 chain * commitment artifact (job_id/final_entry_digest/ * entry_count/published_at) * - Section 8/8.1/8.2: workflow receipts + Merkle root over step * receipt_ids (leaf = SHA-256(utf8(receipt_id)), * odd-level last duplication) * * CLI: * swift aer1verify.swift [--strict] [--chain|--chain06| * --workflow|--receipt] [--profile] [--anchor] [--json] * is scanned recursively for *.json files (sorted by path); each * file's kind is auto-detected: JSON array -> timeline; object type * "verifiable-workflow-receipt" -> workflow; object with "timeline"/ * "entries" -> wrapped timeline; otherwise -> receipt. * Prints one PASS/FAIL line per file (FAIL names the first failing * check), then "CONFORMANCE: /". Exit 0 iff all pass. * * Dependencies: Foundation + CryptoKit only (macOS). * * Disclosure: written by ARION, an autonomous agent (AI-authored). It is an * independent reimplementation from the draft text; the conformance kit's * own runners were consulted only to pin corpus conventions the draft does * not specify. */ import Foundation import CryptoKit /* ------------------------------------------------------------------ * * Options * * ------------------------------------------------------------------ */ struct Opts { var strict = false var kind = "auto" // auto | receipt | chain | chain06 | workflow var profile = false var anchor = false var json = false } struct CheckFailure { let check: String let reason: String } struct CanonError: Error { let msg: String } /* ------------------------------------------------------------------ * * Type helpers (JSONSerialization yields NSNumber/NSString/ * * NSDictionary/NSArray/NSNull) * * ------------------------------------------------------------------ */ func asDict(_ v: Any?) -> [String: Any]? { guard let v = v else { return nil } return v as? [String: Any] } func asArray(_ v: Any?) -> [Any]? { guard let v = v else { return nil } return v as? [Any] } // Real JSON booleans only. NSNumber bridges to Bool leniently, so gate on // the Objective-C type encoding: JSONSerialization emits 'c' (char) only // for genuine booleans; numbers arrive as 'q'/'i'/'d'/'f'. func asBool(_ v: Any?) -> Bool? { guard let v = v else { return nil } guard let n = v as? NSNumber else { return nil } guard String(cString: n.objCType) == "c" else { return nil } return n.boolValue } func asNumber(_ v: Any?) -> NSNumber? { guard let n = v as? NSNumber else { return nil } guard String(cString: n.objCType) != "c" else { return nil } return n } // JS Number.isInteger semantics: any finite number equal to its rounding — // including integral doubles of any magnitude (Number.isInteger(1e20) is // true in JS). Value extraction/comparison uses the Double itself. func isIntegerValued(_ v: Any?) -> Bool { guard let n = asNumber(v) else { return false } let d = n.doubleValue return d.isFinite && d == d.rounded() } func asDouble(_ v: Any?) -> Double? { asNumber(v)?.doubleValue } func isNonEmptyString(_ v: Any?) -> Bool { guard let s = v as? String else { return false } return !s.isEmpty } // JS truthiness for the `doc.timeline || doc.entries` fallback. func jsTruthy(_ v: Any?) -> Bool { guard let v = v else { return false } if v is NSNull { return false } if let b = asBool(v) { return b } if let n = asNumber(v) { return n.doubleValue != 0 } if let s = v as? String { return !s.isEmpty } return true } /* ------------------------------------------------------------------ * * Primitives * * ------------------------------------------------------------------ */ func sha256hex(_ data: Data) -> String { SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined() } func sha256data(_ data: Data) -> Data { Data(SHA256.hash(data: data)) } // Strict UTF-8 decode (Section 4): non-UTF-8 canonical bytes fail closed. func decodesAsUtf8(_ d: Data) -> Bool { String(data: d, encoding: .utf8) != nil } // Base64 surface check (kit convention): alphabet plus at most two pad // chars at the end, total length a multiple of 4. Equivalent to // /^[A-Za-z0-9+/]*={0,2}$/ + s.length % 4 === 0. Non-canonical trailing // pad bits are tolerated at decode time (same as Python's // base64.b64decode(validate=True) and Node's Buffer.from(s, "base64")). func decodeBase64Strict(_ value: Any?, _ strict: Bool) -> Data? { guard let s = value as? String else { return nil } var pads = 0 for ch in s { if ch == "=" { pads += 1 if pads > 2 { return nil } } else { if pads > 0 { return nil } let ok = (ch >= "0" && ch <= "9") || (ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") || ch == "+" || ch == "/" if !ok { return nil } } } if s.count % 4 != 0 { return nil } if s.isEmpty { return Data() } // Buffer.from("", "base64") -> empty return Data(base64Encoded: s) } /* ------------------------------------------------------------------ * * Regex surface (ICU \A...\z anchors == JS ^...$ on whole input; ICU $ * would match before a trailing newline — that changes verdicts, e.g. * id-trailing-newline.json, so never use ^/$) * * ------------------------------------------------------------------ */ func fullMatch(_ s: String, _ pattern: String) -> Bool { s.range(of: pattern, options: .regularExpression) != nil } let UUID_LOOSE_RE = "\\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\z" let UUID_V4_RE = "\\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\\z" let HASH_RE = "\\Asha256:[0-9a-f]{64}\\z" let HEX64_RE = "\\A[0-9a-f]{64}\\z" let PROV_RE = "\\A(EXECUTED BY [A-Z0-9][A-Z0-9 ._-]*|OBSERVED VIA GATEWAY|LOGGED BY AGENT)\\z" let RFC3339_RE = "\\A[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})\\z" let TS_PARTS_RE = "\\A([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})\\z" /* ------------------------------------------------------------------ * * RFC 3339 calendar validation (Section 3) * * ------------------------------------------------------------------ */ func isLeapYear(_ y: Int) -> Bool { (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 } func daysInMonth(_ y: Int, _ m: Int) -> Int { if m == 2 { return isLeapYear(y) ? 29 : 28 } if m == 4 || m == 6 || m == 9 || m == 11 { return 30 } return 31 } // Mirrors verify.js isCalendarDateTime: replace the first "Z" with // "+00:00" so the offset always parses as ±hh:mm, then range-check every // component. Offset bounds: hours <= 23, minutes <= 59, signed total // strictly inside (-1440, 1440) minutes. func isCalendarDateTime(_ s: String, _ strict: Bool) -> Bool { var t = s if let r = t.range(of: "Z") { t = t.replacingCharacters(in: r, with: "+00:00") } guard let re = try? NSRegularExpression(pattern: TS_PARTS_RE), let m = re.firstMatch(in: t, options: [], range: NSRange(t.startIndex..., in: t)) else { return false } let nt = t as NSString func gi(_ i: Int) -> Int { Int(nt.substring(with: m.range(at: i)))! } let year = gi(1), mon = gi(2), day = gi(3) let hh = gi(4), mi = gi(5), ss = gi(6) if year < 1 || year > 9999 { return false } if mon < 1 || mon > 12 { return false } if day < 1 || day > daysInMonth(year, mon) { return false } if hh > 23 || mi > 59 || ss > 59 { return false } let off = nt.substring(with: m.range(at: 8)) if off != "Z" { let oh = Int(off.dropFirst().prefix(2))! let om = Int(off.dropFirst(4).prefix(2))! if oh > 23 || om > 59 { return false } let sign = off.hasPrefix("-") ? -1 : 1 let total = sign * (oh * 60 + om) if !(total > -1440 && total < 1440) { return false } } return true } func isRfc3339Format(_ v: Any?, _ strict: Bool) -> Bool { guard let s = v as? String else { return false } return fullMatch(s, RFC3339_RE) } func isRfc3339(_ v: Any?, _ strict: Bool) -> Bool { isRfc3339Format(v, strict) && isCalendarDateTime(v as! String, strict) } /* ------------------------------------------------------------------ * * Section 3/4/5/6 — core receipt check * * ------------------------------------------------------------------ */ let CORE_MEMBERS = ["id", "receipt_schema_version", "created_at", "tool", "provenance_class", "canonical_bytes", "output_hash", "verification_status"] func checkReceipt(_ receipt: Any?, opts: Opts) -> [String] { let strict = opts.strict var failures: [String] = [] guard let d = asDict(receipt) else { return ["receipt is not a JSON object"] } for field in CORE_MEMBERS { if d[field] == nil { failures.append("missing required member: " + field) } } if !failures.isEmpty { return failures } if (d["id"] as? String).map({ fullMatch($0, strict ? UUID_V4_RE : UUID_LOOSE_RE) }) != true { failures.append(strict ? "id is not a UUID v4" : "id is not a lowercase UUID") } if strict { if (d["receipt_schema_version"] as? String) != "0.3" { failures.append("receipt_schema_version is not 0.3") } } else if !isNonEmptyString(d["receipt_schema_version"]) { failures.append("receipt_schema_version is not a non-empty string") } if !isRfc3339Format(d["created_at"], strict) { failures.append("created_at is not RFC 3339") } else if !isCalendarDateTime(d["created_at"] as! String, strict) { failures.append("created_at is not a valid calendar date/time") } let tool = asDict(d["tool"]) if tool == nil || !["name", "version", "scope"].allSatisfy({ isNonEmptyString(tool![$0]) }) { failures.append("tool is not an object with name/version/scope strings") } if (d["provenance_class"] as? String).map({ fullMatch($0, PROV_RE) }) != true { failures.append("provenance_class is not a known class") } var raw = decodeBase64Strict(d["canonical_bytes"], strict) if raw == nil { failures.append("canonical_bytes is not valid base64") } else if !decodesAsUtf8(raw!) { failures.append("canonical_bytes is not valid UTF-8") raw = nil } if (d["output_hash"] as? String).map({ fullMatch($0, HASH_RE) }) != true { failures.append("output_hash is not sha256: + lowercase hex digest") } else if raw != nil { let want = "sha256:" + sha256hex(raw!) if (d["output_hash"] as! String) != want { failures.append("output_hash does not match sha256(canonical_bytes)") } } if (d["verification_status"] as? String) != "verified" { failures.append("verification_status is not verified") } return failures } /* ------------------------------------------------------------------ * * Reference-producer profile (kit convention) * * ------------------------------------------------------------------ */ func checkReferenceProfile(_ receipt: Any?, opts: Opts) -> [String] { guard let d = asDict(receipt) else { return ["receipt is not a JSON object"] } guard let raw = decodeBase64Strict(d["canonical_bytes"], opts.strict) else { return ["canonical_bytes is not valid base64"] } guard decodesAsUtf8(raw) else { return ["canonical_bytes is not valid UTF-8"] } guard let payload = try? JSONSerialization.jsonObject(with: raw, options: .fragmentsAllowed) else { return ["canonical payload is not JSON"] } guard let pd = asDict(payload) else { return ["canonical payload is not a JSON object"] } if pd["inputs"] == nil { return ["reference-producer payload omits the 'inputs' member"] } return [] } /* ------------------------------------------------------------------ * * Disinterested-tier anchor check * * ------------------------------------------------------------------ */ let TIER_MEMBERS = ["anchored_at", "leaf_hash", "log", "proof"] func anchorUsesTierShape(_ receipt: Any?) -> Bool { guard let a = asDict(asDict(receipt)?["anchor"]) else { return false } return TIER_MEMBERS.contains { a[$0] != nil } } func checkDisinterestedTier(_ receipt: Any?, opts: Opts) -> [String] { let strict = opts.strict var failures: [String] = [] let rid = asDict(receipt)?["id"] if (rid as? String).map({ fullMatch($0, UUID_V4_RE) }) != true { failures.append("id is not a UUID v4 (strict profile)") } guard let anchor = asDict(asDict(receipt)?["anchor"]) else { return ["anchor is not an object"] } if anchor.keys.sorted() != TIER_MEMBERS.sorted() { failures.append("anchor has members other than log/leaf_hash/anchored_at/proof") } if !isNonEmptyString(anchor["log"]) { failures.append("anchor.log is not a non-empty string") } if (anchor["leaf_hash"] as? String).map({ fullMatch($0, HASH_RE) }) != true { failures.append("anchor.leaf_hash is not sha256: + lowercase hex digest") } else if let raw = decodeBase64Strict(asDict(receipt)?["canonical_bytes"], strict) { if !decodesAsUtf8(raw) { failures.append("canonical_bytes is not valid UTF-8 base64") } else if (anchor["leaf_hash"] as! String) != "sha256:" + sha256hex(raw) { failures.append("anchor.leaf_hash does not match canonical_bytes") } } else { failures.append("canonical_bytes is not valid base64") } if !isRfc3339(anchor["anchored_at"], strict) { failures.append("anchor.anchored_at is not a valid calendar date/time") } if asDict(anchor["proof"]) == nil { failures.append("anchor.proof is not an object") } return failures } /* ------------------------------------------------------------------ * * Section 7.1 — canonical JSON for the -07/-08 entry digest * * * * Members sorted by UTF-16 code unit order (== JS string sort), no * * whitespace outside strings, integer-valued numbers in integer form, * * every UTF-16 code unit >= 0x7f escaped as \uXXXX (plus the JSON * * escapes for ", \, and C0 controls). Surrogate pairs fall out * * naturally because we iterate UTF-16 code units. * * ------------------------------------------------------------------ */ func escapeCanonicalString(_ s: String) -> String { var out = "\"" for u in s.utf16 { switch u { case 0x22: out += "\\\"" case 0x5c: out += "\\\\" case 0x08: out += "\\b" case 0x09: out += "\\t" case 0x0a: out += "\\n" case 0x0c: out += "\\f" case 0x0d: out += "\\r" default: if u < 0x20 || u >= 0x7f { out += String(format: "\\u%04x", u) } else if let sc = Unicode.Scalar(u) { out.unicodeScalars.append(sc) } } } return out + "\"" } // JS String(number) approximation: integer-valued numbers print in integer // form — full digits for magnitudes < 1e21 (String(1e20) === "1e+20" is // wrong in JS: it is "100000000000000000000"), scientific above that. // Non-integers use Swift's shortest-roundtrip formatter. func numberString(_ n: NSNumber) -> String { let d = n.doubleValue if d.isFinite && d == d.rounded() { if abs(d) < 9.007199254740992e15 { return String(Int64(d)) } if abs(d) < 1e21 { return String(format: "%.0f", d) } } return String(d) } func utf16Less(_ a: String, _ b: String) -> Bool { Array(a.utf16).lexicographicallyPrecedes(Array(b.utf16)) } func canonicalJson(_ v: Any?) throws -> String { guard let v = v else { throw CanonError(msg: "unserializable value in canonical JSON") } if v is NSNull { return "null" } if let b = asBool(v) { return b ? "true" : "false" } if let n = asNumber(v) { return numberString(n) } if let s = v as? String { return escapeCanonicalString(s) } if let a = asArray(v) { return "[" + (try a.map { try canonicalJson($0) }).joined(separator: ",") + "]" } if let d = asDict(v) { let keys = d.keys.sorted(by: utf16Less) return "{" + (try keys.map { escapeCanonicalString($0) + ":" + (try canonicalJson(d[$0])) }).joined(separator: ",") + "}" } throw CanonError(msg: "unserializable value in canonical JSON") } // The -07/-08 entry digest (Section 7.1). Throws on invalid base64 — // fail closed per Section 7.1's strict-decode requirement. The eight // payload members are emitted directly in their canonical (sorted) key // order: close, id, job_id, output_hash, prev_digest, provenance_class, // seq, tool. func entryDigestV07(_ entry: [String: Any], strict: Bool) throws -> String { guard let raw = decodeBase64Strict(entry["canonical_bytes"], strict) else { throw CanonError(msg: "canonical_bytes is not valid base64") } let closeVal: Any = entry["close"] ?? false let body = "{" + [ "\"close\":" + (try canonicalJson(closeVal)), "\"id\":" + (try canonicalJson(entry["id"])), "\"job_id\":" + (try canonicalJson(entry["job_id"])), "\"output_hash\":" + escapeCanonicalString(sha256hex(raw)), "\"prev_digest\":" + (try canonicalJson(entry["prev_digest"])), "\"provenance_class\":" + (try canonicalJson(entry["provenance_class"])), "\"seq\":" + (try canonicalJson(entry["seq"])), "\"tool\":" + (try canonicalJson(entry["tool"])), ].joined(separator: ",") + "}" return sha256hex(Data(body.utf8)) } /* ------------------------------------------------------------------ * * Section 7.2 — chain rules (-07/-08 construction) * * ------------------------------------------------------------------ */ let GENESIS = String(repeating: "0", count: 64) let CHAIN_STRING_MEMBERS = ["id", "tool", "provenance_class", "job_id", "prev_digest"] func verifyChainV07(_ timeline: Any?, opts: Opts) -> [String] { let strict = opts.strict guard let tl = asArray(timeline), !tl.isEmpty else { return ["timeline is not a non-empty list"] } var failures: [String] = [] var job: String? = nil var prev: String? = nil for i in 0.. [String] { let strict = opts.strict guard let tl = asArray(timeline), !tl.isEmpty else { return ["timeline is not a non-empty list"] } var failures: [String] = [] var prev: String? = nil for i in 0.. String? { guard let tl = asArray(timeline), !tl.isEmpty, let last = asDict(tl.last) else { return nil } return try? entryDigestV07(last, strict: opts.strict) } func checkFinalEntryCommitment(_ timeline: Any?, _ commitment: Any?, opts: Opts) -> [String] { guard let c = commitment as? String, fullMatch(c, HEX64_RE) else { return ["external commitment is not a lowercase hex SHA-256 digest"] } guard let digest = finalEntryDigestV07(timeline, opts: opts) else { return ["cannot recompute the final entry digest"] } if digest != c { return ["external commitment does not bind the final entry digest " + "(expected " + c + ", recomputed " + digest + ")"] } return [] } /* ------------------------------------------------------------------ * * Section 7.3 (-09) — chain commitment artifact * * ------------------------------------------------------------------ */ func verifyChainCommitment(_ timeline: Any?, _ commitment: Any?, opts: Opts) -> [String] { let strict = opts.strict guard let tl = asArray(timeline), !tl.isEmpty else { return ["timeline is not a non-empty list"] } guard let c = asDict(commitment) else { return ["commitment is not a JSON object"] } var failures: [String] = [] let jobId = c["job_id"] if !isNonEmptyString(jobId) { failures.append("commitment job_id is not a non-empty string") } let digest = c["final_entry_digest"] if (digest as? String).map({ fullMatch($0, HEX64_RE) }) != true { failures.append("commitment final_entry_digest is not 64 lowercase " + "hex characters") } let count = c["entry_count"] // __float__ marker mirrors the -10 kit runner (entry_count written as // 5.0 must be rejected; plain JSON parsing loses the int/float // distinction so loaders mark it). if (asDict(count)?["__float__"] != nil) || !isIntegerValued(count) { failures.append("commitment entry_count is not an integer") } let published = c["published_at"] if !isRfc3339Format(published, strict) { failures.append("commitment published_at is not RFC 3339") } else if !isCalendarDateTime(published as! String, strict) { failures.append("commitment published_at is not a valid calendar " + "date/time") } if !failures.isEmpty { return failures } // Member shapes are sound; now bind them to the timeline. if asDouble(count)! != Double(tl.count) { failures.append("commitment entry_count " + numberString(asNumber(count)!) + " does not match the timeline length " + String(tl.count)) } for i in 0.. String { if receiptIds.isEmpty { return sha256hex(Data()) } var level = receiptIds.map { sha256data(Data($0.utf8)) } while level.count > 1 { if level.count % 2 == 1 { level.append(level.last!) } var next: [Data] = [] for i in stride(from: 0, to: level.count, by: 2) { var cat = level[i] cat.append(level[i + 1]) next.append(sha256data(cat)) } level = next } return level[0].map { String(format: "%02x", $0) }.joined() } /* ------------------------------------------------------------------ * * Section 8.2 (offline part) — workflow structural checks * * ------------------------------------------------------------------ */ func verifyWorkflowSteps(_ w: Any?) -> [String] { guard let d = asDict(w) else { return ["workflow is not a JSON object"] } guard let steps = asArray(d["steps"]), !steps.isEmpty else { return ["workflow steps is not a non-empty list"] } var failures: [String] = [] for i in 0..() for s in steps { let rid = asDict(s)!["receipt_id"] as! String if seen.contains(rid) { failures.append("duplicate receipt_id: " + rid) break } seen.insert(rid) } let root = merkleRoot(steps.map { asDict($0)!["receipt_id"] as! String }) if (d["merkle_root"] as? String) != root { failures.append("merkle_root does not match the recomputed Section 8.1 root") } return failures } /* ------------------------------------------------------------------ * * Section 8 (full member check) — workflow receipt files * * ------------------------------------------------------------------ */ func checkWorkflow(_ w: Any?, opts: Opts) -> [String] { let strict = opts.strict var failures: [String] = [] guard let d = asDict(w) else { return ["workflow is not a JSON object"] } for field in ["type", "version", "workflow_id", "receipt_id", "session_id", "goal", "status", "steps", "merkle_root", "output_hash", "verify_url"] { if d[field] == nil { failures.append("missing required member: " + field) } } if !failures.isEmpty { return failures } if (d["type"] as? String) != "verifiable-workflow-receipt" { failures.append("type is not \"verifiable-workflow-receipt\"") } if !isNonEmptyString(d["version"]) { failures.append("version is not a non-empty string") } let uuidRe = strict ? UUID_V4_RE : UUID_LOOSE_RE if (d["workflow_id"] as? String).map({ fullMatch($0, uuidRe) }) != true { failures.append("workflow_id is not a UUID") } if (d["receipt_id"] as? String).map({ fullMatch($0, uuidRe) }) != true { failures.append("receipt_id is not a UUID") } for k in ["session_id", "goal", "status"] { if !isNonEmptyString(d[k]) { failures.append(k + " is not a non-empty string") } } if (d["merkle_root"] as? String).map({ fullMatch($0, HEX64_RE) }) != true { failures.append("merkle_root is not a lowercase hex SHA-256 digest") } if (d["output_hash"] as? String).map({ fullMatch($0, HEX64_RE) }) != true { failures.append("output_hash is not a lowercase hex SHA-256 digest") } let vu = d["verify_url"] as? String if vu == nil || vu!.isEmpty || !fullMatch(vu!, "\\Ahttps?://") { failures.append("verify_url is not an http(s) URL") } if let steps = asArray(d["steps"]) { for i in 0.. (kind: String, failures: [CheckFailure]) { var results: [CheckFailure] = [] var kind = opts.kind if kind == "auto" { if asArray(doc) != nil { kind = "chain" } else if let d = asDict(doc), asArray(d["timeline"]) != nil || asArray(d["entries"]) != nil { kind = "chain" } else if let d = asDict(doc), (d["type"] as? String) == "verifiable-workflow-receipt" { kind = "workflow" } else { kind = "receipt" } } if kind == "chain" || kind == "chain06" { let tl: Any? if asArray(doc) != nil { tl = doc } else if let d = asDict(doc) { tl = jsTruthy(d["timeline"]) ? d["timeline"] : d["entries"] } else { tl = nil } let label = kind == "chain06" ? "chain(-06)" : "chain(-07/-08)" let reasons = kind == "chain06" ? verifyChainV06(tl, opts: opts) : verifyChainV07(tl, opts: opts) results += reasons.map { CheckFailure(check: label, reason: $0) } if let d = asDict(doc), d["final_entry_digest"] as? String != nil { results += checkFinalEntryCommitment(tl, d["final_entry_digest"], opts: opts) .map { CheckFailure(check: "final-entry-commitment", reason: $0) } } if let d = asDict(doc), d["commitment"] != nil { results += verifyChainCommitment(tl, d["commitment"], opts: opts) .map { CheckFailure(check: "chain-commitment(-09)", reason: $0) } } return (kind, results) } if kind == "workflow" { results += checkWorkflow(doc, opts: opts) .map { CheckFailure(check: "workflow", reason: $0) } return (kind, results) } // Receipt. results += checkReceipt(doc, opts: opts) .map { CheckFailure(check: "core", reason: $0) } if opts.profile { results += checkReferenceProfile(doc, opts: opts) .map { CheckFailure(check: "reference-profile", reason: $0) } } if opts.anchor || anchorUsesTierShape(doc) { results += checkDisinterestedTier(doc, opts: opts) .map { CheckFailure(check: "disinterested-tier", reason: $0) } } return (kind, results) } func verifyFile(_ file: String, opts: Opts) -> (kind: String, failures: [CheckFailure]) { guard let data = try? Data(contentsOf: URL(fileURLWithPath: file)), let doc = try? JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) else { return ("?", [CheckFailure(check: "parse", reason: "not valid JSON")]) } return verifyDocument(doc, opts: opts) } /* ------------------------------------------------------------------ * * CLI * * ------------------------------------------------------------------ */ // Directory -> every *.json inside, recursively, sorted by path. Hidden // files and anything under a hidden directory are skipped (mirrors the // !name.startsWith(".") filter in verify.js). verify.js scanned only the // top level of a directory; recursion is the one deliberate extension so // the corpus layout test-vectors/{valid,invalid,invalid-profile,anchored} // can be run with a single target. func collectJsonFiles(_ target: String) -> [String] { let fm = FileManager.default var isDir: ObjCBool = false if fm.fileExists(atPath: target, isDirectory: &isDir), isDir.boolValue { var out: [String] = [] if let en = fm.enumerator(atPath: target) { for case let rel as String in en { let hidden = (rel as NSString).pathComponents .contains { $0.hasPrefix(".") } if hidden { continue } if rel.hasSuffix(".json") { out.append((target as NSString).appendingPathComponent(rel)) } } } return out.sorted() } return [target] } func runCLI(_ argv: [String]) -> Int32 { var opts = Opts() var targets: [String] = [] for a in argv.dropFirst() { switch a { case "--strict": opts.strict = true case "--chain06": opts.kind = "chain06" case "--chain": opts.kind = "chain" case "--workflow": opts.kind = "workflow" case "--receipt": opts.kind = "receipt" case "--profile": opts.profile = true case "--anchor": opts.anchor = true case "--json": opts.json = true default: targets.append(a) } } if targets.isEmpty { FileHandle.standardError.write(Data( ("usage: swift aer1verify.swift [--strict] " + "[--chain|--chain06|--workflow|--receipt] [--profile] " + "[--anchor] [--json]\n").utf8)) return 2 } var all: [String] = [] for t in targets { all.append(contentsOf: collectJsonFiles(t)) } var bad = 0 var report: [[String: Any]] = [] for f in all { let r = verifyFile(f, opts: opts) let ok = r.failures.isEmpty if !ok { bad += 1 } if opts.json { report.append([ "file": f, "kind": r.kind, "ok": ok, "failures": r.failures.map { ["check": $0.check, "reason": $0.reason] }, ]) continue } let label = "[" + r.kind + "]" if ok { print("PASS " + label + " " + f) } else { var line = "FAIL " + label + " " + f + " — " + r.failures[0].reason if r.failures.count > 1 { line += " (+" + String(r.failures.count - 1) + " more)" } print(line) } } if opts.json { if let data = try? JSONSerialization.data( withJSONObject: report, options: [.prettyPrinted, .sortedKeys]), let s = String(data: data, encoding: .utf8) { print(s) } } else { print("\n" + String(all.count - bad) + "/" + String(all.count) + " verified") print("CONFORMANCE: " + String(all.count - bad) + "/" + String(all.count)) } return bad == 0 ? 0 : 1 } exit(runCLI(CommandLine.arguments))