@orangecheck/sdk 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/attestation.js +26 -2
- package/dist/attestation.js.map +1 -1
- package/dist/attestation.mjs +26 -2
- package/dist/attestation.mjs.map +1 -1
- package/dist/canonical.js +27 -5
- package/dist/canonical.js.map +1 -1
- package/dist/canonical.mjs +27 -5
- package/dist/canonical.mjs.map +1 -1
- package/dist/identity.js +130 -44
- package/dist/identity.js.map +1 -1
- package/dist/identity.mjs +130 -44
- package/dist/identity.mjs.map +1 -1
- package/dist/index.js +172 -57
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +172 -57
- package/dist/index.mjs.map +1 -1
- package/dist/verify.js +42 -8
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +42 -8
- package/dist/verify.mjs.map +1 -1
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Buffer } from 'buffer';
|
|
2
2
|
import { Verifier } from 'bip322-js';
|
|
3
3
|
import * as bitcoinMessage from 'bitcoinjs-message';
|
|
4
|
+
import { schnorr } from '@noble/curves/secp256k1';
|
|
5
|
+
import { sha256 } from '@noble/hashes/sha2';
|
|
6
|
+
import { bech32 } from '@scure/base';
|
|
4
7
|
|
|
5
8
|
// src/canonical.ts
|
|
6
9
|
function toLowerHex(bytes) {
|
|
@@ -15,15 +18,27 @@ function randomNonce16BHexLower() {
|
|
|
15
18
|
}
|
|
16
19
|
} catch {
|
|
17
20
|
}
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
throw new Error("crypto.getRandomValues unavailable \u2014 refusing to mint a weak nonce");
|
|
22
|
+
}
|
|
23
|
+
var IDENTITY_PROTOCOL_RE = /^[a-z][a-z0-9_-]*$/;
|
|
24
|
+
var IDENTITY_FORBIDDEN_CHARS = /[\r\n,]/;
|
|
25
|
+
function assertSafeIdentity({ protocol, identifier }) {
|
|
26
|
+
if (!IDENTITY_PROTOCOL_RE.test(protocol)) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`invalid identity protocol: ${JSON.stringify(protocol)} \u2014 must match /^[a-z][a-z0-9_-]*$/`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (!identifier || IDENTITY_FORBIDDEN_CHARS.test(identifier)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`invalid identity identifier: ${JSON.stringify(identifier)} \u2014 must be non-empty and contain no newlines or commas`
|
|
34
|
+
);
|
|
20
35
|
}
|
|
21
|
-
return toLowerHex(arr);
|
|
22
36
|
}
|
|
23
37
|
function formatIdentities(identities = []) {
|
|
24
38
|
if (identities.length === 0) {
|
|
25
39
|
return "";
|
|
26
40
|
}
|
|
41
|
+
for (const id of identities) assertSafeIdentity(id);
|
|
27
42
|
const formatted = identities.map(({ protocol, identifier }) => `${protocol}:${identifier}`).sort();
|
|
28
43
|
return formatted.join(",");
|
|
29
44
|
}
|
|
@@ -31,7 +46,10 @@ function parseIdentities(identitiesStr) {
|
|
|
31
46
|
if (!identitiesStr || identitiesStr.trim() === "") {
|
|
32
47
|
return [];
|
|
33
48
|
}
|
|
34
|
-
|
|
49
|
+
if (IDENTITY_FORBIDDEN_CHARS.test(identitiesStr.replace(/,/g, ""))) {
|
|
50
|
+
throw new Error("identities field contains forbidden characters (newline)");
|
|
51
|
+
}
|
|
52
|
+
const bindings = identitiesStr.split(",").map((binding) => {
|
|
35
53
|
const colonIndex = binding.indexOf(":");
|
|
36
54
|
if (colonIndex === -1) {
|
|
37
55
|
throw new Error(`Invalid identity binding format: ${binding}`);
|
|
@@ -40,7 +58,14 @@ function parseIdentities(identitiesStr) {
|
|
|
40
58
|
protocol: binding.substring(0, colonIndex).trim(),
|
|
41
59
|
identifier: binding.substring(colonIndex + 1).trim()
|
|
42
60
|
};
|
|
43
|
-
})
|
|
61
|
+
});
|
|
62
|
+
for (const b of bindings) {
|
|
63
|
+
if (!b.protocol || !b.identifier) {
|
|
64
|
+
throw new Error(`Invalid identity binding: ${b.protocol}:${b.identifier}`);
|
|
65
|
+
}
|
|
66
|
+
assertSafeIdentity(b);
|
|
67
|
+
}
|
|
68
|
+
return bindings;
|
|
44
69
|
}
|
|
45
70
|
function buildCanonicalMessage({ address, identities }, extensions = {}) {
|
|
46
71
|
const nonce = randomNonce16BHexLower();
|
|
@@ -777,7 +802,13 @@ async function verifySignature({
|
|
|
777
802
|
}
|
|
778
803
|
function parseCanonicalMessage(msg) {
|
|
779
804
|
const lines = msg.split("\n").map((l) => l.trim());
|
|
780
|
-
const getVal = (prefix) =>
|
|
805
|
+
const getVal = (prefix) => {
|
|
806
|
+
const needle = prefix + ":";
|
|
807
|
+
const hit = lines.find((l) => l.toLowerCase().startsWith(needle));
|
|
808
|
+
if (!hit) return void 0;
|
|
809
|
+
const idx = hit.indexOf(":");
|
|
810
|
+
return hit.slice(idx + 1).trim();
|
|
811
|
+
};
|
|
781
812
|
const address = getVal("address") || "";
|
|
782
813
|
const identitiesStr = getVal("identities") || "";
|
|
783
814
|
let identities = [];
|
|
@@ -832,8 +863,7 @@ async function getAddressUtxos(address, network, opts) {
|
|
|
832
863
|
const resp = await fetch(url, {
|
|
833
864
|
method: "GET",
|
|
834
865
|
headers: { Accept: "application/json" },
|
|
835
|
-
signal: AbortSignal.timeout(
|
|
836
|
-
// 10s timeout
|
|
866
|
+
signal: AbortSignal.timeout(5e3)
|
|
837
867
|
});
|
|
838
868
|
if (!resp.ok) {
|
|
839
869
|
throw new Error(`HTTP ${resp.status}`);
|
|
@@ -938,13 +968,18 @@ async function verify2(input, opts = {}) {
|
|
|
938
968
|
codes.push("network_testmode");
|
|
939
969
|
return { ok: false, codes, network, attestation_id };
|
|
940
970
|
}
|
|
941
|
-
if (opts.expectedAud
|
|
942
|
-
|
|
971
|
+
if (opts.expectedAud) {
|
|
972
|
+
const aud = parsed.extensions["aud"];
|
|
973
|
+
if (!aud || aud !== opts.expectedAud) {
|
|
974
|
+
codes.push("aud_mismatch");
|
|
975
|
+
return { ok: false, codes, network, attestation_id };
|
|
976
|
+
}
|
|
943
977
|
}
|
|
944
978
|
if (parsed.extensions["expires"]) {
|
|
945
979
|
const exp = Date.parse(parsed.extensions["expires"]);
|
|
946
980
|
if (!Number.isNaN(exp) && exp < Date.now()) {
|
|
947
981
|
codes.push("expired");
|
|
982
|
+
return { ok: false, codes, network, attestation_id };
|
|
948
983
|
}
|
|
949
984
|
}
|
|
950
985
|
const sigCode = await verifySignature({
|
|
@@ -1022,7 +1057,7 @@ async function verify2(input, opts = {}) {
|
|
|
1022
1057
|
metrics
|
|
1023
1058
|
};
|
|
1024
1059
|
} catch (utxoError) {
|
|
1025
|
-
|
|
1060
|
+
log4.error({ err: utxoError }, "UTXO fetch failed");
|
|
1026
1061
|
codes.push("bad_request");
|
|
1027
1062
|
return { ok: false, codes, network, attestation_id };
|
|
1028
1063
|
}
|
|
@@ -1272,9 +1307,25 @@ var identityVerificationLimiter = createRateLimiter(5, 6e4);
|
|
|
1272
1307
|
|
|
1273
1308
|
// src/identity/dns.ts
|
|
1274
1309
|
var log5 = createLogger("identity-verification/dns");
|
|
1310
|
+
var PRIVATE_HOST_RE = /^(?:localhost|.*\.local|.*\.internal|.*\.localhost|.*\.lan|.*\.home|.*\.arpa|\[?::1\]?|\[?::\]?)$/i;
|
|
1311
|
+
var PRIVATE_IPV4_RE = /^(?:10\.|127\.|169\.254\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.|0\.|100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.|22[4-9]\.|23\d\.|24\d\.|25[0-5]\.)/;
|
|
1312
|
+
var WELL_KNOWN_TIMEOUT_MS = 5e3;
|
|
1313
|
+
function isSafePublicHost(host) {
|
|
1314
|
+
const h = host.toLowerCase();
|
|
1315
|
+
if (PRIVATE_HOST_RE.test(h)) return false;
|
|
1316
|
+
if (PRIVATE_IPV4_RE.test(h)) return false;
|
|
1317
|
+
if (h.startsWith("[") || /^[0-9a-f:]+$/.test(h)) return false;
|
|
1318
|
+
return true;
|
|
1319
|
+
}
|
|
1275
1320
|
async function verifyDnsIdentity(attestationId, domain) {
|
|
1276
1321
|
log5.info({ attestationId, domain }, "Verifying DNS identity");
|
|
1277
1322
|
const cleanDomain = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
1323
|
+
if (!isValidDomain(cleanDomain)) {
|
|
1324
|
+
return { verified: false, error: `invalid domain format: ${cleanDomain}` };
|
|
1325
|
+
}
|
|
1326
|
+
if (!isSafePublicHost(cleanDomain)) {
|
|
1327
|
+
return { verified: false, error: "domain resolves to a disallowed private host" };
|
|
1328
|
+
}
|
|
1278
1329
|
const wellKnownResult = await checkWellKnownFile(attestationId, cleanDomain);
|
|
1279
1330
|
if (wellKnownResult.verified) {
|
|
1280
1331
|
return wellKnownResult;
|
|
@@ -1286,38 +1337,31 @@ async function verifyDnsIdentity(attestationId, domain) {
|
|
|
1286
1337
|
};
|
|
1287
1338
|
}
|
|
1288
1339
|
async function checkWellKnownFile(attestationId, domain) {
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
verified: true,
|
|
1311
|
-
method: "well-known",
|
|
1312
|
-
url
|
|
1313
|
-
};
|
|
1314
|
-
} else {
|
|
1315
|
-
log5.info({ domain, url }, "Well-known file exists but no attestation ID found");
|
|
1316
|
-
}
|
|
1340
|
+
const url = `https://${domain}/.well-known/orangecheck.txt`;
|
|
1341
|
+
try {
|
|
1342
|
+
log5.info({ url }, "Checking well-known file");
|
|
1343
|
+
const response = await fetch(url, {
|
|
1344
|
+
method: "GET",
|
|
1345
|
+
headers: { Accept: "text/plain" },
|
|
1346
|
+
// Don't follow redirects to other domains
|
|
1347
|
+
redirect: "manual",
|
|
1348
|
+
signal: AbortSignal.timeout(WELL_KNOWN_TIMEOUT_MS)
|
|
1349
|
+
});
|
|
1350
|
+
if (response.ok) {
|
|
1351
|
+
const content = await response.text();
|
|
1352
|
+
if (content.includes(attestationId)) {
|
|
1353
|
+
log5.info({ domain, url }, "Found attestation ID in well-known file");
|
|
1354
|
+
return {
|
|
1355
|
+
verified: true,
|
|
1356
|
+
method: "well-known",
|
|
1357
|
+
url
|
|
1358
|
+
};
|
|
1359
|
+
} else {
|
|
1360
|
+
log5.info({ domain, url }, "Well-known file exists but no attestation ID found");
|
|
1317
1361
|
}
|
|
1318
|
-
} catch (err) {
|
|
1319
|
-
log5.warn({ error: err, url }, "Failed to fetch well-known file");
|
|
1320
1362
|
}
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
log5.warn({ error: err, url }, "Failed to fetch well-known file");
|
|
1321
1365
|
}
|
|
1322
1366
|
return {
|
|
1323
1367
|
verified: false,
|
|
@@ -1390,14 +1434,32 @@ function isDnsIdentityVerified(attestationId, _domain, cachedContent) {
|
|
|
1390
1434
|
|
|
1391
1435
|
// src/identity/github.ts
|
|
1392
1436
|
var log6 = createLogger("identity-verification/github");
|
|
1437
|
+
var GITHUB_GIST_RAW_HOST = "gist.githubusercontent.com";
|
|
1438
|
+
var MAX_GISTS_SCANNED = 30;
|
|
1439
|
+
var MAX_FILES_PER_GIST = 10;
|
|
1440
|
+
var FETCH_TIMEOUT_MS = 5e3;
|
|
1441
|
+
var GITHUB_USERNAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/;
|
|
1442
|
+
function isSafeGistRawUrl(raw) {
|
|
1443
|
+
try {
|
|
1444
|
+
const u = new URL(raw);
|
|
1445
|
+
return u.protocol === "https:" && u.hostname === GITHUB_GIST_RAW_HOST;
|
|
1446
|
+
} catch {
|
|
1447
|
+
return false;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1393
1450
|
async function verifyGitHubIdentity(attestationId, username) {
|
|
1394
1451
|
log6.info({ attestationId, username }, "Verifying GitHub identity");
|
|
1452
|
+
if (!GITHUB_USERNAME_RE.test(username)) {
|
|
1453
|
+
return { verified: false, error: "invalid GitHub username format" };
|
|
1454
|
+
}
|
|
1395
1455
|
try {
|
|
1396
|
-
const response = await fetch(
|
|
1397
|
-
|
|
1398
|
-
|
|
1456
|
+
const response = await fetch(
|
|
1457
|
+
`https://api.github.com/users/${encodeURIComponent(username)}/gists?per_page=${MAX_GISTS_SCANNED}`,
|
|
1458
|
+
{
|
|
1459
|
+
headers: { Accept: "application/vnd.github.v3+json" },
|
|
1460
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1399
1461
|
}
|
|
1400
|
-
|
|
1462
|
+
);
|
|
1401
1463
|
if (!response.ok) {
|
|
1402
1464
|
if (response.status === 404) {
|
|
1403
1465
|
return {
|
|
@@ -1412,7 +1474,7 @@ async function verifyGitHubIdentity(attestationId, username) {
|
|
|
1412
1474
|
}
|
|
1413
1475
|
const gists = await response.json();
|
|
1414
1476
|
log6.info({ username, gistCount: gists.length }, "Fetched GitHub gists");
|
|
1415
|
-
for (const gist of gists) {
|
|
1477
|
+
for (const gist of gists.slice(0, MAX_GISTS_SCANNED)) {
|
|
1416
1478
|
if (gist.description?.includes(attestationId)) {
|
|
1417
1479
|
log6.info({ username, gistId: gist.id }, "Found attestation ID in gist description");
|
|
1418
1480
|
return {
|
|
@@ -1421,7 +1483,8 @@ async function verifyGitHubIdentity(attestationId, username) {
|
|
|
1421
1483
|
gistId: gist.id
|
|
1422
1484
|
};
|
|
1423
1485
|
}
|
|
1424
|
-
|
|
1486
|
+
const files = Object.entries(gist.files).slice(0, MAX_FILES_PER_GIST);
|
|
1487
|
+
for (const [filename, file] of files) {
|
|
1425
1488
|
if (file.content && file.content.includes(attestationId)) {
|
|
1426
1489
|
log6.info(
|
|
1427
1490
|
{ username, gistId: gist.id, filename },
|
|
@@ -1433,8 +1496,17 @@ async function verifyGitHubIdentity(attestationId, username) {
|
|
|
1433
1496
|
gistId: gist.id
|
|
1434
1497
|
};
|
|
1435
1498
|
}
|
|
1499
|
+
if (!isSafeGistRawUrl(file.raw_url)) {
|
|
1500
|
+
log6.warn(
|
|
1501
|
+
{ filename, rawUrl: file.raw_url },
|
|
1502
|
+
"skipping gist file: raw_url failed allow-list"
|
|
1503
|
+
);
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1436
1506
|
try {
|
|
1437
|
-
const fileResponse = await fetch(file.raw_url
|
|
1507
|
+
const fileResponse = await fetch(file.raw_url, {
|
|
1508
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1509
|
+
});
|
|
1438
1510
|
if (fileResponse.ok) {
|
|
1439
1511
|
const content = await fileResponse.text();
|
|
1440
1512
|
if (content.includes(attestationId)) {
|
|
@@ -1512,15 +1584,38 @@ function isGitHubIdentityVerified(attestationId, _username, cachedGists) {
|
|
|
1512
1584
|
function getGistCreationUrl() {
|
|
1513
1585
|
return "https://gist.github.com/";
|
|
1514
1586
|
}
|
|
1515
|
-
|
|
1516
|
-
// src/identity/nostr.ts
|
|
1517
1587
|
var log7 = createLogger("identity-verification/nostr");
|
|
1588
|
+
var HEX_64_RE = /^[0-9a-f]{64}$/;
|
|
1589
|
+
function toHex(bytes) {
|
|
1590
|
+
let out = "";
|
|
1591
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
1592
|
+
return out;
|
|
1593
|
+
}
|
|
1594
|
+
function fromHex(hex) {
|
|
1595
|
+
if (hex.length % 2 !== 0) throw new Error("odd-length hex");
|
|
1596
|
+
const out = new Uint8Array(hex.length / 2);
|
|
1597
|
+
for (let i = 0; i < out.length; i++) {
|
|
1598
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
1599
|
+
}
|
|
1600
|
+
return out;
|
|
1601
|
+
}
|
|
1518
1602
|
function npubToHex(npub) {
|
|
1519
|
-
|
|
1520
|
-
|
|
1603
|
+
const trimmed = npub.trim();
|
|
1604
|
+
if (HEX_64_RE.test(trimmed.toLowerCase())) {
|
|
1605
|
+
return trimmed.toLowerCase();
|
|
1606
|
+
}
|
|
1607
|
+
if (!trimmed.toLowerCase().startsWith("npub1")) {
|
|
1608
|
+
throw new Error(`not an npub: ${trimmed.slice(0, 12)}\u2026`);
|
|
1609
|
+
}
|
|
1610
|
+
const decoded = bech32.decode(trimmed.toLowerCase(), 1023);
|
|
1611
|
+
if (decoded.prefix !== "npub") {
|
|
1612
|
+
throw new Error(`bech32 prefix mismatch: expected npub, got ${decoded.prefix}`);
|
|
1613
|
+
}
|
|
1614
|
+
const bytes = bech32.fromWords(decoded.words);
|
|
1615
|
+
if (bytes.length !== 32) {
|
|
1616
|
+
throw new Error(`invalid npub payload length: ${bytes.length}`);
|
|
1521
1617
|
}
|
|
1522
|
-
|
|
1523
|
-
return npub;
|
|
1618
|
+
return toHex(Uint8Array.from(bytes));
|
|
1524
1619
|
}
|
|
1525
1620
|
async function queryNostrEvents(options) {
|
|
1526
1621
|
const { authors, kinds, search, relays = DEFAULT_RELAYS, limit = 100 } = options;
|
|
@@ -1591,11 +1686,31 @@ async function queryNostrEvents(options) {
|
|
|
1591
1686
|
return events;
|
|
1592
1687
|
}
|
|
1593
1688
|
async function verifyNostrEventSignature(event) {
|
|
1594
|
-
|
|
1689
|
+
try {
|
|
1690
|
+
if (!event.id || !event.sig || !event.pubkey || typeof event.id !== "string" || typeof event.sig !== "string" || typeof event.pubkey !== "string" || event.sig.length !== 128 || !HEX_64_RE.test(event.id) || !HEX_64_RE.test(event.pubkey)) {
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
const preimage = JSON.stringify([
|
|
1694
|
+
0,
|
|
1695
|
+
event.pubkey,
|
|
1696
|
+
event.created_at,
|
|
1697
|
+
event.kind,
|
|
1698
|
+
event.tags ?? [],
|
|
1699
|
+
event.content ?? ""
|
|
1700
|
+
]);
|
|
1701
|
+
const computedId = toHex(sha256(new TextEncoder().encode(preimage)));
|
|
1702
|
+
if (computedId !== event.id.toLowerCase()) {
|
|
1703
|
+
log7.warn(
|
|
1704
|
+
{ eventId: event.id, computedId },
|
|
1705
|
+
"nostr event id does not match its content"
|
|
1706
|
+
);
|
|
1707
|
+
return false;
|
|
1708
|
+
}
|
|
1709
|
+
return schnorr.verify(fromHex(event.sig), fromHex(event.id), fromHex(event.pubkey));
|
|
1710
|
+
} catch (err) {
|
|
1711
|
+
log7.warn({ err }, "schnorr verify threw");
|
|
1595
1712
|
return false;
|
|
1596
1713
|
}
|
|
1597
|
-
log7.warn("Nostr signature verification not fully implemented");
|
|
1598
|
-
return true;
|
|
1599
1714
|
}
|
|
1600
1715
|
async function verifyNostrIdentity(attestationId, npub, relays) {
|
|
1601
1716
|
log7.info({ attestationId, npub }, "Verifying Nostr identity");
|