@peac/protocol 0.16.3 → 0.16.5
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/_internal/address-policy.d.ts +28 -0
- package/dist/_internal/address-policy.d.ts.map +1 -0
- package/dist/_internal/record-core/validators/jose-hardening.d.ts +1 -1
- package/dist/_internal/record-core/validators/jose-hardening.d.ts.map +1 -1
- package/dist/_internal/test-helpers/fixture-manifest.d.ts.map +1 -1
- package/dist/index.cjs +383 -93
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +384 -94
- package/dist/index.mjs.map +1 -1
- package/dist/issue.d.ts +2 -2
- package/dist/issue.d.ts.map +1 -1
- package/dist/jwks-resolver.d.ts +1 -1
- package/dist/jwks-resolver.d.ts.map +1 -1
- package/dist/ssrf-safe-fetch.d.ts +18 -11
- package/dist/ssrf-safe-fetch.d.ts.map +1 -1
- package/dist/verifier-core.d.ts.map +1 -1
- package/dist/verify-local.cjs +21 -2
- package/dist/verify-local.cjs.map +1 -1
- package/dist/verify-local.mjs +21 -2
- package/dist/verify-local.mjs.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Address policy for outbound key and pointer fetches.
|
|
3
|
+
*
|
|
4
|
+
* An address is allowed only when it is a global unicast address outside every
|
|
5
|
+
* block in the IANA IPv4 and IPv6 Special-Purpose Address Registries (RFC 6890
|
|
6
|
+
* and updates). IPv4-mapped IPv6 addresses (::ffff:0:0/96) and NAT64
|
|
7
|
+
* well-known prefix addresses (64:ff9b::/96) are judged by the embedded IPv4
|
|
8
|
+
* address. Anything that does not parse as an address is refused.
|
|
9
|
+
*
|
|
10
|
+
* Pure function: no I/O, runtime-neutral.
|
|
11
|
+
*/
|
|
12
|
+
export type AddressBlockReason = 'private_ip' | 'loopback' | 'link_local';
|
|
13
|
+
export type AddressVerdict = {
|
|
14
|
+
allowed: true;
|
|
15
|
+
} | {
|
|
16
|
+
allowed: false;
|
|
17
|
+
reason: AddressBlockReason;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Classify an IP address string against the address policy.
|
|
21
|
+
*/
|
|
22
|
+
export declare function classifyAddress(ip: string): AddressVerdict;
|
|
23
|
+
/**
|
|
24
|
+
* Return the IP literal carried by a URL hostname (brackets removed), or null
|
|
25
|
+
* for a domain name. WHATWG URL parsing has already normalised IPv4 forms.
|
|
26
|
+
*/
|
|
27
|
+
export declare function hostIpLiteral(hostname: string): string | null;
|
|
28
|
+
//# sourceMappingURL=address-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"address-policy.d.ts","sourceRoot":"","sources":["../../src/_internal/address-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,UAAU,GAAG,YAAY,CAAC;AAE1E,MAAM,MAAM,cAAc,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAmJhG;;GAEG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc,CAM1D;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI7D"}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* 1. kid presence and length:
|
|
18
18
|
* - missing kid (undefined / non-string / empty) -> CRYPTO_JWS_MISSING_KID
|
|
19
|
-
* - kid
|
|
19
|
+
* - kid malformed, or longer than 256 UTF-8 bytes -> CRYPTO_JWS_MISSING_KID
|
|
20
20
|
*
|
|
21
21
|
* 2. embedded key material (jwk, x5c, x5u, jku):
|
|
22
22
|
* - any of the four set -> CRYPTO_JWS_EMBEDDED_KEY
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jose-hardening.d.ts","sourceRoot":"","sources":["../../../../src/_internal/record-core/validators/jose-hardening.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,qEAAqE;AACrE,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEzD;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAC3B;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"jose-hardening.d.ts","sourceRoot":"","sources":["../../../../src/_internal/record-core/validators/jose-hardening.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,qEAAqE;AACrE,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEzD;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAC3B;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AA4C7D,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,kBAAkB,GAAG,mBAAmB,CAgC7F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixture-manifest.d.ts","sourceRoot":"","sources":["../../../src/_internal/test-helpers/fixture-manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAEjE,MAAM,MAAM,gBAAgB,GACxB,4BAA4B,GAC5B,8BAA8B,GAC9B,gCAAgC,GAChC,yBAAyB,GACzB,yCAAyC,GACzC,sBAAsB,GACtB,2BAA2B,CAAC;AAEhC,MAAM,MAAM,gBAAgB,GACxB,gCAAgC,GAChC,6BAA6B,GAC7B,2BAA2B,GAC3B,kDAAkD,GAClD,8CAA8C,GAC9C,gDAAgD,GAChD,yCAAyC,GACzC,2CAA2C,GAC3C,kDAAkD,GAClD,2CAA2C,GAC3C,iDAAiD,CAAC;AAEtD,MAAM,MAAM,gBAAgB,GAAG,gBAAgB,GAAG,gBAAgB,CAAC;AAEnE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,qBAAqB,CAAC;IACzD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,qBAAqB,GAAG,gBAAgB,CAAC;IAC5E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;
|
|
1
|
+
{"version":3,"file":"fixture-manifest.d.ts","sourceRoot":"","sources":["../../../src/_internal/test-helpers/fixture-manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAEjE,MAAM,MAAM,gBAAgB,GACxB,4BAA4B,GAC5B,8BAA8B,GAC9B,gCAAgC,GAChC,yBAAyB,GACzB,yCAAyC,GACzC,sBAAsB,GACtB,2BAA2B,CAAC;AAEhC,MAAM,MAAM,gBAAgB,GACxB,gCAAgC,GAChC,6BAA6B,GAC7B,2BAA2B,GAC3B,kDAAkD,GAClD,8CAA8C,GAC9C,gDAAgD,GAChD,yCAAyC,GACzC,2CAA2C,GAC3C,kDAAkD,GAClD,2CAA2C,GAC3C,iDAAiD,CAAC;AAEtD,MAAM,MAAM,gBAAgB,GAAG,gBAAgB,GAAG,gBAAgB,CAAC;AAEnE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,qBAAqB,CAAC;IACzD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,qBAAqB,GAAG,gBAAgB,CAAC;IAC5E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAkfD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,uBAAuB,CAAC,EAAE,MAAM,GAAG,eAAe,CA6ErF"}
|
package/dist/index.cjs
CHANGED
|
@@ -211,13 +211,32 @@ function validateTypeExtensionMappingInternal(input) {
|
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
// src/_internal/record-core/validators/jose-hardening.ts
|
|
214
|
-
var
|
|
214
|
+
var KID_MAX_UTF8_BYTES = 256;
|
|
215
|
+
function isAcceptableKid(kid) {
|
|
216
|
+
if (typeof kid !== "string" || kid.length === 0) return false;
|
|
217
|
+
let bytes = 0;
|
|
218
|
+
for (let i = 0; i < kid.length; i++) {
|
|
219
|
+
const c = kid.charCodeAt(i);
|
|
220
|
+
if (c < 128) bytes += 1;
|
|
221
|
+
else if (c < 2048) bytes += 2;
|
|
222
|
+
else if (c >= 55296 && c <= 56319) {
|
|
223
|
+
const next = i + 1 < kid.length ? kid.charCodeAt(i + 1) : 0;
|
|
224
|
+
if (next < 56320 || next > 57343) return false;
|
|
225
|
+
bytes += 4;
|
|
226
|
+
i++;
|
|
227
|
+
} else if (c >= 56320 && c <= 57343) {
|
|
228
|
+
return false;
|
|
229
|
+
} else bytes += 3;
|
|
230
|
+
if (bytes > KID_MAX_UTF8_BYTES) return false;
|
|
231
|
+
}
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
215
234
|
var ACCEPTED = { accepted: true };
|
|
216
235
|
function rejected(errorCode) {
|
|
217
236
|
return { accepted: false, errorCode };
|
|
218
237
|
}
|
|
219
238
|
function validateJoseHardeningInternal(header) {
|
|
220
|
-
if (!header.kid
|
|
239
|
+
if (!isAcceptableKid(header.kid)) {
|
|
221
240
|
return rejected("CRYPTO_JWS_MISSING_KID");
|
|
222
241
|
}
|
|
223
242
|
if (header.jwk !== void 0 || header.x5c !== void 0 || header.x5u !== void 0 || header.jku !== void 0) {
|
|
@@ -942,6 +961,25 @@ var IssueError = class extends Error {
|
|
|
942
961
|
this.peacError = peacError;
|
|
943
962
|
}
|
|
944
963
|
};
|
|
964
|
+
var CRYPTO_ISSUANCE_CODE_MAP = {
|
|
965
|
+
CRYPTO_JWS_MISSING_KID: "E_JWS_MISSING_KID",
|
|
966
|
+
CRYPTO_IJSON_INVALID_STRING: "E_IJSON_INVALID_STRING",
|
|
967
|
+
CRYPTO_IJSON_NUMBER_OUT_OF_RANGE: "E_IJSON_NUMBER_OUT_OF_RANGE",
|
|
968
|
+
CRYPTO_IJSON_DUPLICATE_MEMBER_NAME: "E_IJSON_DUPLICATE_MEMBER_NAME"
|
|
969
|
+
};
|
|
970
|
+
function issueErrorFromCryptoError(err) {
|
|
971
|
+
const code = CRYPTO_ISSUANCE_CODE_MAP[err.code] ?? "E_INVALID_FORMAT";
|
|
972
|
+
const issueError = new IssueError({
|
|
973
|
+
code,
|
|
974
|
+
category: "validation",
|
|
975
|
+
severity: "error",
|
|
976
|
+
retryable: false,
|
|
977
|
+
http_status: 400,
|
|
978
|
+
details: { message: err.message }
|
|
979
|
+
});
|
|
980
|
+
issueError.cause = err;
|
|
981
|
+
return issueError;
|
|
982
|
+
}
|
|
945
983
|
async function issueWire01(options) {
|
|
946
984
|
if (!options.iss.startsWith("https://")) {
|
|
947
985
|
throw new Error("Issuer URL must start with https://");
|
|
@@ -1150,7 +1188,15 @@ async function issueWire02(options) {
|
|
|
1150
1188
|
});
|
|
1151
1189
|
}
|
|
1152
1190
|
}
|
|
1153
|
-
|
|
1191
|
+
let jws;
|
|
1192
|
+
try {
|
|
1193
|
+
jws = await defaultCodec.encode(claims, options.privateKey, options.kid);
|
|
1194
|
+
} catch (err) {
|
|
1195
|
+
if (err instanceof crypto$1.CryptoError) {
|
|
1196
|
+
throw issueErrorFromCryptoError(err);
|
|
1197
|
+
}
|
|
1198
|
+
throw err;
|
|
1199
|
+
}
|
|
1154
1200
|
if (legacyPath && isShadowEnabled(options)) {
|
|
1155
1201
|
scheduleShadow({
|
|
1156
1202
|
call: "issue",
|
|
@@ -1484,6 +1530,138 @@ async function fetchDiscovery(issuerUrl) {
|
|
|
1484
1530
|
);
|
|
1485
1531
|
}
|
|
1486
1532
|
}
|
|
1533
|
+
|
|
1534
|
+
// src/_internal/address-policy.ts
|
|
1535
|
+
var IPV4_OCTET = /^(?:0|[1-9]\d{0,2})$/;
|
|
1536
|
+
var IPV6_GROUP = /^[0-9a-fA-F]{1,4}$/;
|
|
1537
|
+
function parseIPv4(text) {
|
|
1538
|
+
const parts = text.split(".");
|
|
1539
|
+
if (parts.length !== 4) return null;
|
|
1540
|
+
let value = 0;
|
|
1541
|
+
for (const part of parts) {
|
|
1542
|
+
if (!IPV4_OCTET.test(part)) return null;
|
|
1543
|
+
const octet = Number(part);
|
|
1544
|
+
if (octet > 255) return null;
|
|
1545
|
+
value = value * 256 + octet;
|
|
1546
|
+
}
|
|
1547
|
+
return value;
|
|
1548
|
+
}
|
|
1549
|
+
function parseGroups(text) {
|
|
1550
|
+
if (text === "") return [];
|
|
1551
|
+
const groups = [];
|
|
1552
|
+
for (const group of text.split(":")) {
|
|
1553
|
+
if (!IPV6_GROUP.test(group)) return null;
|
|
1554
|
+
groups.push(parseInt(group, 16));
|
|
1555
|
+
}
|
|
1556
|
+
return groups;
|
|
1557
|
+
}
|
|
1558
|
+
function parseIPv6(text) {
|
|
1559
|
+
if (text.length === 0 || text.length > 45) return null;
|
|
1560
|
+
let body = text;
|
|
1561
|
+
const lastColon = body.lastIndexOf(":");
|
|
1562
|
+
if (lastColon === -1) return null;
|
|
1563
|
+
const last = body.slice(lastColon + 1);
|
|
1564
|
+
if (last.includes(".")) {
|
|
1565
|
+
const v4 = parseIPv4(last);
|
|
1566
|
+
if (v4 === null) return null;
|
|
1567
|
+
body = `${body.slice(0, lastColon + 1)}${Math.floor(v4 / 65536).toString(16)}:${(v4 % 65536).toString(16)}`;
|
|
1568
|
+
}
|
|
1569
|
+
const halves = body.split("::");
|
|
1570
|
+
if (halves.length > 2) return null;
|
|
1571
|
+
const head = parseGroups(halves[0]);
|
|
1572
|
+
if (head === null) return null;
|
|
1573
|
+
if (halves.length === 1) return head.length === 8 ? head : null;
|
|
1574
|
+
const tail = parseGroups(halves[1]);
|
|
1575
|
+
if (tail === null) return null;
|
|
1576
|
+
const fill = 8 - head.length - tail.length;
|
|
1577
|
+
if (fill < 1) return null;
|
|
1578
|
+
return [...head, ...new Array(fill).fill(0), ...tail];
|
|
1579
|
+
}
|
|
1580
|
+
var IPV4_BLOCKS = [
|
|
1581
|
+
["0.0.0.0", 8, "private_ip"],
|
|
1582
|
+
["10.0.0.0", 8, "private_ip"],
|
|
1583
|
+
["100.64.0.0", 10, "private_ip"],
|
|
1584
|
+
["127.0.0.0", 8, "loopback"],
|
|
1585
|
+
["169.254.0.0", 16, "link_local"],
|
|
1586
|
+
["172.16.0.0", 12, "private_ip"],
|
|
1587
|
+
["192.0.0.0", 24, "private_ip"],
|
|
1588
|
+
["192.0.2.0", 24, "private_ip"],
|
|
1589
|
+
["192.31.196.0", 24, "private_ip"],
|
|
1590
|
+
["192.52.193.0", 24, "private_ip"],
|
|
1591
|
+
["192.88.99.0", 24, "private_ip"],
|
|
1592
|
+
["192.168.0.0", 16, "private_ip"],
|
|
1593
|
+
["192.175.48.0", 24, "private_ip"],
|
|
1594
|
+
["198.18.0.0", 15, "private_ip"],
|
|
1595
|
+
["198.51.100.0", 24, "private_ip"],
|
|
1596
|
+
["203.0.113.0", 24, "private_ip"],
|
|
1597
|
+
["224.0.0.0", 4, "private_ip"],
|
|
1598
|
+
["240.0.0.0", 4, "private_ip"]
|
|
1599
|
+
];
|
|
1600
|
+
var IPV4_BLOCK_TABLE = IPV4_BLOCKS.map(([network, prefix, reason]) => {
|
|
1601
|
+
const base = parseIPv4(network);
|
|
1602
|
+
const size = 2 ** (32 - prefix);
|
|
1603
|
+
return { start: base, end: base + size - 1, reason };
|
|
1604
|
+
});
|
|
1605
|
+
function classifyIPv4(value) {
|
|
1606
|
+
for (const block of IPV4_BLOCK_TABLE) {
|
|
1607
|
+
if (value >= block.start && value <= block.end) {
|
|
1608
|
+
return { allowed: false, reason: block.reason };
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return { allowed: true };
|
|
1612
|
+
}
|
|
1613
|
+
function matchesPrefix(groups, network, bits) {
|
|
1614
|
+
for (let i = 0; i < 8; i++) {
|
|
1615
|
+
const bitsHere = Math.min(16, Math.max(0, bits - 16 * i));
|
|
1616
|
+
if (bitsHere === 0) return true;
|
|
1617
|
+
const mask = 65535 << 16 - bitsHere & 65535;
|
|
1618
|
+
if ((groups[i] & mask) !== (network[i] & mask)) return false;
|
|
1619
|
+
}
|
|
1620
|
+
return true;
|
|
1621
|
+
}
|
|
1622
|
+
function v6(text) {
|
|
1623
|
+
return parseIPv6(text);
|
|
1624
|
+
}
|
|
1625
|
+
var GLOBAL_UNICAST_V6 = v6("2000::");
|
|
1626
|
+
var IPV6_BLOCKS_IN_GLOBAL = [
|
|
1627
|
+
[v6("2001::"), 23],
|
|
1628
|
+
[v6("2001:db8::"), 32],
|
|
1629
|
+
[v6("2002::"), 16],
|
|
1630
|
+
[v6("2620:4f:8000::"), 48],
|
|
1631
|
+
[v6("3fff::"), 20]
|
|
1632
|
+
];
|
|
1633
|
+
var IPV6_LOOPBACK = v6("::1");
|
|
1634
|
+
var IPV6_LINK_LOCAL = v6("fe80::");
|
|
1635
|
+
var IPV4_MAPPED = v6("::ffff:0:0");
|
|
1636
|
+
var NAT64_WELL_KNOWN = v6("64:ff9b::");
|
|
1637
|
+
function classifyIPv6(groups) {
|
|
1638
|
+
if (matchesPrefix(groups, IPV4_MAPPED, 96) || matchesPrefix(groups, NAT64_WELL_KNOWN, 96)) {
|
|
1639
|
+
return classifyIPv4(groups[6] * 65536 + groups[7]);
|
|
1640
|
+
}
|
|
1641
|
+
if (!matchesPrefix(groups, GLOBAL_UNICAST_V6, 3)) {
|
|
1642
|
+
if (matchesPrefix(groups, IPV6_LOOPBACK, 128)) return { allowed: false, reason: "loopback" };
|
|
1643
|
+
if (matchesPrefix(groups, IPV6_LINK_LOCAL, 10)) return { allowed: false, reason: "link_local" };
|
|
1644
|
+
return { allowed: false, reason: "private_ip" };
|
|
1645
|
+
}
|
|
1646
|
+
for (const [network, bits] of IPV6_BLOCKS_IN_GLOBAL) {
|
|
1647
|
+
if (matchesPrefix(groups, network, bits)) return { allowed: false, reason: "private_ip" };
|
|
1648
|
+
}
|
|
1649
|
+
return { allowed: true };
|
|
1650
|
+
}
|
|
1651
|
+
function classifyAddress(ip) {
|
|
1652
|
+
const v4 = parseIPv4(ip);
|
|
1653
|
+
if (v4 !== null) return classifyIPv4(v4);
|
|
1654
|
+
const groups = parseIPv6(ip);
|
|
1655
|
+
if (groups !== null) return classifyIPv6(groups);
|
|
1656
|
+
return { allowed: false, reason: "private_ip" };
|
|
1657
|
+
}
|
|
1658
|
+
function hostIpLiteral(hostname) {
|
|
1659
|
+
if (hostname.startsWith("[") && hostname.endsWith("]")) return hostname.slice(1, -1);
|
|
1660
|
+
if (/^[0-9.]+$/.test(hostname)) return hostname;
|
|
1661
|
+
return null;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// src/ssrf-safe-fetch.ts
|
|
1487
1665
|
var cachedCapabilities = null;
|
|
1488
1666
|
function getSSRFCapabilities() {
|
|
1489
1667
|
if (cachedCapabilities) {
|
|
@@ -1501,9 +1679,9 @@ function detectCapabilities() {
|
|
|
1501
1679
|
networkIsolation: false,
|
|
1502
1680
|
protectionLevel: "full",
|
|
1503
1681
|
notes: [
|
|
1504
|
-
"
|
|
1505
|
-
"
|
|
1506
|
-
"
|
|
1682
|
+
"Hostnames are resolved once via the Node.js dns module",
|
|
1683
|
+
"Every resolved address must be a global unicast address",
|
|
1684
|
+
"The connection is made only to a vetted address"
|
|
1507
1685
|
]
|
|
1508
1686
|
};
|
|
1509
1687
|
}
|
|
@@ -1581,61 +1759,16 @@ function detectCapabilities() {
|
|
|
1581
1759
|
function resetSSRFCapabilitiesCache() {
|
|
1582
1760
|
cachedCapabilities = null;
|
|
1583
1761
|
}
|
|
1584
|
-
function parseIPv4(ip) {
|
|
1585
|
-
const parts = ip.split(".");
|
|
1586
|
-
if (parts.length !== 4) return null;
|
|
1587
|
-
const octets = [];
|
|
1588
|
-
for (const part of parts) {
|
|
1589
|
-
const num = parseInt(part, 10);
|
|
1590
|
-
if (isNaN(num) || num < 0 || num > 255) return null;
|
|
1591
|
-
octets.push(num);
|
|
1592
|
-
}
|
|
1593
|
-
return { octets };
|
|
1594
|
-
}
|
|
1595
|
-
function isInCIDR(ip, cidr) {
|
|
1596
|
-
const [rangeStr, maskStr] = cidr.split("/");
|
|
1597
|
-
const range = parseIPv4(rangeStr);
|
|
1598
|
-
if (!range) return false;
|
|
1599
|
-
const maskBits = parseInt(maskStr, 10);
|
|
1600
|
-
if (isNaN(maskBits) || maskBits < 0 || maskBits > 32) return false;
|
|
1601
|
-
const ipNum = ip.octets[0] << 24 | ip.octets[1] << 16 | ip.octets[2] << 8 | ip.octets[3];
|
|
1602
|
-
const rangeNum = range.octets[0] << 24 | range.octets[1] << 16 | range.octets[2] << 8 | range.octets[3];
|
|
1603
|
-
const mask = maskBits === 0 ? 0 : ~((1 << 32 - maskBits) - 1);
|
|
1604
|
-
return (ipNum & mask) === (rangeNum & mask);
|
|
1605
|
-
}
|
|
1606
|
-
function isIPv6Loopback(ip) {
|
|
1607
|
-
const normalized = ip.toLowerCase().replace(/^::ffff:/, "");
|
|
1608
|
-
return normalized === "::1" || normalized === "0:0:0:0:0:0:0:1";
|
|
1609
|
-
}
|
|
1610
|
-
function isIPv6LinkLocal(ip) {
|
|
1611
|
-
const normalized = ip.toLowerCase();
|
|
1612
|
-
return normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb");
|
|
1613
|
-
}
|
|
1614
1762
|
function isBlockedIP(ip) {
|
|
1615
|
-
const
|
|
1616
|
-
|
|
1617
|
-
const ipv4 = parseIPv4(effectiveIP);
|
|
1618
|
-
if (ipv4) {
|
|
1619
|
-
if (isInCIDR(ipv4, "10.0.0.0/8") || isInCIDR(ipv4, "172.16.0.0/12") || isInCIDR(ipv4, "192.168.0.0/16")) {
|
|
1620
|
-
return { blocked: true, reason: "private_ip" };
|
|
1621
|
-
}
|
|
1622
|
-
if (isInCIDR(ipv4, "127.0.0.0/8")) {
|
|
1623
|
-
return { blocked: true, reason: "loopback" };
|
|
1624
|
-
}
|
|
1625
|
-
if (isInCIDR(ipv4, "169.254.0.0/16")) {
|
|
1626
|
-
return { blocked: true, reason: "link_local" };
|
|
1627
|
-
}
|
|
1628
|
-
return { blocked: false };
|
|
1629
|
-
}
|
|
1630
|
-
if (isIPv6Loopback(ip)) {
|
|
1631
|
-
return { blocked: true, reason: "loopback" };
|
|
1632
|
-
}
|
|
1633
|
-
if (isIPv6LinkLocal(ip)) {
|
|
1634
|
-
return { blocked: true, reason: "link_local" };
|
|
1635
|
-
}
|
|
1636
|
-
return { blocked: false };
|
|
1763
|
+
const verdict = classifyAddress(ip);
|
|
1764
|
+
return verdict.allowed ? { blocked: false } : { blocked: true, reason: verdict.reason };
|
|
1637
1765
|
}
|
|
1638
1766
|
async function resolveHostname(hostname) {
|
|
1767
|
+
const literal = hostIpLiteral(hostname);
|
|
1768
|
+
if (literal !== null) {
|
|
1769
|
+
const nodeRuntime = typeof process !== "undefined" && Boolean(process.versions?.node);
|
|
1770
|
+
return { ok: true, ips: [literal], browser: !nodeRuntime };
|
|
1771
|
+
}
|
|
1639
1772
|
if (typeof process !== "undefined" && process.versions?.node) {
|
|
1640
1773
|
try {
|
|
1641
1774
|
const dns = await import('dns');
|
|
@@ -1666,7 +1799,10 @@ async function resolveHostname(hostname) {
|
|
|
1666
1799
|
message: `DNS resolution failed for ${hostname}: ${ipv4Error.message}`
|
|
1667
1800
|
};
|
|
1668
1801
|
}
|
|
1669
|
-
return {
|
|
1802
|
+
return {
|
|
1803
|
+
ok: false,
|
|
1804
|
+
message: `DNS resolution returned no addresses for ${hostname}`
|
|
1805
|
+
};
|
|
1670
1806
|
} catch (err) {
|
|
1671
1807
|
return {
|
|
1672
1808
|
ok: false,
|
|
@@ -1676,6 +1812,88 @@ async function resolveHostname(hostname) {
|
|
|
1676
1812
|
}
|
|
1677
1813
|
return { ok: true, ips: [], browser: true };
|
|
1678
1814
|
}
|
|
1815
|
+
var NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
|
|
1816
|
+
async function discardBody(response) {
|
|
1817
|
+
await response.body?.cancel();
|
|
1818
|
+
}
|
|
1819
|
+
async function fetchFromVettedAddresses(url, addresses, headers, signal) {
|
|
1820
|
+
const https = await import('https');
|
|
1821
|
+
const { isIP } = await import('net');
|
|
1822
|
+
const target = new URL(url);
|
|
1823
|
+
const host = hostIpLiteral(target.hostname) ?? target.hostname;
|
|
1824
|
+
if (addresses.length === 0 || isIP(host) !== 0 && !addresses.includes(host)) {
|
|
1825
|
+
throw new Error(`Refusing to connect to an address that was not vetted: ${host}`);
|
|
1826
|
+
}
|
|
1827
|
+
const entries = addresses.map((address) => ({
|
|
1828
|
+
address,
|
|
1829
|
+
family: address.includes(":") ? 6 : 4
|
|
1830
|
+
}));
|
|
1831
|
+
const lookup = (_hostname, options, callback) => {
|
|
1832
|
+
if (options.all) {
|
|
1833
|
+
callback(null, entries);
|
|
1834
|
+
} else {
|
|
1835
|
+
callback(null, entries[0].address, entries[0].family);
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
return new Promise((resolve, reject) => {
|
|
1839
|
+
const req = https.request(
|
|
1840
|
+
target,
|
|
1841
|
+
{ method: "GET", headers, lookup, agent: false, signal },
|
|
1842
|
+
(res) => {
|
|
1843
|
+
try {
|
|
1844
|
+
const status = res.statusCode ?? 0;
|
|
1845
|
+
const responseHeaders = new Headers();
|
|
1846
|
+
for (const [name, value] of Object.entries(res.headers)) {
|
|
1847
|
+
if (value === void 0) continue;
|
|
1848
|
+
if (Array.isArray(value)) {
|
|
1849
|
+
for (const item of value) responseHeaders.append(name, item);
|
|
1850
|
+
} else {
|
|
1851
|
+
responseHeaders.set(name, value);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
if (NULL_BODY_STATUSES.has(status)) {
|
|
1855
|
+
res.resume();
|
|
1856
|
+
resolve(new Response(null, { status, headers: responseHeaders }));
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
let settled = false;
|
|
1860
|
+
const body = new ReadableStream({
|
|
1861
|
+
start(controller) {
|
|
1862
|
+
res.on("data", (chunk) => {
|
|
1863
|
+
if (settled) return;
|
|
1864
|
+
controller.enqueue(new Uint8Array(chunk));
|
|
1865
|
+
if ((controller.desiredSize ?? 0) <= 0) res.pause();
|
|
1866
|
+
});
|
|
1867
|
+
res.on("end", () => {
|
|
1868
|
+
if (settled) return;
|
|
1869
|
+
settled = true;
|
|
1870
|
+
controller.close();
|
|
1871
|
+
});
|
|
1872
|
+
res.on("error", (err) => {
|
|
1873
|
+
if (settled) return;
|
|
1874
|
+
settled = true;
|
|
1875
|
+
controller.error(err);
|
|
1876
|
+
});
|
|
1877
|
+
},
|
|
1878
|
+
pull() {
|
|
1879
|
+
res.resume();
|
|
1880
|
+
},
|
|
1881
|
+
cancel() {
|
|
1882
|
+
settled = true;
|
|
1883
|
+
res.destroy();
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
resolve(new Response(body, { status, headers: responseHeaders }));
|
|
1887
|
+
} catch (err) {
|
|
1888
|
+
res.destroy();
|
|
1889
|
+
reject(err);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
);
|
|
1893
|
+
req.on("error", reject);
|
|
1894
|
+
req.end();
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1679
1897
|
async function ssrfSafeFetch(url, options = {}) {
|
|
1680
1898
|
const {
|
|
1681
1899
|
timeoutMs = kernel.VERIFIER_LIMITS.fetchTimeoutMs,
|
|
@@ -1724,38 +1942,44 @@ async function ssrfSafeFetch(url, options = {}) {
|
|
|
1724
1942
|
blockedUrl: url
|
|
1725
1943
|
};
|
|
1726
1944
|
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
};
|
|
1737
|
-
}
|
|
1945
|
+
for (const ip of dnsResult.ips) {
|
|
1946
|
+
const blockResult = isBlockedIP(ip);
|
|
1947
|
+
if (blockResult.blocked) {
|
|
1948
|
+
return {
|
|
1949
|
+
ok: false,
|
|
1950
|
+
reason: blockResult.reason,
|
|
1951
|
+
message: `Blocked ${blockResult.reason} address: ${ip} for ${url}`,
|
|
1952
|
+
blockedUrl: url
|
|
1953
|
+
};
|
|
1738
1954
|
}
|
|
1739
1955
|
}
|
|
1740
1956
|
let redirectCount = 0;
|
|
1741
1957
|
let currentUrl = url;
|
|
1958
|
+
let vettedAddresses = dnsResult.browser ? null : dnsResult.ips;
|
|
1742
1959
|
const originalOrigin = parsedUrl.origin;
|
|
1743
1960
|
while (true) {
|
|
1744
1961
|
try {
|
|
1745
1962
|
const controller = new AbortController();
|
|
1746
1963
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1747
|
-
const
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1964
|
+
const requestHeaders = {
|
|
1965
|
+
Accept: "application/json, text/plain",
|
|
1966
|
+
...headers
|
|
1967
|
+
};
|
|
1968
|
+
const response = vettedAddresses === null ? await fetch(currentUrl, {
|
|
1969
|
+
headers: requestHeaders,
|
|
1752
1970
|
signal: controller.signal,
|
|
1753
1971
|
redirect: "manual"
|
|
1754
1972
|
// Handle redirects manually for security
|
|
1755
|
-
})
|
|
1973
|
+
}) : await fetchFromVettedAddresses(
|
|
1974
|
+
currentUrl,
|
|
1975
|
+
vettedAddresses,
|
|
1976
|
+
requestHeaders,
|
|
1977
|
+
controller.signal
|
|
1978
|
+
);
|
|
1756
1979
|
clearTimeout(timeoutId);
|
|
1757
1980
|
if (response.status >= 300 && response.status < 400) {
|
|
1758
1981
|
const location = response.headers.get("location");
|
|
1982
|
+
await discardBody(response);
|
|
1759
1983
|
if (!location) {
|
|
1760
1984
|
return {
|
|
1761
1985
|
ok: false,
|
|
@@ -1824,24 +2048,24 @@ async function ssrfSafeFetch(url, options = {}) {
|
|
|
1824
2048
|
blockedUrl: redirectUrl.href
|
|
1825
2049
|
};
|
|
1826
2050
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
};
|
|
1837
|
-
}
|
|
2051
|
+
for (const ip of redirectDnsResult.ips) {
|
|
2052
|
+
const blockResult = isBlockedIP(ip);
|
|
2053
|
+
if (blockResult.blocked) {
|
|
2054
|
+
return {
|
|
2055
|
+
ok: false,
|
|
2056
|
+
reason: blockResult.reason,
|
|
2057
|
+
message: `Redirect to blocked ${blockResult.reason} address: ${ip}`,
|
|
2058
|
+
blockedUrl: redirectUrl.href
|
|
2059
|
+
};
|
|
1838
2060
|
}
|
|
1839
2061
|
}
|
|
1840
2062
|
currentUrl = redirectUrl.href;
|
|
2063
|
+
vettedAddresses = redirectDnsResult.browser ? null : redirectDnsResult.ips;
|
|
1841
2064
|
continue;
|
|
1842
2065
|
}
|
|
1843
2066
|
const contentLength = response.headers.get("content-length");
|
|
1844
2067
|
if (contentLength && parseInt(contentLength, 10) > maxBytes) {
|
|
2068
|
+
await discardBody(response);
|
|
1845
2069
|
return {
|
|
1846
2070
|
ok: false,
|
|
1847
2071
|
reason: "response_too_large",
|
|
@@ -2072,6 +2296,25 @@ function mapSSRFError(reason, context) {
|
|
|
2072
2296
|
}
|
|
2073
2297
|
return { code, reason };
|
|
2074
2298
|
}
|
|
2299
|
+
var CANONICAL_ED25519_X = /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
|
|
2300
|
+
function isPlainObject(value) {
|
|
2301
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2302
|
+
}
|
|
2303
|
+
function jwkEntryProblem(entry) {
|
|
2304
|
+
if (!isPlainObject(entry)) return "not an object";
|
|
2305
|
+
if (entry.kty !== "OKP") return "kty must be OKP";
|
|
2306
|
+
if (entry.crv !== "Ed25519") return "crv must be Ed25519";
|
|
2307
|
+
if (typeof entry.x !== "string" || !CANONICAL_ED25519_X.test(entry.x)) {
|
|
2308
|
+
return "x must be a canonical base64url 32-byte value";
|
|
2309
|
+
}
|
|
2310
|
+
if (Object.prototype.hasOwnProperty.call(entry, "d")) return "private key member d present";
|
|
2311
|
+
if (entry.kid !== void 0 && typeof entry.kid !== "string") return "kid must be a string";
|
|
2312
|
+
if (entry.use !== void 0 && entry.use !== "sig") return "use must be sig";
|
|
2313
|
+
if (entry.alg !== void 0 && entry.alg !== "EdDSA" && entry.alg !== "Ed25519") {
|
|
2314
|
+
return "alg must be EdDSA or Ed25519";
|
|
2315
|
+
}
|
|
2316
|
+
return null;
|
|
2317
|
+
}
|
|
2075
2318
|
async function resolveJWKS(issuerUrl, options) {
|
|
2076
2319
|
const cacheTtlMs = options?.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
2077
2320
|
const maxCacheEntries = options?.maxCacheEntries ?? DEFAULT_MAX_CACHE_ENTRIES;
|
|
@@ -2161,9 +2404,9 @@ async function resolveJWKS(issuerUrl, options) {
|
|
|
2161
2404
|
blockedUrl: jwksResult.blockedUrl
|
|
2162
2405
|
};
|
|
2163
2406
|
}
|
|
2164
|
-
let
|
|
2407
|
+
let parsed;
|
|
2165
2408
|
try {
|
|
2166
|
-
|
|
2409
|
+
parsed = JSON.parse(jwksResult.body);
|
|
2167
2410
|
} catch {
|
|
2168
2411
|
return {
|
|
2169
2412
|
ok: false,
|
|
@@ -2171,20 +2414,37 @@ async function resolveJWKS(issuerUrl, options) {
|
|
|
2171
2414
|
message: "JWKS response is not valid JSON"
|
|
2172
2415
|
};
|
|
2173
2416
|
}
|
|
2174
|
-
|
|
2417
|
+
const rawKeys = isPlainObject(parsed) ? parsed.keys : void 0;
|
|
2418
|
+
if (!Array.isArray(rawKeys)) {
|
|
2175
2419
|
return {
|
|
2176
2420
|
ok: false,
|
|
2177
2421
|
code: "E_VERIFY_JWKS_INVALID",
|
|
2178
2422
|
message: "JWKS missing required keys array"
|
|
2179
2423
|
};
|
|
2180
2424
|
}
|
|
2181
|
-
if (
|
|
2425
|
+
if (rawKeys.length > kernel.VERIFIER_LIMITS.maxJwksKeys) {
|
|
2182
2426
|
return {
|
|
2183
2427
|
ok: false,
|
|
2184
2428
|
code: "E_VERIFY_JWKS_TOO_MANY_KEYS",
|
|
2185
|
-
message: `JWKS has too many keys: ${
|
|
2429
|
+
message: `JWKS has too many keys: ${rawKeys.length} > ${kernel.VERIFIER_LIMITS.maxJwksKeys}`
|
|
2186
2430
|
};
|
|
2187
2431
|
}
|
|
2432
|
+
const validKeys = [];
|
|
2433
|
+
const skipped = [];
|
|
2434
|
+
rawKeys.forEach((entry, index) => {
|
|
2435
|
+
const problem = jwkEntryProblem(entry);
|
|
2436
|
+
if (problem === null) {
|
|
2437
|
+
validKeys.push(entry);
|
|
2438
|
+
} else {
|
|
2439
|
+
skipped.push(`#${index}: ${problem}`);
|
|
2440
|
+
}
|
|
2441
|
+
});
|
|
2442
|
+
if (skipped.length > 0) {
|
|
2443
|
+
console.warn(
|
|
2444
|
+
`[peac:jwks] Skipped ${skipped.length} invalid JWKS entr${skipped.length === 1 ? "y" : "ies"} for ${normalizedIssuer} (${skipped.join("; ")})`
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2447
|
+
const jwks = { keys: validKeys };
|
|
2188
2448
|
const kidReuseError = checkKidReuse(normalizedIssuer, jwks, now);
|
|
2189
2449
|
if (kidReuseError) {
|
|
2190
2450
|
return {
|
|
@@ -3223,9 +3483,13 @@ async function fetchIssuerJWKS(issuerOrigin) {
|
|
|
3223
3483
|
return {
|
|
3224
3484
|
jwks: result.jwks,
|
|
3225
3485
|
fromCache: result.fromCache,
|
|
3226
|
-
rawBytes: result.rawBytes
|
|
3486
|
+
rawBytes: result.rawBytes,
|
|
3487
|
+
revokedKeys: result.revokedKeys
|
|
3227
3488
|
};
|
|
3228
3489
|
}
|
|
3490
|
+
function findRevokedKey(kid, revokedKeys) {
|
|
3491
|
+
return revokedKeys?.find((rk) => rk.kid === kid);
|
|
3492
|
+
}
|
|
3229
3493
|
async function verifyReceiptCore(options) {
|
|
3230
3494
|
const {
|
|
3231
3495
|
receipt,
|
|
@@ -3395,6 +3659,19 @@ async function verifyReceiptCore(options) {
|
|
|
3395
3659
|
from_cache: jwksResult.fromCache,
|
|
3396
3660
|
keys_count: jwksResult.jwks.keys.length
|
|
3397
3661
|
});
|
|
3662
|
+
const revokedPinned = findRevokedKey(kid, jwksResult.revokedKeys);
|
|
3663
|
+
if (revokedPinned) {
|
|
3664
|
+
builder.fail("key.resolve", "E_REVOKED_KEY_USED", {
|
|
3665
|
+
kid,
|
|
3666
|
+
revoked_at: revokedPinned.revoked_at,
|
|
3667
|
+
...revokedPinned.reason && { reason: revokedPinned.reason }
|
|
3668
|
+
});
|
|
3669
|
+
builder.failure("key_not_found", normalizedIssuer, kid);
|
|
3670
|
+
return {
|
|
3671
|
+
valid: false,
|
|
3672
|
+
report: includeMeta ? builder.addTimestamp().build() : builder.build()
|
|
3673
|
+
};
|
|
3674
|
+
}
|
|
3398
3675
|
const jwk = jwksResult.jwks.keys.find((k) => k.kid === kid);
|
|
3399
3676
|
if (!jwk) {
|
|
3400
3677
|
builder.fail("key.resolve", "E_VERIFY_KEY_NOT_FOUND", {
|
|
@@ -3456,6 +3733,19 @@ async function verifyReceiptCore(options) {
|
|
|
3456
3733
|
from_cache: jwksResult.fromCache,
|
|
3457
3734
|
keys_count: jwksResult.jwks.keys.length
|
|
3458
3735
|
});
|
|
3736
|
+
const revoked = findRevokedKey(kid, jwksResult.revokedKeys);
|
|
3737
|
+
if (revoked) {
|
|
3738
|
+
builder.fail("key.resolve", "E_REVOKED_KEY_USED", {
|
|
3739
|
+
kid,
|
|
3740
|
+
revoked_at: revoked.revoked_at,
|
|
3741
|
+
...revoked.reason && { reason: revoked.reason }
|
|
3742
|
+
});
|
|
3743
|
+
builder.failure("key_not_found", normalizedIssuer, kid);
|
|
3744
|
+
return {
|
|
3745
|
+
valid: false,
|
|
3746
|
+
report: includeMeta ? builder.addTimestamp().build() : builder.build()
|
|
3747
|
+
};
|
|
3748
|
+
}
|
|
3459
3749
|
const jwk = jwksResult.jwks.keys.find((k) => k.kid === kid);
|
|
3460
3750
|
if (!jwk) {
|
|
3461
3751
|
builder.fail("key.resolve", "E_VERIFY_KEY_NOT_FOUND", {
|