@evanp/activitypub-bot 0.36.2 → 0.37.1
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 +3 -1
- package/lib/digester.js +6 -0
- package/lib/httpmessagesignature.js +212 -0
- package/lib/httpsignatureauthenticator.js +129 -46
- package/package.json +2 -2
package/lib/app.js
CHANGED
|
@@ -44,6 +44,7 @@ import DoNothingBot from './bots/donothing.js'
|
|
|
44
44
|
import { FanoutWorker } from './fanoutworker.js'
|
|
45
45
|
import { IntakeWorker } from './intakeworker.js'
|
|
46
46
|
import { RemoteObjectCache } from './remoteobjectcache.js'
|
|
47
|
+
import { HTTPMessageSignature } from './httpmessagesignature.js'
|
|
47
48
|
|
|
48
49
|
const currentDir = dirname(fileURLToPath(import.meta.url))
|
|
49
50
|
const DEFAULT_INDEX_FILENAME = resolve(currentDir, '..', 'web', 'index.html')
|
|
@@ -66,6 +67,7 @@ export async function makeApp ({ databaseUrl, origin, bots, logLevel = 'silent',
|
|
|
66
67
|
await runMigrations(connection)
|
|
67
68
|
const formatter = new UrlFormatter(origin)
|
|
68
69
|
const signer = new HTTPSignature(logger)
|
|
70
|
+
const messageSigner = new HTTPMessageSignature(logger)
|
|
69
71
|
const digester = new Digester(logger)
|
|
70
72
|
const actorStorage = new ActorStorage(connection, formatter)
|
|
71
73
|
const botDataStorage = new BotDataStorage(connection)
|
|
@@ -75,7 +77,7 @@ export async function makeApp ({ databaseUrl, origin, bots, logLevel = 'silent',
|
|
|
75
77
|
const remoteObjectCache = new RemoteObjectCache(connection, logger)
|
|
76
78
|
const client = new ActivityPubClient(keyStorage, formatter, signer, digester, logger, limiter, remoteObjectCache)
|
|
77
79
|
const remoteKeyStorage = new RemoteKeyStorage(client, connection, logger)
|
|
78
|
-
const signature = new HTTPSignatureAuthenticator(remoteKeyStorage, signer, digester, logger)
|
|
80
|
+
const signature = new HTTPSignatureAuthenticator(remoteKeyStorage, signer, messageSigner, digester, logger)
|
|
79
81
|
const jobQueue = new JobQueue(connection, logger)
|
|
80
82
|
const distributor = new ActivityDistributor(
|
|
81
83
|
client,
|
package/lib/digester.js
CHANGED
|
@@ -12,6 +12,12 @@ export class Digester {
|
|
|
12
12
|
return `sha-256=${digest.digest('base64')}`
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
async contentDigest (body) {
|
|
16
|
+
const digest = crypto.createHash('sha256')
|
|
17
|
+
digest.update(body)
|
|
18
|
+
return `sha-256=:${digest.digest('base64')}:`
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
equals (digest1, digest2) {
|
|
16
22
|
const [alg1, hash1] = digest1.split('=', 2)
|
|
17
23
|
const [alg2, hash2] = digest2.split('=', 2)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import crypto from 'node:crypto'
|
|
3
|
+
|
|
4
|
+
export class HTTPMessageSignature {
|
|
5
|
+
static #preferredAlgs = [
|
|
6
|
+
'ecdsa-p384-sha384',
|
|
7
|
+
'ecdsa-p256-sha256',
|
|
8
|
+
'rsa-pss-sha512',
|
|
9
|
+
'rsa-pss-sha256',
|
|
10
|
+
'rsa-v1_5-sha256'
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
#logger
|
|
14
|
+
|
|
15
|
+
constructor (logger) {
|
|
16
|
+
assert.ok(logger)
|
|
17
|
+
assert.strictEqual(typeof logger, 'object')
|
|
18
|
+
this.#logger = logger
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
keyId (signatureInput) {
|
|
22
|
+
assert.ok(signatureInput)
|
|
23
|
+
assert.strictEqual(typeof signatureInput, 'string')
|
|
24
|
+
const inputs = this.#parseSignatureInput(signatureInput)
|
|
25
|
+
const input = this.#bestInput(inputs)
|
|
26
|
+
return (input)
|
|
27
|
+
? input.keyid
|
|
28
|
+
: null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
created (signatureInput) {
|
|
32
|
+
assert.ok(signatureInput)
|
|
33
|
+
assert.strictEqual(typeof signatureInput, 'string')
|
|
34
|
+
const inputs = this.#parseSignatureInput(signatureInput)
|
|
35
|
+
const input = this.#bestInput(inputs)
|
|
36
|
+
return (input)
|
|
37
|
+
? input.created
|
|
38
|
+
: null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async sign ({ privateKey, keyId, url, method, headers }) {
|
|
42
|
+
const parsed = new URL(url)
|
|
43
|
+
|
|
44
|
+
const signatureInput = []
|
|
45
|
+
|
|
46
|
+
signatureInput.push(['@method', method.toUpperCase()])
|
|
47
|
+
signatureInput.push(['@authority', parsed.host])
|
|
48
|
+
signatureInput.push(['@path', parsed.pathname])
|
|
49
|
+
if (parsed.search) {
|
|
50
|
+
signatureInput.push(['@query', parsed.search])
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const name in headers) {
|
|
54
|
+
const lcname = name.toLowerCase()
|
|
55
|
+
signatureInput.push([lcname, headers[name]])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const created = Math.floor(Date.now() / 1000)
|
|
59
|
+
const componentList = signatureInput.map(([name]) => `"${name}"`).join(' ')
|
|
60
|
+
const signatureParams = `(${componentList});keyid="${keyId}";alg="rsa-v1_5-sha256";created=${created}`
|
|
61
|
+
|
|
62
|
+
signatureInput.push(['@signature-params', signatureParams])
|
|
63
|
+
|
|
64
|
+
const data = signatureInput.map(pair => `"${pair[0]}": ${pair[1]}`).join('\n')
|
|
65
|
+
|
|
66
|
+
const signer = crypto.createSign('sha256')
|
|
67
|
+
signer.update(data)
|
|
68
|
+
const signature = signer.sign(privateKey).toString('base64')
|
|
69
|
+
signer.end()
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
'signature-input': `sig1=${signatureParams}`,
|
|
73
|
+
signature: `sig1=:${signature}:`
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async validate (publicKeyPem, signatureInput, signature, method, url, headers) {
|
|
78
|
+
const inputs = this.#parseSignatureInput(signatureInput)
|
|
79
|
+
const input = this.#bestInput(inputs)
|
|
80
|
+
if (!input) {
|
|
81
|
+
throw new Error('No input with supported algorithms')
|
|
82
|
+
}
|
|
83
|
+
const bytes = this.#sigBytes(signature, input.name)
|
|
84
|
+
if (!bytes) {
|
|
85
|
+
throw new Error('No input with supported algorithms')
|
|
86
|
+
}
|
|
87
|
+
const data = this.#inputData(input, method, url, headers)
|
|
88
|
+
const verifier = this.#getVerifier(input.alg)
|
|
89
|
+
verifier.update(data)
|
|
90
|
+
const options = this.#getVerifierOptions(input.alg)
|
|
91
|
+
return verifier.verify(publicKeyPem, bytes, options)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#parseSignatureInput (signatureInput) {
|
|
95
|
+
const SIG_RE = /(\w+)=(\([^)]*\))((?:;[^,]*)*)/g
|
|
96
|
+
const PARAM_RE = /;(\w+)=("(?:[^"\\]|\\.)*"|\d+)/g
|
|
97
|
+
const signatures = {}
|
|
98
|
+
for (const match of signatureInput.matchAll(SIG_RE)) {
|
|
99
|
+
const name = match[1]
|
|
100
|
+
const attrStr = `${match[2]}${match[3]}`
|
|
101
|
+
const params = match[2].slice(1, -1)
|
|
102
|
+
.split(' ')
|
|
103
|
+
.filter(s => s.length > 0)
|
|
104
|
+
.map(token => {
|
|
105
|
+
const m = token.match(/^"([^"]+)"(.*)$/)
|
|
106
|
+
if (!m) return token
|
|
107
|
+
return m[2] ? `${m[1]}${m[2]}` : m[1]
|
|
108
|
+
})
|
|
109
|
+
const sigvals = {}
|
|
110
|
+
for (const paramMatch of match[3].matchAll(PARAM_RE)) {
|
|
111
|
+
const k = paramMatch[1]
|
|
112
|
+
const v = paramMatch[2]
|
|
113
|
+
sigvals[k] = v.match(/^\d+$/) ? parseInt(v) : v.slice(1, -1)
|
|
114
|
+
}
|
|
115
|
+
signatures[name] = { name, params, attrStr, ...sigvals }
|
|
116
|
+
}
|
|
117
|
+
return signatures
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#bestInput (inputs) {
|
|
121
|
+
for (const alg of HTTPMessageSignature.#preferredAlgs) {
|
|
122
|
+
const entry = Object.values(inputs).find(sig => sig.alg === alg)
|
|
123
|
+
if (entry) {
|
|
124
|
+
return entry
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#inputData (input, method, url, headers) {
|
|
131
|
+
const signatureParams = []
|
|
132
|
+
const parsed = URL.parse(url)
|
|
133
|
+
for (const param of input.params) {
|
|
134
|
+
let value
|
|
135
|
+
switch (param) {
|
|
136
|
+
case '@method':
|
|
137
|
+
value = method.toUpperCase()
|
|
138
|
+
break
|
|
139
|
+
case '@authority':
|
|
140
|
+
value = headers.host
|
|
141
|
+
break
|
|
142
|
+
case '@path':
|
|
143
|
+
value = parsed.pathname
|
|
144
|
+
break
|
|
145
|
+
case '@query':
|
|
146
|
+
value = parsed.search
|
|
147
|
+
break
|
|
148
|
+
case '@target-uri':
|
|
149
|
+
value = url
|
|
150
|
+
break
|
|
151
|
+
case '@scheme':
|
|
152
|
+
value = parsed.protocol.slice(0, -1)
|
|
153
|
+
break
|
|
154
|
+
case '@request-target':
|
|
155
|
+
value = (parsed.search)
|
|
156
|
+
? `${parsed.pathname}${parsed.search}`
|
|
157
|
+
: parsed.pathname
|
|
158
|
+
break
|
|
159
|
+
default:
|
|
160
|
+
if (param.startsWith('@query-param')) {
|
|
161
|
+
const nameMatch = param.match(/;name="([^"]+)"/)
|
|
162
|
+
if (!nameMatch) throw new Error('Missing name for @query-param')
|
|
163
|
+
const paramName = nameMatch[1]
|
|
164
|
+
signatureParams.push(['@query-param', parsed.searchParams.get(paramName), `name="${paramName}"`])
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
if (param.length > 0 && param[0] === '@') {
|
|
168
|
+
throw new Error(`Unrecognized derived component ${param}`)
|
|
169
|
+
}
|
|
170
|
+
value = headers[param]
|
|
171
|
+
}
|
|
172
|
+
signatureParams.push([param, value])
|
|
173
|
+
}
|
|
174
|
+
signatureParams.push(['@signature-params', input.attrStr])
|
|
175
|
+
return signatureParams.map(
|
|
176
|
+
arr => arr.length === 3
|
|
177
|
+
? `"${arr[0]}";${arr[2]}: ${arr[1]}`
|
|
178
|
+
: `"${arr[0]}": ${arr[1]}`
|
|
179
|
+
).join('\n')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#getVerifier (alg) {
|
|
183
|
+
switch (alg) {
|
|
184
|
+
case 'ecdsa-p384-sha384':
|
|
185
|
+
return crypto.createVerify('sha384')
|
|
186
|
+
case 'ecdsa-p256-sha256':
|
|
187
|
+
return crypto.createVerify('sha256')
|
|
188
|
+
case 'rsa-pss-sha512':
|
|
189
|
+
return crypto.createVerify('sha512')
|
|
190
|
+
case 'rsa-pss-sha256':
|
|
191
|
+
return crypto.createVerify('sha256')
|
|
192
|
+
case 'rsa-v1_5-sha256':
|
|
193
|
+
return crypto.createVerify('sha256')
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#getVerifierOptions (alg) {
|
|
198
|
+
switch (alg) {
|
|
199
|
+
case 'rsa-pss-sha512':
|
|
200
|
+
case 'rsa-pss-sha256':
|
|
201
|
+
return { padding: crypto.constants.RSA_PKCS1_PSS_PADDING }
|
|
202
|
+
default:
|
|
203
|
+
return { }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#sigBytes (sigHeader, name) {
|
|
208
|
+
const sigMatch = sigHeader.match(new RegExp(`${name}=:([^:]+):`))
|
|
209
|
+
if (!sigMatch) return false
|
|
210
|
+
return Buffer.from(sigMatch[1], 'base64')
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -1,27 +1,44 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
|
|
1
3
|
import createHttpError from 'http-errors'
|
|
2
4
|
|
|
3
5
|
import BotMaker from './botmaker.js'
|
|
4
6
|
|
|
5
7
|
export class HTTPSignatureAuthenticator {
|
|
6
8
|
static #maxDateDiff = 5 * 60 * 1000 // 5 minutes
|
|
7
|
-
#remoteKeyStorage
|
|
8
|
-
#logger
|
|
9
|
-
#digester
|
|
10
|
-
#signer
|
|
11
|
-
|
|
9
|
+
#remoteKeyStorage
|
|
10
|
+
#logger
|
|
11
|
+
#digester
|
|
12
|
+
#signer
|
|
13
|
+
#messageSigner
|
|
14
|
+
|
|
15
|
+
constructor (remoteKeyStorage, signer, messageSigner, digester, logger) {
|
|
16
|
+
assert.ok(remoteKeyStorage)
|
|
17
|
+
assert.strictEqual(typeof remoteKeyStorage, 'object')
|
|
18
|
+
assert.ok(signer)
|
|
19
|
+
assert.strictEqual(typeof signer, 'object')
|
|
20
|
+
assert.ok(messageSigner)
|
|
21
|
+
assert.strictEqual(typeof messageSigner, 'object')
|
|
22
|
+
assert.ok(digester)
|
|
23
|
+
assert.strictEqual(typeof digester, 'object')
|
|
24
|
+
assert.ok(logger)
|
|
25
|
+
assert.strictEqual(typeof logger, 'object')
|
|
12
26
|
this.#remoteKeyStorage = remoteKeyStorage
|
|
13
27
|
this.#signer = signer
|
|
28
|
+
this.#messageSigner = messageSigner
|
|
14
29
|
this.#digester = digester
|
|
15
30
|
this.#logger = logger.child({ class: this.constructor.name })
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
async authenticate (req, res, next) {
|
|
34
|
+
const { formatter, origin, bots } = req.app.locals
|
|
35
|
+
|
|
19
36
|
const signature = req.get('Signature')
|
|
20
37
|
if (!signature) {
|
|
21
38
|
// Just continue
|
|
22
39
|
return next()
|
|
23
40
|
}
|
|
24
|
-
|
|
41
|
+
|
|
25
42
|
const originalUrl = req.originalUrl
|
|
26
43
|
const fullUrl = `${origin}${originalUrl}`
|
|
27
44
|
let parts
|
|
@@ -41,67 +58,133 @@ export class HTTPSignatureAuthenticator {
|
|
|
41
58
|
return next()
|
|
42
59
|
}
|
|
43
60
|
}
|
|
61
|
+
|
|
62
|
+
const signatureInput = req.get('Signature-Input')
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
if (signatureInput) {
|
|
66
|
+
await this.#authenticateMessageSignature(signature, signatureInput, originalUrl, req, res, next)
|
|
67
|
+
} else {
|
|
68
|
+
await this.#authenticateSignature(signature, originalUrl, req, res, next)
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
this.#logger.debug({ err }, 'Error authenticating key')
|
|
72
|
+
return next(err)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async #authenticateSignature (signature, originalUrl, req, res, next) {
|
|
44
77
|
this.#logger.debug({ signature }, 'Got signed request')
|
|
45
78
|
const date = req.get('Date')
|
|
46
79
|
if (!date) {
|
|
47
|
-
|
|
80
|
+
throw createHttpError(400, 'No date provided')
|
|
48
81
|
}
|
|
49
|
-
|
|
50
|
-
if (Math.abs(Date.parse(date) - Date.now()) >
|
|
82
|
+
if (Math.abs(Date.parse(date) - Date.now()) >
|
|
51
83
|
HTTPSignatureAuthenticator.#maxDateDiff) {
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
} catch (err) {
|
|
55
|
-
// for date parsing errors
|
|
56
|
-
return next(err)
|
|
84
|
+
throw createHttpError(400, 'Time skew too large')
|
|
57
85
|
}
|
|
58
86
|
if (req.rawBodyText && req.rawBodyText.length > 0) {
|
|
59
87
|
const digest = req.get('Digest')
|
|
60
88
|
if (!digest) {
|
|
61
|
-
|
|
89
|
+
throw createHttpError(400, 'No digest provided')
|
|
62
90
|
}
|
|
63
91
|
const calculated = await this.#digester.digest(req.rawBodyText)
|
|
64
92
|
if (!this.#digester.equals(digest, calculated)) {
|
|
65
93
|
this.#logger.debug(`calculated: ${calculated} digest: ${digest}`)
|
|
66
|
-
|
|
94
|
+
throw createHttpError(400, 'Digest mismatch')
|
|
67
95
|
}
|
|
68
96
|
}
|
|
69
97
|
const { method, headers } = req
|
|
70
98
|
this.#logger.debug({ originalUrl }, 'original URL')
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
99
|
+
const keyId = this.#signer.keyId(signature)
|
|
100
|
+
this.#logger.debug({ keyId }, 'Signed with keyId')
|
|
101
|
+
const ok = await this.#remoteKeyStorage.getPublicKey(keyId)
|
|
102
|
+
if (!ok) {
|
|
103
|
+
throw createHttpError(400, 'public key not found')
|
|
104
|
+
}
|
|
105
|
+
let owner = ok.owner
|
|
106
|
+
let publicKeyPem = ok.publicKeyPem
|
|
107
|
+
let result = await this.#signer.validate(publicKeyPem, signature, method, originalUrl, headers)
|
|
108
|
+
this.#logger.debug(`First validation result: ${result}`)
|
|
109
|
+
if (!result) {
|
|
110
|
+
// May be key rotation. Try again with uncached key
|
|
111
|
+
const ok2 = await this.#remoteKeyStorage.getPublicKey(keyId, false)
|
|
112
|
+
if (ok2.publicKeyPem === ok.publicKeyPem) {
|
|
113
|
+
this.#logger.debug('same keys')
|
|
114
|
+
} else {
|
|
115
|
+
this.#logger.debug('different keys')
|
|
116
|
+
owner = ok2.owner
|
|
117
|
+
publicKeyPem = ok2.publicKeyPem
|
|
118
|
+
result = await this.#signer.validate(publicKeyPem, signature, method, originalUrl, headers)
|
|
119
|
+
this.#logger.debug(`Validation result: ${result}`)
|
|
77
120
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
121
|
+
}
|
|
122
|
+
if (result) {
|
|
123
|
+
this.#logger.debug(`Signature valid for ${keyId}`)
|
|
124
|
+
req.auth = req.auth || {}
|
|
125
|
+
req.auth.subject = owner
|
|
126
|
+
return next()
|
|
127
|
+
} else {
|
|
128
|
+
throw createHttpError(401, 'Unauthorized')
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async #authenticateMessageSignature (signature, signatureInput, originalUrl, req, res, next) {
|
|
133
|
+
if (req.rawBodyText && req.rawBodyText.length > 0) {
|
|
134
|
+
const digest = req.get('Content-Digest')
|
|
135
|
+
if (!digest) {
|
|
136
|
+
throw createHttpError(400, 'No digest provided')
|
|
94
137
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
138
|
+
const calculated = await this.#digester.contentDigest(req.rawBodyText)
|
|
139
|
+
if (!this.#digester.equals(digest, calculated)) {
|
|
140
|
+
this.#logger.debug(`calculated: ${calculated} digest: ${digest}`)
|
|
141
|
+
throw createHttpError(400, 'Digest mismatch')
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const created = this.#messageSigner.created(signatureInput)
|
|
145
|
+
if (!created) {
|
|
146
|
+
throw createHttpError(400, 'No created timestamp provided')
|
|
147
|
+
}
|
|
148
|
+
if (Math.abs(Date.now() - created * 1000) > HTTPSignatureAuthenticator.#maxDateDiff) {
|
|
149
|
+
throw createHttpError(400, 'Time skew too large')
|
|
150
|
+
}
|
|
151
|
+
const { method, headers } = req
|
|
152
|
+
const { origin } = req.app.locals
|
|
153
|
+
const url = `${origin}${originalUrl}`
|
|
154
|
+
|
|
155
|
+
const keyId = this.#messageSigner.keyId(signatureInput)
|
|
156
|
+
if (!keyId) {
|
|
157
|
+
throw createHttpError(400, 'no public key provided')
|
|
158
|
+
}
|
|
159
|
+
this.#logger.debug({ keyId }, 'Signed with keyId')
|
|
160
|
+
const ok = await this.#remoteKeyStorage.getPublicKey(keyId)
|
|
161
|
+
if (!ok) {
|
|
162
|
+
throw createHttpError(400, 'public key not found')
|
|
163
|
+
}
|
|
164
|
+
let owner = ok.owner
|
|
165
|
+
let publicKeyPem = ok.publicKeyPem
|
|
166
|
+
let result = await this.#messageSigner.validate(publicKeyPem, signatureInput, signature, method, url, headers)
|
|
167
|
+
this.#logger.debug(`First validation result: ${result}`)
|
|
168
|
+
if (!result) {
|
|
169
|
+
// May be key rotation. Try again with uncached key
|
|
170
|
+
const ok2 = await this.#remoteKeyStorage.getPublicKey(keyId, false)
|
|
171
|
+
if (ok2.publicKeyPem === ok.publicKeyPem) {
|
|
172
|
+
this.#logger.debug('same keys')
|
|
100
173
|
} else {
|
|
101
|
-
|
|
174
|
+
this.#logger.debug('different keys')
|
|
175
|
+
owner = ok2.owner
|
|
176
|
+
publicKeyPem = ok2.publicKeyPem
|
|
177
|
+
result = await this.#messageSigner.validate(publicKeyPem, signatureInput, signature, method, url, headers)
|
|
178
|
+
this.#logger.debug(`Validation result: ${result}`)
|
|
102
179
|
}
|
|
103
|
-
}
|
|
104
|
-
|
|
180
|
+
}
|
|
181
|
+
if (result) {
|
|
182
|
+
this.#logger.debug(`Signature valid for ${keyId}`)
|
|
183
|
+
req.auth = req.auth || {}
|
|
184
|
+
req.auth.subject = owner
|
|
185
|
+
return next()
|
|
186
|
+
} else {
|
|
187
|
+
throw createHttpError(401, 'Unauthorized')
|
|
105
188
|
}
|
|
106
189
|
}
|
|
107
190
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evanp/activitypub-bot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.1",
|
|
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",
|