@evanp/activitypub-bot 0.37.0 → 0.38.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/lib/app.js +14 -1
- package/lib/httpmessagesignature.js +64 -12
- package/lib/httpsignatureauthenticator.js +12 -3
- package/package.json +2 -2
package/lib/app.js
CHANGED
|
@@ -45,10 +45,12 @@ import { FanoutWorker } from './fanoutworker.js'
|
|
|
45
45
|
import { IntakeWorker } from './intakeworker.js'
|
|
46
46
|
import { RemoteObjectCache } from './remoteobjectcache.js'
|
|
47
47
|
import { HTTPMessageSignature } from './httpmessagesignature.js'
|
|
48
|
+
import { randomUUID } from 'node:crypto'
|
|
48
49
|
|
|
49
50
|
const currentDir = dirname(fileURLToPath(import.meta.url))
|
|
50
51
|
const DEFAULT_INDEX_FILENAME = resolve(currentDir, '..', 'web', 'index.html')
|
|
51
52
|
const DEFAULT_PROFILE_FILENAME = resolve(currentDir, '..', 'web', 'profile.html')
|
|
53
|
+
const UUID_REGEXP = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
52
54
|
|
|
53
55
|
function createWorkers (logger, count, WorkerClass, ...args) {
|
|
54
56
|
const workers = Array.from({ length: count }, () => new WorkerClass(...args))
|
|
@@ -182,9 +184,20 @@ export async function makeApp ({ databaseUrl, origin, bots, logLevel = 'silent',
|
|
|
182
184
|
profileFileName
|
|
183
185
|
}
|
|
184
186
|
|
|
187
|
+
app.use(async (req, res, next) => {
|
|
188
|
+
let id = req.get('x-request-id')
|
|
189
|
+
if (!id || !id.match(UUID_REGEXP)) {
|
|
190
|
+
id = randomUUID()
|
|
191
|
+
}
|
|
192
|
+
req.id = id
|
|
193
|
+
res.set('X-Request-ID', id)
|
|
194
|
+
next()
|
|
195
|
+
})
|
|
196
|
+
|
|
185
197
|
app.use(HTTPLogger({
|
|
186
198
|
logger,
|
|
187
|
-
level: logLevel
|
|
199
|
+
level: logLevel,
|
|
200
|
+
genReqId: (req) => req.id
|
|
188
201
|
}))
|
|
189
202
|
|
|
190
203
|
app.use(express.json({
|
|
@@ -24,7 +24,7 @@ export class HTTPMessageSignature {
|
|
|
24
24
|
const inputs = this.#parseSignatureInput(signatureInput)
|
|
25
25
|
const input = this.#bestInput(inputs)
|
|
26
26
|
return (input)
|
|
27
|
-
? input.
|
|
27
|
+
? input.keyid
|
|
28
28
|
: null
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -39,12 +39,14 @@ export class HTTPMessageSignature {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
async sign ({ privateKey, keyId, url, method, headers }) {
|
|
42
|
+
this.#logger.debug({ privateKey, keyId, url, method, headers }, 'signing')
|
|
43
|
+
|
|
42
44
|
const parsed = new URL(url)
|
|
43
45
|
|
|
44
46
|
const signatureInput = []
|
|
45
47
|
|
|
46
|
-
signatureInput.push(['@method', method])
|
|
47
|
-
signatureInput.push(['@authority', parsed.
|
|
48
|
+
signatureInput.push(['@method', method.toUpperCase()])
|
|
49
|
+
signatureInput.push(['@authority', parsed.host])
|
|
48
50
|
signatureInput.push(['@path', parsed.pathname])
|
|
49
51
|
if (parsed.search) {
|
|
50
52
|
signatureInput.push(['@query', parsed.search])
|
|
@@ -57,34 +59,54 @@ export class HTTPMessageSignature {
|
|
|
57
59
|
|
|
58
60
|
const created = Math.floor(Date.now() / 1000)
|
|
59
61
|
const componentList = signatureInput.map(([name]) => `"${name}"`).join(' ')
|
|
60
|
-
const signatureParams = `(${componentList});
|
|
62
|
+
const signatureParams = `(${componentList});keyid="${keyId}";alg="rsa-v1_5-sha256";created=${created}`
|
|
61
63
|
|
|
62
64
|
signatureInput.push(['@signature-params', signatureParams])
|
|
63
65
|
|
|
66
|
+
this.#logger.debug({ signatureInput }, 'built data structure')
|
|
67
|
+
|
|
64
68
|
const data = signatureInput.map(pair => `"${pair[0]}": ${pair[1]}`).join('\n')
|
|
65
69
|
|
|
70
|
+
this.#logger.debug({ data }, 'data')
|
|
71
|
+
|
|
66
72
|
const signer = crypto.createSign('sha256')
|
|
67
73
|
signer.update(data)
|
|
68
74
|
const signature = signer.sign(privateKey).toString('base64')
|
|
69
75
|
signer.end()
|
|
70
76
|
|
|
71
|
-
|
|
77
|
+
const result = {
|
|
72
78
|
'signature-input': `sig1=${signatureParams}`,
|
|
73
79
|
signature: `sig1=:${signature}:`
|
|
74
80
|
}
|
|
81
|
+
|
|
82
|
+
this.#logger.debug({ result }, 'returning headers')
|
|
83
|
+
|
|
84
|
+
return result
|
|
75
85
|
}
|
|
76
86
|
|
|
77
|
-
async validate (publicKeyPem, signatureInput, signature, method,
|
|
87
|
+
async validate (publicKeyPem, signatureInput, signature, method, url, headers) {
|
|
88
|
+
this.#logger.debug(
|
|
89
|
+
{ publicKeyPem, signatureInput, signature, method, url, headers }, 'validating signature'
|
|
90
|
+
)
|
|
78
91
|
const inputs = this.#parseSignatureInput(signatureInput)
|
|
92
|
+
this.#logger.debug(
|
|
93
|
+
{ inputs }, 'validating signature'
|
|
94
|
+
)
|
|
79
95
|
const input = this.#bestInput(inputs)
|
|
80
96
|
if (!input) {
|
|
81
97
|
throw new Error('No input with supported algorithms')
|
|
82
98
|
}
|
|
99
|
+
this.#logger.debug(
|
|
100
|
+
{ input }, 'best input'
|
|
101
|
+
)
|
|
83
102
|
const bytes = this.#sigBytes(signature, input.name)
|
|
84
103
|
if (!bytes) {
|
|
85
104
|
throw new Error('No input with supported algorithms')
|
|
86
105
|
}
|
|
87
|
-
const data = this.#inputData(input, method,
|
|
106
|
+
const data = this.#inputData(input, method, url, headers)
|
|
107
|
+
this.#logger.debug(
|
|
108
|
+
{ data }, 'input data'
|
|
109
|
+
)
|
|
88
110
|
const verifier = this.#getVerifier(input.alg)
|
|
89
111
|
verifier.update(data)
|
|
90
112
|
const options = this.#getVerifierOptions(input.alg)
|
|
@@ -101,7 +123,11 @@ export class HTTPMessageSignature {
|
|
|
101
123
|
const params = match[2].slice(1, -1)
|
|
102
124
|
.split(' ')
|
|
103
125
|
.filter(s => s.length > 0)
|
|
104
|
-
.map(
|
|
126
|
+
.map(token => {
|
|
127
|
+
const m = token.match(/^"([^"]+)"(.*)$/)
|
|
128
|
+
if (!m) return token
|
|
129
|
+
return m[2] ? `${m[1]}${m[2]}` : m[1]
|
|
130
|
+
})
|
|
105
131
|
const sigvals = {}
|
|
106
132
|
for (const paramMatch of match[3].matchAll(PARAM_RE)) {
|
|
107
133
|
const k = paramMatch[1]
|
|
@@ -123,8 +149,9 @@ export class HTTPMessageSignature {
|
|
|
123
149
|
return null
|
|
124
150
|
}
|
|
125
151
|
|
|
126
|
-
#inputData (input, method,
|
|
152
|
+
#inputData (input, method, url, headers) {
|
|
127
153
|
const signatureParams = []
|
|
154
|
+
const parsed = URL.parse(url)
|
|
128
155
|
for (const param of input.params) {
|
|
129
156
|
let value
|
|
130
157
|
switch (param) {
|
|
@@ -135,18 +162,43 @@ export class HTTPMessageSignature {
|
|
|
135
162
|
value = headers.host
|
|
136
163
|
break
|
|
137
164
|
case '@path':
|
|
138
|
-
value =
|
|
165
|
+
value = parsed.pathname
|
|
139
166
|
break
|
|
140
167
|
case '@query':
|
|
141
|
-
value =
|
|
168
|
+
value = parsed.search
|
|
169
|
+
break
|
|
170
|
+
case '@target-uri':
|
|
171
|
+
value = url
|
|
172
|
+
break
|
|
173
|
+
case '@scheme':
|
|
174
|
+
value = parsed.protocol.slice(0, -1)
|
|
175
|
+
break
|
|
176
|
+
case '@request-target':
|
|
177
|
+
value = (parsed.search)
|
|
178
|
+
? `${parsed.pathname}${parsed.search}`
|
|
179
|
+
: parsed.pathname
|
|
142
180
|
break
|
|
143
181
|
default:
|
|
182
|
+
if (param.startsWith('@query-param')) {
|
|
183
|
+
const nameMatch = param.match(/;name="([^"]+)"/)
|
|
184
|
+
if (!nameMatch) throw new Error('Missing name for @query-param')
|
|
185
|
+
const paramName = nameMatch[1]
|
|
186
|
+
signatureParams.push(['@query-param', parsed.searchParams.get(paramName), `name="${paramName}"`])
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
if (param.length > 0 && param[0] === '@') {
|
|
190
|
+
throw new Error(`Unrecognized derived component ${param}`)
|
|
191
|
+
}
|
|
144
192
|
value = headers[param]
|
|
145
193
|
}
|
|
146
194
|
signatureParams.push([param, value])
|
|
147
195
|
}
|
|
148
196
|
signatureParams.push(['@signature-params', input.attrStr])
|
|
149
|
-
return signatureParams.map(
|
|
197
|
+
return signatureParams.map(
|
|
198
|
+
arr => arr.length === 3
|
|
199
|
+
? `"${arr[0]}";${arr[2]}: ${arr[1]}`
|
|
200
|
+
: `"${arr[0]}": ${arr[1]}`
|
|
201
|
+
).join('\n')
|
|
150
202
|
}
|
|
151
203
|
|
|
152
204
|
#getVerifier (alg) {
|
|
@@ -130,6 +130,15 @@ export class HTTPSignatureAuthenticator {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
async #authenticateMessageSignature (signature, signatureInput, originalUrl, req, res, next) {
|
|
133
|
+
this.#logger.debug(
|
|
134
|
+
{ signature, signatureInput, originalUrl },
|
|
135
|
+
'authenticating message signature'
|
|
136
|
+
)
|
|
137
|
+
const date = req.get('Date')
|
|
138
|
+
if (date && Math.abs(Date.parse(date) - Date.now()) >
|
|
139
|
+
HTTPSignatureAuthenticator.#maxDateDiff) {
|
|
140
|
+
throw createHttpError(400, 'Time skew too large')
|
|
141
|
+
}
|
|
133
142
|
if (req.rawBodyText && req.rawBodyText.length > 0) {
|
|
134
143
|
const digest = req.get('Content-Digest')
|
|
135
144
|
if (!digest) {
|
|
@@ -150,7 +159,7 @@ export class HTTPSignatureAuthenticator {
|
|
|
150
159
|
}
|
|
151
160
|
const { method, headers } = req
|
|
152
161
|
const { origin } = req.app.locals
|
|
153
|
-
const
|
|
162
|
+
const url = `${origin}${originalUrl}`
|
|
154
163
|
|
|
155
164
|
const keyId = this.#messageSigner.keyId(signatureInput)
|
|
156
165
|
if (!keyId) {
|
|
@@ -163,7 +172,7 @@ export class HTTPSignatureAuthenticator {
|
|
|
163
172
|
}
|
|
164
173
|
let owner = ok.owner
|
|
165
174
|
let publicKeyPem = ok.publicKeyPem
|
|
166
|
-
let result = await this.#messageSigner.validate(publicKeyPem, signatureInput, signature, method,
|
|
175
|
+
let result = await this.#messageSigner.validate(publicKeyPem, signatureInput, signature, method, url, headers)
|
|
167
176
|
this.#logger.debug(`First validation result: ${result}`)
|
|
168
177
|
if (!result) {
|
|
169
178
|
// May be key rotation. Try again with uncached key
|
|
@@ -174,7 +183,7 @@ export class HTTPSignatureAuthenticator {
|
|
|
174
183
|
this.#logger.debug('different keys')
|
|
175
184
|
owner = ok2.owner
|
|
176
185
|
publicKeyPem = ok2.publicKeyPem
|
|
177
|
-
result = await this.#messageSigner.validate(publicKeyPem, signatureInput, signature, method,
|
|
186
|
+
result = await this.#messageSigner.validate(publicKeyPem, signatureInput, signature, method, url, headers)
|
|
178
187
|
this.#logger.debug(`Validation result: ${result}`)
|
|
179
188
|
}
|
|
180
189
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evanp/activitypub-bot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "server-side ActivityPub bot framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"sequelize": "^6.37.7"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@evanp/activitypub-nock": "^0.
|
|
46
|
+
"@evanp/activitypub-nock": "^0.9.2",
|
|
47
47
|
"eslint": "^8.57.1",
|
|
48
48
|
"eslint-config-standard": "^17.1.0",
|
|
49
49
|
"eslint-plugin-import": "^2.29.1",
|