@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.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
var buffer = require('buffer');
|
|
4
4
|
var bip322Js = require('bip322-js');
|
|
5
5
|
var bitcoinMessage = require('bitcoinjs-message');
|
|
6
|
+
var secp256k1 = require('@noble/curves/secp256k1');
|
|
7
|
+
var sha2 = require('@noble/hashes/sha2');
|
|
8
|
+
var base = require('@scure/base');
|
|
6
9
|
|
|
7
10
|
function _interopNamespace(e) {
|
|
8
11
|
if (e && e.__esModule) return e;
|
|
@@ -37,15 +40,27 @@ function randomNonce16BHexLower() {
|
|
|
37
40
|
}
|
|
38
41
|
} catch {
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
throw new Error("crypto.getRandomValues unavailable \u2014 refusing to mint a weak nonce");
|
|
44
|
+
}
|
|
45
|
+
var IDENTITY_PROTOCOL_RE = /^[a-z][a-z0-9_-]*$/;
|
|
46
|
+
var IDENTITY_FORBIDDEN_CHARS = /[\r\n,]/;
|
|
47
|
+
function assertSafeIdentity({ protocol, identifier }) {
|
|
48
|
+
if (!IDENTITY_PROTOCOL_RE.test(protocol)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`invalid identity protocol: ${JSON.stringify(protocol)} \u2014 must match /^[a-z][a-z0-9_-]*$/`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (!identifier || IDENTITY_FORBIDDEN_CHARS.test(identifier)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`invalid identity identifier: ${JSON.stringify(identifier)} \u2014 must be non-empty and contain no newlines or commas`
|
|
56
|
+
);
|
|
42
57
|
}
|
|
43
|
-
return toLowerHex(arr);
|
|
44
58
|
}
|
|
45
59
|
function formatIdentities(identities = []) {
|
|
46
60
|
if (identities.length === 0) {
|
|
47
61
|
return "";
|
|
48
62
|
}
|
|
63
|
+
for (const id of identities) assertSafeIdentity(id);
|
|
49
64
|
const formatted = identities.map(({ protocol, identifier }) => `${protocol}:${identifier}`).sort();
|
|
50
65
|
return formatted.join(",");
|
|
51
66
|
}
|
|
@@ -53,7 +68,10 @@ function parseIdentities(identitiesStr) {
|
|
|
53
68
|
if (!identitiesStr || identitiesStr.trim() === "") {
|
|
54
69
|
return [];
|
|
55
70
|
}
|
|
56
|
-
|
|
71
|
+
if (IDENTITY_FORBIDDEN_CHARS.test(identitiesStr.replace(/,/g, ""))) {
|
|
72
|
+
throw new Error("identities field contains forbidden characters (newline)");
|
|
73
|
+
}
|
|
74
|
+
const bindings = identitiesStr.split(",").map((binding) => {
|
|
57
75
|
const colonIndex = binding.indexOf(":");
|
|
58
76
|
if (colonIndex === -1) {
|
|
59
77
|
throw new Error(`Invalid identity binding format: ${binding}`);
|
|
@@ -62,7 +80,14 @@ function parseIdentities(identitiesStr) {
|
|
|
62
80
|
protocol: binding.substring(0, colonIndex).trim(),
|
|
63
81
|
identifier: binding.substring(colonIndex + 1).trim()
|
|
64
82
|
};
|
|
65
|
-
})
|
|
83
|
+
});
|
|
84
|
+
for (const b of bindings) {
|
|
85
|
+
if (!b.protocol || !b.identifier) {
|
|
86
|
+
throw new Error(`Invalid identity binding: ${b.protocol}:${b.identifier}`);
|
|
87
|
+
}
|
|
88
|
+
assertSafeIdentity(b);
|
|
89
|
+
}
|
|
90
|
+
return bindings;
|
|
66
91
|
}
|
|
67
92
|
function buildCanonicalMessage({ address, identities }, extensions = {}) {
|
|
68
93
|
const nonce = randomNonce16BHexLower();
|
|
@@ -799,7 +824,13 @@ async function verifySignature({
|
|
|
799
824
|
}
|
|
800
825
|
function parseCanonicalMessage(msg) {
|
|
801
826
|
const lines = msg.split("\n").map((l) => l.trim());
|
|
802
|
-
const getVal = (prefix) =>
|
|
827
|
+
const getVal = (prefix) => {
|
|
828
|
+
const needle = prefix + ":";
|
|
829
|
+
const hit = lines.find((l) => l.toLowerCase().startsWith(needle));
|
|
830
|
+
if (!hit) return void 0;
|
|
831
|
+
const idx = hit.indexOf(":");
|
|
832
|
+
return hit.slice(idx + 1).trim();
|
|
833
|
+
};
|
|
803
834
|
const address = getVal("address") || "";
|
|
804
835
|
const identitiesStr = getVal("identities") || "";
|
|
805
836
|
let identities = [];
|
|
@@ -854,8 +885,7 @@ async function getAddressUtxos(address, network, opts) {
|
|
|
854
885
|
const resp = await fetch(url, {
|
|
855
886
|
method: "GET",
|
|
856
887
|
headers: { Accept: "application/json" },
|
|
857
|
-
signal: AbortSignal.timeout(
|
|
858
|
-
// 10s timeout
|
|
888
|
+
signal: AbortSignal.timeout(5e3)
|
|
859
889
|
});
|
|
860
890
|
if (!resp.ok) {
|
|
861
891
|
throw new Error(`HTTP ${resp.status}`);
|
|
@@ -960,13 +990,18 @@ async function verify2(input, opts = {}) {
|
|
|
960
990
|
codes.push("network_testmode");
|
|
961
991
|
return { ok: false, codes, network, attestation_id };
|
|
962
992
|
}
|
|
963
|
-
if (opts.expectedAud
|
|
964
|
-
|
|
993
|
+
if (opts.expectedAud) {
|
|
994
|
+
const aud = parsed.extensions["aud"];
|
|
995
|
+
if (!aud || aud !== opts.expectedAud) {
|
|
996
|
+
codes.push("aud_mismatch");
|
|
997
|
+
return { ok: false, codes, network, attestation_id };
|
|
998
|
+
}
|
|
965
999
|
}
|
|
966
1000
|
if (parsed.extensions["expires"]) {
|
|
967
1001
|
const exp = Date.parse(parsed.extensions["expires"]);
|
|
968
1002
|
if (!Number.isNaN(exp) && exp < Date.now()) {
|
|
969
1003
|
codes.push("expired");
|
|
1004
|
+
return { ok: false, codes, network, attestation_id };
|
|
970
1005
|
}
|
|
971
1006
|
}
|
|
972
1007
|
const sigCode = await verifySignature({
|
|
@@ -1044,7 +1079,7 @@ async function verify2(input, opts = {}) {
|
|
|
1044
1079
|
metrics
|
|
1045
1080
|
};
|
|
1046
1081
|
} catch (utxoError) {
|
|
1047
|
-
|
|
1082
|
+
log4.error({ err: utxoError }, "UTXO fetch failed");
|
|
1048
1083
|
codes.push("bad_request");
|
|
1049
1084
|
return { ok: false, codes, network, attestation_id };
|
|
1050
1085
|
}
|
|
@@ -1294,9 +1329,25 @@ var identityVerificationLimiter = createRateLimiter(5, 6e4);
|
|
|
1294
1329
|
|
|
1295
1330
|
// src/identity/dns.ts
|
|
1296
1331
|
var log5 = createLogger("identity-verification/dns");
|
|
1332
|
+
var PRIVATE_HOST_RE = /^(?:localhost|.*\.local|.*\.internal|.*\.localhost|.*\.lan|.*\.home|.*\.arpa|\[?::1\]?|\[?::\]?)$/i;
|
|
1333
|
+
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]\.)/;
|
|
1334
|
+
var WELL_KNOWN_TIMEOUT_MS = 5e3;
|
|
1335
|
+
function isSafePublicHost(host) {
|
|
1336
|
+
const h = host.toLowerCase();
|
|
1337
|
+
if (PRIVATE_HOST_RE.test(h)) return false;
|
|
1338
|
+
if (PRIVATE_IPV4_RE.test(h)) return false;
|
|
1339
|
+
if (h.startsWith("[") || /^[0-9a-f:]+$/.test(h)) return false;
|
|
1340
|
+
return true;
|
|
1341
|
+
}
|
|
1297
1342
|
async function verifyDnsIdentity(attestationId, domain) {
|
|
1298
1343
|
log5.info({ attestationId, domain }, "Verifying DNS identity");
|
|
1299
1344
|
const cleanDomain = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
1345
|
+
if (!isValidDomain(cleanDomain)) {
|
|
1346
|
+
return { verified: false, error: `invalid domain format: ${cleanDomain}` };
|
|
1347
|
+
}
|
|
1348
|
+
if (!isSafePublicHost(cleanDomain)) {
|
|
1349
|
+
return { verified: false, error: "domain resolves to a disallowed private host" };
|
|
1350
|
+
}
|
|
1300
1351
|
const wellKnownResult = await checkWellKnownFile(attestationId, cleanDomain);
|
|
1301
1352
|
if (wellKnownResult.verified) {
|
|
1302
1353
|
return wellKnownResult;
|
|
@@ -1308,38 +1359,31 @@ async function verifyDnsIdentity(attestationId, domain) {
|
|
|
1308
1359
|
};
|
|
1309
1360
|
}
|
|
1310
1361
|
async function checkWellKnownFile(attestationId, domain) {
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
verified: true,
|
|
1333
|
-
method: "well-known",
|
|
1334
|
-
url
|
|
1335
|
-
};
|
|
1336
|
-
} else {
|
|
1337
|
-
log5.info({ domain, url }, "Well-known file exists but no attestation ID found");
|
|
1338
|
-
}
|
|
1362
|
+
const url = `https://${domain}/.well-known/orangecheck.txt`;
|
|
1363
|
+
try {
|
|
1364
|
+
log5.info({ url }, "Checking well-known file");
|
|
1365
|
+
const response = await fetch(url, {
|
|
1366
|
+
method: "GET",
|
|
1367
|
+
headers: { Accept: "text/plain" },
|
|
1368
|
+
// Don't follow redirects to other domains
|
|
1369
|
+
redirect: "manual",
|
|
1370
|
+
signal: AbortSignal.timeout(WELL_KNOWN_TIMEOUT_MS)
|
|
1371
|
+
});
|
|
1372
|
+
if (response.ok) {
|
|
1373
|
+
const content = await response.text();
|
|
1374
|
+
if (content.includes(attestationId)) {
|
|
1375
|
+
log5.info({ domain, url }, "Found attestation ID in well-known file");
|
|
1376
|
+
return {
|
|
1377
|
+
verified: true,
|
|
1378
|
+
method: "well-known",
|
|
1379
|
+
url
|
|
1380
|
+
};
|
|
1381
|
+
} else {
|
|
1382
|
+
log5.info({ domain, url }, "Well-known file exists but no attestation ID found");
|
|
1339
1383
|
}
|
|
1340
|
-
} catch (err) {
|
|
1341
|
-
log5.warn({ error: err, url }, "Failed to fetch well-known file");
|
|
1342
1384
|
}
|
|
1385
|
+
} catch (err) {
|
|
1386
|
+
log5.warn({ error: err, url }, "Failed to fetch well-known file");
|
|
1343
1387
|
}
|
|
1344
1388
|
return {
|
|
1345
1389
|
verified: false,
|
|
@@ -1412,14 +1456,32 @@ function isDnsIdentityVerified(attestationId, _domain, cachedContent) {
|
|
|
1412
1456
|
|
|
1413
1457
|
// src/identity/github.ts
|
|
1414
1458
|
var log6 = createLogger("identity-verification/github");
|
|
1459
|
+
var GITHUB_GIST_RAW_HOST = "gist.githubusercontent.com";
|
|
1460
|
+
var MAX_GISTS_SCANNED = 30;
|
|
1461
|
+
var MAX_FILES_PER_GIST = 10;
|
|
1462
|
+
var FETCH_TIMEOUT_MS = 5e3;
|
|
1463
|
+
var GITHUB_USERNAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/;
|
|
1464
|
+
function isSafeGistRawUrl(raw) {
|
|
1465
|
+
try {
|
|
1466
|
+
const u = new URL(raw);
|
|
1467
|
+
return u.protocol === "https:" && u.hostname === GITHUB_GIST_RAW_HOST;
|
|
1468
|
+
} catch {
|
|
1469
|
+
return false;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1415
1472
|
async function verifyGitHubIdentity(attestationId, username) {
|
|
1416
1473
|
log6.info({ attestationId, username }, "Verifying GitHub identity");
|
|
1474
|
+
if (!GITHUB_USERNAME_RE.test(username)) {
|
|
1475
|
+
return { verified: false, error: "invalid GitHub username format" };
|
|
1476
|
+
}
|
|
1417
1477
|
try {
|
|
1418
|
-
const response = await fetch(
|
|
1419
|
-
|
|
1420
|
-
|
|
1478
|
+
const response = await fetch(
|
|
1479
|
+
`https://api.github.com/users/${encodeURIComponent(username)}/gists?per_page=${MAX_GISTS_SCANNED}`,
|
|
1480
|
+
{
|
|
1481
|
+
headers: { Accept: "application/vnd.github.v3+json" },
|
|
1482
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1421
1483
|
}
|
|
1422
|
-
|
|
1484
|
+
);
|
|
1423
1485
|
if (!response.ok) {
|
|
1424
1486
|
if (response.status === 404) {
|
|
1425
1487
|
return {
|
|
@@ -1434,7 +1496,7 @@ async function verifyGitHubIdentity(attestationId, username) {
|
|
|
1434
1496
|
}
|
|
1435
1497
|
const gists = await response.json();
|
|
1436
1498
|
log6.info({ username, gistCount: gists.length }, "Fetched GitHub gists");
|
|
1437
|
-
for (const gist of gists) {
|
|
1499
|
+
for (const gist of gists.slice(0, MAX_GISTS_SCANNED)) {
|
|
1438
1500
|
if (gist.description?.includes(attestationId)) {
|
|
1439
1501
|
log6.info({ username, gistId: gist.id }, "Found attestation ID in gist description");
|
|
1440
1502
|
return {
|
|
@@ -1443,7 +1505,8 @@ async function verifyGitHubIdentity(attestationId, username) {
|
|
|
1443
1505
|
gistId: gist.id
|
|
1444
1506
|
};
|
|
1445
1507
|
}
|
|
1446
|
-
|
|
1508
|
+
const files = Object.entries(gist.files).slice(0, MAX_FILES_PER_GIST);
|
|
1509
|
+
for (const [filename, file] of files) {
|
|
1447
1510
|
if (file.content && file.content.includes(attestationId)) {
|
|
1448
1511
|
log6.info(
|
|
1449
1512
|
{ username, gistId: gist.id, filename },
|
|
@@ -1455,8 +1518,17 @@ async function verifyGitHubIdentity(attestationId, username) {
|
|
|
1455
1518
|
gistId: gist.id
|
|
1456
1519
|
};
|
|
1457
1520
|
}
|
|
1521
|
+
if (!isSafeGistRawUrl(file.raw_url)) {
|
|
1522
|
+
log6.warn(
|
|
1523
|
+
{ filename, rawUrl: file.raw_url },
|
|
1524
|
+
"skipping gist file: raw_url failed allow-list"
|
|
1525
|
+
);
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1458
1528
|
try {
|
|
1459
|
-
const fileResponse = await fetch(file.raw_url
|
|
1529
|
+
const fileResponse = await fetch(file.raw_url, {
|
|
1530
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1531
|
+
});
|
|
1460
1532
|
if (fileResponse.ok) {
|
|
1461
1533
|
const content = await fileResponse.text();
|
|
1462
1534
|
if (content.includes(attestationId)) {
|
|
@@ -1534,15 +1606,38 @@ function isGitHubIdentityVerified(attestationId, _username, cachedGists) {
|
|
|
1534
1606
|
function getGistCreationUrl() {
|
|
1535
1607
|
return "https://gist.github.com/";
|
|
1536
1608
|
}
|
|
1537
|
-
|
|
1538
|
-
// src/identity/nostr.ts
|
|
1539
1609
|
var log7 = createLogger("identity-verification/nostr");
|
|
1610
|
+
var HEX_64_RE = /^[0-9a-f]{64}$/;
|
|
1611
|
+
function toHex(bytes) {
|
|
1612
|
+
let out = "";
|
|
1613
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
1614
|
+
return out;
|
|
1615
|
+
}
|
|
1616
|
+
function fromHex(hex) {
|
|
1617
|
+
if (hex.length % 2 !== 0) throw new Error("odd-length hex");
|
|
1618
|
+
const out = new Uint8Array(hex.length / 2);
|
|
1619
|
+
for (let i = 0; i < out.length; i++) {
|
|
1620
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
1621
|
+
}
|
|
1622
|
+
return out;
|
|
1623
|
+
}
|
|
1540
1624
|
function npubToHex(npub) {
|
|
1541
|
-
|
|
1542
|
-
|
|
1625
|
+
const trimmed = npub.trim();
|
|
1626
|
+
if (HEX_64_RE.test(trimmed.toLowerCase())) {
|
|
1627
|
+
return trimmed.toLowerCase();
|
|
1628
|
+
}
|
|
1629
|
+
if (!trimmed.toLowerCase().startsWith("npub1")) {
|
|
1630
|
+
throw new Error(`not an npub: ${trimmed.slice(0, 12)}\u2026`);
|
|
1631
|
+
}
|
|
1632
|
+
const decoded = base.bech32.decode(trimmed.toLowerCase(), 1023);
|
|
1633
|
+
if (decoded.prefix !== "npub") {
|
|
1634
|
+
throw new Error(`bech32 prefix mismatch: expected npub, got ${decoded.prefix}`);
|
|
1635
|
+
}
|
|
1636
|
+
const bytes = base.bech32.fromWords(decoded.words);
|
|
1637
|
+
if (bytes.length !== 32) {
|
|
1638
|
+
throw new Error(`invalid npub payload length: ${bytes.length}`);
|
|
1543
1639
|
}
|
|
1544
|
-
|
|
1545
|
-
return npub;
|
|
1640
|
+
return toHex(Uint8Array.from(bytes));
|
|
1546
1641
|
}
|
|
1547
1642
|
async function queryNostrEvents(options) {
|
|
1548
1643
|
const { authors, kinds, search, relays = DEFAULT_RELAYS, limit = 100 } = options;
|
|
@@ -1613,11 +1708,31 @@ async function queryNostrEvents(options) {
|
|
|
1613
1708
|
return events;
|
|
1614
1709
|
}
|
|
1615
1710
|
async function verifyNostrEventSignature(event) {
|
|
1616
|
-
|
|
1711
|
+
try {
|
|
1712
|
+
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)) {
|
|
1713
|
+
return false;
|
|
1714
|
+
}
|
|
1715
|
+
const preimage = JSON.stringify([
|
|
1716
|
+
0,
|
|
1717
|
+
event.pubkey,
|
|
1718
|
+
event.created_at,
|
|
1719
|
+
event.kind,
|
|
1720
|
+
event.tags ?? [],
|
|
1721
|
+
event.content ?? ""
|
|
1722
|
+
]);
|
|
1723
|
+
const computedId = toHex(sha2.sha256(new TextEncoder().encode(preimage)));
|
|
1724
|
+
if (computedId !== event.id.toLowerCase()) {
|
|
1725
|
+
log7.warn(
|
|
1726
|
+
{ eventId: event.id, computedId },
|
|
1727
|
+
"nostr event id does not match its content"
|
|
1728
|
+
);
|
|
1729
|
+
return false;
|
|
1730
|
+
}
|
|
1731
|
+
return secp256k1.schnorr.verify(fromHex(event.sig), fromHex(event.id), fromHex(event.pubkey));
|
|
1732
|
+
} catch (err) {
|
|
1733
|
+
log7.warn({ err }, "schnorr verify threw");
|
|
1617
1734
|
return false;
|
|
1618
1735
|
}
|
|
1619
|
-
log7.warn("Nostr signature verification not fully implemented");
|
|
1620
|
-
return true;
|
|
1621
1736
|
}
|
|
1622
1737
|
async function verifyNostrIdentity(attestationId, npub, relays) {
|
|
1623
1738
|
log7.info({ attestationId, npub }, "Verifying Nostr identity");
|