sealedrecord 0.4.1 → 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 +58 -0
- package/README.md +10 -0
- package/bin/sealedrecord.js +57 -0
- package/package.json +7 -2
package/COMPARISON.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# How this format relates to C2PA
|
|
2
|
+
|
|
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.
|
|
6
|
+
|
|
7
|
+
## What each one is
|
|
8
|
+
|
|
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.
|
|
10
|
+
|
|
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
|
+
|
|
13
|
+
## Where they differ
|
|
14
|
+
|
|
15
|
+
### 1. A finished asset versus a sequence of work
|
|
16
|
+
|
|
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
|
+
|
|
19
|
+
### 2. Credentialed identity versus carried keys
|
|
20
|
+
|
|
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
|
+
|
|
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
|
+
|
|
25
|
+
### 3. Inside the file versus beside it
|
|
26
|
+
|
|
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
|
+
|
|
29
|
+
### 4. Validator dependencies
|
|
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 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
|
+
|
|
33
|
+
## Where they agree
|
|
34
|
+
|
|
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
|
+
|
|
37
|
+
## How they compose
|
|
38
|
+
|
|
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
|
+
|
|
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
|
+
|
|
43
|
+
## When C2PA alone is enough
|
|
44
|
+
|
|
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
|
+
|
|
47
|
+
## When a sealed record is the missing piece
|
|
48
|
+
|
|
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
|
+
|
|
51
|
+
## Background
|
|
52
|
+
|
|
53
|
+
The design argument behind the record format, including why provenance is treated as a substrate onto which institutions attest, is in:
|
|
54
|
+
|
|
55
|
+
- Provenance Over Detection. SSRN 6402298. https://papers.ssrn.com/abstract=6402298
|
|
56
|
+
- Provenance as Substrate. SSRN 6730343. https://papers.ssrn.com/abstract=6730343
|
|
57
|
+
|
|
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.
|
|
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,7 +11,9 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"index.js",
|
|
13
13
|
"src/",
|
|
14
|
-
"
|
|
14
|
+
"bin/",
|
|
15
|
+
"docs/FORMAT.md",
|
|
16
|
+
"COMPARISON.md"
|
|
15
17
|
],
|
|
16
18
|
"engines": {
|
|
17
19
|
"node": ">=20"
|
|
@@ -35,5 +37,8 @@
|
|
|
35
37
|
],
|
|
36
38
|
"devDependencies": {
|
|
37
39
|
"vitest": "^4.1.11"
|
|
40
|
+
},
|
|
41
|
+
"bin": {
|
|
42
|
+
"sealedrecord": "bin/sealedrecord.js"
|
|
38
43
|
}
|
|
39
44
|
}
|