@orangecheck/sdk 0.1.5 → 1.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/README.md +9 -5
- package/dist/attestation.js +2 -2
- package/dist/attestation.js.map +1 -1
- package/dist/attestation.mjs +2 -2
- package/dist/attestation.mjs.map +1 -1
- package/dist/canonical.js +1 -1
- package/dist/canonical.js.map +1 -1
- package/dist/canonical.mjs +1 -1
- package/dist/canonical.mjs.map +1 -1
- package/dist/index.d.mts +45 -1
- package/dist/index.d.ts +45 -1
- package/dist/index.js +193 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +191 -8
- package/dist/index.mjs.map +1 -1
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs.map +1 -1
- package/package.json +101 -98
package/dist/index.js
CHANGED
|
@@ -134,7 +134,7 @@ async function createAttestationEnvelope(message, signature, scheme, address, id
|
|
|
134
134
|
const messageBytes = encoder.encode(message);
|
|
135
135
|
const base64 = btoa(String.fromCharCode(...messageBytes));
|
|
136
136
|
const message_b64url = base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
137
|
-
const verification_url = `https://ochk.io/attest/${attestation_id}`;
|
|
137
|
+
const verification_url = `https://attest.ochk.io/attest/${attestation_id}`;
|
|
138
138
|
return {
|
|
139
139
|
attestation_id,
|
|
140
140
|
scheme,
|
|
@@ -685,7 +685,7 @@ async function getAttestationsForIdentity(protocol, identifier, relays = DEFAULT
|
|
|
685
685
|
function formatIdentitiesForDisplay(identities) {
|
|
686
686
|
return identities.map((id) => `${id.protocol}:${id.identifier}`).join(", ");
|
|
687
687
|
}
|
|
688
|
-
function getVerificationUrl(attestationId, baseUrl = "https://ochk.io") {
|
|
688
|
+
function getVerificationUrl(attestationId, baseUrl = "https://attest.ochk.io") {
|
|
689
689
|
return `${baseUrl}/attest/${attestationId}`;
|
|
690
690
|
}
|
|
691
691
|
function extractAttestationIdFromUrl(url) {
|
|
@@ -1177,6 +1177,189 @@ async function check(params) {
|
|
|
1177
1177
|
...reasons.length ? { reasons } : {}
|
|
1178
1178
|
};
|
|
1179
1179
|
}
|
|
1180
|
+
var HEADER = "orangecheck-binding";
|
|
1181
|
+
var ACK = "I attest the keys named in this message are one principal.";
|
|
1182
|
+
var BINDING_KIND = 30079;
|
|
1183
|
+
function toHex(bytes) {
|
|
1184
|
+
let out = "";
|
|
1185
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
1186
|
+
return out;
|
|
1187
|
+
}
|
|
1188
|
+
function fromHex(hex) {
|
|
1189
|
+
const clean = hex.toLowerCase();
|
|
1190
|
+
if (clean.length % 2 !== 0 || /[^0-9a-f]/.test(clean)) {
|
|
1191
|
+
throw new Error("invalid hex");
|
|
1192
|
+
}
|
|
1193
|
+
const out = new Uint8Array(clean.length / 2);
|
|
1194
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
1195
|
+
return out;
|
|
1196
|
+
}
|
|
1197
|
+
function assertPrintableAscii(field, value) {
|
|
1198
|
+
for (let i = 0; i < value.length; i++) {
|
|
1199
|
+
const c = value.charCodeAt(i);
|
|
1200
|
+
if (c < 32 || c > 126) {
|
|
1201
|
+
throw new Error(
|
|
1202
|
+
`binding ${field} contains an unsafe character (0x${c.toString(16)}) \u2014 line-smuggling defense (SPEC-BINDING \xA73.5)`
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
function buildBindingMessage(input) {
|
|
1208
|
+
assertPrintableAscii("principal", input.principal);
|
|
1209
|
+
assertPrintableAscii("btc", input.btc);
|
|
1210
|
+
assertPrintableAscii("nostr", input.nostr);
|
|
1211
|
+
assertPrintableAscii("nonce", input.nonce);
|
|
1212
|
+
assertPrintableAscii("issued_at", input.issued_at);
|
|
1213
|
+
const core = [
|
|
1214
|
+
HEADER,
|
|
1215
|
+
"v: 1",
|
|
1216
|
+
`principal: ${input.principal}`,
|
|
1217
|
+
`btc: ${input.btc}`,
|
|
1218
|
+
`nostr: ${input.nostr}`,
|
|
1219
|
+
`nonce: ${input.nonce}`,
|
|
1220
|
+
`issued_at: ${input.issued_at}`,
|
|
1221
|
+
`ack: ${ACK}`
|
|
1222
|
+
];
|
|
1223
|
+
const ext = [];
|
|
1224
|
+
if (input.extensions) {
|
|
1225
|
+
for (const key of Object.keys(input.extensions).sort()) {
|
|
1226
|
+
const value = input.extensions[key];
|
|
1227
|
+
if (value == null) continue;
|
|
1228
|
+
if (!/^[a-z]+$/.test(key)) {
|
|
1229
|
+
throw new Error(`binding extension key "${key}" must be lowercase ASCII`);
|
|
1230
|
+
}
|
|
1231
|
+
assertPrintableAscii(`extension ${key}`, value);
|
|
1232
|
+
ext.push(`${key}: ${value}`);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return [...core, ...ext].join("\n") + "\n";
|
|
1236
|
+
}
|
|
1237
|
+
function bindingId(message) {
|
|
1238
|
+
return toHex(sha2_js.sha256(new TextEncoder().encode(message)));
|
|
1239
|
+
}
|
|
1240
|
+
function linePrefix(line, prefix) {
|
|
1241
|
+
if (typeof line !== "string" || !line.startsWith(prefix)) return null;
|
|
1242
|
+
return line.slice(prefix.length);
|
|
1243
|
+
}
|
|
1244
|
+
function nostrEventId(ev) {
|
|
1245
|
+
const serialized = JSON.stringify([
|
|
1246
|
+
0,
|
|
1247
|
+
ev.pubkey,
|
|
1248
|
+
ev.created_at,
|
|
1249
|
+
ev.kind,
|
|
1250
|
+
ev.tags,
|
|
1251
|
+
ev.content
|
|
1252
|
+
]);
|
|
1253
|
+
return toHex(sha2_js.sha256(new TextEncoder().encode(serialized)));
|
|
1254
|
+
}
|
|
1255
|
+
function npubToXOnlyHex(npub) {
|
|
1256
|
+
const decoded = base.bech32.decode(npub, 90);
|
|
1257
|
+
if (decoded.prefix !== "npub") throw new Error("not an npub");
|
|
1258
|
+
const bytes = Uint8Array.from(base.bech32.fromWords(decoded.words));
|
|
1259
|
+
if (bytes.length !== 32) throw new Error("npub payload is not 32 bytes");
|
|
1260
|
+
return toHex(bytes);
|
|
1261
|
+
}
|
|
1262
|
+
function verifyBinding(envelope) {
|
|
1263
|
+
const fail = (status) => ({
|
|
1264
|
+
valid: false,
|
|
1265
|
+
status
|
|
1266
|
+
});
|
|
1267
|
+
try {
|
|
1268
|
+
const msg = envelope.message;
|
|
1269
|
+
if (typeof msg !== "string" || msg.length === 0) return fail("malformed");
|
|
1270
|
+
const lines = msg.split("\n");
|
|
1271
|
+
if ((lines[0] ?? "") !== HEADER) return fail("bad_header");
|
|
1272
|
+
if (lines.length < 9 || lines[lines.length - 1] !== "") return fail("malformed");
|
|
1273
|
+
const body = lines.slice(0, -1);
|
|
1274
|
+
if (body.length < 8) return fail("malformed");
|
|
1275
|
+
if (body[1] !== "v: 1") return fail("malformed");
|
|
1276
|
+
if (body[7] !== `ack: ${ACK}`) return fail("bad_ack");
|
|
1277
|
+
const principal = linePrefix(body[2], "principal: ");
|
|
1278
|
+
const btc = linePrefix(body[3], "btc: ");
|
|
1279
|
+
const nostr = linePrefix(body[4], "nostr: ");
|
|
1280
|
+
const nonce = linePrefix(body[5], "nonce: ");
|
|
1281
|
+
const issuedAt = linePrefix(body[6], "issued_at: ");
|
|
1282
|
+
if (principal == null || btc == null || nostr == null || nonce == null || issuedAt == null) {
|
|
1283
|
+
return fail("malformed");
|
|
1284
|
+
}
|
|
1285
|
+
for (const value of [principal, btc, nostr, nonce, issuedAt]) {
|
|
1286
|
+
for (let i = 0; i < value.length; i++) {
|
|
1287
|
+
const c = value.charCodeAt(i);
|
|
1288
|
+
if (c < 32 || c > 126) return fail("malformed");
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (!/^did:oc:[0-9a-f]{32}$/.test(principal)) return fail("bad_principal");
|
|
1292
|
+
if (!/^[0-9a-f]{32}$/.test(nonce)) return fail("malformed");
|
|
1293
|
+
const ext = {};
|
|
1294
|
+
let prevKey = "";
|
|
1295
|
+
for (const line of body.slice(8)) {
|
|
1296
|
+
const idx = line.indexOf(": ");
|
|
1297
|
+
if (idx <= 0) return fail("malformed");
|
|
1298
|
+
const key = line.slice(0, idx);
|
|
1299
|
+
const value = line.slice(idx + 2);
|
|
1300
|
+
if (!/^[a-z]+$/.test(key)) return fail("malformed");
|
|
1301
|
+
if (key <= prevKey) return fail("ext_unsorted");
|
|
1302
|
+
prevKey = key;
|
|
1303
|
+
ext[key] = value;
|
|
1304
|
+
}
|
|
1305
|
+
const bid = bindingId(msg);
|
|
1306
|
+
if (envelope.binding_id && envelope.binding_id.toLowerCase() !== bid) {
|
|
1307
|
+
return fail("id_mismatch");
|
|
1308
|
+
}
|
|
1309
|
+
if (envelope.btc && envelope.btc !== btc) return fail("field_mismatch");
|
|
1310
|
+
if (envelope.nostr && envelope.nostr !== nostr) return fail("field_mismatch");
|
|
1311
|
+
if (envelope.principal && envelope.principal !== principal) return fail("field_mismatch");
|
|
1312
|
+
if (ext.expires) {
|
|
1313
|
+
const t = Date.parse(ext.expires);
|
|
1314
|
+
if (!Number.isNaN(t) && Date.now() > t) return fail("expired");
|
|
1315
|
+
}
|
|
1316
|
+
if (typeof envelope.btc_signature !== "string" || !envelope.btc_signature) {
|
|
1317
|
+
return fail("btc_sig_invalid");
|
|
1318
|
+
}
|
|
1319
|
+
const scheme = envelope.btc_scheme ?? "bip322";
|
|
1320
|
+
let btcOk = false;
|
|
1321
|
+
try {
|
|
1322
|
+
if (scheme === "bip322") {
|
|
1323
|
+
btcOk = bip322Js.Verifier.verifySignature(btc, msg, envelope.btc_signature);
|
|
1324
|
+
} else if (scheme === "legacy") {
|
|
1325
|
+
btcOk = bitcoinMessage__namespace.verify(msg, btc, envelope.btc_signature);
|
|
1326
|
+
} else {
|
|
1327
|
+
return fail("bad_scheme");
|
|
1328
|
+
}
|
|
1329
|
+
} catch {
|
|
1330
|
+
btcOk = false;
|
|
1331
|
+
}
|
|
1332
|
+
if (!btcOk) return fail("btc_sig_invalid");
|
|
1333
|
+
const ev = envelope.nostr_event;
|
|
1334
|
+
if (!ev || ev.kind !== BINDING_KIND || typeof ev.id !== "string") return fail("malformed");
|
|
1335
|
+
if (nostrEventId(ev) !== ev.id.toLowerCase()) return fail("nostr_id_invalid");
|
|
1336
|
+
let nostrSigOk = false;
|
|
1337
|
+
try {
|
|
1338
|
+
nostrSigOk = secp256k1_js.schnorr.verify(fromHex(ev.sig), fromHex(ev.id), fromHex(ev.pubkey));
|
|
1339
|
+
} catch {
|
|
1340
|
+
nostrSigOk = false;
|
|
1341
|
+
}
|
|
1342
|
+
if (!nostrSigOk) return fail("nostr_sig_invalid");
|
|
1343
|
+
let nostrXOnly;
|
|
1344
|
+
try {
|
|
1345
|
+
nostrXOnly = npubToXOnlyHex(nostr);
|
|
1346
|
+
} catch {
|
|
1347
|
+
return fail("nostr_key_mismatch");
|
|
1348
|
+
}
|
|
1349
|
+
if (nostrXOnly !== ev.pubkey.toLowerCase()) return fail("nostr_key_mismatch");
|
|
1350
|
+
let content;
|
|
1351
|
+
try {
|
|
1352
|
+
content = JSON.parse(ev.content);
|
|
1353
|
+
} catch {
|
|
1354
|
+
return fail("message_mismatch");
|
|
1355
|
+
}
|
|
1356
|
+
if (content.message !== msg) return fail("message_mismatch");
|
|
1357
|
+
if (content.btc_signature !== envelope.btc_signature) return fail("message_mismatch");
|
|
1358
|
+
return { valid: true, status: "binding_ok", binding_id: bid, principal, btc, nostr };
|
|
1359
|
+
} catch {
|
|
1360
|
+
return fail("malformed");
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1180
1363
|
var AUTH_HEADER = "orangecheck-auth";
|
|
1181
1364
|
var AUTH_ACK = "I authorize this session under the OrangeCheck auth challenge.";
|
|
1182
1365
|
function randomNonce() {
|
|
@@ -1632,12 +1815,12 @@ function getGistCreationUrl() {
|
|
|
1632
1815
|
}
|
|
1633
1816
|
var log7 = createLogger("identity-verification/nostr");
|
|
1634
1817
|
var HEX_64_RE = /^[0-9a-f]{64}$/;
|
|
1635
|
-
function
|
|
1818
|
+
function toHex2(bytes) {
|
|
1636
1819
|
let out = "";
|
|
1637
1820
|
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
1638
1821
|
return out;
|
|
1639
1822
|
}
|
|
1640
|
-
function
|
|
1823
|
+
function fromHex2(hex) {
|
|
1641
1824
|
if (hex.length % 2 !== 0) throw new Error("odd-length hex");
|
|
1642
1825
|
const out = new Uint8Array(hex.length / 2);
|
|
1643
1826
|
for (let i = 0; i < out.length; i++) {
|
|
@@ -1661,7 +1844,7 @@ function npubToHex(npub) {
|
|
|
1661
1844
|
if (bytes.length !== 32) {
|
|
1662
1845
|
throw new Error(`invalid npub payload length: ${bytes.length}`);
|
|
1663
1846
|
}
|
|
1664
|
-
return
|
|
1847
|
+
return toHex2(Uint8Array.from(bytes));
|
|
1665
1848
|
}
|
|
1666
1849
|
async function queryNostrEvents(options) {
|
|
1667
1850
|
const { authors, kinds, search, relays = DEFAULT_RELAYS, limit = 100 } = options;
|
|
@@ -1744,7 +1927,7 @@ async function verifyNostrEventSignature(event) {
|
|
|
1744
1927
|
event.tags ?? [],
|
|
1745
1928
|
event.content ?? ""
|
|
1746
1929
|
]);
|
|
1747
|
-
const computedId =
|
|
1930
|
+
const computedId = toHex2(sha2_js.sha256(new TextEncoder().encode(preimage)));
|
|
1748
1931
|
if (computedId !== event.id.toLowerCase()) {
|
|
1749
1932
|
log7.warn(
|
|
1750
1933
|
{ eventId: event.id, computedId },
|
|
@@ -1752,7 +1935,7 @@ async function verifyNostrEventSignature(event) {
|
|
|
1752
1935
|
);
|
|
1753
1936
|
return false;
|
|
1754
1937
|
}
|
|
1755
|
-
return secp256k1_js.schnorr.verify(
|
|
1938
|
+
return secp256k1_js.schnorr.verify(fromHex2(event.sig), fromHex2(event.id), fromHex2(event.pubkey));
|
|
1756
1939
|
} catch (err) {
|
|
1757
1940
|
log7.warn({ err }, "schnorr verify threw");
|
|
1758
1941
|
return false;
|
|
@@ -2114,6 +2297,8 @@ var STATUS_META = {
|
|
|
2114
2297
|
|
|
2115
2298
|
exports.DEFAULT_RELAYS = DEFAULT_RELAYS;
|
|
2116
2299
|
exports.STATUS_META = STATUS_META;
|
|
2300
|
+
exports.bindingId = bindingId;
|
|
2301
|
+
exports.buildBindingMessage = buildBindingMessage;
|
|
2117
2302
|
exports.buildCanonicalMessage = buildCanonicalMessage;
|
|
2118
2303
|
exports.check = check;
|
|
2119
2304
|
exports.computeAllScores = computeAllScores;
|
|
@@ -2165,6 +2350,7 @@ exports.supportsAutomaticVerification = supportsAutomaticVerification;
|
|
|
2165
2350
|
exports.verify = verify2;
|
|
2166
2351
|
exports.verifyAllIdentities = verifyAllIdentities;
|
|
2167
2352
|
exports.verifyAttestationById = verifyAttestationById;
|
|
2353
|
+
exports.verifyBinding = verifyBinding;
|
|
2168
2354
|
exports.verifyChallenge = verifyChallenge;
|
|
2169
2355
|
exports.verifyDnsIdentity = verifyDnsIdentity;
|
|
2170
2356
|
exports.verifyGitHubIdentity = verifyGitHubIdentity;
|