sello 0.1.1 → 0.1.2
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/dist/cbor.js +337 -0
- package/dist/cli/bench.js +389 -0
- package/dist/cli/demo.js +113 -0
- package/dist/cli/sello.js +515 -0
- package/dist/cose/protected-header.js +210 -0
- package/dist/cose/sign1.js +124 -0
- package/dist/crypto/ed25519.js +117 -0
- package/dist/crypto/identifiers.js +64 -0
- package/dist/hpke/base.js +349 -0
- package/dist/hpke/receipt.js +79 -0
- package/dist/index.js +15 -0
- package/dist/log/canonical-url.js +168 -0
- package/dist/log/mock-log.js +147 -0
- package/dist/log/rekor.js +120 -0
- package/dist/log/types.js +0 -0
- package/dist/mcp/middleware.js +162 -0
- package/dist/owner/verify.js +271 -0
- package/dist/receipt/body.js +210 -0
- package/dist/registry/json-registry.js +233 -0
- package/dist/sdk/index.js +22 -0
- package/dist/sdk/keys.js +191 -0
- package/dist/sdk/logs.js +196 -0
- package/dist/sdk/publisher.js +106 -0
- package/dist/sdk/service.js +561 -0
- package/dist/service/create-receipt.js +174 -0
- package/dist/token/jws-profile.js +174 -0
- package/docs/release-checklist.md +1 -0
- package/package.json +9 -5
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { sha256, assertTokenRef, toHex } from "../crypto/identifiers.js";
|
|
2
|
+
import { decodeProtectedHeader } from "../cose/protected-header.js";
|
|
3
|
+
import { decodeReceiptEnvelope } from "../cose/sign1.js";
|
|
4
|
+
import {
|
|
5
|
+
|
|
6
|
+
assertCanonicalLogUrl,
|
|
7
|
+
logUrlsEqual,
|
|
8
|
+
} from "./canonical-url.js";
|
|
9
|
+
export class MockTransparencyLog {
|
|
10
|
+
logUrl ;
|
|
11
|
+
#entries = [];
|
|
12
|
+
#tokenIndex = new Map ();
|
|
13
|
+
|
|
14
|
+
constructor(logUrl ) {
|
|
15
|
+
assertCanonicalLogUrl(logUrl, "logUrl");
|
|
16
|
+
this.logUrl = logUrl;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
append(envelope , integratedTime = nowUtcSeconds()) {
|
|
20
|
+
assertBytes(envelope, "envelope");
|
|
21
|
+
assertUtcTimestamp(integratedTime, "integratedTime");
|
|
22
|
+
|
|
23
|
+
const decodedEnvelope = decodeReceiptEnvelope(envelope);
|
|
24
|
+
const protectedHeader = decodeProtectedHeader(decodedEnvelope.protectedBytes);
|
|
25
|
+
if (!logUrlsEqual(protectedHeader.sello_log_url, this.logUrl)) {
|
|
26
|
+
throw new TypeError("envelope sello_log_url must match mock log URL");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const index = this.#entries.length;
|
|
30
|
+
const proof = buildProof(this.logUrl, index, integratedTime, envelope);
|
|
31
|
+
const entry = freezeEntry({
|
|
32
|
+
logUrl: this.logUrl,
|
|
33
|
+
index,
|
|
34
|
+
integratedTime,
|
|
35
|
+
envelope,
|
|
36
|
+
proof,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
this.#entries.push(entry);
|
|
40
|
+
|
|
41
|
+
const tokenRefHex = toHex(protectedHeader.sello_token_ref);
|
|
42
|
+
const indexes = this.#tokenIndex.get(tokenRefHex) ?? [];
|
|
43
|
+
indexes.push(index);
|
|
44
|
+
this.#tokenIndex.set(tokenRefHex, indexes);
|
|
45
|
+
|
|
46
|
+
return cloneEntry(entry);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
queryByTokenRef(tokenRef ) {
|
|
50
|
+
assertTokenRef(tokenRef, "tokenRef");
|
|
51
|
+
const indexes = this.#tokenIndex.get(toHex(tokenRef)) ?? [];
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
completeness: "complete",
|
|
55
|
+
entries: indexes.map((index) => cloneEntry(this.#entries[index])),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
verifyInclusionProof(entry ) {
|
|
60
|
+
try {
|
|
61
|
+
assertCanonicalLogUrl(entry.logUrl, "entry.logUrl");
|
|
62
|
+
if (!logUrlsEqual(entry.logUrl, this.logUrl)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const expected = buildProof(
|
|
67
|
+
this.logUrl,
|
|
68
|
+
entry.index,
|
|
69
|
+
entry.integratedTime,
|
|
70
|
+
entry.envelope,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
entry.proof.logUrl === expected.logUrl &&
|
|
75
|
+
entry.proof.index === expected.index &&
|
|
76
|
+
entry.proof.integratedTime === expected.integratedTime &&
|
|
77
|
+
entry.proof.envelopeHash === expected.envelopeHash &&
|
|
78
|
+
entry.proof.proofHash === expected.proofHash
|
|
79
|
+
);
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildProof(
|
|
87
|
+
logUrl ,
|
|
88
|
+
index ,
|
|
89
|
+
integratedTime ,
|
|
90
|
+
envelope ,
|
|
91
|
+
) {
|
|
92
|
+
const envelopeHash = toHex(sha256(envelope));
|
|
93
|
+
const proofHash = toHex(
|
|
94
|
+
sha256(
|
|
95
|
+
new TextEncoder().encode(
|
|
96
|
+
`${logUrl}\n${index}\n${integratedTime}\n${envelopeHash}`,
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
logUrl,
|
|
103
|
+
index,
|
|
104
|
+
integratedTime,
|
|
105
|
+
envelopeHash,
|
|
106
|
+
proofHash,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function freezeEntry(entry ) {
|
|
111
|
+
return {
|
|
112
|
+
logUrl: entry.logUrl,
|
|
113
|
+
index: entry.index,
|
|
114
|
+
integratedTime: entry.integratedTime,
|
|
115
|
+
envelope: new Uint8Array(entry.envelope),
|
|
116
|
+
proof: { ...entry.proof },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function cloneEntry(entry ) {
|
|
121
|
+
return {
|
|
122
|
+
logUrl: entry.logUrl,
|
|
123
|
+
index: entry.index,
|
|
124
|
+
integratedTime: entry.integratedTime,
|
|
125
|
+
envelope: new Uint8Array(entry.envelope),
|
|
126
|
+
proof: { ...entry.proof },
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function nowUtcSeconds() {
|
|
131
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function assertUtcTimestamp(value , name ) {
|
|
135
|
+
if (
|
|
136
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(value) ||
|
|
137
|
+
Number.isNaN(Date.parse(value))
|
|
138
|
+
) {
|
|
139
|
+
throw new TypeError(`${name} must be an RFC 3339 UTC timestamp`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function assertBytes(value , name ) {
|
|
144
|
+
if (!(value instanceof Uint8Array)) {
|
|
145
|
+
throw new TypeError(`${name} must be a Uint8Array`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { decodeProtectedHeader } from "../cose/protected-header.js";
|
|
2
|
+
import { decodeReceiptEnvelope } from "../cose/sign1.js";
|
|
3
|
+
import { assertTokenRef, toHex } from "../crypto/identifiers.js";
|
|
4
|
+
import {
|
|
5
|
+
|
|
6
|
+
assertCanonicalLogUrl,
|
|
7
|
+
logUrlsEqual,
|
|
8
|
+
} from "./canonical-url.js";
|
|
9
|
+
export class RekorDiscoveryLog {
|
|
10
|
+
logUrl ;
|
|
11
|
+
#entries = [];
|
|
12
|
+
#verifyInclusionProof ;
|
|
13
|
+
|
|
14
|
+
constructor(input ) {
|
|
15
|
+
assertCanonicalLogUrl(input.logUrl, "logUrl");
|
|
16
|
+
this.logUrl = input.logUrl;
|
|
17
|
+
this.#verifyInclusionProof = input.verifyInclusionProof ?? (() => false);
|
|
18
|
+
|
|
19
|
+
for (const entry of input.entries ?? []) {
|
|
20
|
+
this.addDiscoveredEntry(entry);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
addDiscoveredEntry(input ) {
|
|
25
|
+
const entryLogUrl = input.logUrl ?? this.logUrl;
|
|
26
|
+
assertCanonicalLogUrl(entryLogUrl, "entry.logUrl");
|
|
27
|
+
assertUtcTimestamp(input.integratedTime, "integratedTime");
|
|
28
|
+
assertSafeIndex(input.index);
|
|
29
|
+
assertBytes(input.envelope, "envelope");
|
|
30
|
+
|
|
31
|
+
const tokenRefHex = input.tokenRef
|
|
32
|
+
? tokenRefToHex(input.tokenRef)
|
|
33
|
+
: tokenRefFromEnvelope(input.envelope);
|
|
34
|
+
|
|
35
|
+
const entry = cloneEntry({
|
|
36
|
+
logUrl: entryLogUrl,
|
|
37
|
+
index: input.index,
|
|
38
|
+
integratedTime: input.integratedTime,
|
|
39
|
+
envelope: input.envelope,
|
|
40
|
+
proof: input.proof,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
this.#entries.push({ tokenRefHex, entry });
|
|
44
|
+
return cloneEntry(entry);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
queryByTokenRef(tokenRef ) {
|
|
48
|
+
const tokenRefHex = tokenRefToHex(tokenRef);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
completeness: "discovery-only",
|
|
52
|
+
entries: this.#entries
|
|
53
|
+
.filter((indexed) => indexed.tokenRefHex === tokenRefHex)
|
|
54
|
+
.map((indexed) => cloneEntry(indexed.entry)),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
verifyInclusionProof(entry ) {
|
|
59
|
+
try {
|
|
60
|
+
assertCanonicalLogUrl(entry.logUrl, "entry.logUrl");
|
|
61
|
+
if (!logUrlsEqual(entry.logUrl, this.logUrl)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return this.#verifyInclusionProof(entry);
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function tokenRefFromEnvelope(envelope ) {
|
|
73
|
+
const decodedEnvelope = decodeReceiptEnvelope(envelope);
|
|
74
|
+
const protectedHeader = decodeProtectedHeader(decodedEnvelope.protectedBytes);
|
|
75
|
+
return tokenRefToHex(protectedHeader.sello_token_ref);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function tokenRefToHex(tokenRef ) {
|
|
79
|
+
assertTokenRef(tokenRef, "tokenRef");
|
|
80
|
+
return toHex(tokenRef);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function cloneEntry(entry ) {
|
|
84
|
+
return {
|
|
85
|
+
logUrl: entry.logUrl,
|
|
86
|
+
index: entry.index,
|
|
87
|
+
integratedTime: entry.integratedTime,
|
|
88
|
+
envelope: new Uint8Array(entry.envelope),
|
|
89
|
+
proof: cloneProof(entry.proof),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cloneProof(proof ) {
|
|
94
|
+
if (proof === null || typeof proof !== "object") {
|
|
95
|
+
return proof;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return globalThis.structuredClone(proof);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function assertUtcTimestamp(value , name ) {
|
|
102
|
+
if (
|
|
103
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(value) ||
|
|
104
|
+
Number.isNaN(Date.parse(value))
|
|
105
|
+
) {
|
|
106
|
+
throw new TypeError(`${name} must be an RFC 3339 UTC timestamp`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function assertSafeIndex(index ) {
|
|
111
|
+
if (!Number.isSafeInteger(index) || index < 0) {
|
|
112
|
+
throw new TypeError("index must be a non-negative safe integer");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function assertBytes(value , name ) {
|
|
117
|
+
if (!(value instanceof Uint8Array)) {
|
|
118
|
+
throw new TypeError(`${name} must be a Uint8Array`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createReceipt, } from "../service/create-receipt.js";
|
|
2
|
+
import { verifySelloJwsToken } from "../token/jws-profile.js";
|
|
3
|
+
export class SelloMcpDeniedError extends Error {
|
|
4
|
+
receipt ;
|
|
5
|
+
|
|
6
|
+
constructor(receipt ) {
|
|
7
|
+
super("MCP request denied by Sello middleware");
|
|
8
|
+
this.name = "SelloMcpDeniedError";
|
|
9
|
+
this.receipt = receipt;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createSelloMcpMiddleware (
|
|
14
|
+
input ,
|
|
15
|
+
) {
|
|
16
|
+
return async (request ) => {
|
|
17
|
+
const authorizationToken = resolveValue(input.authorizationToken, request);
|
|
18
|
+
const verifiedToken = verifySelloJwsToken({
|
|
19
|
+
authorizationToken,
|
|
20
|
+
issuerPublicKey: input.tokenIssuerPublicKey,
|
|
21
|
+
});
|
|
22
|
+
const baseReceiptInput = {
|
|
23
|
+
authorizationTokenBytes: verifiedToken.authorizationTokenBytes,
|
|
24
|
+
ownerHpkePublicKey: verifiedToken.ownerHpkePublicKey,
|
|
25
|
+
selloLogs: verifiedToken.selloLogs ?? input.fallbackSelloLogs ?? [],
|
|
26
|
+
serviceKid: input.serviceKid,
|
|
27
|
+
servicePrivateKey: input.servicePrivateKey,
|
|
28
|
+
serviceIdentifier: input.serviceIdentifier,
|
|
29
|
+
log: input.log,
|
|
30
|
+
actionType: resolveActionType(input.actionType, request),
|
|
31
|
+
actionInputBytes: (input.canonicalizeInput ?? canonicalJsonBytes)(request),
|
|
32
|
+
timestamp: (input.now ?? nowUtcSeconds)(),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (input.isDenied && (await input.isDenied(request))) {
|
|
36
|
+
const response = input.deniedResponse
|
|
37
|
+
? await input.deniedResponse(request)
|
|
38
|
+
: undefined;
|
|
39
|
+
const receipt = createReceipt({
|
|
40
|
+
...baseReceiptInput,
|
|
41
|
+
actionOutputBytes: new Uint8Array(),
|
|
42
|
+
resultStatus: "denied",
|
|
43
|
+
});
|
|
44
|
+
input.onReceipt?.({ resultStatus: "denied", receipt, response });
|
|
45
|
+
|
|
46
|
+
if (input.deniedResponse) {
|
|
47
|
+
return response ;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw new SelloMcpDeniedError(receipt);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let response ;
|
|
54
|
+
try {
|
|
55
|
+
response = await input.handler(request);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const receipt = createReceipt({
|
|
58
|
+
...baseReceiptInput,
|
|
59
|
+
actionOutputBytes: (input.canonicalizeError ?? canonicalErrorBytes)(
|
|
60
|
+
error,
|
|
61
|
+
),
|
|
62
|
+
resultStatus: "error",
|
|
63
|
+
});
|
|
64
|
+
input.onReceipt?.({ resultStatus: "error", receipt, error });
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const receipt = createReceipt({
|
|
69
|
+
...baseReceiptInput,
|
|
70
|
+
actionOutputBytes: (input.canonicalizeOutput ?? canonicalJsonBytes)(
|
|
71
|
+
response,
|
|
72
|
+
),
|
|
73
|
+
resultStatus: "success",
|
|
74
|
+
});
|
|
75
|
+
input.onReceipt?.({ resultStatus: "success", receipt, response });
|
|
76
|
+
return response;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function canonicalJsonBytes(value ) {
|
|
81
|
+
return new TextEncoder().encode(canonicalJson(value));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function canonicalJson(value ) {
|
|
85
|
+
if (value === null) {
|
|
86
|
+
return "null";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
switch (typeof value) {
|
|
90
|
+
case "boolean":
|
|
91
|
+
case "number":
|
|
92
|
+
case "string":
|
|
93
|
+
return primitiveJson(value);
|
|
94
|
+
case "object":
|
|
95
|
+
if (Array.isArray(value)) {
|
|
96
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return objectJson(value );
|
|
100
|
+
default:
|
|
101
|
+
throw new TypeError("value must be JSON-serializable");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function primitiveJson(value ) {
|
|
106
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
107
|
+
throw new TypeError("number must be finite");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return JSON.stringify(value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function objectJson(value ) {
|
|
114
|
+
const prototype = Object.getPrototypeOf(value);
|
|
115
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
116
|
+
throw new TypeError("object must be a plain JSON object");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const entries = Object.entries(value);
|
|
120
|
+
entries.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
121
|
+
|
|
122
|
+
return `{${entries
|
|
123
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}`)
|
|
124
|
+
.join(",")}}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function canonicalErrorBytes(error ) {
|
|
128
|
+
if (error instanceof Error) {
|
|
129
|
+
return canonicalJsonBytes({
|
|
130
|
+
name: error.name,
|
|
131
|
+
message: error.message,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return canonicalJsonBytes({ error: String(error) });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function resolveValue (
|
|
139
|
+
value ,
|
|
140
|
+
request ,
|
|
141
|
+
) {
|
|
142
|
+
if (typeof value === "function") {
|
|
143
|
+
return (value )(request);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resolveActionType (
|
|
150
|
+
actionType ,
|
|
151
|
+
request ,
|
|
152
|
+
) {
|
|
153
|
+
if (typeof actionType === "function") {
|
|
154
|
+
return actionType(request);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return actionType ?? "tools/call";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function nowUtcSeconds() {
|
|
161
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
162
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { decodeProtectedHeader } from "../cose/protected-header.js";
|
|
2
|
+
import { decodeReceiptEnvelope, verifyReceiptEnvelope } from "../cose/sign1.js";
|
|
3
|
+
import { deriveTokenIdentifiers, toHex } from "../crypto/identifiers.js";
|
|
4
|
+
import { openReceiptBody } from "../hpke/receipt.js";
|
|
5
|
+
import {
|
|
6
|
+
|
|
7
|
+
logUrlsEqual,
|
|
8
|
+
} from "../log/canonical-url.js";
|
|
9
|
+
import { decodeReceiptBody, } from "../receipt/body.js";
|
|
10
|
+
import {
|
|
11
|
+
|
|
12
|
+
assertKeyNotRevoked,
|
|
13
|
+
resolveServiceKey,
|
|
14
|
+
} from "../registry/json-registry.js";
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
export function verifyReceipts(input ) {
|
|
61
|
+
const identifiers = deriveTokenIdentifiers(input.authorizationTokenBytes);
|
|
62
|
+
const trustedLogUrls = input.trustedLogs.map((log) => log.logUrl);
|
|
63
|
+
const receipts = [];
|
|
64
|
+
const rejected = [];
|
|
65
|
+
const exactDedup = new Map ();
|
|
66
|
+
const sameSecond = new Map ();
|
|
67
|
+
|
|
68
|
+
for (const log of input.trustedLogs) {
|
|
69
|
+
const result = log.queryByTokenRef(identifiers.sello_token_ref);
|
|
70
|
+
|
|
71
|
+
for (const entry of result.entries) {
|
|
72
|
+
const verified = verifyOneEntry({
|
|
73
|
+
entry,
|
|
74
|
+
log,
|
|
75
|
+
logCompleteness: result.completeness,
|
|
76
|
+
trustedLogUrls,
|
|
77
|
+
tokenRef: identifiers.sello_token_ref,
|
|
78
|
+
registry: input.registry,
|
|
79
|
+
ownerPrivateKey: input.ownerPrivateKey,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (verified.status === "rejected") {
|
|
83
|
+
rejected.push(verified);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const exactKey = buildExactDedupKey(verified);
|
|
88
|
+
const existingIndex = exactDedup.get(exactKey);
|
|
89
|
+
|
|
90
|
+
if (existingIndex !== undefined) {
|
|
91
|
+
receipts.push({
|
|
92
|
+
...verified,
|
|
93
|
+
status: "duplicate",
|
|
94
|
+
duplicateOf: existingIndex,
|
|
95
|
+
});
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const sameSecondKey = buildSameSecondKey(verified);
|
|
100
|
+
const sameSecondIndex = sameSecond.get(sameSecondKey);
|
|
101
|
+
|
|
102
|
+
if (sameSecondIndex !== undefined) {
|
|
103
|
+
receipts[sameSecondIndex] = {
|
|
104
|
+
...receipts[sameSecondIndex],
|
|
105
|
+
sameSecondActivity: true,
|
|
106
|
+
};
|
|
107
|
+
verified.sameSecondActivity = true;
|
|
108
|
+
} else {
|
|
109
|
+
sameSecond.set(sameSecondKey, receipts.length);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
exactDedup.set(exactKey, receipts.length);
|
|
113
|
+
receipts.push(verified);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { receipts, rejected };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
function verifyOneEntry(input ) {
|
|
131
|
+
let protectedHeaderBytes ;
|
|
132
|
+
let payload ;
|
|
133
|
+
let protectedHeader ;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const envelope = decodeReceiptEnvelope(input.entry.envelope);
|
|
137
|
+
protectedHeaderBytes = envelope.protectedBytes;
|
|
138
|
+
payload = envelope.payload;
|
|
139
|
+
protectedHeader = decodeProtectedHeader(protectedHeaderBytes);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return reject("invalid_receipt_body", error, input.entry);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!logUrlsEqual(protectedHeader.sello_log_url, input.log.logUrl)) {
|
|
145
|
+
return reject(
|
|
146
|
+
"log_url_mismatch",
|
|
147
|
+
"receipt log URL does not match returning log",
|
|
148
|
+
input.entry,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!input.trustedLogUrls.some((logUrl) => logUrlsEqual(logUrl, input.log.logUrl))) {
|
|
153
|
+
return reject("untrusted_log", "returning log is not trusted", input.entry);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!bytesEqual(protectedHeader.sello_token_ref, input.tokenRef)) {
|
|
157
|
+
return reject(
|
|
158
|
+
"token_ref_mismatch",
|
|
159
|
+
"receipt token ref does not match requested token",
|
|
160
|
+
input.entry,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!input.log.verifyInclusionProof(input.entry)) {
|
|
165
|
+
return reject("inclusion_proof_failed", "inclusion proof failed", input.entry);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let service;
|
|
169
|
+
try {
|
|
170
|
+
service = resolveServiceKey(input.registry, protectedHeader.kid);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return reject("unknown_kid", error, input.entry);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
assertKeyNotRevoked(
|
|
177
|
+
input.registry,
|
|
178
|
+
protectedHeader.kid,
|
|
179
|
+
input.entry.integratedTime,
|
|
180
|
+
);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return reject("revoked_key", error, input.entry);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
verifyReceiptEnvelope({
|
|
187
|
+
envelope: input.entry.envelope,
|
|
188
|
+
servicePublicKey: service.publicKeyEd25519,
|
|
189
|
+
});
|
|
190
|
+
} catch (error) {
|
|
191
|
+
return reject("cose_signature_failed", error, input.entry);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let plaintext;
|
|
195
|
+
try {
|
|
196
|
+
plaintext = openReceiptBody({
|
|
197
|
+
payload,
|
|
198
|
+
ownerPrivateKey: input.ownerPrivateKey,
|
|
199
|
+
protectedHeaderBytes,
|
|
200
|
+
serviceIdentifier: service.serviceIdentifier,
|
|
201
|
+
selloTokenRef: protectedHeader.sello_token_ref,
|
|
202
|
+
});
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return reject("hpke_open_failed", error, input.entry);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
return {
|
|
209
|
+
status: "valid",
|
|
210
|
+
receipt: decodeReceiptBody(plaintext),
|
|
211
|
+
serviceIdentifier: service.serviceIdentifier,
|
|
212
|
+
kidHex: toHex(protectedHeader.kid),
|
|
213
|
+
tokenRefHex: toHex(protectedHeader.sello_token_ref),
|
|
214
|
+
logUrl: input.log.logUrl,
|
|
215
|
+
logCompleteness: input.logCompleteness,
|
|
216
|
+
integratedTime: input.entry.integratedTime,
|
|
217
|
+
sameSecondActivity: false,
|
|
218
|
+
};
|
|
219
|
+
} catch (error) {
|
|
220
|
+
return reject("invalid_receipt_body", error, input.entry);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function buildExactDedupKey(record ) {
|
|
225
|
+
return [
|
|
226
|
+
buildSameSecondKey(record),
|
|
227
|
+
record.receipt["action-type"],
|
|
228
|
+
toHex(record.receipt["action-input-hash"]),
|
|
229
|
+
toHex(record.receipt["action-output-hash"]),
|
|
230
|
+
].join("|");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildSameSecondKey(record ) {
|
|
234
|
+
return [
|
|
235
|
+
record.kidHex,
|
|
236
|
+
record.tokenRefHex,
|
|
237
|
+
truncateTimestampToSecond(record.receipt.timestamp),
|
|
238
|
+
].join("|");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function truncateTimestampToSecond(timestamp ) {
|
|
242
|
+
return timestamp.replace(/\.\d+Z$/, "Z");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function reject(
|
|
246
|
+
code ,
|
|
247
|
+
error ,
|
|
248
|
+
entry ,
|
|
249
|
+
) {
|
|
250
|
+
return {
|
|
251
|
+
status: "rejected",
|
|
252
|
+
code,
|
|
253
|
+
message: error instanceof Error ? error.message : String(error),
|
|
254
|
+
logUrl: entry.logUrl,
|
|
255
|
+
integratedTime: entry.integratedTime,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function bytesEqual(a , b ) {
|
|
260
|
+
if (a.byteLength !== b.byteLength) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (let index = 0; index < a.byteLength; index += 1) {
|
|
265
|
+
if (a[index] !== b[index]) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return true;
|
|
271
|
+
}
|