sealedrecord 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/COMPARISON.md CHANGED
@@ -1,58 +1,58 @@
1
1
  # How this format relates to C2PA
2
2
 
3
- A frequent and fair question: C2PA (Coalition for Content Provenance and Authenticity) already specifies signed provenance for media. Why does a second format exist? Short answer: the two describe different objects and rest on different trust. They compose. This document says where the line is, without advocacy, so the question has a checkable answer.
3
+ C2PA (Coalition for Content Provenance and Authenticity) already specifies signed provenance for media, so a second format needs a reason to exist. The reason is that the two describe different objects and rest on different trust, and that they fit together. This document says where the line falls, in enough detail that the claim can be checked against both specifications.
4
+
5
+ It was written by a member of the content-authenticity community against C2PA Technical Specification 2.4 (https://spec.c2pa.org/). When the specification moves, this document should move with it. Corrections are welcome as issues on this repository.
4
6
 
5
7
  ## What each one is
6
8
 
7
9
  **A C2PA manifest** is a signed statement about one asset: what it is, how it was made or edited, and which earlier assets it came from (ingredients). It is embedded in the asset's own container, bound to the asset's bytes by a hard binding, and signed by a certificate whose validity a validator judges against a trust list.
8
10
 
9
- **A sealed record** (this format, `docs/FORMAT.md`) is a session log: an ordered chain of entries from one or more people over a span of time, where every entry's digest commits to its own content and to the digest before it, each enrolled entry carries an Ed25519 signature from its actor, and the record carries the signers' public keys itself. It lives outside the assets. It names them by hash, and for uncompressed audio by a hash of the samples alone.
10
-
11
- A manifest describes a result. A record describes the work.
11
+ **A sealed record** (this format, `docs/FORMAT.md`) is a session log. Entries from one or more people are chained in order over a span of time; every entry's digest commits to its own content and to the digest before it; each enrolled entry carries an Ed25519 signature from its actor; the signers' public keys travel in the record. It lives outside the assets and names them by hash, and for uncompressed audio by a hash of the samples alone.
12
12
 
13
13
  ## Where they differ
14
14
 
15
- ### 1. One asset, one signer, one moment versus a chain
15
+ ### 1. A finished asset versus a sequence of work
16
16
 
17
- A manifest speaks for one asset at the moment it was signed. Multiple contributors appear only as ingredients pointing backward, each with its own manifest. There is no primitive in C2PA for "these three people acted in this order over this hour, and none of it has been reordered, inserted, or removed since." A sealed record is that primitive. Change entry 4 and the chain breaks at entry 4; the reader says so with both digests.
17
+ A manifest speaks for one asset at the moment it was signed. Earlier contributors appear as ingredients pointing backward, each with its own manifest, and the CAWG identity assertion (section 2 below) lets several named actors sign parts of one manifest. None of these mechanisms establishes sequence. Nothing in C2PA or CAWG can state that three people acted in a given order over a given hour and that nothing has since been reordered, inserted, or removed. A sealed record states exactly that. Change entry 4 and the chain breaks at entry 4, and the reader reports both digests.
18
18
 
19
- ### 2. Institutional identity versus carried keys
19
+ ### 2. Credentialed identity versus carried keys
20
20
 
21
- A C2PA signature is only as good as the certificate chain behind it. A signer outside the validator's trust list is reported as unknown, which is correct behaviour for that design: identity is delegated to certificate authorities. A sealed record delegates nothing. Its signers' public keys travel in the file, the reader recomputes every signature against them, and whether a key belongs to a person is a question the format deliberately leaves to whoever needs to answer it. Receipts and timestamp proofs can then attest on top of the record; they are metadata about the chain, never part of it.
21
+ A C2PA signature is as strong as the certificate chain behind it. A signer outside the validator's trust list is reported as unknown, which is the correct behaviour for a design that delegates identity to certificate authorities. A sealed record delegates nothing: its signers' public keys are in the file, the reader recomputes every signature against them, and whether a key belongs to a person is left to whoever needs the answer. Receipts and timestamp proofs can attest on top of the record afterwards, as metadata about the chain that never enters it.
22
22
 
23
- This is the difference in one sentence: C2PA proves who, according to an authority; a sealed record proves that the same key signed, in this order, according to arithmetic.
23
+ The closest mechanism on the C2PA side is the Creator Assertions Working Group's identity assertion (CAWG Identity Assertion 1.2, https://cawg.io/identity/). A named actor signs a set of the manifest's assertions with their own credential, separately from the claim signer, and one manifest may carry several such assertions from distinct actors. That moves identity a real distance away from the claim signer's certificate. Two differences remain. The credential is still an X.509 certificate or a verifiable credential from an issuer, so identity resolves to something outside the file, where a sealed record resolves to a public key inside it and stops. And identity assertions within one manifest carry no order relative to each other; each says which assertions an actor vouches for, and none says who acted after whom.
24
24
 
25
25
  ### 3. Inside the file versus beside it
26
26
 
27
- A manifest is embedded in the asset, so it goes wherever the asset goes, until something strips it. Re-encoding, re-containering, and many distribution paths drop embedded manifests; the C2PA specification acknowledges this and defines soft bindings (fingerprints and watermarks) to recover the connection. A sealed record was never inside the asset. The sample anchor recomputes from the audio bytes alone, so a file that was retagged after sealing still finds its entry, and a file whose manifest was stripped still verifies against the record it is named in. What the record cannot survive is a transcode, which changes the samples; that is the same limit a hard binding has.
27
+ A manifest is embedded in the asset and goes wherever the asset goes, until something strips it. Re-encoding, re-containering, and many distribution paths drop embedded manifests; the C2PA specification acknowledges this and defines soft bindings (fingerprints and watermarks) to recover the connection. A sealed record was never inside the asset. The sample anchor recomputes from the audio bytes alone, so a file retagged after sealing still finds its entry, and a file whose manifest was stripped still verifies against the record that names it. A transcode changes the samples and defeats the anchor, the same limit a hard binding has.
28
28
 
29
29
  ### 4. Validator dependencies
30
30
 
31
- A C2PA validator needs a trust list, X.509 chain validation, CBOR and COSE parsing, and the container-specific JUMBF logic. The reference reader for this format needs WebCrypto SHA-256 and Ed25519 and nothing else, and the specification is written so that a second reader can be built from the document without reading this code. That is a design goal, not a claim of superiority: a smaller surface is easier to verify independently, which is the whole point of a verifier.
31
+ A C2PA validator needs a trust list, X.509 chain validation, CBOR and COSE parsing, and the container-specific JUMBF logic. The reference reader for this format needs WebCrypto SHA-256 and Ed25519, and the specification is written so that a second reader can be built from the document without consulting this code. A smaller surface is easier to verify independently. That is the design goal; it says nothing about which design is better for the problems C2PA was built for.
32
32
 
33
33
  ## Where they agree
34
34
 
35
- Both commit to bytes with SHA-256. Both sign with standard primitives. Both treat the signed object as evidence, not as truth about the world: a valid manifest proves a signature, not honesty, and a holding record proves integrity and order, not the accuracy of what people signed into it. Both are honest about the limits of hard bindings.
35
+ Both commit to bytes with SHA-256 and sign with standard algorithms. The act of signing is kept apart from the claim of identity on each side: CAWG places the named actor's signature beside the claim signer's, and a sealed record places the actor's signature on the entry and leaves the binding of key to person outside the format. A valid manifest proves a signature was made, and a holding record proves integrity and order; neither proves that what was signed is true. The limits of hard bindings are stated plainly in both specifications.
36
36
 
37
37
  ## How they compose
38
38
 
39
- The natural arrangement is one where the manifest points at the record. A C2PA assertion can carry the session identifier and the sample anchor; a validator that reads the manifest then has a lookup path to the log the asset came from, and a reader of the log can confirm the asset by its anchor. The label on the bottle names the ledger. Neither replaces the other: the manifest is what travels with the file and what platforms know how to display; the record is what survives the file's travels and what proves the sequence of work.
39
+ The natural arrangement has the manifest point at the record. An assertion in the manifest carries the session identifier and the sample anchor, so a validator reading the manifest has a lookup path to the log the asset came from, and a reader of the log can confirm the asset by its anchor. The manifest travels with the file and is what platforms know how to display. The record survives the file's travels and holds the sequence of work.
40
40
 
41
- If C2PA later defines a durable, multi-signer, chained session assertion, the right move for this format is to specify an encoding of the record as that assertion and keep the same reader. The format is the commitment scheme, not the container.
41
+ Should C2PA later define a durable, multi-signer, chained session assertion, the right move for this format is to specify an encoding of the record as that assertion and keep the same reader. What the format commits to does not depend on the container it rides in.
42
42
 
43
43
  ## When C2PA alone is enough
44
44
 
45
- Single asset, single signer, provenance established at publication, identity anchored in a certificate authority the audience already trusts. In that case a sealed record adds nothing you need. Say so and use the manifest.
45
+ A single asset, one signer, provenance established at publication, and identity anchored in a certificate authority the audience already trusts. A sealed record adds nothing there. Use the manifest.
46
46
 
47
47
  ## When a sealed record is the missing piece
48
48
 
49
- More than one contributor. Work that happens over time before anything is published. Identity that cannot or should not depend on a certificate authority. Files that will be stripped, retagged, or re-hosted before anyone asks where they came from. Verification that must be possible with no institution consulted.
49
+ More than one contributor, or work that happens over time before anything is published. Identity that cannot or should not depend on a certificate authority. Files that will be stripped, retagged, or re-hosted before anyone asks where they came from. Verification that has to work with no institution consulted.
50
50
 
51
51
  ## Background
52
52
 
53
- The design argument behind the record format, including why provenance is treated as a substrate that institutions attest onto rather than a service they provide, is in:
53
+ The design argument behind the record format, including why provenance is treated as a substrate onto which institutions attest, is in:
54
54
 
55
55
  - Provenance Over Detection. SSRN 6402298. https://papers.ssrn.com/abstract=6402298
56
56
  - Provenance as Substrate. SSRN 6730343. https://papers.ssrn.com/abstract=6730343
57
57
 
58
- The C2PA specification is at https://c2pa.org/specifications/. Statements about it above describe the published design at a general level; consult the specification for the normative text.
58
+ The C2PA Technical Specification (2.4 at the time of writing) and the companion Soft Binding API are at https://spec.c2pa.org/. Statements about them above describe the published design at a general level; the specification carries the normative text.
package/README.md CHANGED
@@ -8,6 +8,16 @@ Specification: [docs/FORMAT.md](docs/FORMAT.md). How this relates to C2PA: [COMP
8
8
 
9
9
  https://juanlentino.github.io/sealedrecord/ is this library served as static files: drop a record, get a reading. The page imports the published npm tarball of the version it names, is rebuilt only by the release workflow, and makes no network request after it loads. The example buttons use the conformance vectors below.
10
10
 
11
+ ## Command line
12
+
13
+ ```sh
14
+ npx sealedrecord verify record.json # reading and receipts
15
+ npx sealedrecord verify record.json take.wav # plus each file: exact, same audio, or no match
16
+ npx sealedrecord verify --json record.json # the reading object, for scripts
17
+ ```
18
+
19
+ Exit code 0 when the record holds, 1 when it is altered or malformed, 2 when the chain recomputes but was never sealed, 3 on usage or I/O errors. No flags beyond `--json`.
20
+
11
21
  ## Install and verify
12
22
 
13
23
  ```sh
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ /* The library with exit codes.
3
+ sealedrecord verify [--json] <record.json> [file ...]
4
+ exit 0 holds · 1 altered or malformed · 2 unsealed · 3 usage or I/O */
5
+
6
+ import { readFileSync } from "node:fs";
7
+ import { basename } from "node:path";
8
+ import { verifyPackage, verifyReceipts, pcmHash, findAnchors, findPcmAnchors } from "../index.js";
9
+
10
+ const USAGE = "usage: sealedrecord verify [--json] <record.json> [file ...]";
11
+ const EXIT = { holds: 0, altered: 1, malformed: 1, unsealed: 2 };
12
+ const HEX = (b) => [...new Uint8Array(b)].map((x) => x.toString(16).padStart(2, "0")).join("");
13
+
14
+ const readOr3 = (path) => {
15
+ try { return readFileSync(path); } catch (e) { console.error(`cannot read ${path}: ${e.message}`); process.exit(3); }
16
+ };
17
+
18
+ const checkFile = async (path, entries) => {
19
+ const bytes = readOr3(path);
20
+ const buf = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
21
+ const sha256 = HEX(await crypto.subtle.digest("SHA-256", buf));
22
+ const exact = findAnchors(sha256, entries);
23
+ if (exact.length) return { file: path, match: "exact", seq: exact.map((e) => e.seq) };
24
+ const audio = findPcmAnchors(await pcmHash(buf), entries);
25
+ if (audio.length) return { file: path, match: "same audio", seq: audio.map((e) => e.seq) };
26
+ return { file: path, match: "no match", seq: [] };
27
+ };
28
+
29
+ const main = async (argv) => {
30
+ const json = argv.includes("--json");
31
+ const args = argv.filter((a) => a !== "--json");
32
+ if (args[0] !== "verify" || !args[1]) { console.error(USAGE); return 3; }
33
+ const [, recordPath, ...files] = args;
34
+
35
+ let pkg;
36
+ try { pkg = JSON.parse(readOr3(recordPath).toString("utf8")); } catch { pkg = null; }
37
+ const reading = await verifyPackage(pkg);
38
+ const ok = reading.kind !== "malformed";
39
+ const receipts = ok ? await verifyReceipts({ ...pkg, entries: reading.entries }) : null;
40
+ const checks = ok ? await Promise.all(files.map((f) => checkFile(f, reading.entries))) : [];
41
+
42
+ if (json) {
43
+ console.log(JSON.stringify({ reading, receipts, files: checks }, null, 2));
44
+ return EXIT[reading.kind];
45
+ }
46
+ const head = reading.kind === "altered" ? `altered at entry ${reading.breakSeq}` : reading.kind;
47
+ console.log(head);
48
+ if (reading.detail) console.log(` ${reading.detail}`);
49
+ if (ok) {
50
+ console.log(` ${reading.entries.length} of ${reading.total} entries read; signatures ${reading.signed ? "verified" : "not checked"}`);
51
+ console.log(` ${receipts.verified} of ${receipts.receipted} receipts verify${receipts.problems.length ? `; ${receipts.problems.join("; ")}` : ""}`);
52
+ }
53
+ for (const c of checks) console.log(`${basename(c.file)}: ${c.match}${c.seq.length ? ` (entry ${c.seq.join(", ")})` : ""}`);
54
+ return EXIT[reading.kind];
55
+ };
56
+
57
+ process.exit(await main(process.argv.slice(2)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sealedrecord",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Verify a sealed session record: recompute the SHA-256 chain, check Ed25519 entry signatures and time receipts, and match audio files to their anchors. WebCrypto only, browser and Node.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -11,6 +11,7 @@
11
11
  "files": [
12
12
  "index.js",
13
13
  "src/",
14
+ "bin/",
14
15
  "docs/FORMAT.md",
15
16
  "COMPARISON.md"
16
17
  ],
@@ -36,5 +37,8 @@
36
37
  ],
37
38
  "devDependencies": {
38
39
  "vitest": "^4.1.11"
40
+ },
41
+ "bin": {
42
+ "sealedrecord": "bin/sealedrecord.js"
39
43
  }
40
44
  }