@messh/sdk 0.0.0-stage → 0.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/LICENSE +202 -0
- package/README.md +43 -2
- package/dist/errors.d.ts +23 -0
- package/dist/errors.js +32 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +151 -0
- package/dist/keys.d.ts +25 -0
- package/dist/keys.js +198 -0
- package/dist/verify.d.ts +116 -0
- package/dist/verify.js +333 -0
- package/dist/wba.d.ts +34 -0
- package/dist/wba.js +120 -0
- package/package.json +55 -4
package/dist/wba.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Web Bot Auth wire format: RFC 9421 signature base, structured-field
|
|
2
|
+
// parsing, RFC 7638 thumbprints and RFC 9530 Content-Digest.
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
export const TAG = "web-bot-auth";
|
|
5
|
+
export const LABEL = "sig1";
|
|
6
|
+
export const DIRECTORY_PATH = "/.well-known/http-message-signatures-directory";
|
|
7
|
+
export const MAX_LIFETIME = 300;
|
|
8
|
+
const b64url = (b) => Buffer.from(b).toString("base64url");
|
|
9
|
+
export function thumbprint(publicKey) {
|
|
10
|
+
return createHash("sha256").update(`{"crv":"Ed25519","kty":"OKP","x":"${b64url(publicKey)}"}`).digest("base64url");
|
|
11
|
+
}
|
|
12
|
+
/** SSH wire-format string: uint32 length, then the bytes. */
|
|
13
|
+
export function sshString(b) {
|
|
14
|
+
const n = Buffer.alloc(4);
|
|
15
|
+
n.writeUInt32BE(b.length);
|
|
16
|
+
return Buffer.concat([n, b]);
|
|
17
|
+
}
|
|
18
|
+
/** The key's SSH fingerprint, as ssh-keygen -l and messh print it: SHA256:… */
|
|
19
|
+
export function fingerprint(publicKey) {
|
|
20
|
+
const blob = Buffer.concat([sshString(Buffer.from("ssh-ed25519")), sshString(publicKey)]);
|
|
21
|
+
return "SHA256:" + createHash("sha256").update(blob).digest("base64").replace(/=+$/, "");
|
|
22
|
+
}
|
|
23
|
+
export function contentDigest(body) {
|
|
24
|
+
return `sha-256=:${createHash("sha256").update(body).digest("base64")}:`;
|
|
25
|
+
}
|
|
26
|
+
export const quote = (s) => JSON.stringify(s);
|
|
27
|
+
export function params(names, created, expires, keyid, nonce) {
|
|
28
|
+
return `(${names.map(quote).join(" ")});created=${created};expires=${expires};keyid=${quote(keyid)};alg="ed25519";nonce=${quote(nonce)};tag="${TAG}"`;
|
|
29
|
+
}
|
|
30
|
+
export function base(components, sigParams) {
|
|
31
|
+
const lines = components.map(([n, v]) => `${quote(n)}: ${v}`);
|
|
32
|
+
lines.push(`"@signature-params": ${sigParams}`);
|
|
33
|
+
return Buffer.from(lines.join("\n"));
|
|
34
|
+
}
|
|
35
|
+
function splitTop(s) {
|
|
36
|
+
const out = [];
|
|
37
|
+
let depth = 0, inStr = false, start = 0;
|
|
38
|
+
for (let i = 0; i < s.length; i++) {
|
|
39
|
+
const c = s[i];
|
|
40
|
+
if (inStr && c === "\\") {
|
|
41
|
+
i++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (c === '"')
|
|
45
|
+
inStr = !inStr;
|
|
46
|
+
else if (!inStr) {
|
|
47
|
+
if (c === "(")
|
|
48
|
+
depth++;
|
|
49
|
+
else if (c === ")")
|
|
50
|
+
depth--;
|
|
51
|
+
else if (c === "," && depth === 0) {
|
|
52
|
+
out.push(s.slice(start, i).trim());
|
|
53
|
+
start = i + 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
out.push(s.slice(start).trim());
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
const cut = (s, sep) => {
|
|
61
|
+
const i = s.indexOf(sep);
|
|
62
|
+
return i < 0 ? [s, "", false] : [s.slice(0, i), s.slice(i + sep.length), true];
|
|
63
|
+
};
|
|
64
|
+
const unquote = (v) => (v.length >= 2 && v[0] === '"' && v[v.length - 1] === '"' ? JSON.parse(v) : v);
|
|
65
|
+
function member(label, raw) {
|
|
66
|
+
if (!raw.startsWith("("))
|
|
67
|
+
throw new Error("Signature-Input member is not an inner list");
|
|
68
|
+
const end = raw.indexOf(")");
|
|
69
|
+
if (end < 0)
|
|
70
|
+
throw new Error("unterminated inner list");
|
|
71
|
+
const components = raw.slice(1, end).split(/\s+/).filter(Boolean).map((item) => {
|
|
72
|
+
if (!item.startsWith('"') || !item.endsWith('"'))
|
|
73
|
+
throw new Error(`bad component ${item}`);
|
|
74
|
+
return JSON.parse(item);
|
|
75
|
+
});
|
|
76
|
+
const ps = {};
|
|
77
|
+
for (const p of raw.slice(end + 1).split(";").slice(1)) {
|
|
78
|
+
const [k, v] = cut(p, "=");
|
|
79
|
+
ps[k.trim()] = unquote(v);
|
|
80
|
+
}
|
|
81
|
+
return { label, components, params: ps, raw };
|
|
82
|
+
}
|
|
83
|
+
export function pickWebBotAuth(header) {
|
|
84
|
+
for (const m of splitTop(header)) {
|
|
85
|
+
const [label, raw, ok] = cut(m, "=");
|
|
86
|
+
if (!ok)
|
|
87
|
+
continue;
|
|
88
|
+
const sp = member(label, raw);
|
|
89
|
+
if (sp.params.tag === TAG)
|
|
90
|
+
return sp;
|
|
91
|
+
}
|
|
92
|
+
throw new Error('no signature with tag="web-bot-auth"');
|
|
93
|
+
}
|
|
94
|
+
export function signatureBytes(header, label) {
|
|
95
|
+
for (const m of splitTop(header)) {
|
|
96
|
+
const [l, v] = cut(m, "=");
|
|
97
|
+
if (l === label && v.length > 2 && v[0] === ":" && v[v.length - 1] === ":")
|
|
98
|
+
return Buffer.from(v.slice(1, -1), "base64");
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`no Signature for ${label}`);
|
|
101
|
+
}
|
|
102
|
+
export function parseAgent(header, label) {
|
|
103
|
+
const h = header.trim();
|
|
104
|
+
if (h.startsWith('"'))
|
|
105
|
+
return JSON.parse(h);
|
|
106
|
+
for (const m of splitTop(h)) {
|
|
107
|
+
const [l, v, ok] = cut(m, "=");
|
|
108
|
+
if (ok && l === label)
|
|
109
|
+
return JSON.parse(v);
|
|
110
|
+
}
|
|
111
|
+
throw new Error(`Signature-Agent has no value for ${label}`);
|
|
112
|
+
}
|
|
113
|
+
export function jwkPublicKey(jwk) {
|
|
114
|
+
if (jwk?.kty !== "OKP" || jwk.crv !== "Ed25519" || typeof jwk.x !== "string")
|
|
115
|
+
return null;
|
|
116
|
+
const pub = Buffer.from(jwk.x, "base64url");
|
|
117
|
+
if (pub.length !== 32 || (jwk.kid && jwk.kid !== thumbprint(pub)))
|
|
118
|
+
return null;
|
|
119
|
+
return pub;
|
|
120
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@messh/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sign and verify HTTP requests as a messh identity (Web Bot Auth, RFC 9421, Ed25519)",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Mukul Lohar",
|
|
7
|
+
"homepage": "https://messh.io",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/messhio/messh.git",
|
|
11
|
+
"directory": "sdk/ts"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/messhio/messh/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"messh",
|
|
18
|
+
"web-bot-auth",
|
|
19
|
+
"rfc9421",
|
|
20
|
+
"http-message-signatures",
|
|
21
|
+
"ed25519",
|
|
22
|
+
"agents"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./verify": {
|
|
34
|
+
"types": "./dist/verify.d.ts",
|
|
35
|
+
"import": "./dist/verify.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"sideEffects": false,
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc -p tsconfig.json",
|
|
46
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
47
|
+
"prepublishOnly": "npm test"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public",
|
|
51
|
+
"provenance": true
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^20.11.0",
|
|
55
|
+
"typescript": "^5.4.0"
|
|
56
|
+
}
|
|
57
|
+
}
|