sello 0.1.0 → 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/README.md +2 -0
- 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/decisions.md +2 -2
- package/docs/release-checklist.md +4 -3
- package/docs/sdk-quickstart.md +2 -0
- package/package.json +10 -6
- package/src/cli/sello.ts +5 -3
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
cborTag,
|
|
5
|
+
decodeCbor,
|
|
6
|
+
encodeCbor,
|
|
7
|
+
} from "../cbor.js";
|
|
8
|
+
import { assertAgentIdentifier } from "../crypto/identifiers.js";
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
export const ZERO_SHA256_DIGEST = new Uint8Array(32);
|
|
23
|
+
|
|
24
|
+
const RESULT_STATUSES = new Set (["success", "error", "denied"]);
|
|
25
|
+
const RECEIPT_KEYS = new Set([
|
|
26
|
+
"agent-identifier",
|
|
27
|
+
"action-type",
|
|
28
|
+
"action-input-hash",
|
|
29
|
+
"action-output-hash",
|
|
30
|
+
"result-status",
|
|
31
|
+
"timestamp",
|
|
32
|
+
"service-defined-fields",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export function encodeReceiptBody(receipt ) {
|
|
36
|
+
validateReceiptBody(receipt);
|
|
37
|
+
|
|
38
|
+
const map = new Map([
|
|
39
|
+
["agent-identifier", receipt["agent-identifier"]],
|
|
40
|
+
["action-type", receipt["action-type"]],
|
|
41
|
+
["action-input-hash", receipt["action-input-hash"]],
|
|
42
|
+
["action-output-hash", receipt["action-output-hash"]],
|
|
43
|
+
["result-status", receipt["result-status"]],
|
|
44
|
+
["timestamp", cborTag(0, receipt.timestamp)],
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
if (receipt["service-defined-fields"]) {
|
|
48
|
+
map.set("service-defined-fields", receipt["service-defined-fields"]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return encodeCbor(map);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function decodeReceiptBody(bytes ) {
|
|
55
|
+
const value = decodeCbor(bytes);
|
|
56
|
+
|
|
57
|
+
if (!(value instanceof Map)) {
|
|
58
|
+
throw new TypeError("receipt body must be a CBOR map");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const key of value.keys()) {
|
|
62
|
+
if (typeof key !== "string" || !RECEIPT_KEYS.has(key)) {
|
|
63
|
+
throw new TypeError(`receipt body contains unknown field ${String(key)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const timestamp = value.get("timestamp");
|
|
68
|
+
const receipt = {
|
|
69
|
+
"agent-identifier": expectString(value, "agent-identifier"),
|
|
70
|
+
"action-type": expectString(value, "action-type"),
|
|
71
|
+
"action-input-hash": expectBytes(value, "action-input-hash"),
|
|
72
|
+
"action-output-hash": expectBytes(value, "action-output-hash"),
|
|
73
|
+
"result-status": expectResultStatus(value, "result-status"),
|
|
74
|
+
timestamp: expectTag0Timestamp(timestamp),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (value.has("service-defined-fields")) {
|
|
78
|
+
const serviceFields = value.get("service-defined-fields");
|
|
79
|
+
if (!(serviceFields instanceof Map)) {
|
|
80
|
+
throw new TypeError("service-defined-fields must be a CBOR map");
|
|
81
|
+
}
|
|
82
|
+
assertServiceDefinedFields(serviceFields);
|
|
83
|
+
receipt["service-defined-fields"] = serviceFields;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
validateReceiptBody(receipt);
|
|
87
|
+
return receipt;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function validateReceiptBody(receipt ) {
|
|
91
|
+
assertObject(receipt, "receipt");
|
|
92
|
+
assertAgentIdentifier(receipt["agent-identifier"], "agent-identifier");
|
|
93
|
+
assertString(receipt["action-type"], "action-type");
|
|
94
|
+
assertSha256Digest(receipt["action-input-hash"], "action-input-hash");
|
|
95
|
+
assertSha256Digest(receipt["action-output-hash"], "action-output-hash");
|
|
96
|
+
assertResultStatus(receipt["result-status"], "result-status");
|
|
97
|
+
assertUtcTimestamp(receipt.timestamp, "timestamp");
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
receipt["result-status"] === "denied" &&
|
|
101
|
+
!bytesEqual(receipt["action-output-hash"], ZERO_SHA256_DIGEST)
|
|
102
|
+
) {
|
|
103
|
+
throw new TypeError("denied receipts must use all-zero action-output-hash");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (receipt["service-defined-fields"] !== undefined) {
|
|
107
|
+
if (!(receipt["service-defined-fields"] instanceof Map)) {
|
|
108
|
+
throw new TypeError("service-defined-fields must be a Map");
|
|
109
|
+
}
|
|
110
|
+
assertServiceDefinedFields(receipt["service-defined-fields"]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function expectString(map , key ) {
|
|
115
|
+
const value = map.get(key);
|
|
116
|
+
assertString(value, key);
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function expectBytes(map , key ) {
|
|
121
|
+
const value = map.get(key);
|
|
122
|
+
assertSha256Digest(value, key);
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function expectResultStatus(map , key ) {
|
|
127
|
+
const value = map.get(key);
|
|
128
|
+
assertResultStatus(value, key);
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function expectTag0Timestamp(value ) {
|
|
133
|
+
if (!isTagged(value) || value.tag !== 0 || typeof value.value !== "string") {
|
|
134
|
+
throw new TypeError("timestamp must be CBOR tag 0 containing a string");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
assertUtcTimestamp(value.value, "timestamp");
|
|
138
|
+
return value.value;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function assertObject(value , name ) {
|
|
142
|
+
if (typeof value !== "object" || value === null) {
|
|
143
|
+
throw new TypeError(`${name} must be an object`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function assertString(value , name ) {
|
|
148
|
+
if (typeof value !== "string") {
|
|
149
|
+
throw new TypeError(`${name} must be a string`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function assertSha256Digest(value , name ) {
|
|
154
|
+
if (!(value instanceof Uint8Array) || value.byteLength !== 32) {
|
|
155
|
+
throw new TypeError(`${name} must be a 32-byte SHA-256 digest`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function assertResultStatus(value , name ) {
|
|
160
|
+
if (typeof value !== "string" || !RESULT_STATUSES.has(value )) {
|
|
161
|
+
throw new TypeError(`${name} must be success, error, or denied`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function assertUtcTimestamp(value , name ) {
|
|
166
|
+
assertString(value, name);
|
|
167
|
+
|
|
168
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(value)) {
|
|
169
|
+
throw new TypeError(`${name} must be an RFC 3339 UTC timestamp`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const parsed = Date.parse(value);
|
|
173
|
+
if (Number.isNaN(parsed)) {
|
|
174
|
+
throw new TypeError(`${name} must be a valid timestamp`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function assertServiceDefinedFields(value ) {
|
|
179
|
+
for (const [key, entryValue] of value.entries()) {
|
|
180
|
+
if (typeof key !== "string") {
|
|
181
|
+
throw new TypeError("service-defined-fields keys must be service identifiers");
|
|
182
|
+
}
|
|
183
|
+
if (!(entryValue instanceof Map)) {
|
|
184
|
+
throw new TypeError("service-defined-fields values must be CBOR maps");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isTagged(value ) {
|
|
190
|
+
return (
|
|
191
|
+
typeof value === "object" &&
|
|
192
|
+
value !== null &&
|
|
193
|
+
"tag" in value &&
|
|
194
|
+
"value" in value
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function bytesEqual(a , b ) {
|
|
199
|
+
if (a.byteLength !== b.byteLength) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (let index = 0; index < a.byteLength; index += 1) {
|
|
204
|
+
if (a[index] !== b[index]) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertEd25519PublicKey,
|
|
3
|
+
assertEd25519Signature,
|
|
4
|
+
signEd25519,
|
|
5
|
+
verifyEd25519Signature,
|
|
6
|
+
} from "../crypto/ed25519.js";
|
|
7
|
+
import { toHex } from "../crypto/identifiers.js";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
const textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
32
|
+
const BASE64URL_32_BYTE_LENGTH = 43;
|
|
33
|
+
|
|
34
|
+
export function signRegistryJson(
|
|
35
|
+
registryBytes ,
|
|
36
|
+
trustRootPrivateKey ,
|
|
37
|
+
) {
|
|
38
|
+
assertBytes(registryBytes, "registryBytes");
|
|
39
|
+
return base64urlEncode(signEd25519(registryBytes, trustRootPrivateKey));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function loadSignedRegistry(
|
|
43
|
+
input ,
|
|
44
|
+
) {
|
|
45
|
+
verifyRegistrySignature(
|
|
46
|
+
input.registryBytes,
|
|
47
|
+
input.signatureBase64Url,
|
|
48
|
+
input.trustRootPublicKey,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return parseRegistry(input.registryBytes);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function verifyRegistrySignature(
|
|
55
|
+
registryBytes ,
|
|
56
|
+
signatureBase64Url ,
|
|
57
|
+
trustRootPublicKey ,
|
|
58
|
+
) {
|
|
59
|
+
assertBytes(registryBytes, "registryBytes");
|
|
60
|
+
assertEd25519PublicKey(trustRootPublicKey, "trustRootPublicKey");
|
|
61
|
+
const signature = base64urlDecode(signatureBase64Url, "registry signature");
|
|
62
|
+
assertEd25519Signature(signature, "registry signature");
|
|
63
|
+
|
|
64
|
+
if (!verifyEd25519Signature(registryBytes, signature, trustRootPublicKey)) {
|
|
65
|
+
throw new TypeError("registry signature verification failed");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function parseRegistry(registryBytes ) {
|
|
70
|
+
assertBytes(registryBytes, "registryBytes");
|
|
71
|
+
const parsed = JSON.parse(textDecoder.decode(registryBytes));
|
|
72
|
+
|
|
73
|
+
if (!isRecord(parsed)) {
|
|
74
|
+
throw new TypeError("registry must be a JSON object");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const entries = new Map ();
|
|
78
|
+
const revoked = parseRevoked(parsed.revoked);
|
|
79
|
+
|
|
80
|
+
for (const [kidHex, value] of Object.entries(parsed)) {
|
|
81
|
+
if (kidHex === "revoked") {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
assertKidHex(kidHex, "registry kid");
|
|
86
|
+
|
|
87
|
+
if (!isRecord(value)) {
|
|
88
|
+
throw new TypeError(`registry entry ${kidHex} must be an object`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const serviceIdentifier = value.service_identifier;
|
|
92
|
+
if (typeof serviceIdentifier !== "string" || serviceIdentifier.length === 0) {
|
|
93
|
+
throw new TypeError(`registry entry ${kidHex} service_identifier must be a non-empty string`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const encodedPublicKey = value.public_key_ed25519;
|
|
97
|
+
if (typeof encodedPublicKey !== "string") {
|
|
98
|
+
throw new TypeError(`registry entry ${kidHex} public_key_ed25519 must be a string`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const publicKeyEd25519 = base64urlDecodeFixed32(
|
|
102
|
+
encodedPublicKey,
|
|
103
|
+
`registry entry ${kidHex} public_key_ed25519`,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
entries.set(kidHex, {
|
|
107
|
+
kidHex,
|
|
108
|
+
serviceIdentifier,
|
|
109
|
+
publicKeyEd25519,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { entries, revoked };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resolveServiceKey(
|
|
117
|
+
registry ,
|
|
118
|
+
kid ,
|
|
119
|
+
) {
|
|
120
|
+
const kidHex = toHex(kid);
|
|
121
|
+
const entry = registry.entries.get(kidHex);
|
|
122
|
+
|
|
123
|
+
if (!entry) {
|
|
124
|
+
throw new TypeError(`unknown kid ${kidHex}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
kidHex: entry.kidHex,
|
|
129
|
+
serviceIdentifier: entry.serviceIdentifier,
|
|
130
|
+
publicKeyEd25519: new Uint8Array(entry.publicKeyEd25519),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function assertKeyNotRevoked(
|
|
135
|
+
registry ,
|
|
136
|
+
kid ,
|
|
137
|
+
integratedTime ,
|
|
138
|
+
) {
|
|
139
|
+
const kidHex = toHex(kid);
|
|
140
|
+
const revoked = registry.revoked.get(kidHex);
|
|
141
|
+
|
|
142
|
+
if (!revoked) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (integratedTime === undefined) {
|
|
147
|
+
throw new TypeError(`kid ${kidHex} is revoked and requires verifiable integrated time`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const integratedTimeMs =
|
|
151
|
+
integratedTime instanceof Date
|
|
152
|
+
? integratedTime.getTime()
|
|
153
|
+
: parseUtcTimestamp(integratedTime, "integratedTime");
|
|
154
|
+
const revokedAtMs = parseUtcTimestamp(revoked.revokedAt, `revoked_at for ${kidHex}`);
|
|
155
|
+
|
|
156
|
+
if (integratedTimeMs >= revokedAtMs) {
|
|
157
|
+
throw new TypeError(`kid ${kidHex} was revoked at ${revoked.revokedAt}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseRevoked(value ) {
|
|
162
|
+
const revoked = new Map ();
|
|
163
|
+
|
|
164
|
+
if (value === undefined) {
|
|
165
|
+
return revoked;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!isRecord(value)) {
|
|
169
|
+
throw new TypeError("registry revoked must be an object");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const [kidHex, entry] of Object.entries(value)) {
|
|
173
|
+
assertKidHex(kidHex, "revoked kid");
|
|
174
|
+
|
|
175
|
+
if (!isRecord(entry) || typeof entry.revoked_at !== "string") {
|
|
176
|
+
throw new TypeError(`revoked entry ${kidHex} must contain revoked_at`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
parseUtcTimestamp(entry.revoked_at, `revoked_at for ${kidHex}`);
|
|
180
|
+
revoked.set(kidHex, { kidHex, revokedAt: entry.revoked_at });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return revoked;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function base64urlEncode(bytes ) {
|
|
187
|
+
return Buffer.from(bytes).toString("base64url");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function base64urlDecodeFixed32(value , name ) {
|
|
191
|
+
if (value.length !== BASE64URL_32_BYTE_LENGTH) {
|
|
192
|
+
throw new TypeError(`${name} must be base64url encoding of 32 bytes`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const decoded = base64urlDecode(value, name);
|
|
196
|
+
assertEd25519PublicKey(decoded, name);
|
|
197
|
+
return decoded;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function base64urlDecode(value , name ) {
|
|
201
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) {
|
|
202
|
+
throw new TypeError(`${name} must be unpadded base64url`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return Uint8Array.from(Buffer.from(value, "base64url"));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function assertKidHex(value , name ) {
|
|
209
|
+
if (!/^(?:[0-9a-f]{2})+$/.test(value)) {
|
|
210
|
+
throw new TypeError(`${name} must be lowercase even-length hex`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseUtcTimestamp(value , name ) {
|
|
215
|
+
if (
|
|
216
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(value) ||
|
|
217
|
+
Number.isNaN(Date.parse(value))
|
|
218
|
+
) {
|
|
219
|
+
throw new TypeError(`${name} must be an RFC 3339 UTC timestamp`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return Date.parse(value);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function assertBytes(value , name ) {
|
|
226
|
+
if (!(value instanceof Uint8Array)) {
|
|
227
|
+
throw new TypeError(`${name} must be a Uint8Array`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isRecord(value ) {
|
|
232
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
233
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createSelloService } from "./service.js";
|
|
2
|
+
import * as logs from "./logs.js";
|
|
3
|
+
|
|
4
|
+
export const sello = {
|
|
5
|
+
service: createSelloService,
|
|
6
|
+
logs,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export { createSelloService };
|
|
10
|
+
export {
|
|
11
|
+
decodeBase64url,
|
|
12
|
+
encodeOwnerKey,
|
|
13
|
+
encodeServiceKey,
|
|
14
|
+
normalizeEd25519PrivateKey,
|
|
15
|
+
normalizeEd25519PublicKey,
|
|
16
|
+
normalizeHpkePrivateKey,
|
|
17
|
+
normalizeKid,
|
|
18
|
+
normalizeServiceKey,
|
|
19
|
+
} from "./keys.js";
|
|
20
|
+
export * from "./logs.js";
|
|
21
|
+
export * from "./publisher.js";
|
|
22
|
+
export * from "./service.js";
|
package/dist/sdk/keys.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertEd25519PrivateKey,
|
|
3
|
+
assertEd25519PublicKey,
|
|
4
|
+
} from "../crypto/ed25519.js";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
const textEncoder = new TextEncoder();
|
|
21
|
+
|
|
22
|
+
export function normalizeServiceKey(
|
|
23
|
+
input ,
|
|
24
|
+
fallbackKid ,
|
|
25
|
+
) {
|
|
26
|
+
if (input === undefined) {
|
|
27
|
+
throw new TypeError("SELLO_SERVICE_KEY is required");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (typeof input === "object" && !(input instanceof Uint8Array)) {
|
|
31
|
+
const kid = normalizeKid(input.kid, "serviceKey.kid");
|
|
32
|
+
const privateKey = normalizeEd25519PrivateKey(
|
|
33
|
+
input.privateKey,
|
|
34
|
+
"serviceKey.privateKey",
|
|
35
|
+
);
|
|
36
|
+
return { kid, privateKey };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof input !== "string") {
|
|
40
|
+
throw new TypeError("serviceKey must be a string or { kid, privateKey }");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const encoded = stripKnownServiceKeyPrefix(input);
|
|
44
|
+
const separator = encoded.indexOf(".");
|
|
45
|
+
if (separator !== -1) {
|
|
46
|
+
const kid = decodeBase64url(encoded.slice(0, separator), "service key kid");
|
|
47
|
+
const privateKey = normalizeEd25519PrivateKey(
|
|
48
|
+
encoded.slice(separator + 1),
|
|
49
|
+
"service key private key",
|
|
50
|
+
);
|
|
51
|
+
return { kid, privateKey };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (fallbackKid === undefined) {
|
|
55
|
+
throw new TypeError(
|
|
56
|
+
"SELLO_SERVICE_KEY must include a kid, or SELLO_SERVICE_KID must be set",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
kid: normalizeKid(fallbackKid, "service kid"),
|
|
62
|
+
privateKey: normalizeEd25519PrivateKey(input, "service key private key"),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function encodeServiceKey(
|
|
67
|
+
kid ,
|
|
68
|
+
privateKey ,
|
|
69
|
+
prefix = "sello_dev",
|
|
70
|
+
) {
|
|
71
|
+
assertBytes(kid, "kid");
|
|
72
|
+
assertEd25519PrivateKey(privateKey, "privateKey");
|
|
73
|
+
return `${prefix}_${base64urlEncode(kid)}.${base64urlEncode(privateKey)}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function normalizeKid(input , name = "kid") {
|
|
77
|
+
if (input instanceof Uint8Array) {
|
|
78
|
+
if (input.byteLength === 0) {
|
|
79
|
+
throw new TypeError(`${name} must not be empty`);
|
|
80
|
+
}
|
|
81
|
+
return new Uint8Array(input);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (typeof input !== "string" || input.length === 0) {
|
|
85
|
+
throw new TypeError(`${name} must be a non-empty string or Uint8Array`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return textEncoder.encode(input);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function normalizeEd25519PrivateKey(
|
|
92
|
+
input ,
|
|
93
|
+
name = "privateKey",
|
|
94
|
+
) {
|
|
95
|
+
const key = normalizeFixedBase64urlKey(input, 32, name);
|
|
96
|
+
assertEd25519PrivateKey(key, name);
|
|
97
|
+
return key;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function normalizeEd25519PublicKey(
|
|
101
|
+
input ,
|
|
102
|
+
name = "publicKey",
|
|
103
|
+
) {
|
|
104
|
+
const key = normalizeFixedBase64urlKey(input, 32, name);
|
|
105
|
+
assertEd25519PublicKey(key, name);
|
|
106
|
+
return key;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function normalizeHpkePrivateKey(
|
|
110
|
+
input ,
|
|
111
|
+
name = "ownerPrivateKey",
|
|
112
|
+
) {
|
|
113
|
+
return normalizeFixedBase64urlKey(stripKnownOwnerKeyPrefix(input), 32, name);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function encodeOwnerKey(privateKey , prefix = "sello_owner_dev") {
|
|
117
|
+
assertByteLength(privateKey, 32, "privateKey");
|
|
118
|
+
return `${prefix}_${base64urlEncode(privateKey)}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function base64urlEncode(bytes ) {
|
|
122
|
+
assertBytes(bytes, "bytes");
|
|
123
|
+
return Buffer.from(bytes).toString("base64url");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function decodeBase64url(value , name = "value") {
|
|
127
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) {
|
|
128
|
+
throw new TypeError(`${name} must be unpadded base64url`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return Uint8Array.from(Buffer.from(value, "base64url"));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeFixedBase64urlKey(
|
|
135
|
+
input ,
|
|
136
|
+
length ,
|
|
137
|
+
name ,
|
|
138
|
+
) {
|
|
139
|
+
if (input instanceof Uint8Array) {
|
|
140
|
+
assertByteLength(input, length, name);
|
|
141
|
+
return new Uint8Array(input);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (typeof input !== "string") {
|
|
145
|
+
throw new TypeError(`${name} must be a string or Uint8Array`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const decoded = decodeBase64url(input, name);
|
|
149
|
+
assertByteLength(decoded, length, name);
|
|
150
|
+
return decoded;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function stripKnownServiceKeyPrefix(input ) {
|
|
154
|
+
for (const prefix of ["sello_dev_", "sello_live_local_"]) {
|
|
155
|
+
if (input.startsWith(prefix)) {
|
|
156
|
+
return input.slice(prefix.length);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return input;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function stripKnownOwnerKeyPrefix(input ) {
|
|
164
|
+
if (typeof input !== "string") {
|
|
165
|
+
return input;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const prefix of ["sello_owner_dev_", "sello_owner_live_"]) {
|
|
169
|
+
if (input.startsWith(prefix)) {
|
|
170
|
+
return input.slice(prefix.length);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return input;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function assertByteLength(
|
|
178
|
+
value ,
|
|
179
|
+
length ,
|
|
180
|
+
name ,
|
|
181
|
+
) {
|
|
182
|
+
if (!(value instanceof Uint8Array) || value.byteLength !== length) {
|
|
183
|
+
throw new TypeError(`${name} must be a ${length}-byte Uint8Array`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function assertBytes(value , name ) {
|
|
188
|
+
if (!(value instanceof Uint8Array)) {
|
|
189
|
+
throw new TypeError(`${name} must be a Uint8Array`);
|
|
190
|
+
}
|
|
191
|
+
}
|