@absolutejs/vulnerabilities-witness 0.1.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 +80 -0
- package/README.md +69 -0
- package/dist/http.d.ts +5 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +285 -0
- package/dist/manifest.d.ts +1 -0
- package/dist/manifest.js +38 -0
- package/dist/manifest.json +42 -0
- package/dist/postgres.d.ts +4 -0
- package/dist/postgres.js +131 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +376 -0
- package/dist/service.d.ts +23 -0
- package/dist/signing.d.ts +32 -0
- package/dist/store.d.ts +21 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
**Licensor:** Alex Kahn
|
|
4
|
+
|
|
5
|
+
**Licensed Work:** @absolutejs/vulnerabilities-witness (https://github.com/absolutejs/vulnerabilities-witness)
|
|
6
|
+
|
|
7
|
+
**Change Date:** July 19, 2030
|
|
8
|
+
|
|
9
|
+
**Change License:** Apache License, Version 2.0
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Terms
|
|
14
|
+
|
|
15
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
16
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
17
|
+
Licensor may make an Additional Use Grant, permitting limited production use.
|
|
18
|
+
|
|
19
|
+
### Additional Use Grant
|
|
20
|
+
|
|
21
|
+
You may use the Licensed Work in production, provided your use does not include
|
|
22
|
+
any of the following:
|
|
23
|
+
|
|
24
|
+
1. **Offering a Competing Service.** You may not offer the Licensed Work, or
|
|
25
|
+
any derivative or substantial portion of it, to third parties as a hosted or
|
|
26
|
+
managed vulnerability-management, exposure-management, container-security,
|
|
27
|
+
cloud-security-posture-management, transparency-witnessing, or
|
|
28
|
+
attack-surface-management service. This includes any product whose primary
|
|
29
|
+
value to its users is the functionality the Licensed Work provides.
|
|
30
|
+
|
|
31
|
+
2. **Resale or Redistribution as a Standalone Product.** You may not sell,
|
|
32
|
+
license, or distribute the Licensed Work, or any derivative or fork of it,
|
|
33
|
+
as a standalone commercial product.
|
|
34
|
+
|
|
35
|
+
3. **Removal of Attribution.** Any derivative work, fork, or redistribution of
|
|
36
|
+
the Licensed Work must prominently credit AbsoluteJS and include a link to
|
|
37
|
+
the original project repository
|
|
38
|
+
(https://github.com/absolutejs/vulnerabilities-witness).
|
|
39
|
+
|
|
40
|
+
For clarity, the following uses are expressly permitted:
|
|
41
|
+
|
|
42
|
+
- Using the Licensed Work to witness, verify, audit, or report on vulnerability
|
|
43
|
+
evidence for your own applications, websites, internal tools, or SaaS
|
|
44
|
+
products, so long as vulnerability management or transparency witnessing is
|
|
45
|
+
not the primary product you are selling.
|
|
46
|
+
- Using the Licensed Work as a dependency in commercial software you build and
|
|
47
|
+
sell, as long as that software is not a competing hosted security service.
|
|
48
|
+
- Providing consulting, development, or professional services to clients using
|
|
49
|
+
the Licensed Work.
|
|
50
|
+
- Forking and modifying the Licensed Work for your own internal use, provided
|
|
51
|
+
attribution is maintained.
|
|
52
|
+
|
|
53
|
+
### Change Date and Change License
|
|
54
|
+
|
|
55
|
+
On the Change Date specified above, the Licensed Work will be made available
|
|
56
|
+
under the Change License (Apache License, Version 2.0).
|
|
57
|
+
|
|
58
|
+
### Trademark
|
|
59
|
+
|
|
60
|
+
This license does not grant rights to use the AbsoluteJS name or trademarks in
|
|
61
|
+
a manner that suggests endorsement of an unaffiliated fork.
|
|
62
|
+
|
|
63
|
+
### Notices
|
|
64
|
+
|
|
65
|
+
You must not remove or obscure any licensing, copyright, or other notices
|
|
66
|
+
included in the Licensed Work.
|
|
67
|
+
|
|
68
|
+
### No Warranty
|
|
69
|
+
|
|
70
|
+
THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
71
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
72
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
73
|
+
LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM, OUT
|
|
74
|
+
OF, OR IN CONNECTION WITH THE LICENSED WORK OR ITS USE.
|
|
75
|
+
|
|
76
|
+
## Contact
|
|
77
|
+
|
|
78
|
+
- **Alex Kahn**
|
|
79
|
+
- alexkahndev@gmail.com
|
|
80
|
+
- alexkahndev.github.io
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# @absolutejs/vulnerabilities-witness
|
|
2
|
+
|
|
3
|
+
Independent transparency witnessing for AbsoluteJS vulnerability evidence.
|
|
4
|
+
The package verifies the complete signed evidence-key transparency log before
|
|
5
|
+
issuing an Ed25519 checkpoint receipt. Durable stores reject a lower log size
|
|
6
|
+
as rollback and reject two different heads observed for the same authenticated
|
|
7
|
+
subject and log size as equivocation.
|
|
8
|
+
|
|
9
|
+
The HTTP service has three routes:
|
|
10
|
+
|
|
11
|
+
- `POST /v1/checkpoints` authenticates a bearer token, verifies the submitted
|
|
12
|
+
transparency log, and returns `{ checkpoint, registry }`.
|
|
13
|
+
- `GET /v1/keys` publishes the witness key registry and cross-signed rotation
|
|
14
|
+
chain.
|
|
15
|
+
- `GET /health` returns service liveness.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import {
|
|
19
|
+
EVIDENCE_WITNESS_REQUEST_CONTRACT,
|
|
20
|
+
createEvidenceWitnessHttpHandler,
|
|
21
|
+
createEvidenceWitnessService,
|
|
22
|
+
createEvidenceWitnessSigningState,
|
|
23
|
+
} from "@absolutejs/vulnerabilities-witness";
|
|
24
|
+
import {
|
|
25
|
+
createPostgresEvidenceWitnessStore,
|
|
26
|
+
ensurePostgresEvidenceWitnessSchema,
|
|
27
|
+
} from "@absolutejs/vulnerabilities-witness/postgres";
|
|
28
|
+
|
|
29
|
+
const signingState = createEvidenceWitnessSigningState();
|
|
30
|
+
await ensurePostgresEvidenceWitnessSchema(sql);
|
|
31
|
+
|
|
32
|
+
const service = createEvidenceWitnessService({
|
|
33
|
+
loadSigningState,
|
|
34
|
+
origin: "https://witness.example",
|
|
35
|
+
signingState,
|
|
36
|
+
store: createPostgresEvidenceWitnessStore(sql),
|
|
37
|
+
storeSigningState,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const fetch = createEvidenceWitnessHttpHandler({
|
|
41
|
+
authenticate: async (token) => subjectsByToken.get(token) ?? null,
|
|
42
|
+
service,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
Bun.serve({ fetch, port: 3000 });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The signing identity contains a private key and belongs in a durable secret
|
|
49
|
+
broker, not PostgreSQL or source control. `service.rotate()` stores the new
|
|
50
|
+
secret state before activating it and publishes the cross-signed transition in
|
|
51
|
+
the registry. In a multi-replica service, the supplied secret-store integration
|
|
52
|
+
must serialize rotations.
|
|
53
|
+
|
|
54
|
+
## Standalone service
|
|
55
|
+
|
|
56
|
+
The included `absolute-vulnerability-witness` executable uses PostgreSQL for
|
|
57
|
+
observations and accepts these deployment secrets:
|
|
58
|
+
|
|
59
|
+
- `DATABASE_URL`
|
|
60
|
+
- `EVIDENCE_WITNESS_ORIGIN`
|
|
61
|
+
- `EVIDENCE_WITNESS_SIGNING_STATE_JSON`
|
|
62
|
+
- `EVIDENCE_WITNESS_TOKENS_JSON`, an object mapping stable subjects to bearer
|
|
63
|
+
tokens
|
|
64
|
+
- `PORT`, defaulting to `3000`
|
|
65
|
+
|
|
66
|
+
Run independent instances under different administrative and infrastructure
|
|
67
|
+
boundaries. Clients should pin each genesis witness key through an independent
|
|
68
|
+
channel and require a quorum with
|
|
69
|
+
`verifyEvidenceWitnessQuorum` from `@absolutejs/vulnerabilities`.
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type createEvidenceWitnessService } from "./service";
|
|
2
|
+
export declare const createEvidenceWitnessHttpHandler: (options: {
|
|
3
|
+
authenticate: (token: string) => Promise<string | null>;
|
|
4
|
+
service: ReturnType<typeof createEvidenceWitnessService>;
|
|
5
|
+
}) => (request: Request) => Promise<Response>;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/service.ts
|
|
3
|
+
import {
|
|
4
|
+
verifyEvidenceKeyTransparencyLog
|
|
5
|
+
} from "@absolutejs/vulnerabilities/evidence-transparency";
|
|
6
|
+
import { createEvidenceWitnessCheckpointForHead } from "@absolutejs/vulnerabilities/evidence-witness";
|
|
7
|
+
|
|
8
|
+
// src/signing.ts
|
|
9
|
+
import {
|
|
10
|
+
createEvidenceSigningIdentity,
|
|
11
|
+
createEvidenceSigningKeyTransition,
|
|
12
|
+
evidenceVerificationKeyFrom,
|
|
13
|
+
parseEvidenceSigningIdentity,
|
|
14
|
+
parseEvidenceVerificationKey,
|
|
15
|
+
verifyEvidenceSigningKeyTransition
|
|
16
|
+
} from "@absolutejs/vulnerabilities/evidence-bundle";
|
|
17
|
+
var SIGNING_STATE_VERSION = 1;
|
|
18
|
+
var keysMatch = (left, right) => left.keyId === right.keyId && left.fingerprint === right.fingerprint && left.publicKey === right.publicKey;
|
|
19
|
+
var registryKeys = (state) => {
|
|
20
|
+
const keys = new Map;
|
|
21
|
+
const add = (key) => keys.set(key.keyId, key);
|
|
22
|
+
add(evidenceVerificationKeyFrom(state.identity));
|
|
23
|
+
for (const transition of state.transitions) {
|
|
24
|
+
add(transition.previousKey);
|
|
25
|
+
add(transition.nextKey);
|
|
26
|
+
}
|
|
27
|
+
return [...keys.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
28
|
+
};
|
|
29
|
+
var createEvidenceWitnessSigningState = (createdAt = new Date().toISOString()) => ({
|
|
30
|
+
identity: createEvidenceSigningIdentity({ createdAt }),
|
|
31
|
+
transitions: [],
|
|
32
|
+
version: SIGNING_STATE_VERSION
|
|
33
|
+
});
|
|
34
|
+
var parseEvidenceWitnessSigningState = (value) => {
|
|
35
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
36
|
+
throw new Error("Evidence witness signing state is invalid");
|
|
37
|
+
const input = value;
|
|
38
|
+
if (input.version !== SIGNING_STATE_VERSION || !input.identity || !Array.isArray(input.transitions))
|
|
39
|
+
throw new Error("Evidence witness signing state is invalid");
|
|
40
|
+
const identity = parseEvidenceSigningIdentity(input.identity);
|
|
41
|
+
const transitions = input.transitions.map((transition) => {
|
|
42
|
+
if (!verifyEvidenceSigningKeyTransition(transition))
|
|
43
|
+
throw new Error("Evidence witness key transition is invalid");
|
|
44
|
+
return structuredClone(transition);
|
|
45
|
+
});
|
|
46
|
+
const registry = evidenceWitnessKeyRegistryFrom({
|
|
47
|
+
identity,
|
|
48
|
+
transitions,
|
|
49
|
+
version: SIGNING_STATE_VERSION
|
|
50
|
+
});
|
|
51
|
+
if (verifyEvidenceWitnessKeyRegistry({
|
|
52
|
+
registry,
|
|
53
|
+
trustedKeys: [registry.keys[0]]
|
|
54
|
+
}).trust !== "trusted")
|
|
55
|
+
throw new Error("Evidence witness key chain is disconnected");
|
|
56
|
+
return { identity, transitions, version: SIGNING_STATE_VERSION };
|
|
57
|
+
};
|
|
58
|
+
var encodeEvidenceWitnessSigningState = (state) => JSON.stringify(parseEvidenceWitnessSigningState(state));
|
|
59
|
+
var rotateEvidenceWitnessSigningState = (state, rotatedAt = new Date().toISOString()) => {
|
|
60
|
+
const current = parseEvidenceWitnessSigningState(state);
|
|
61
|
+
const identity = createEvidenceSigningIdentity({ createdAt: rotatedAt });
|
|
62
|
+
const transition = createEvidenceSigningKeyTransition({
|
|
63
|
+
nextKey: evidenceVerificationKeyFrom(identity),
|
|
64
|
+
previousIdentity: current.identity,
|
|
65
|
+
rotatedAt
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
identity,
|
|
69
|
+
transitions: [...current.transitions, transition],
|
|
70
|
+
version: SIGNING_STATE_VERSION
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
var evidenceWitnessKeyRegistryFrom = (state) => ({
|
|
74
|
+
activeKeyId: state.identity.keyId,
|
|
75
|
+
keys: registryKeys(state),
|
|
76
|
+
transitions: structuredClone(state.transitions),
|
|
77
|
+
version: SIGNING_STATE_VERSION
|
|
78
|
+
});
|
|
79
|
+
var verifyEvidenceWitnessKeyRegistry = (input) => {
|
|
80
|
+
try {
|
|
81
|
+
if (input.registry.version !== SIGNING_STATE_VERSION)
|
|
82
|
+
throw new Error("Evidence witness registry version is unsupported");
|
|
83
|
+
const keys = input.registry.keys.map(parseEvidenceVerificationKey);
|
|
84
|
+
const transitions = input.registry.transitions.map((transition) => {
|
|
85
|
+
if (!verifyEvidenceSigningKeyTransition(transition))
|
|
86
|
+
throw new Error("Evidence witness key transition is invalid");
|
|
87
|
+
return transition;
|
|
88
|
+
});
|
|
89
|
+
let cursor = keys.find(({ keyId }) => keyId === input.registry.activeKeyId);
|
|
90
|
+
if (!cursor)
|
|
91
|
+
throw new Error("Evidence witness active key is missing");
|
|
92
|
+
const visited = new Set;
|
|
93
|
+
let traversed = 0;
|
|
94
|
+
while (!input.trustedKeys.some((trusted) => keysMatch(trusted, cursor))) {
|
|
95
|
+
if (visited.has(cursor.keyId))
|
|
96
|
+
throw new Error("Evidence witness key chain contains a cycle");
|
|
97
|
+
visited.add(cursor.keyId);
|
|
98
|
+
const transition = transitions.find((entry) => keysMatch(entry.nextKey, cursor));
|
|
99
|
+
if (!transition)
|
|
100
|
+
return { activeKey: cursor, trust: "untrusted" };
|
|
101
|
+
cursor = transition.previousKey;
|
|
102
|
+
traversed += 1;
|
|
103
|
+
}
|
|
104
|
+
if (traversed !== transitions.length)
|
|
105
|
+
throw new Error("Evidence witness key registry contains disconnected history");
|
|
106
|
+
return {
|
|
107
|
+
activeKey: keys.find(({ keyId }) => keyId === input.registry.activeKeyId),
|
|
108
|
+
trust: "trusted"
|
|
109
|
+
};
|
|
110
|
+
} catch {
|
|
111
|
+
return { activeKey: null, trust: "untrusted" };
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// src/service.ts
|
|
116
|
+
var EVIDENCE_WITNESS_REQUEST_CONTRACT = "absolutejs.vulnerability-evidence-witness-request/v1";
|
|
117
|
+
var createEvidenceWitnessService = (options) => {
|
|
118
|
+
const origin = new URL(options.origin);
|
|
119
|
+
if (origin.protocol !== "https:")
|
|
120
|
+
throw new Error("Evidence witness origin must use HTTPS");
|
|
121
|
+
let state = parseEvidenceWitnessSigningState(options.signingState);
|
|
122
|
+
let mutationQueue = Promise.resolve();
|
|
123
|
+
const refresh = async () => {
|
|
124
|
+
state = parseEvidenceWitnessSigningState(JSON.parse(await options.loadSigningState()));
|
|
125
|
+
return state;
|
|
126
|
+
};
|
|
127
|
+
const registry = async () => {
|
|
128
|
+
await refresh();
|
|
129
|
+
return evidenceWitnessKeyRegistryFrom(state);
|
|
130
|
+
};
|
|
131
|
+
const checkpoint = async (subject, request) => {
|
|
132
|
+
if (request.contract !== EVIDENCE_WITNESS_REQUEST_CONTRACT)
|
|
133
|
+
throw new Error("Evidence witness request contract is unsupported");
|
|
134
|
+
const [genesis] = request.log.entries;
|
|
135
|
+
if (!genesis || genesis.event.kind !== "key_created")
|
|
136
|
+
throw new Error("Evidence transparency genesis is missing");
|
|
137
|
+
const verification = verifyEvidenceKeyTransparencyLog({
|
|
138
|
+
log: request.log,
|
|
139
|
+
trustedKeys: [genesis.event.key]
|
|
140
|
+
});
|
|
141
|
+
if (verification.trust !== "trusted")
|
|
142
|
+
throw new Error("Evidence transparency log is invalid");
|
|
143
|
+
const normalizedSubject = subject.trim();
|
|
144
|
+
if (!normalizedSubject)
|
|
145
|
+
throw new Error("Evidence witness subject is required");
|
|
146
|
+
await refresh();
|
|
147
|
+
const signed = createEvidenceWitnessCheckpointForHead({
|
|
148
|
+
identity: state.identity,
|
|
149
|
+
logHead: request.log.head,
|
|
150
|
+
logSize: request.log.entries.length,
|
|
151
|
+
origin: origin.href
|
|
152
|
+
});
|
|
153
|
+
const observed = await options.store.observe({
|
|
154
|
+
checkpoint: signed,
|
|
155
|
+
history: request.log.entries.map(({ digest }) => digest),
|
|
156
|
+
subject: normalizedSubject
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
checkpoint: observed.checkpoint,
|
|
160
|
+
registry: evidenceWitnessKeyRegistryFrom(state)
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
const rotate = (rotatedAt = new Date().toISOString()) => {
|
|
164
|
+
const queued = mutationQueue.then(async () => {
|
|
165
|
+
await refresh();
|
|
166
|
+
const next = rotateEvidenceWitnessSigningState(state, rotatedAt);
|
|
167
|
+
await options.storeSigningState(encodeEvidenceWitnessSigningState(next));
|
|
168
|
+
state = next;
|
|
169
|
+
return evidenceWitnessKeyRegistryFrom(state);
|
|
170
|
+
});
|
|
171
|
+
mutationQueue = queued.then(() => {
|
|
172
|
+
return;
|
|
173
|
+
}, () => {
|
|
174
|
+
return;
|
|
175
|
+
});
|
|
176
|
+
return queued;
|
|
177
|
+
};
|
|
178
|
+
return { checkpoint, registry, rotate };
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// src/store.ts
|
|
182
|
+
class EvidenceWitnessEquivocationError extends Error {
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
class EvidenceWitnessRollbackError extends Error {
|
|
186
|
+
}
|
|
187
|
+
var normalizedSubject = (subject) => {
|
|
188
|
+
const value = subject.trim();
|
|
189
|
+
if (!value)
|
|
190
|
+
throw new Error("Evidence witness subject is required");
|
|
191
|
+
return value;
|
|
192
|
+
};
|
|
193
|
+
var createMemoryEvidenceWitnessStore = () => {
|
|
194
|
+
const subjects = new Map;
|
|
195
|
+
const latest = async (subject) => {
|
|
196
|
+
const values = subjects.get(normalizedSubject(subject));
|
|
197
|
+
if (!values || values.size === 0)
|
|
198
|
+
return null;
|
|
199
|
+
const size = Math.max(...values.keys());
|
|
200
|
+
return structuredClone(values.get(size));
|
|
201
|
+
};
|
|
202
|
+
const observe = async (input) => {
|
|
203
|
+
const subject = normalizedSubject(input.subject);
|
|
204
|
+
let values = subjects.get(subject);
|
|
205
|
+
if (!values) {
|
|
206
|
+
values = new Map;
|
|
207
|
+
subjects.set(subject, values);
|
|
208
|
+
}
|
|
209
|
+
const existing = values.get(input.checkpoint.logSize);
|
|
210
|
+
if (existing) {
|
|
211
|
+
if (existing.checkpoint.logHead !== input.checkpoint.logHead)
|
|
212
|
+
throw new EvidenceWitnessEquivocationError("Evidence transparency log equivocated at an observed size");
|
|
213
|
+
return {
|
|
214
|
+
checkpoint: structuredClone(existing.checkpoint),
|
|
215
|
+
status: "replay"
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const current = await latest(subject);
|
|
219
|
+
if (current && current.checkpoint.logSize > input.checkpoint.logSize)
|
|
220
|
+
throw new EvidenceWitnessRollbackError("Evidence transparency log attempted to roll back");
|
|
221
|
+
if (current && input.history[current.checkpoint.logSize - 1] !== current.checkpoint.logHead)
|
|
222
|
+
throw new EvidenceWitnessEquivocationError("Evidence transparency log forked from observed history");
|
|
223
|
+
const observation = structuredClone({
|
|
224
|
+
checkpoint: input.checkpoint,
|
|
225
|
+
subject
|
|
226
|
+
});
|
|
227
|
+
values.set(input.checkpoint.logSize, observation);
|
|
228
|
+
return {
|
|
229
|
+
checkpoint: structuredClone(input.checkpoint),
|
|
230
|
+
status: "accepted"
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
return { latest, observe };
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// src/http.ts
|
|
237
|
+
var json = (value, status = 200) => Response.json(value, {
|
|
238
|
+
headers: { "cache-control": "no-store" },
|
|
239
|
+
status
|
|
240
|
+
});
|
|
241
|
+
var bearer = (request) => {
|
|
242
|
+
const authorization = request.headers.get("authorization") ?? "";
|
|
243
|
+
return authorization.startsWith("Bearer ") ? authorization.slice("Bearer ".length).trim() : "";
|
|
244
|
+
};
|
|
245
|
+
var createEvidenceWitnessHttpHandler = (options) => async (request) => {
|
|
246
|
+
const url = new URL(request.url);
|
|
247
|
+
if (request.method === "GET" && url.pathname === "/health")
|
|
248
|
+
return json({ status: "ok" });
|
|
249
|
+
if (request.method === "GET" && url.pathname === "/v1/keys")
|
|
250
|
+
return json(await options.service.registry());
|
|
251
|
+
if (request.method !== "POST" || url.pathname !== "/v1/checkpoints")
|
|
252
|
+
return json({ error: "not_found" }, 404);
|
|
253
|
+
const subject = await options.authenticate(bearer(request));
|
|
254
|
+
if (!subject)
|
|
255
|
+
return json({ error: "unauthorized" }, 401);
|
|
256
|
+
try {
|
|
257
|
+
const input = await request.json();
|
|
258
|
+
if (input.contract !== EVIDENCE_WITNESS_REQUEST_CONTRACT)
|
|
259
|
+
return json({ error: "unsupported_contract" }, 400);
|
|
260
|
+
return json(await options.service.checkpoint(subject, input));
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (error instanceof EvidenceWitnessEquivocationError)
|
|
263
|
+
return json({ error: "equivocation", message: error.message }, 409);
|
|
264
|
+
if (error instanceof EvidenceWitnessRollbackError)
|
|
265
|
+
return json({ error: "rollback", message: error.message }, 409);
|
|
266
|
+
return json({
|
|
267
|
+
error: "invalid_request",
|
|
268
|
+
message: error instanceof Error ? error.message : "Invalid request"
|
|
269
|
+
}, 400);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
export {
|
|
273
|
+
verifyEvidenceWitnessKeyRegistry,
|
|
274
|
+
rotateEvidenceWitnessSigningState,
|
|
275
|
+
parseEvidenceWitnessSigningState,
|
|
276
|
+
evidenceWitnessKeyRegistryFrom,
|
|
277
|
+
encodeEvidenceWitnessSigningState,
|
|
278
|
+
createMemoryEvidenceWitnessStore,
|
|
279
|
+
createEvidenceWitnessSigningState,
|
|
280
|
+
createEvidenceWitnessService,
|
|
281
|
+
createEvidenceWitnessHttpHandler,
|
|
282
|
+
EvidenceWitnessRollbackError,
|
|
283
|
+
EvidenceWitnessEquivocationError,
|
|
284
|
+
EVIDENCE_WITNESS_REQUEST_CONTRACT
|
|
285
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const manifest: import("@absolutejs/manifest").PackageManifest<Record<string, never>, never>;
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/manifest.ts
|
|
3
|
+
import { defineManifest } from "@absolutejs/manifest";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
var manifest = defineManifest()({
|
|
6
|
+
contract: 2,
|
|
7
|
+
discovery: {
|
|
8
|
+
audiences: ["platform-operators", "security-teams", "auditors"],
|
|
9
|
+
intents: [
|
|
10
|
+
"independently witness vulnerability evidence transparency",
|
|
11
|
+
"detect transparency rollback and equivocation",
|
|
12
|
+
"rotate witness signing keys",
|
|
13
|
+
"serve durable signed witness receipts"
|
|
14
|
+
],
|
|
15
|
+
keywords: [
|
|
16
|
+
"vulnerabilities",
|
|
17
|
+
"witness",
|
|
18
|
+
"key-transparency",
|
|
19
|
+
"equivocation",
|
|
20
|
+
"quorum",
|
|
21
|
+
"PostgreSQL"
|
|
22
|
+
],
|
|
23
|
+
protocols: ["HTTPS", "Ed25519", "PostgreSQL"]
|
|
24
|
+
},
|
|
25
|
+
identity: {
|
|
26
|
+
accent: "#dc2626",
|
|
27
|
+
category: "operations",
|
|
28
|
+
description: "Independent durable witness service for signed vulnerability-evidence transparency logs.",
|
|
29
|
+
docsUrl: "https://github.com/absolutejs/vulnerabilities-witness",
|
|
30
|
+
name: "@absolutejs/vulnerabilities-witness",
|
|
31
|
+
tagline: "Make evidence-log rollback and equivocation externally visible."
|
|
32
|
+
},
|
|
33
|
+
settings: Type.Object({}),
|
|
34
|
+
wiring: []
|
|
35
|
+
});
|
|
36
|
+
export {
|
|
37
|
+
manifest
|
|
38
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contract": 2,
|
|
3
|
+
"discovery": {
|
|
4
|
+
"audiences": [
|
|
5
|
+
"platform-operators",
|
|
6
|
+
"security-teams",
|
|
7
|
+
"auditors"
|
|
8
|
+
],
|
|
9
|
+
"intents": [
|
|
10
|
+
"independently witness vulnerability evidence transparency",
|
|
11
|
+
"detect transparency rollback and equivocation",
|
|
12
|
+
"rotate witness signing keys",
|
|
13
|
+
"serve durable signed witness receipts"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"vulnerabilities",
|
|
17
|
+
"witness",
|
|
18
|
+
"key-transparency",
|
|
19
|
+
"equivocation",
|
|
20
|
+
"quorum",
|
|
21
|
+
"PostgreSQL"
|
|
22
|
+
],
|
|
23
|
+
"protocols": [
|
|
24
|
+
"HTTPS",
|
|
25
|
+
"Ed25519",
|
|
26
|
+
"PostgreSQL"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"identity": {
|
|
30
|
+
"accent": "#dc2626",
|
|
31
|
+
"category": "operations",
|
|
32
|
+
"description": "Independent durable witness service for signed vulnerability-evidence transparency logs.",
|
|
33
|
+
"docsUrl": "https://github.com/absolutejs/vulnerabilities-witness",
|
|
34
|
+
"name": "@absolutejs/vulnerabilities-witness",
|
|
35
|
+
"tagline": "Make evidence-log rollback and equivocation externally visible."
|
|
36
|
+
},
|
|
37
|
+
"settings": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {}
|
|
40
|
+
},
|
|
41
|
+
"wiring": []
|
|
42
|
+
}
|
package/dist/postgres.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/store.ts
|
|
3
|
+
class EvidenceWitnessEquivocationError extends Error {
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
class EvidenceWitnessRollbackError extends Error {
|
|
7
|
+
}
|
|
8
|
+
var normalizedSubject = (subject) => {
|
|
9
|
+
const value = subject.trim();
|
|
10
|
+
if (!value)
|
|
11
|
+
throw new Error("Evidence witness subject is required");
|
|
12
|
+
return value;
|
|
13
|
+
};
|
|
14
|
+
var createMemoryEvidenceWitnessStore = () => {
|
|
15
|
+
const subjects = new Map;
|
|
16
|
+
const latest = async (subject) => {
|
|
17
|
+
const values = subjects.get(normalizedSubject(subject));
|
|
18
|
+
if (!values || values.size === 0)
|
|
19
|
+
return null;
|
|
20
|
+
const size = Math.max(...values.keys());
|
|
21
|
+
return structuredClone(values.get(size));
|
|
22
|
+
};
|
|
23
|
+
const observe = async (input) => {
|
|
24
|
+
const subject = normalizedSubject(input.subject);
|
|
25
|
+
let values = subjects.get(subject);
|
|
26
|
+
if (!values) {
|
|
27
|
+
values = new Map;
|
|
28
|
+
subjects.set(subject, values);
|
|
29
|
+
}
|
|
30
|
+
const existing = values.get(input.checkpoint.logSize);
|
|
31
|
+
if (existing) {
|
|
32
|
+
if (existing.checkpoint.logHead !== input.checkpoint.logHead)
|
|
33
|
+
throw new EvidenceWitnessEquivocationError("Evidence transparency log equivocated at an observed size");
|
|
34
|
+
return {
|
|
35
|
+
checkpoint: structuredClone(existing.checkpoint),
|
|
36
|
+
status: "replay"
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const current = await latest(subject);
|
|
40
|
+
if (current && current.checkpoint.logSize > input.checkpoint.logSize)
|
|
41
|
+
throw new EvidenceWitnessRollbackError("Evidence transparency log attempted to roll back");
|
|
42
|
+
if (current && input.history[current.checkpoint.logSize - 1] !== current.checkpoint.logHead)
|
|
43
|
+
throw new EvidenceWitnessEquivocationError("Evidence transparency log forked from observed history");
|
|
44
|
+
const observation = structuredClone({
|
|
45
|
+
checkpoint: input.checkpoint,
|
|
46
|
+
subject
|
|
47
|
+
});
|
|
48
|
+
values.set(input.checkpoint.logSize, observation);
|
|
49
|
+
return {
|
|
50
|
+
checkpoint: structuredClone(input.checkpoint),
|
|
51
|
+
status: "accepted"
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
return { latest, observe };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/postgres.ts
|
|
58
|
+
var LOCK_NAME = "absolutejs:evidence-witness-observation";
|
|
59
|
+
var json = (value) => typeof value === "string" ? JSON.parse(value) : value;
|
|
60
|
+
var ensurePostgresEvidenceWitnessSchema = async (sql) => {
|
|
61
|
+
await sql.unsafe(`
|
|
62
|
+
CREATE TABLE IF NOT EXISTS absolute_vulnerability_evidence_witness_observations (
|
|
63
|
+
subject TEXT NOT NULL,
|
|
64
|
+
log_size INTEGER NOT NULL CHECK (log_size > 0),
|
|
65
|
+
log_head VARCHAR(71) NOT NULL,
|
|
66
|
+
observed_at TIMESTAMPTZ NOT NULL,
|
|
67
|
+
receipt JSONB NOT NULL,
|
|
68
|
+
witness_key_id TEXT NOT NULL,
|
|
69
|
+
PRIMARY KEY (subject, log_size)
|
|
70
|
+
)
|
|
71
|
+
`);
|
|
72
|
+
};
|
|
73
|
+
var createPostgresEvidenceWitnessStore = (sql) => {
|
|
74
|
+
const latest = async (subject) => {
|
|
75
|
+
const rows = await sql`
|
|
76
|
+
SELECT receipt
|
|
77
|
+
FROM absolute_vulnerability_evidence_witness_observations
|
|
78
|
+
WHERE subject = ${subject}
|
|
79
|
+
ORDER BY log_size DESC
|
|
80
|
+
LIMIT 1
|
|
81
|
+
`;
|
|
82
|
+
const row = Array.from(rows)[0];
|
|
83
|
+
return row ? { checkpoint: json(row.receipt), subject } : null;
|
|
84
|
+
};
|
|
85
|
+
const observe = (input) => sql.begin(async (transaction) => {
|
|
86
|
+
await transaction`
|
|
87
|
+
SELECT pg_advisory_xact_lock(
|
|
88
|
+
hashtext(${LOCK_NAME}), hashtext(${input.subject})
|
|
89
|
+
)
|
|
90
|
+
`;
|
|
91
|
+
const rows = await transaction`
|
|
92
|
+
SELECT log_head, log_size, receipt
|
|
93
|
+
FROM absolute_vulnerability_evidence_witness_observations
|
|
94
|
+
WHERE subject = ${input.subject}
|
|
95
|
+
ORDER BY log_size DESC
|
|
96
|
+
LIMIT 1
|
|
97
|
+
`;
|
|
98
|
+
const current = Array.from(rows)[0];
|
|
99
|
+
if (current?.log_size === input.checkpoint.logSize) {
|
|
100
|
+
if (current.log_head !== input.checkpoint.logHead)
|
|
101
|
+
throw new EvidenceWitnessEquivocationError("Evidence transparency log equivocated at an observed size");
|
|
102
|
+
return {
|
|
103
|
+
checkpoint: json(current.receipt),
|
|
104
|
+
status: "replay"
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (current && current.log_size > input.checkpoint.logSize)
|
|
108
|
+
throw new EvidenceWitnessRollbackError("Evidence transparency log attempted to roll back");
|
|
109
|
+
if (current && input.history[current.log_size - 1] !== current.log_head)
|
|
110
|
+
throw new EvidenceWitnessEquivocationError("Evidence transparency log forked from observed history");
|
|
111
|
+
await transaction`
|
|
112
|
+
INSERT INTO absolute_vulnerability_evidence_witness_observations (
|
|
113
|
+
subject, log_size, log_head, observed_at, receipt, witness_key_id
|
|
114
|
+
) VALUES (
|
|
115
|
+
${input.subject}, ${input.checkpoint.logSize},
|
|
116
|
+
${input.checkpoint.logHead}, ${input.checkpoint.observedAt},
|
|
117
|
+
${JSON.stringify(input.checkpoint)}::jsonb,
|
|
118
|
+
${input.checkpoint.witness.keyId}
|
|
119
|
+
)
|
|
120
|
+
`;
|
|
121
|
+
return {
|
|
122
|
+
checkpoint: structuredClone(input.checkpoint),
|
|
123
|
+
status: "accepted"
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
return { latest, observe };
|
|
127
|
+
};
|
|
128
|
+
export {
|
|
129
|
+
ensurePostgresEvidenceWitnessSchema,
|
|
130
|
+
createPostgresEvidenceWitnessStore
|
|
131
|
+
};
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/service.ts
|
|
5
|
+
import {
|
|
6
|
+
verifyEvidenceKeyTransparencyLog
|
|
7
|
+
} from "@absolutejs/vulnerabilities/evidence-transparency";
|
|
8
|
+
import { createEvidenceWitnessCheckpointForHead } from "@absolutejs/vulnerabilities/evidence-witness";
|
|
9
|
+
|
|
10
|
+
// src/signing.ts
|
|
11
|
+
import {
|
|
12
|
+
createEvidenceSigningIdentity,
|
|
13
|
+
createEvidenceSigningKeyTransition,
|
|
14
|
+
evidenceVerificationKeyFrom,
|
|
15
|
+
parseEvidenceSigningIdentity,
|
|
16
|
+
parseEvidenceVerificationKey,
|
|
17
|
+
verifyEvidenceSigningKeyTransition
|
|
18
|
+
} from "@absolutejs/vulnerabilities/evidence-bundle";
|
|
19
|
+
var SIGNING_STATE_VERSION = 1;
|
|
20
|
+
var keysMatch = (left, right) => left.keyId === right.keyId && left.fingerprint === right.fingerprint && left.publicKey === right.publicKey;
|
|
21
|
+
var registryKeys = (state) => {
|
|
22
|
+
const keys = new Map;
|
|
23
|
+
const add = (key) => keys.set(key.keyId, key);
|
|
24
|
+
add(evidenceVerificationKeyFrom(state.identity));
|
|
25
|
+
for (const transition of state.transitions) {
|
|
26
|
+
add(transition.previousKey);
|
|
27
|
+
add(transition.nextKey);
|
|
28
|
+
}
|
|
29
|
+
return [...keys.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
30
|
+
};
|
|
31
|
+
var createEvidenceWitnessSigningState = (createdAt = new Date().toISOString()) => ({
|
|
32
|
+
identity: createEvidenceSigningIdentity({ createdAt }),
|
|
33
|
+
transitions: [],
|
|
34
|
+
version: SIGNING_STATE_VERSION
|
|
35
|
+
});
|
|
36
|
+
var parseEvidenceWitnessSigningState = (value) => {
|
|
37
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
38
|
+
throw new Error("Evidence witness signing state is invalid");
|
|
39
|
+
const input = value;
|
|
40
|
+
if (input.version !== SIGNING_STATE_VERSION || !input.identity || !Array.isArray(input.transitions))
|
|
41
|
+
throw new Error("Evidence witness signing state is invalid");
|
|
42
|
+
const identity = parseEvidenceSigningIdentity(input.identity);
|
|
43
|
+
const transitions = input.transitions.map((transition) => {
|
|
44
|
+
if (!verifyEvidenceSigningKeyTransition(transition))
|
|
45
|
+
throw new Error("Evidence witness key transition is invalid");
|
|
46
|
+
return structuredClone(transition);
|
|
47
|
+
});
|
|
48
|
+
const registry = evidenceWitnessKeyRegistryFrom({
|
|
49
|
+
identity,
|
|
50
|
+
transitions,
|
|
51
|
+
version: SIGNING_STATE_VERSION
|
|
52
|
+
});
|
|
53
|
+
if (verifyEvidenceWitnessKeyRegistry({
|
|
54
|
+
registry,
|
|
55
|
+
trustedKeys: [registry.keys[0]]
|
|
56
|
+
}).trust !== "trusted")
|
|
57
|
+
throw new Error("Evidence witness key chain is disconnected");
|
|
58
|
+
return { identity, transitions, version: SIGNING_STATE_VERSION };
|
|
59
|
+
};
|
|
60
|
+
var encodeEvidenceWitnessSigningState = (state) => JSON.stringify(parseEvidenceWitnessSigningState(state));
|
|
61
|
+
var rotateEvidenceWitnessSigningState = (state, rotatedAt = new Date().toISOString()) => {
|
|
62
|
+
const current = parseEvidenceWitnessSigningState(state);
|
|
63
|
+
const identity = createEvidenceSigningIdentity({ createdAt: rotatedAt });
|
|
64
|
+
const transition = createEvidenceSigningKeyTransition({
|
|
65
|
+
nextKey: evidenceVerificationKeyFrom(identity),
|
|
66
|
+
previousIdentity: current.identity,
|
|
67
|
+
rotatedAt
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
identity,
|
|
71
|
+
transitions: [...current.transitions, transition],
|
|
72
|
+
version: SIGNING_STATE_VERSION
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
var evidenceWitnessKeyRegistryFrom = (state) => ({
|
|
76
|
+
activeKeyId: state.identity.keyId,
|
|
77
|
+
keys: registryKeys(state),
|
|
78
|
+
transitions: structuredClone(state.transitions),
|
|
79
|
+
version: SIGNING_STATE_VERSION
|
|
80
|
+
});
|
|
81
|
+
var verifyEvidenceWitnessKeyRegistry = (input) => {
|
|
82
|
+
try {
|
|
83
|
+
if (input.registry.version !== SIGNING_STATE_VERSION)
|
|
84
|
+
throw new Error("Evidence witness registry version is unsupported");
|
|
85
|
+
const keys = input.registry.keys.map(parseEvidenceVerificationKey);
|
|
86
|
+
const transitions = input.registry.transitions.map((transition) => {
|
|
87
|
+
if (!verifyEvidenceSigningKeyTransition(transition))
|
|
88
|
+
throw new Error("Evidence witness key transition is invalid");
|
|
89
|
+
return transition;
|
|
90
|
+
});
|
|
91
|
+
let cursor = keys.find(({ keyId }) => keyId === input.registry.activeKeyId);
|
|
92
|
+
if (!cursor)
|
|
93
|
+
throw new Error("Evidence witness active key is missing");
|
|
94
|
+
const visited = new Set;
|
|
95
|
+
let traversed = 0;
|
|
96
|
+
while (!input.trustedKeys.some((trusted) => keysMatch(trusted, cursor))) {
|
|
97
|
+
if (visited.has(cursor.keyId))
|
|
98
|
+
throw new Error("Evidence witness key chain contains a cycle");
|
|
99
|
+
visited.add(cursor.keyId);
|
|
100
|
+
const transition = transitions.find((entry) => keysMatch(entry.nextKey, cursor));
|
|
101
|
+
if (!transition)
|
|
102
|
+
return { activeKey: cursor, trust: "untrusted" };
|
|
103
|
+
cursor = transition.previousKey;
|
|
104
|
+
traversed += 1;
|
|
105
|
+
}
|
|
106
|
+
if (traversed !== transitions.length)
|
|
107
|
+
throw new Error("Evidence witness key registry contains disconnected history");
|
|
108
|
+
return {
|
|
109
|
+
activeKey: keys.find(({ keyId }) => keyId === input.registry.activeKeyId),
|
|
110
|
+
trust: "trusted"
|
|
111
|
+
};
|
|
112
|
+
} catch {
|
|
113
|
+
return { activeKey: null, trust: "untrusted" };
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// src/service.ts
|
|
118
|
+
var EVIDENCE_WITNESS_REQUEST_CONTRACT = "absolutejs.vulnerability-evidence-witness-request/v1";
|
|
119
|
+
var createEvidenceWitnessService = (options) => {
|
|
120
|
+
const origin = new URL(options.origin);
|
|
121
|
+
if (origin.protocol !== "https:")
|
|
122
|
+
throw new Error("Evidence witness origin must use HTTPS");
|
|
123
|
+
let state = parseEvidenceWitnessSigningState(options.signingState);
|
|
124
|
+
let mutationQueue = Promise.resolve();
|
|
125
|
+
const refresh = async () => {
|
|
126
|
+
state = parseEvidenceWitnessSigningState(JSON.parse(await options.loadSigningState()));
|
|
127
|
+
return state;
|
|
128
|
+
};
|
|
129
|
+
const registry = async () => {
|
|
130
|
+
await refresh();
|
|
131
|
+
return evidenceWitnessKeyRegistryFrom(state);
|
|
132
|
+
};
|
|
133
|
+
const checkpoint = async (subject, request) => {
|
|
134
|
+
if (request.contract !== EVIDENCE_WITNESS_REQUEST_CONTRACT)
|
|
135
|
+
throw new Error("Evidence witness request contract is unsupported");
|
|
136
|
+
const [genesis] = request.log.entries;
|
|
137
|
+
if (!genesis || genesis.event.kind !== "key_created")
|
|
138
|
+
throw new Error("Evidence transparency genesis is missing");
|
|
139
|
+
const verification = verifyEvidenceKeyTransparencyLog({
|
|
140
|
+
log: request.log,
|
|
141
|
+
trustedKeys: [genesis.event.key]
|
|
142
|
+
});
|
|
143
|
+
if (verification.trust !== "trusted")
|
|
144
|
+
throw new Error("Evidence transparency log is invalid");
|
|
145
|
+
const normalizedSubject = subject.trim();
|
|
146
|
+
if (!normalizedSubject)
|
|
147
|
+
throw new Error("Evidence witness subject is required");
|
|
148
|
+
await refresh();
|
|
149
|
+
const signed = createEvidenceWitnessCheckpointForHead({
|
|
150
|
+
identity: state.identity,
|
|
151
|
+
logHead: request.log.head,
|
|
152
|
+
logSize: request.log.entries.length,
|
|
153
|
+
origin: origin.href
|
|
154
|
+
});
|
|
155
|
+
const observed = await options.store.observe({
|
|
156
|
+
checkpoint: signed,
|
|
157
|
+
history: request.log.entries.map(({ digest }) => digest),
|
|
158
|
+
subject: normalizedSubject
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
checkpoint: observed.checkpoint,
|
|
162
|
+
registry: evidenceWitnessKeyRegistryFrom(state)
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
const rotate = (rotatedAt = new Date().toISOString()) => {
|
|
166
|
+
const queued = mutationQueue.then(async () => {
|
|
167
|
+
await refresh();
|
|
168
|
+
const next = rotateEvidenceWitnessSigningState(state, rotatedAt);
|
|
169
|
+
await options.storeSigningState(encodeEvidenceWitnessSigningState(next));
|
|
170
|
+
state = next;
|
|
171
|
+
return evidenceWitnessKeyRegistryFrom(state);
|
|
172
|
+
});
|
|
173
|
+
mutationQueue = queued.then(() => {
|
|
174
|
+
return;
|
|
175
|
+
}, () => {
|
|
176
|
+
return;
|
|
177
|
+
});
|
|
178
|
+
return queued;
|
|
179
|
+
};
|
|
180
|
+
return { checkpoint, registry, rotate };
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// src/store.ts
|
|
184
|
+
class EvidenceWitnessEquivocationError extends Error {
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
class EvidenceWitnessRollbackError extends Error {
|
|
188
|
+
}
|
|
189
|
+
var normalizedSubject = (subject) => {
|
|
190
|
+
const value = subject.trim();
|
|
191
|
+
if (!value)
|
|
192
|
+
throw new Error("Evidence witness subject is required");
|
|
193
|
+
return value;
|
|
194
|
+
};
|
|
195
|
+
var createMemoryEvidenceWitnessStore = () => {
|
|
196
|
+
const subjects = new Map;
|
|
197
|
+
const latest = async (subject) => {
|
|
198
|
+
const values = subjects.get(normalizedSubject(subject));
|
|
199
|
+
if (!values || values.size === 0)
|
|
200
|
+
return null;
|
|
201
|
+
const size = Math.max(...values.keys());
|
|
202
|
+
return structuredClone(values.get(size));
|
|
203
|
+
};
|
|
204
|
+
const observe = async (input) => {
|
|
205
|
+
const subject = normalizedSubject(input.subject);
|
|
206
|
+
let values = subjects.get(subject);
|
|
207
|
+
if (!values) {
|
|
208
|
+
values = new Map;
|
|
209
|
+
subjects.set(subject, values);
|
|
210
|
+
}
|
|
211
|
+
const existing = values.get(input.checkpoint.logSize);
|
|
212
|
+
if (existing) {
|
|
213
|
+
if (existing.checkpoint.logHead !== input.checkpoint.logHead)
|
|
214
|
+
throw new EvidenceWitnessEquivocationError("Evidence transparency log equivocated at an observed size");
|
|
215
|
+
return {
|
|
216
|
+
checkpoint: structuredClone(existing.checkpoint),
|
|
217
|
+
status: "replay"
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const current = await latest(subject);
|
|
221
|
+
if (current && current.checkpoint.logSize > input.checkpoint.logSize)
|
|
222
|
+
throw new EvidenceWitnessRollbackError("Evidence transparency log attempted to roll back");
|
|
223
|
+
if (current && input.history[current.checkpoint.logSize - 1] !== current.checkpoint.logHead)
|
|
224
|
+
throw new EvidenceWitnessEquivocationError("Evidence transparency log forked from observed history");
|
|
225
|
+
const observation = structuredClone({
|
|
226
|
+
checkpoint: input.checkpoint,
|
|
227
|
+
subject
|
|
228
|
+
});
|
|
229
|
+
values.set(input.checkpoint.logSize, observation);
|
|
230
|
+
return {
|
|
231
|
+
checkpoint: structuredClone(input.checkpoint),
|
|
232
|
+
status: "accepted"
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
return { latest, observe };
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// src/http.ts
|
|
239
|
+
var json = (value, status = 200) => Response.json(value, {
|
|
240
|
+
headers: { "cache-control": "no-store" },
|
|
241
|
+
status
|
|
242
|
+
});
|
|
243
|
+
var bearer = (request) => {
|
|
244
|
+
const authorization = request.headers.get("authorization") ?? "";
|
|
245
|
+
return authorization.startsWith("Bearer ") ? authorization.slice("Bearer ".length).trim() : "";
|
|
246
|
+
};
|
|
247
|
+
var createEvidenceWitnessHttpHandler = (options) => async (request) => {
|
|
248
|
+
const url = new URL(request.url);
|
|
249
|
+
if (request.method === "GET" && url.pathname === "/health")
|
|
250
|
+
return json({ status: "ok" });
|
|
251
|
+
if (request.method === "GET" && url.pathname === "/v1/keys")
|
|
252
|
+
return json(await options.service.registry());
|
|
253
|
+
if (request.method !== "POST" || url.pathname !== "/v1/checkpoints")
|
|
254
|
+
return json({ error: "not_found" }, 404);
|
|
255
|
+
const subject = await options.authenticate(bearer(request));
|
|
256
|
+
if (!subject)
|
|
257
|
+
return json({ error: "unauthorized" }, 401);
|
|
258
|
+
try {
|
|
259
|
+
const input = await request.json();
|
|
260
|
+
if (input.contract !== EVIDENCE_WITNESS_REQUEST_CONTRACT)
|
|
261
|
+
return json({ error: "unsupported_contract" }, 400);
|
|
262
|
+
return json(await options.service.checkpoint(subject, input));
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (error instanceof EvidenceWitnessEquivocationError)
|
|
265
|
+
return json({ error: "equivocation", message: error.message }, 409);
|
|
266
|
+
if (error instanceof EvidenceWitnessRollbackError)
|
|
267
|
+
return json({ error: "rollback", message: error.message }, 409);
|
|
268
|
+
return json({
|
|
269
|
+
error: "invalid_request",
|
|
270
|
+
message: error instanceof Error ? error.message : "Invalid request"
|
|
271
|
+
}, 400);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
// src/postgres.ts
|
|
275
|
+
var LOCK_NAME = "absolutejs:evidence-witness-observation";
|
|
276
|
+
var json2 = (value) => typeof value === "string" ? JSON.parse(value) : value;
|
|
277
|
+
var ensurePostgresEvidenceWitnessSchema = async (sql) => {
|
|
278
|
+
await sql.unsafe(`
|
|
279
|
+
CREATE TABLE IF NOT EXISTS absolute_vulnerability_evidence_witness_observations (
|
|
280
|
+
subject TEXT NOT NULL,
|
|
281
|
+
log_size INTEGER NOT NULL CHECK (log_size > 0),
|
|
282
|
+
log_head VARCHAR(71) NOT NULL,
|
|
283
|
+
observed_at TIMESTAMPTZ NOT NULL,
|
|
284
|
+
receipt JSONB NOT NULL,
|
|
285
|
+
witness_key_id TEXT NOT NULL,
|
|
286
|
+
PRIMARY KEY (subject, log_size)
|
|
287
|
+
)
|
|
288
|
+
`);
|
|
289
|
+
};
|
|
290
|
+
var createPostgresEvidenceWitnessStore = (sql) => {
|
|
291
|
+
const latest = async (subject) => {
|
|
292
|
+
const rows = await sql`
|
|
293
|
+
SELECT receipt
|
|
294
|
+
FROM absolute_vulnerability_evidence_witness_observations
|
|
295
|
+
WHERE subject = ${subject}
|
|
296
|
+
ORDER BY log_size DESC
|
|
297
|
+
LIMIT 1
|
|
298
|
+
`;
|
|
299
|
+
const row = Array.from(rows)[0];
|
|
300
|
+
return row ? { checkpoint: json2(row.receipt), subject } : null;
|
|
301
|
+
};
|
|
302
|
+
const observe = (input) => sql.begin(async (transaction) => {
|
|
303
|
+
await transaction`
|
|
304
|
+
SELECT pg_advisory_xact_lock(
|
|
305
|
+
hashtext(${LOCK_NAME}), hashtext(${input.subject})
|
|
306
|
+
)
|
|
307
|
+
`;
|
|
308
|
+
const rows = await transaction`
|
|
309
|
+
SELECT log_head, log_size, receipt
|
|
310
|
+
FROM absolute_vulnerability_evidence_witness_observations
|
|
311
|
+
WHERE subject = ${input.subject}
|
|
312
|
+
ORDER BY log_size DESC
|
|
313
|
+
LIMIT 1
|
|
314
|
+
`;
|
|
315
|
+
const current = Array.from(rows)[0];
|
|
316
|
+
if (current?.log_size === input.checkpoint.logSize) {
|
|
317
|
+
if (current.log_head !== input.checkpoint.logHead)
|
|
318
|
+
throw new EvidenceWitnessEquivocationError("Evidence transparency log equivocated at an observed size");
|
|
319
|
+
return {
|
|
320
|
+
checkpoint: json2(current.receipt),
|
|
321
|
+
status: "replay"
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (current && current.log_size > input.checkpoint.logSize)
|
|
325
|
+
throw new EvidenceWitnessRollbackError("Evidence transparency log attempted to roll back");
|
|
326
|
+
if (current && input.history[current.log_size - 1] !== current.log_head)
|
|
327
|
+
throw new EvidenceWitnessEquivocationError("Evidence transparency log forked from observed history");
|
|
328
|
+
await transaction`
|
|
329
|
+
INSERT INTO absolute_vulnerability_evidence_witness_observations (
|
|
330
|
+
subject, log_size, log_head, observed_at, receipt, witness_key_id
|
|
331
|
+
) VALUES (
|
|
332
|
+
${input.subject}, ${input.checkpoint.logSize},
|
|
333
|
+
${input.checkpoint.logHead}, ${input.checkpoint.observedAt},
|
|
334
|
+
${JSON.stringify(input.checkpoint)}::jsonb,
|
|
335
|
+
${input.checkpoint.witness.keyId}
|
|
336
|
+
)
|
|
337
|
+
`;
|
|
338
|
+
return {
|
|
339
|
+
checkpoint: structuredClone(input.checkpoint),
|
|
340
|
+
status: "accepted"
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
return { latest, observe };
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// src/server.ts
|
|
347
|
+
var {SQL } = globalThis.Bun;
|
|
348
|
+
var required = (name) => {
|
|
349
|
+
const value = process.env[name]?.trim();
|
|
350
|
+
if (!value)
|
|
351
|
+
throw new Error(`Missing required environment: ${name}`);
|
|
352
|
+
return value;
|
|
353
|
+
};
|
|
354
|
+
var database = new SQL(required("DATABASE_URL"));
|
|
355
|
+
await ensurePostgresEvidenceWitnessSchema(database);
|
|
356
|
+
var encodedSigningState = required("EVIDENCE_WITNESS_SIGNING_STATE_JSON");
|
|
357
|
+
var signingState = parseEvidenceWitnessSigningState(JSON.parse(encodedSigningState));
|
|
358
|
+
var tokens = JSON.parse(required("EVIDENCE_WITNESS_TOKENS_JSON"));
|
|
359
|
+
var subjectsByToken = new Map(Object.entries(tokens).map(([subject, token]) => [token, subject]));
|
|
360
|
+
var service2 = createEvidenceWitnessService({
|
|
361
|
+
loadSigningState: async () => encodedSigningState,
|
|
362
|
+
origin: required("EVIDENCE_WITNESS_ORIGIN"),
|
|
363
|
+
signingState,
|
|
364
|
+
store: createPostgresEvidenceWitnessStore(database),
|
|
365
|
+
storeSigningState: async () => {
|
|
366
|
+
throw new Error("Server-managed key rotation requires a durable secret-store integration");
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
var handler = createEvidenceWitnessHttpHandler({
|
|
370
|
+
authenticate: async (token) => subjectsByToken.get(token) ?? null,
|
|
371
|
+
service: service2
|
|
372
|
+
});
|
|
373
|
+
var port = Number(process.env.PORT ?? "3000");
|
|
374
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535)
|
|
375
|
+
throw new Error("PORT must be a valid TCP port");
|
|
376
|
+
Bun.serve({ fetch: handler, port });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type EvidenceKeyTransparencyLog } from "@absolutejs/vulnerabilities/evidence-transparency";
|
|
2
|
+
import { type EvidenceWitnessKeyRegistry, type EvidenceWitnessSigningState } from "./signing";
|
|
3
|
+
import type { EvidenceWitnessStore } from "./store";
|
|
4
|
+
export declare const EVIDENCE_WITNESS_REQUEST_CONTRACT: "absolutejs.vulnerability-evidence-witness-request/v1";
|
|
5
|
+
export type EvidenceWitnessRequest = {
|
|
6
|
+
contract: typeof EVIDENCE_WITNESS_REQUEST_CONTRACT;
|
|
7
|
+
log: EvidenceKeyTransparencyLog;
|
|
8
|
+
};
|
|
9
|
+
export type EvidenceWitnessResponse = {
|
|
10
|
+
checkpoint: Awaited<ReturnType<EvidenceWitnessStore["observe"]>>["checkpoint"];
|
|
11
|
+
registry: EvidenceWitnessKeyRegistry;
|
|
12
|
+
};
|
|
13
|
+
export declare const createEvidenceWitnessService: (options: {
|
|
14
|
+
loadSigningState: () => Promise<string>;
|
|
15
|
+
origin: string;
|
|
16
|
+
signingState: EvidenceWitnessSigningState;
|
|
17
|
+
store: EvidenceWitnessStore;
|
|
18
|
+
storeSigningState: (encoded: string) => Promise<void>;
|
|
19
|
+
}) => {
|
|
20
|
+
checkpoint: (subject: string, request: EvidenceWitnessRequest) => Promise<EvidenceWitnessResponse>;
|
|
21
|
+
registry: () => Promise<EvidenceWitnessKeyRegistry>;
|
|
22
|
+
rotate: (rotatedAt?: string) => Promise<EvidenceWitnessKeyRegistry>;
|
|
23
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type EvidenceSigningIdentity, type EvidenceSigningKeyTransition, type EvidenceVerificationKey } from "@absolutejs/vulnerabilities/evidence-bundle";
|
|
2
|
+
declare const SIGNING_STATE_VERSION = 1;
|
|
3
|
+
export type EvidenceWitnessSigningState = {
|
|
4
|
+
identity: EvidenceSigningIdentity;
|
|
5
|
+
transitions: EvidenceSigningKeyTransition[];
|
|
6
|
+
version: typeof SIGNING_STATE_VERSION;
|
|
7
|
+
};
|
|
8
|
+
export type EvidenceWitnessKeyRegistry = {
|
|
9
|
+
activeKeyId: string;
|
|
10
|
+
keys: EvidenceVerificationKey[];
|
|
11
|
+
transitions: EvidenceSigningKeyTransition[];
|
|
12
|
+
version: typeof SIGNING_STATE_VERSION;
|
|
13
|
+
};
|
|
14
|
+
export declare const createEvidenceWitnessSigningState: (createdAt?: string) => EvidenceWitnessSigningState;
|
|
15
|
+
export declare const parseEvidenceWitnessSigningState: (value: unknown) => EvidenceWitnessSigningState;
|
|
16
|
+
export declare const encodeEvidenceWitnessSigningState: (state: EvidenceWitnessSigningState) => string;
|
|
17
|
+
export declare const rotateEvidenceWitnessSigningState: (state: EvidenceWitnessSigningState, rotatedAt?: string) => EvidenceWitnessSigningState;
|
|
18
|
+
export declare const evidenceWitnessKeyRegistryFrom: (state: EvidenceWitnessSigningState) => EvidenceWitnessKeyRegistry;
|
|
19
|
+
export declare const verifyEvidenceWitnessKeyRegistry: (input: {
|
|
20
|
+
registry: EvidenceWitnessKeyRegistry;
|
|
21
|
+
trustedKeys: readonly EvidenceVerificationKey[];
|
|
22
|
+
}) => {
|
|
23
|
+
activeKey: EvidenceVerificationKey;
|
|
24
|
+
trust: "untrusted";
|
|
25
|
+
} | {
|
|
26
|
+
activeKey: EvidenceVerificationKey;
|
|
27
|
+
trust: "trusted";
|
|
28
|
+
} | {
|
|
29
|
+
activeKey: null;
|
|
30
|
+
trust: "untrusted";
|
|
31
|
+
};
|
|
32
|
+
export {};
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SignedEvidenceWitnessCheckpoint } from "@absolutejs/vulnerabilities/evidence-witness";
|
|
2
|
+
export type EvidenceWitnessObservation = {
|
|
3
|
+
checkpoint: SignedEvidenceWitnessCheckpoint;
|
|
4
|
+
subject: string;
|
|
5
|
+
};
|
|
6
|
+
export type EvidenceWitnessObservationInput = EvidenceWitnessObservation & {
|
|
7
|
+
history: readonly `sha256:${string}`[];
|
|
8
|
+
};
|
|
9
|
+
export type EvidenceWitnessObservationResult = {
|
|
10
|
+
checkpoint: SignedEvidenceWitnessCheckpoint;
|
|
11
|
+
status: "accepted" | "replay";
|
|
12
|
+
};
|
|
13
|
+
export interface EvidenceWitnessStore {
|
|
14
|
+
latest(subject: string): Promise<EvidenceWitnessObservation | null>;
|
|
15
|
+
observe(observation: EvidenceWitnessObservationInput): Promise<EvidenceWitnessObservationResult>;
|
|
16
|
+
}
|
|
17
|
+
export declare class EvidenceWitnessEquivocationError extends Error {
|
|
18
|
+
}
|
|
19
|
+
export declare class EvidenceWitnessRollbackError extends Error {
|
|
20
|
+
}
|
|
21
|
+
export declare const createMemoryEvidenceWitnessStore: () => EvidenceWitnessStore;
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@absolutejs/vulnerabilities-witness",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Independent durable transparency witness with rollback, equivocation, key-rotation, and HTTP service primitives for AbsoluteJS vulnerability evidence.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "BSL-1.1",
|
|
7
|
+
"author": "Alex Kahn",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/absolutejs/vulnerabilities-witness.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/absolutejs/vulnerabilities-witness",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/absolutejs/vulnerabilities-witness/issues"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"absolute-vulnerability-witness": "./dist/server.js"
|
|
21
|
+
},
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"absolutejs": {
|
|
25
|
+
"manifestContract": 2
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./postgres": {
|
|
33
|
+
"types": "./dist/postgres.d.ts",
|
|
34
|
+
"import": "./dist/postgres.js"
|
|
35
|
+
},
|
|
36
|
+
"./manifest": {
|
|
37
|
+
"types": "./dist/manifest.d.ts",
|
|
38
|
+
"import": "./dist/manifest.js"
|
|
39
|
+
},
|
|
40
|
+
"./manifest.json": "./dist/manifest.json"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"format": "prettier --write \"./**/*.{ts,json,md}\"",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"test": "bun test",
|
|
51
|
+
"build": "rm -rf dist && bun build --root src src/index.ts src/postgres.ts src/server.ts src/manifest.ts --outdir dist --target=bun --external @absolutejs/manifest --external @absolutejs/vulnerabilities --external @sinclair/typebox && tsc -p tsconfig.build.json && absolute-manifest emit",
|
|
52
|
+
"check:package": "bun run format && bun run typecheck && bun run test && bun run build"
|
|
53
|
+
},
|
|
54
|
+
"keywords": [
|
|
55
|
+
"absolutejs",
|
|
56
|
+
"equivocation",
|
|
57
|
+
"key-transparency",
|
|
58
|
+
"security",
|
|
59
|
+
"vulnerability-management",
|
|
60
|
+
"witness"
|
|
61
|
+
],
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@absolutejs/manifest": "0.3.0",
|
|
64
|
+
"@absolutejs/vulnerabilities": "0.13.0",
|
|
65
|
+
"@sinclair/typebox": "0.34.52"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@types/bun": "1.3.14",
|
|
69
|
+
"prettier": "3.8.1",
|
|
70
|
+
"typescript": "5.9.3"
|
|
71
|
+
}
|
|
72
|
+
}
|