@moonglade/provenance 0.0.0-stage → 2.6.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/LICENSE +21 -0
- package/NOTICE.md +9 -0
- package/README.md +76 -2
- package/SPEC.md +176 -0
- package/TIMESTAMP.md +21 -0
- package/dist/artifact-cli.d.ts +2 -0
- package/dist/artifact-cli.js +108 -0
- package/dist/artifactEnvelope.d.ts +38 -0
- package/dist/artifactEnvelope.js +61 -0
- package/dist/artifactHook.d.ts +30 -0
- package/dist/artifactHook.js +114 -0
- package/dist/badge.d.ts +83 -0
- package/dist/badge.js +403 -0
- package/dist/index.d.ts +228 -0
- package/dist/index.js +782 -0
- package/dist/mcp.d.ts +87 -0
- package/dist/mcp.js +352 -0
- package/dist/merkle.d.ts +71 -0
- package/dist/merkle.js +176 -0
- package/dist/self-test.d.ts +21 -0
- package/dist/self-test.js +280 -0
- package/dist/timestamp.d.ts +274 -0
- package/dist/timestamp.js +1103 -0
- package/package.json +75 -4
- package/trust/README.md +19 -0
- package/trust/registry.json +12 -0
- package/trust/root-public.asc +10 -0
- package/wasm/mgc_wasm.d.ts +53 -0
- package/wasm/mgc_wasm.js +389 -0
- package/wasm/mgc_wasm_bg.wasm +0 -0
- package/wasm/mgc_wasm_bg.wasm.d.ts +20 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moonglade AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Dependency and fixture notices
|
|
2
|
+
|
|
3
|
+
This package uses the MIT license. Its direct runtime dependencies are PKI.js 3.4.1 and ASN1.js 3.0.10, both BSD-3-Clause. The locked runtime dependency tree also includes @noble/hashes (MIT), bytestreamjs (BSD-3-Clause), pvtsutils (MIT), pvutils (MIT) and tslib (0BSD).
|
|
4
|
+
|
|
5
|
+
Each dependency retains its own copyright and license files when installed. No dependency code is vendored in this package.
|
|
6
|
+
|
|
7
|
+
Timestamp tests generate synthetic certificates and ephemeral keys with PKI.js and Node WebCrypto. Merkle and canonicalization vectors are project test data. The historical FreeTSA fixture and unreviewed TSA anchors are excluded.
|
|
8
|
+
|
|
9
|
+
The Moonglade name identifies the project. The MIT license does not grant additional trademark permission. The exported root certificate deliberately contains the release maintainer's public signer identity. It is not an artifact verification key and does not grant agents architect authority.
|
package/README.md
CHANGED
|
@@ -1,3 +1,77 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @moonglade/provenance 2.6.0
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Ed25519 signatures, canonical file hashes, RFC 3161 timestamp checks and Merkle inclusion proofs for Node 22 and 24.
|
|
4
|
+
|
|
5
|
+
## Build from source
|
|
6
|
+
|
|
7
|
+
Run these commands from the repository root:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm ci
|
|
11
|
+
npm test
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The npm package can be installed after a release is authorized. This candidate has not been published.
|
|
15
|
+
|
|
16
|
+
## Sign and verify
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
const { ProvenanceEngine } = require('@moonglade/provenance');
|
|
20
|
+
// Supply your own Ed25519 keys. Never use the release root key for artifacts.
|
|
21
|
+
async function example(file, privateKey, publicKey, root) {
|
|
22
|
+
await ProvenanceEngine.sealDocument(file, privateKey, 'example-signer', 'local', { vaultRoot: root });
|
|
23
|
+
return ProvenanceEngine.verifyDocument(file, publicKey, {
|
|
24
|
+
expectedScope: ProvenanceEngine.scopeFor(file, root),
|
|
25
|
+
requiredSigners: ['example-signer']
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Get trusted public keys through a channel you trust. An editable sidecar cannot establish a signer's identity. A valid signature establishes possession of a key and integrity of the signed payload. It does not prove the content is correct, identify its actual author, or grant the signer authority.
|
|
31
|
+
|
|
32
|
+
Supply `expectedScope` to bind verification to an expected location or namespace. Use `requiredSigners` when particular signatures are required. The low-level verifier does not infer either policy.
|
|
33
|
+
|
|
34
|
+
## Canonicalization
|
|
35
|
+
|
|
36
|
+
For text, the engine removes one leading UTF-8 BOM, converts CRLF to LF and applies Unicode NFC. Bare CR is preserved. Invalid UTF-8 is rejected.
|
|
37
|
+
|
|
38
|
+
Known binary formats keep their exact bytes. For unknown extensions, a NUL in the first 1024 bytes selects binary handling; otherwise the engine requires valid UTF-8 text. Text integrity follows these normalization rules rather than raw byte identity.
|
|
39
|
+
|
|
40
|
+
## Verification results
|
|
41
|
+
|
|
42
|
+
`SEALED` requires a verified Ed25519 signature. With no key, a matching recorded hash is shown in amber as `HASH-ONLY`. The sidecar is editable, so that result provides no signature assurance. Malformed sidecars cannot produce a seal.
|
|
43
|
+
|
|
44
|
+
Timestamp results are separate: `ABSENT`, `INVALID`, `INTEGRITY_ONLY` or `ANCHORED`. Time comes from the verified token, never an unsigned sidecar field. `ANCHORED` requires caller-supplied CA certificates, chain validation at token time, ESSCertIDv2 and a critical timestamp-only EKU. Certificate revocation is not checked. Without anchors, a valid self-signed TSA token can provide integrity-only evidence. Network or host-clock observations without tokens do not become RFC 3161 attestations.
|
|
45
|
+
|
|
46
|
+
## CLI
|
|
47
|
+
|
|
48
|
+
The installed names are `provenance`, `provenance-artifact` and `provenance-gate`. The last starts the MCP server. CLI commands are:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
provenance --help
|
|
52
|
+
provenance --version
|
|
53
|
+
provenance verify <file>
|
|
54
|
+
provenance badge <file>
|
|
55
|
+
provenance observe <file>
|
|
56
|
+
provenance seal-provisional <file>
|
|
57
|
+
provenance move <source> <destination>
|
|
58
|
+
provenance test [--json]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Set `MOONGLADE_PUBLIC_KEY_PEM` for verification. Provisional sealing requires `MOONGLADE_PRIVATE_KEY_PEM` and `MOONGLADE_SIGNER`. The verification root is `MOONGLADE_ROOT`, or the current directory when unset. `MOONGLADE_REQUIRED_SIGNERS` is an optional comma-separated list.
|
|
62
|
+
|
|
63
|
+
Failed verification and refused sealing return a nonzero exit code. Move refuses destination collisions and any source sidecar. To relocate an attested artifact, copy it, explicitly reseal and verify the destination, then retire the original separately. Existing signatures keep their original scope.
|
|
64
|
+
|
|
65
|
+
## MCP and filesystem access
|
|
66
|
+
|
|
67
|
+
The read-only MCP server exposes `verify_artifact_provenance`, `audit_vault_merkle_root` and `assert_gate_status`. It has no signing or hook tools. Each supplied path authorizes a local read under the operator's filesystem permissions. The process is not a sandbox. Connect it only to principals allowed that access.
|
|
68
|
+
|
|
69
|
+
Gate assertions check the supplied signature and policy; they do not establish human approval or signer authority. Input frames are limited to 1 MiB. File reads reject symlinks and have a 64 MiB limit; sidecars are limited to 1 MiB, and timestamp tokens and responses to 512 KiB. Protect workspace permissions. Concurrent hostile filesystem writers are outside the supported trust model.
|
|
70
|
+
|
|
71
|
+
## API and limits
|
|
72
|
+
|
|
73
|
+
`dist/index.d.ts` declares the public exports: the engine, vault, verification policy, canonicalization and bounded reads; Merkle helpers; timestamp parsing, verification and acquisition; badge helpers; artifact and envelope utilities; read-only MCP tools and self-test.
|
|
74
|
+
|
|
75
|
+
The built-in verifier returns `INDETERMINATE` for OpenPGP and SSHSIG attestations. The legacy library OpenPGP signing helper is not covered by the default test suite; its keyring selection is caller-controlled. Default hooks, platform keystores, the hook CLI and sibling packages are excluded.
|
|
76
|
+
|
|
77
|
+
See [SPEC.md](SPEC.md), [TIMESTAMP.md](TIMESTAMP.md), [NOTICE.md](NOTICE.md) and [trust/README.md](trust/README.md). GPG checks the OpenPGP release identity separately. Artifact verification uses raw Ed25519 SPKI PEM keys. Candidate documentation is intentionally unsealed. The root release key is not an agent key.
|
package/SPEC.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Attestation Bundle v3 (Extended): Generic Claims, Dual-Hash Binding & Multi-Party Countersigning
|
|
2
|
+
|
|
3
|
+
**Status:** Implemented in `@moonglade/provenance` v2.6.0+ / v3-Extended Engine
|
|
4
|
+
**Author:** Marc Ottley (Moonglade AI)
|
|
5
|
+
**Supersedes:** Bundle v2 Specification (`SPEC.md`)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Motivation & Design Intent
|
|
10
|
+
|
|
11
|
+
Attestation Bundle v2 solved field binding and domain separation for local-first document attestation:
|
|
12
|
+
```text
|
|
13
|
+
moonglade-provenance/v2\n
|
|
14
|
+
scope:<scope>\n
|
|
15
|
+
sha256:<sha256 of canonical bytes>\n
|
|
16
|
+
signer:<identity>\n
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
While v2 securely bound the document's content hash to its repository scope and signer, real-world systems require attaching **rich, structured domain metadata** that must also be tamper-evident:
|
|
20
|
+
- **Photography & Generative Media:** EXIF camera telemetry, focal length, color space, creator royalty splits, and Creative Commons / commercial licensing terms.
|
|
21
|
+
- **Enterprise PDF & Legal Documents:** Contract identifiers, governing jurisdictions, classification levels, and counterparty entities.
|
|
22
|
+
- **Forensic Science & Chain of Custody:** Evidence acquisition timestamps, laboratory operator IDs, case numbers, and forensic instrument serial numbers.
|
|
23
|
+
- **Agentic AI Governance:** Model version, generation prompt hash, hallucination audit logs, and parent artifact DAG pointers.
|
|
24
|
+
|
|
25
|
+
Attestation Bundle v3 introduces **Generic Signed Claims** with **Dual-Hash Cryptographic Preimage Binding** and **Multi-Party Countersigning**, preserving 100% backward compatibility with v2 bundles.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. Generic Claims Architecture
|
|
30
|
+
|
|
31
|
+
### 2.1 Arbitrary Datatype Support
|
|
32
|
+
Claims are not restricted to predefined strings. Any valid JSON data structure can be used as a claim key or value:
|
|
33
|
+
- **Primitive types:** Strings, numbers (safe integers only; carry a decimal as a string or a scaled integer), booleans, null.
|
|
34
|
+
- **Structured types:** Arrays, nested objects, associative lookup tables.
|
|
35
|
+
|
|
36
|
+
### 2.2 Deterministic Canonicalization (RFC 8785 JCS)
|
|
37
|
+
To ensure that claims serialize identically across different programming languages and runtimes (Node.js, Python, Rust, Go, browser extensions):
|
|
38
|
+
1. **Object Key Sorting:** All object keys are sorted lexicographically by UTF-16 code units.
|
|
39
|
+
2. **Whitespace Stripping:** No whitespace is permitted between keys, colons, commas, or values.
|
|
40
|
+
3. **Number Formatting:** Numbers MUST be IEEE 754 integers within -(2^53-1) to 2^53-1, judged by value (draft -03 section 3.3): `1.0` and `1e2` are accepted and written `1` and `100`, `-0` is written `0`. Non-integral numbers (such as `1.5`), values beyond the safe range, `NaN` and `Infinity` are rejected by the TypeScript, Python and Rust engines alike.
|
|
41
|
+
4. **String Escaping:** Characters below `0x20` are escaped using standard escape sequences (`\\n`, `\\r`, `\\t`) or `\\u00xx`.
|
|
42
|
+
5. **Undefined Exclusion:** Undefined object properties are omitted from serialization.
|
|
43
|
+
|
|
44
|
+
Formally:
|
|
45
|
+
$$\\text{ClaimsHash} = \\text{SHA-256}(\\text{JCS}(\\text{claims}))$$
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 3. The 5-Line Signing Preimage Framing
|
|
50
|
+
|
|
51
|
+
### 3.1 Framing Specification
|
|
52
|
+
The digital signature covers an unambiguous, newline-delimited framing payload that binds both the artifact bytes and the claims:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
moonglade-provenance/v3\n
|
|
56
|
+
scope:<scope-uri>\n
|
|
57
|
+
target-sha256:<64-hex of canonical artifact bytes>\n
|
|
58
|
+
signer:<signer-identity>\n
|
|
59
|
+
claims-sha256:<64-hex of JCS canonical claims map>\n
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3.2 ABNF Definition
|
|
63
|
+
|
|
64
|
+
```abnf
|
|
65
|
+
V3-Payload = "moonglade-provenance/v3" LF
|
|
66
|
+
"scope:" Scope-URI LF
|
|
67
|
+
"target-sha256:" 64HEXDIG LF
|
|
68
|
+
"signer:" Identity-String LF
|
|
69
|
+
"claims-sha256:" 64HEXDIG LF
|
|
70
|
+
|
|
71
|
+
Scope-URI = 1*(ALPHA / DIGIT / "-" / "_" / "." / "/" / ":")
|
|
72
|
+
Identity-String = 1*(VCHAR)
|
|
73
|
+
64HEXDIG = 64HEXDIG
|
|
74
|
+
LF = %x0A
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3.3 Security Invariants
|
|
78
|
+
1. **Domain Separation:** The header `"moonglade-provenance/v3\n"` guarantees that v3 signatures cannot be replayed or downgraded to v2 or v1 formats.
|
|
79
|
+
2. **Dual-Hash Binding:** The signature cryptographically binds the canonical artifact bytes (`target-sha256`) and the claims payload (`claims-sha256`) into a single atomic assertion. Tampering with either the artifact OR the claims invalidates the signature.
|
|
80
|
+
3. **Control Character Rejection:** Control characters (`0x00` through `0x1F` and `0x7F`) in any field abort signing immediately, preventing delimiter injection attacks.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 4. Sidecar Schema (v3)
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"standard": "Moonglade Provenance Attestation Bundle v3",
|
|
89
|
+
"artifact": "bathsheba_twilight_raw.jpg",
|
|
90
|
+
"claims": {
|
|
91
|
+
"photographer": "Marc Ottley",
|
|
92
|
+
"focal_length_mm": 85,
|
|
93
|
+
"camera_make": "Sony",
|
|
94
|
+
"camera_model": "ILCE-7RM5",
|
|
95
|
+
"iso": 100,
|
|
96
|
+
"aperture": "1.8",
|
|
97
|
+
"shutter_speed": "1/250",
|
|
98
|
+
"license": "CC-BY-NC-ND-4.0",
|
|
99
|
+
"royalty_split": {
|
|
100
|
+
"photographer_pct": 70,
|
|
101
|
+
"gallery_fund_pct": 30
|
|
102
|
+
},
|
|
103
|
+
"tags": ["bathsheba", "east-coast", "golden-hour", "raw"]
|
|
104
|
+
},
|
|
105
|
+
"claims_sha256": "3c89b27e8d1a49f5...",
|
|
106
|
+
"attestations": [
|
|
107
|
+
{
|
|
108
|
+
"method": "moonglade-ed25519-v3",
|
|
109
|
+
"signer": "marc.ottley@moongladeai.net",
|
|
110
|
+
"scope": "moonglade:vault:gallery/bathsheba_twilight_raw.jpg",
|
|
111
|
+
"target_sha256": "9f83a2...",
|
|
112
|
+
"claims_sha256": "3c89b27e8d1a49f5...",
|
|
113
|
+
"created": "2026-09-14T14:35:00Z",
|
|
114
|
+
"git_commit": "c4d2e1f",
|
|
115
|
+
"signature": "base64-ed25519-sig..."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"method": "moonglade-ed25519-v3",
|
|
119
|
+
"signer": "gallery.curator@moongladeai.net",
|
|
120
|
+
"scope": "moonglade:vault:gallery/bathsheba_twilight_raw.jpg",
|
|
121
|
+
"target_sha256": "9f83a2...",
|
|
122
|
+
"claims_sha256": "3c89b27e8d1a49f5...",
|
|
123
|
+
"created": "2026-09-14T14:40:00Z",
|
|
124
|
+
"git_commit": "c4d2e1f",
|
|
125
|
+
"signature": "base64-ed25519-countersig..."
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"method": "rfc3161",
|
|
129
|
+
"tier": "L1_CRYPTO_PRIMARY",
|
|
130
|
+
"tsa": "https://freetsa.org/tsr",
|
|
131
|
+
"tsa_time": "2026-09-14T14:35:03Z",
|
|
132
|
+
"token_b64": "MIIB..."
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
"sha256_at_last_write": "9f83a2...",
|
|
136
|
+
"updated": "2026-09-14T14:40:00Z",
|
|
137
|
+
"status": "canonical"
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 5. Multi-Party Countersigning Protocol
|
|
144
|
+
|
|
145
|
+
In distributed workflows (e.g. Creator to Publisher, Laboratory Analyst to Quality Inspector):
|
|
146
|
+
1. **Primary Seal:** The creator seals the artifact with their private key, attaching the canonical claims.
|
|
147
|
+
2. **Review & Countersign:**
|
|
148
|
+
- The countersigner reads `<artifact>.provenance.json`.
|
|
149
|
+
- The engine validates that `target_sha256` matches the artifact bytes and `claims_sha256` matches the JCS-canonicalized claims.
|
|
150
|
+
- The countersigner executes `countersignDocument()`:
|
|
151
|
+
$$\\text{Sig}_{\\text{counter}} = \\text{Sign}_{K_{\\text{counter}}}(\\text{V3-Payload})$$
|
|
152
|
+
- Appends the countersignature to `attestations`.
|
|
153
|
+
3. **Independent Keyring Verification:**
|
|
154
|
+
- Recipients pass a keyring mapping signer identities to public keys:
|
|
155
|
+
```typescript
|
|
156
|
+
const keyring = {
|
|
157
|
+
"marc.ottley@moongladeai.net": marcPublicKeyPem,
|
|
158
|
+
"gallery.curator@moongladeai.net": galleryPublicKeyPem
|
|
159
|
+
};
|
|
160
|
+
const result = await ProvenanceEngine.verifyDocument(path, keyring);
|
|
161
|
+
```
|
|
162
|
+
- Every signature in the bundle must verify against its corresponding key. If any key is missing or forged, the verification fails closed.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 6. Threat Model & Verification Guarantees
|
|
167
|
+
|
|
168
|
+
| Attack Vector | Detection Mechanism | Result |
|
|
169
|
+
| :--- | :--- | :--- |
|
|
170
|
+
| **Artifact Byte Mutation** | Target hash mismatch | `Hash mismatch: Document has been mutated` |
|
|
171
|
+
| **Claims Value Tampering** | Recomputed JCS hash mismatch | `Claims hash mismatch: claims payload has been mutated` |
|
|
172
|
+
| **Claims Substitution** | Preimage `claims-sha256` mismatch | `Signature validation failed` |
|
|
173
|
+
| **Scope Smuggling** | Strict POSIX relative URI binding | `Attestation records a different hash / scope mismatch` |
|
|
174
|
+
| **Signature Downgrade** | Version header framing (`moonglade-provenance/v3`) | Legacy v1/v2 parsers fail closed on version string |
|
|
175
|
+
| **Forged Countersignature** | Keyring verification fails on invalid signature | `Signature validation failed for signer` |
|
|
176
|
+
| **Timestamp Forgery** | RFC 3161 X.509 cert chain and ASN.1 message imprint | `Timestamp token verification failed` |\n
|
package/TIMESTAMP.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Timestamp verification in 2.6.0
|
|
2
|
+
|
|
3
|
+
## Token verification
|
|
4
|
+
|
|
5
|
+
`verifyTimeStampToken` checks the CMS signature over signed attributes, signed content type, TSTInfo digest, SHA-256 document imprint, one TSA signer, ESS certificate binding, certificate validity at token generation time and critical timestamp-only EKU. If KeyUsage is present it must permit signatures. SHA-256/384/512 CMS digests and RSA PKCS#1 v1.5 or ECDSA signatures are supported. Other algorithms fail closed. Legacy ESSCertID is supported for integrity-only checks; anchored policy requires ESSCertIDv2.
|
|
6
|
+
|
|
7
|
+
## Trust anchors
|
|
8
|
+
|
|
9
|
+
Without caller-supplied `trustAnchors`, a token can earn INTEGRITY_ONLY, including self-signed tokens. Supplying CA trustAnchors requires successful PKI.js certificate path validation at the token generation time and yields ANCHORED. An empty anchor list is an error. Sidecar-supplied anchors are ignored. No OCSP/CRL revocation lookup is performed and no revocation assurance is claimed. No public TSA anchor is installed implicitly.
|
|
10
|
+
|
|
11
|
+
## Acquisition and response checks
|
|
12
|
+
|
|
13
|
+
`validateTimeStampResp` checks response structure; it does not verify signatures. It parses the actual TSTInfo imprint/nonce rather than searching bytes. verifyTimeStampToken establishes cryptographic integrity and optional trust. probeTsa performs both response/nonce checking and token signature/imprint validation. queryTsa limits responses to 512 KiB and imposes an absolute deadline. Acquisition routing labels L1/L2 on probes describe selected endpoint pools only; returned token results are INTEGRITY_ONLY, never trusted by routing. Network HTTP Date and host-clock fallbacks are unauthenticated observations, not timestamp attestations. No host-time substitute is inserted into a token result.
|
|
14
|
+
|
|
15
|
+
## Public functions
|
|
16
|
+
|
|
17
|
+
Public functions: encodeDerLength, encodeDerSequence, decodeDerLength, parsePkiStatus, validateTimeStampResp, verifyTimeStampToken, buildTimeStampReq, extractGenTime, queryTsa, probeTsa, probeNetworkTime and concurrentDegradingTimestamp. EndpointCircuitBreaker and exported endpoint/policy constants provide acquisition scheduling. extractGenTime is a legacy unauthenticated diagnostic parser, never used for a verified timestamp verdict.
|
|
18
|
+
|
|
19
|
+
## Test coverage
|
|
20
|
+
|
|
21
|
+
Tests generate ephemeral synthetic CA/TSA keys in memory. No private fixture key or third-party TSA token ships. Live TSA behavior and revocation are outside the hermetic tests. Services are used under their own terms and are not endorsed by this package. Sources: [PKI.js certificate validation](https://pkijs.org/docs/api/classes/CertificateChainValidationEngine/), [RFC 5816](https://www.rfc-editor.org/rfc/rfc5816).
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const index_1 = require("./index");
|
|
5
|
+
const artifactHook_1 = require("./artifactHook");
|
|
6
|
+
const self_test_1 = require("./self-test");
|
|
7
|
+
const badge_1 = require("./badge");
|
|
8
|
+
async function main() {
|
|
9
|
+
const [, , action, file, dest] = process.argv;
|
|
10
|
+
if (action === '--help' || action === '-h') {
|
|
11
|
+
console.log('provenance <verify|badge|observe|seal-provisional|move|test> <file> [dest]. Keys: MOONGLADE_PUBLIC_KEY_PEM / MOONGLADE_PRIVATE_KEY_PEM. Verification root: MOONGLADE_ROOT or current directory.');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (action === '--version') {
|
|
15
|
+
console.log(require('../package.json').version);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (action === 'verify') {
|
|
19
|
+
if (!file || !process.env.MOONGLADE_PUBLIC_KEY_PEM)
|
|
20
|
+
throw new Error('verify requires a file and MOONGLADE_PUBLIC_KEY_PEM');
|
|
21
|
+
const expectedScope = index_1.ProvenanceEngine.scopeFor(file, process.env.MOONGLADE_ROOT || process.cwd());
|
|
22
|
+
const requiredSigners = process.env.MOONGLADE_REQUIRED_SIGNERS?.split(',');
|
|
23
|
+
const result = await index_1.ProvenanceEngine.verifyDocument(file, process.env.MOONGLADE_PUBLIC_KEY_PEM, { expectedScope, requiredSigners });
|
|
24
|
+
console.log(JSON.stringify(result, null, 2));
|
|
25
|
+
if (!result.verified)
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (action === 'test' || action === 'self-test') {
|
|
30
|
+
const isJson = process.argv.includes('--json');
|
|
31
|
+
const report = await (0, self_test_1.runSelfTests)();
|
|
32
|
+
if (isJson) {
|
|
33
|
+
console.log(JSON.stringify(report, null, 2));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
(0, self_test_1.printSelfTestReport)(report);
|
|
37
|
+
}
|
|
38
|
+
if (!report.passed)
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (action === 'badge') {
|
|
43
|
+
if (!file) {
|
|
44
|
+
console.error('usage: provenance badge <file> [--format=text|markdown|json|shields] [--short]');
|
|
45
|
+
process.exitCode = 2;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const isJson = process.argv.includes('--json') || process.argv.includes('--format=json');
|
|
49
|
+
const isText = process.argv.includes('--format=text');
|
|
50
|
+
const isShields = process.argv.includes('--format=shields') || process.argv.includes('--format=shields-url');
|
|
51
|
+
const isHtml = process.argv.includes('--format=html') || process.argv.includes('--html');
|
|
52
|
+
const isColor = process.argv.includes('--color') || process.argv.includes('-c');
|
|
53
|
+
const isShort = process.argv.includes('--short');
|
|
54
|
+
const badgeInfo = await (0, badge_1.getArtifactBadge)(file, {
|
|
55
|
+
style: isShort ? 'short' : 'detailed',
|
|
56
|
+
color: isColor ? 'ansi' : false,
|
|
57
|
+
publicKeyPem: process.env.MOONGLADE_PUBLIC_KEY_PEM,
|
|
58
|
+
requiredScope: process.env.MOONGLADE_PUBLIC_KEY_PEM ? index_1.ProvenanceEngine.scopeFor(file, process.env.MOONGLADE_ROOT || process.cwd()) : undefined
|
|
59
|
+
});
|
|
60
|
+
if (isJson) {
|
|
61
|
+
console.log(JSON.stringify(badgeInfo, null, 2));
|
|
62
|
+
}
|
|
63
|
+
else if (isHtml) {
|
|
64
|
+
console.log(badgeInfo.html ?? badgeInfo.badgeText);
|
|
65
|
+
}
|
|
66
|
+
else if (isShields) {
|
|
67
|
+
console.log(badgeInfo.shieldsUrl);
|
|
68
|
+
}
|
|
69
|
+
else if (isText) {
|
|
70
|
+
console.log(isColor ? (badgeInfo.ansiText ?? badgeInfo.badgeText) : badgeInfo.badgeText);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.log(isColor ? `${badgeInfo.markdown.replace(badgeInfo.badgeText, badgeInfo.ansiText ?? badgeInfo.badgeText)}` : badgeInfo.markdown);
|
|
74
|
+
}
|
|
75
|
+
if (badgeInfo.status === 'MUTATED' || badgeInfo.status === 'INVALID_SIGNATURE' || badgeInfo.status === 'NOT_FOUND') {
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!action || !file || !['observe', 'seal-provisional', 'move'].includes(action)) {
|
|
81
|
+
console.error('usage: provenance <observe|seal-provisional|move|test|badge> <file> [dest]');
|
|
82
|
+
process.exitCode = 2;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (action === 'move') {
|
|
86
|
+
if (!dest) {
|
|
87
|
+
console.error('usage: provenance-artifact move <source> <dest>');
|
|
88
|
+
process.exitCode = 2;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const result = await (0, artifactHook_1.moveArtifactWithAttestation)(file, dest);
|
|
92
|
+
console.log(JSON.stringify(result, null, 2));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const common = {
|
|
96
|
+
vaultRoot: process.cwd(),
|
|
97
|
+
signerIdentity: process.env.MOONGLADE_SIGNER,
|
|
98
|
+
generator: {
|
|
99
|
+
agent: process.env.MOONGLADE_AGENT ?? 'unknown',
|
|
100
|
+
model: process.env.MOONGLADE_MODEL
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const result = await (0, artifactHook_1.processArtifact)(file, { ...common, action: action, privateKeyPem: process.env.MOONGLADE_PRIVATE_KEY_PEM });
|
|
104
|
+
if (result.skipped)
|
|
105
|
+
process.exitCode = 1;
|
|
106
|
+
console.log(JSON.stringify(result, null, 2));
|
|
107
|
+
}
|
|
108
|
+
main().catch(error => { console.error(error.message); process.exitCode = 1; });
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const ARTIFACT_ENVELOPE_FORMAT = "moonglade-artifact-envelope-v1";
|
|
2
|
+
export type ArtifactType = 'markdown' | 'image' | '3d-scene' | 'memory' | 'model-output' | 'release' | 'other';
|
|
3
|
+
export type ArtifactLifecycle = 'unverified' | 'observed' | 'signed' | 'published' | 'revoked';
|
|
4
|
+
export interface ArtifactEvidence {
|
|
5
|
+
integrity: 'unknown' | 'valid' | 'failed';
|
|
6
|
+
signature: 'none' | 'provisional' | 'valid' | 'failed';
|
|
7
|
+
timestamp: 'none' | 'indeterminate' | 'valid' | 'failed';
|
|
8
|
+
scope: 'unknown' | 'valid' | 'failed';
|
|
9
|
+
}
|
|
10
|
+
export interface ArtifactEnvelope {
|
|
11
|
+
format: typeof ARTIFACT_ENVELOPE_FORMAT;
|
|
12
|
+
artifact_id: string;
|
|
13
|
+
artifact_type: ArtifactType;
|
|
14
|
+
canonical_bytes: string;
|
|
15
|
+
source_uri?: string;
|
|
16
|
+
scope?: string;
|
|
17
|
+
created_at: string;
|
|
18
|
+
parents: string[];
|
|
19
|
+
attestations: Array<{
|
|
20
|
+
method: string;
|
|
21
|
+
signer?: string;
|
|
22
|
+
fingerprint?: string;
|
|
23
|
+
status?: string;
|
|
24
|
+
signature?: string;
|
|
25
|
+
}>;
|
|
26
|
+
status: ArtifactLifecycle;
|
|
27
|
+
evidence: ArtifactEvidence;
|
|
28
|
+
generator?: {
|
|
29
|
+
agent?: string;
|
|
30
|
+
model?: string;
|
|
31
|
+
session?: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export declare function artifactId(canonicalBytes: Buffer): string;
|
|
35
|
+
export declare function createArtifactEnvelope(input: Omit<ArtifactEnvelope, 'format' | 'artifact_id'> & {
|
|
36
|
+
bytes: Buffer;
|
|
37
|
+
}): ArtifactEnvelope;
|
|
38
|
+
export declare function validateArtifactEnvelope(value: unknown): value is ArtifactEnvelope;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ARTIFACT_ENVELOPE_FORMAT = void 0;
|
|
37
|
+
exports.artifactId = artifactId;
|
|
38
|
+
exports.createArtifactEnvelope = createArtifactEnvelope;
|
|
39
|
+
exports.validateArtifactEnvelope = validateArtifactEnvelope;
|
|
40
|
+
const crypto = __importStar(require("crypto"));
|
|
41
|
+
exports.ARTIFACT_ENVELOPE_FORMAT = 'moonglade-artifact-envelope-v1';
|
|
42
|
+
function artifactId(canonicalBytes) {
|
|
43
|
+
return `sha256:${crypto.createHash('sha256').update(canonicalBytes).digest('hex')}`;
|
|
44
|
+
}
|
|
45
|
+
function createArtifactEnvelope(input) {
|
|
46
|
+
const { bytes, ...rest } = input;
|
|
47
|
+
return { format: exports.ARTIFACT_ENVELOPE_FORMAT, artifact_id: artifactId(bytes), ...rest };
|
|
48
|
+
}
|
|
49
|
+
function validateArtifactEnvelope(value) {
|
|
50
|
+
if (!value || typeof value !== 'object')
|
|
51
|
+
return false;
|
|
52
|
+
const v = value;
|
|
53
|
+
return v.format === exports.ARTIFACT_ENVELOPE_FORMAT && typeof v.artifact_id === 'string' && /^sha256:[0-9a-f]{64}$/.test(v.artifact_id)
|
|
54
|
+
&& typeof v.canonical_bytes === 'string' && typeof v.created_at === 'string'
|
|
55
|
+
&& Array.isArray(v.parents) && Array.isArray(v.attestations)
|
|
56
|
+
&& ['unverified', 'observed', 'signed', 'published', 'revoked'].includes(v.status ?? '')
|
|
57
|
+
&& !!v.evidence && ['unknown', 'valid', 'failed'].includes(v.evidence.integrity ?? '')
|
|
58
|
+
&& ['none', 'provisional', 'valid', 'failed'].includes(v.evidence.signature ?? '')
|
|
59
|
+
&& ['none', 'indeterminate', 'valid', 'failed'].includes(v.evidence.timestamp ?? '')
|
|
60
|
+
&& ['unknown', 'valid', 'failed'].includes(v.evidence.scope ?? '');
|
|
61
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ProvenanceSidecar } from './index';
|
|
2
|
+
export type ArtifactAction = 'observe' | 'seal-provisional';
|
|
3
|
+
export interface ArtifactHookOptions {
|
|
4
|
+
action?: ArtifactAction;
|
|
5
|
+
vaultRoot?: string;
|
|
6
|
+
privateKeyPem?: string;
|
|
7
|
+
signerIdentity?: string;
|
|
8
|
+
generator?: {
|
|
9
|
+
agent: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
session?: string;
|
|
12
|
+
};
|
|
13
|
+
parents?: string[];
|
|
14
|
+
gitCommit?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ArtifactHookResult {
|
|
17
|
+
path: string;
|
|
18
|
+
action: ArtifactAction;
|
|
19
|
+
skipped: boolean;
|
|
20
|
+
reason?: string;
|
|
21
|
+
sidecar?: ProvenanceSidecar;
|
|
22
|
+
}
|
|
23
|
+
export interface ArtifactMoveResult {
|
|
24
|
+
artifactPath: string;
|
|
25
|
+
attestationPath: string;
|
|
26
|
+
attestationMoved: boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare function processArtifact(filePath: string, opts?: ArtifactHookOptions): Promise<ArtifactHookResult>;
|
|
29
|
+
/** Move an artifact and its adjacent attestation as one atomic logical unit. */
|
|
30
|
+
export declare function moveArtifactWithAttestation(sourcePath: string, destinationPath: string): Promise<ArtifactMoveResult>;
|