@evanp/activitypub-bot 0.38.4 → 0.39.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.
@@ -4,9 +4,9 @@ import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
 
6
6
  import fetch from 'node-fetch'
7
- import createHttpError from 'http-errors'
8
7
 
9
8
  import as2 from './activitystreams.js'
9
+ import { SignaturePolicyStorage } from './signaturepolicystorage.js'
10
10
 
11
11
  const __filename = fileURLToPath(import.meta.url)
12
12
  const __dirname = path.dirname(__filename)
@@ -22,6 +22,32 @@ const COLLECTION_TYPES = [
22
22
  `${NS}OrderedCollection`
23
23
  ]
24
24
 
25
+ function normalizeHeaders (headers) {
26
+ if (!headers) {
27
+ return headers
28
+ }
29
+ if (typeof headers.forEach === 'function') {
30
+ const result = {}
31
+ headers.forEach((value, key) => {
32
+ result[key] = value
33
+ })
34
+ return result
35
+ }
36
+ return headers
37
+ }
38
+
39
+ export class ActivityPubClientError extends Error {
40
+ constructor (status, message, { url, method, headers, body } = {}) {
41
+ super(message)
42
+ this.name = 'ActivityPubClientError'
43
+ this.status = status
44
+ this.url = url
45
+ this.method = method
46
+ this.headers = normalizeHeaders(headers)
47
+ this.body = body
48
+ }
49
+ }
50
+
25
51
  export class ActivityPubClient {
26
52
  static #githubUrl = 'https://github.com/evanp/activitypub-bot'
27
53
  static #userAgent = `activitypub.bot/${version} (${ActivityPubClient.#githubUrl})`
@@ -37,8 +63,10 @@ export class ActivityPubClient {
37
63
  #logger = null
38
64
  #limiter
39
65
  #cache
66
+ #messageSigner
67
+ #policyStorage
40
68
 
41
- constructor (keyStorage, urlFormatter, signer, digester, logger, limiter, cache) {
69
+ constructor (keyStorage, urlFormatter, signer, digester, logger, limiter, cache, messageSigner, policyStorage) {
42
70
  assert.strictEqual(typeof keyStorage, 'object')
43
71
  assert.strictEqual(typeof urlFormatter, 'object')
44
72
  assert.strictEqual(typeof signer, 'object')
@@ -46,6 +74,8 @@ export class ActivityPubClient {
46
74
  assert.strictEqual(typeof logger, 'object')
47
75
  assert.strictEqual(typeof limiter, 'object')
48
76
  assert.strictEqual(typeof cache, 'object')
77
+ assert.strictEqual(typeof messageSigner, 'object')
78
+ assert.strictEqual(typeof policyStorage, 'object')
49
79
  this.#keyStorage = keyStorage
50
80
  this.#urlFormatter = urlFormatter
51
81
  this.#signer = signer
@@ -53,6 +83,8 @@ export class ActivityPubClient {
53
83
  this.#logger = logger.child({ class: this.constructor.name })
54
84
  this.#limiter = limiter
55
85
  this.#cache = cache
86
+ this.#messageSigner = messageSigner
87
+ this.#policyStorage = policyStorage
56
88
  }
57
89
 
58
90
  async get (url, username = this.#urlFormatter.hostname) {
@@ -129,21 +161,47 @@ export class ActivityPubClient {
129
161
  this.#logger.debug({ headers }, 'Sending headers')
130
162
  const method = 'GET'
131
163
  this.#logger.debug({ url: baseUrl }, 'Signing GET request')
164
+ let storedPolicy, lastPolicy
132
165
  if (sign) {
133
- headers.signature =
166
+ storedPolicy = await this.#policyStorage.get(parsed.origin)
167
+ if (!storedPolicy || storedPolicy === SignaturePolicyStorage.RFC9421) {
168
+ lastPolicy = SignaturePolicyStorage.RFC9421
169
+ const sigHeaders = await this.#messageSign({ username, url: baseUrl, method, headers })
170
+ Object.assign(headers, sigHeaders || {})
171
+ } else if (storedPolicy === SignaturePolicyStorage.DRAFT_CAVAGE_12) {
172
+ lastPolicy = SignaturePolicyStorage.DRAFT_CAVAGE_12
173
+ headers.signature =
134
174
  await this.#sign({ username, url: baseUrl, method, headers })
175
+ } else {
176
+ throw new Error(`Unexpected signature policy ${storedPolicy}`)
177
+ }
135
178
  }
136
179
  const hostname = parsed.hostname
137
180
  this.#logger.debug({ url: baseUrl, hostname }, 'Waiting for rate limiter')
138
181
  await this.#limiter.limit(hostname)
139
182
  this.#logger.debug({ url: baseUrl }, 'Fetching with GET')
140
- const res = await fetch(baseUrl,
183
+ let res = await fetch(baseUrl,
141
184
  {
142
185
  method,
143
186
  headers
144
187
  }
145
188
  )
146
189
  this.#logger.debug({ hostname, status: res.status }, 'response received')
190
+ if ([401, 403].includes(res.status) &&
191
+ sign &&
192
+ !storedPolicy) {
193
+ lastPolicy = SignaturePolicyStorage.DRAFT_CAVAGE_12
194
+ delete headers['signature-input']
195
+ headers.signature =
196
+ await this.#sign({ username, url: baseUrl, method, headers })
197
+ res = await fetch(baseUrl,
198
+ {
199
+ method,
200
+ headers
201
+ }
202
+ )
203
+ }
204
+
147
205
  await this.#limiter.update(hostname, res.headers)
148
206
  this.#logger.debug({ url }, 'Finished GET')
149
207
  if (useCache && res.status === 304) {
@@ -155,10 +213,10 @@ export class ActivityPubClient {
155
213
  { status: res.status, body, url: baseUrl },
156
214
  'Could not fetch url'
157
215
  )
158
- throw createHttpError(
216
+ throw new ActivityPubClientError(
159
217
  res.status,
160
218
  `Could not fetch ${baseUrl}`,
161
- { headers: res.headers }
219
+ { url: baseUrl, method, headers: res.headers, body }
162
220
  )
163
221
  }
164
222
 
@@ -176,6 +234,10 @@ export class ActivityPubClient {
176
234
  throw err
177
235
  }
178
236
 
237
+ if (sign && !storedPolicy && lastPolicy) {
238
+ await this.#policyStorage.set(parsed.origin, lastPolicy)
239
+ }
240
+
179
241
  await this.#cache.set(baseUrl, username, json, res.headers)
180
242
 
181
243
  return json
@@ -206,6 +268,7 @@ export class ActivityPubClient {
206
268
  assert.ok(username)
207
269
  assert.equal(typeof username, 'string')
208
270
  assert.ok(username !== '*')
271
+ const parsed = (new URL(url))
209
272
  const json = await obj.export()
210
273
  this.#fixupJson(json)
211
274
  const body = JSON.stringify(json)
@@ -218,28 +281,56 @@ export class ActivityPubClient {
218
281
  const method = 'POST'
219
282
  assert.ok(headers)
220
283
  this.#logger.debug({ url }, 'Signing POST')
221
- headers.signature = await this.#sign({ username, url, method, headers })
222
- const hostname = (new URL(url)).hostname
284
+ let lastPolicy
285
+ const storedPolicy = await this.#policyStorage.get(parsed.origin)
286
+ if (!storedPolicy || storedPolicy === SignaturePolicyStorage.RFC9421) {
287
+ lastPolicy = SignaturePolicyStorage.RFC9421
288
+ const sigHeaders = await this.#messageSign({ username, url, method, headers })
289
+ Object.assign(headers, sigHeaders || {})
290
+ } else if (storedPolicy === SignaturePolicyStorage.DRAFT_CAVAGE_12) {
291
+ lastPolicy = SignaturePolicyStorage.DRAFT_CAVAGE_12
292
+ headers.signature =
293
+ await this.#sign({ username, url, method, headers })
294
+ } else {
295
+ throw new Error(`Unexpected signature policy ${storedPolicy}`)
296
+ }
297
+ const hostname = parsed.hostname
223
298
  this.#logger.debug({ url, hostname }, 'Waiting for rate limiter')
224
299
  await this.#limiter.limit(hostname)
225
300
  this.#logger.debug({ url }, 'Fetching POST')
226
- const res = await fetch(url,
301
+ let res = await fetch(url,
227
302
  {
228
303
  method,
229
304
  headers,
230
305
  body
231
306
  }
232
307
  )
308
+ if ([401, 403].includes(res.status) && !storedPolicy) {
309
+ lastPolicy = SignaturePolicyStorage.DRAFT_CAVAGE_12
310
+ delete headers['signature-input']
311
+ headers.signature =
312
+ await this.#sign({ username, url, method, headers })
313
+ res = await fetch(url,
314
+ {
315
+ method,
316
+ headers,
317
+ body
318
+ }
319
+ )
320
+ }
233
321
  this.#logger.debug({ hostname }, 'updating limiter')
234
322
  await this.#limiter.update(hostname, res.headers)
235
323
  this.#logger.debug({ url }, 'Done fetching POST')
236
324
  if (res.status < 200 || res.status > 299) {
237
- throw createHttpError(
325
+ throw new ActivityPubClientError(
238
326
  res.status,
239
- await res.text(),
240
- { headers: res.headers }
327
+ `Could not post to ${url}`,
328
+ { url, method, headers: res.headers }
241
329
  )
242
330
  }
331
+ if (!storedPolicy && lastPolicy) {
332
+ await this.#policyStorage.set(parsed.origin, lastPolicy)
333
+ }
243
334
  }
244
335
 
245
336
  async #sign ({ username, url, method, headers }) {
@@ -252,6 +343,16 @@ export class ActivityPubClient {
252
343
  return this.#signer.sign({ privateKey, keyId, url, method, headers })
253
344
  }
254
345
 
346
+ async #messageSign ({ username, url, method, headers }) {
347
+ assert.ok(url)
348
+ assert.ok(method)
349
+ assert.ok(headers)
350
+ assert.ok(username)
351
+ const privateKey = await this.#keyStorage.getPrivateKey(username)
352
+ const keyId = this.#urlFormatter.format({ username, type: 'publickey' })
353
+ return this.#messageSigner.sign({ privateKey, keyId, url, method, headers })
354
+ }
355
+
255
356
  #isCollection (obj) {
256
357
  return (Array.isArray(obj.type))
257
358
  ? obj.type.some(item => COLLECTION_TYPES.includes(item))
package/lib/app.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import http from 'node:http'
2
2
  import { resolve, dirname } from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
+ import { randomUUID } from 'node:crypto'
4
5
 
5
6
  import { Sequelize } from 'sequelize'
6
7
  import express from 'express'
@@ -8,7 +9,7 @@ import Logger from 'pino'
8
9
  import HTTPLogger from 'pino-http'
9
10
 
10
11
  import { ActivityDistributor } from './activitydistributor.js'
11
- import { ActivityPubClient } from './activitypubclient.js'
12
+ import { ActivityPubClient, ActivityPubClientError } from './activitypubclient.js'
12
13
  import { ActorStorage } from './actorstorage.js'
13
14
  import { BotDataStorage } from './botdatastorage.js'
14
15
  import { KeyStorage } from './keystorage.js'
@@ -45,7 +46,7 @@ import { FanoutWorker } from './fanoutworker.js'
45
46
  import { IntakeWorker } from './intakeworker.js'
46
47
  import { RemoteObjectCache } from './remoteobjectcache.js'
47
48
  import { HTTPMessageSignature } from './httpmessagesignature.js'
48
- import { randomUUID } from 'node:crypto'
49
+ import { SignaturePolicyStorage } from './signaturepolicystorage.js'
49
50
 
50
51
  const currentDir = dirname(fileURLToPath(import.meta.url))
51
52
  const DEFAULT_INDEX_FILENAME = resolve(currentDir, '..', 'web', 'index.html')
@@ -77,7 +78,8 @@ export async function makeApp ({ databaseUrl, origin, bots, logLevel = 'silent',
77
78
  const objectStorage = new ObjectStorage(connection)
78
79
  const limiter = new RateLimiter(connection, logger)
79
80
  const remoteObjectCache = new RemoteObjectCache(connection, logger)
80
- const client = new ActivityPubClient(keyStorage, formatter, signer, digester, logger, limiter, remoteObjectCache)
81
+ const policyStore = new SignaturePolicyStorage(connection, logger)
82
+ const client = new ActivityPubClient(keyStorage, formatter, signer, digester, logger, limiter, remoteObjectCache, messageSigner, policyStore)
81
83
  const remoteKeyStorage = new RemoteKeyStorage(client, connection, logger)
82
84
  const signature = new HTTPSignatureAuthenticator(remoteKeyStorage, signer, messageSigner, digester, logger)
83
85
  const jobQueue = new JobQueue(connection, logger)
@@ -253,24 +255,46 @@ export async function makeApp ({ databaseUrl, origin, bots, logLevel = 'silent',
253
255
 
254
256
  app.use((err, req, res, next) => {
255
257
  const { logger } = req.app.locals
256
- let status = 500
257
- if (err.status) {
258
- status = err.status
259
- }
260
- const title = (http.STATUS_CODES[status])
261
- ? http.STATUS_CODES[status]
262
- : 'Unknown Status'
263
-
264
- if (status >= 500 && status < 600) {
265
- logger.error(err)
266
- } else if (status >= 400 && status < 500) {
267
- logger.warn(err)
258
+ if (err instanceof ActivityPubClientError) {
259
+ logger.error({
260
+ err,
261
+ status: err.status,
262
+ url: err.url,
263
+ method: err.method
264
+ }, 'ActivityPub client request failed')
265
+ res.status(500).type('application/problem+json').json({
266
+ type: 'about:blank',
267
+ title: 'Internal Server Error',
268
+ status: 500,
269
+ detail: 'Internal Server Error'
270
+ })
268
271
  } else {
269
- logger.debug(err)
272
+ let status = 500
273
+ if (err.status) {
274
+ status = err.status
275
+ }
276
+ const title = (http.STATUS_CODES[status])
277
+ ? http.STATUS_CODES[status]
278
+ : 'Unknown Status'
279
+
280
+ if (status >= 500 && status < 600) {
281
+ logger.error(err)
282
+ } else if (status >= 400 && status < 500) {
283
+ logger.warn(err)
284
+ } else {
285
+ logger.debug(err)
286
+ }
287
+ const type = err.type || 'about:blank'
288
+ const problemTitle = err.title || title
289
+ const extra = Object.fromEntries(
290
+ Object.entries(err).filter(
291
+ ([k]) => !['status', 'type', 'title', 'detail', 'stack'].includes(k)
292
+ )
293
+ )
294
+ res.status(status)
295
+ res.type('application/problem+json')
296
+ res.json({ type, title: problemTitle, status, detail: err.message, ...extra })
270
297
  }
271
- res.status(status)
272
- res.type('application/problem+json')
273
- res.json({ type: 'about:blank', title, status, detail: err.message })
274
298
  })
275
299
 
276
300
  app.onIdle = async () => {
package/lib/errors.js ADDED
@@ -0,0 +1,158 @@
1
+ import http from 'node:http'
2
+
3
+ const FEP_C180 = 'https://w3id.org/fep/c180'
4
+
5
+ export class ProblemDetailsError extends Error {
6
+ constructor (status, detail, extra = {}) {
7
+ super(detail)
8
+ this.status = status
9
+ const { type, title, ...rest } = extra
10
+ this.type = type || 'about:blank'
11
+ this.title = title || http.STATUS_CODES[status] || 'Unknown Status'
12
+ this.detail = detail
13
+ Object.assign(this, rest)
14
+ }
15
+ }
16
+
17
+ export class UnsupportedTypeError extends ProblemDetailsError {
18
+ constructor (detail, extra = {}) {
19
+ // Rename `type` to `objectType` to avoid conflict with the problem type URI field
20
+ const { type: objectType, ...rest } = extra
21
+ super(400, detail, {
22
+ type: `${FEP_C180}#unsupported-type`,
23
+ title: 'Unsupported type',
24
+ ...rest,
25
+ ...(objectType != null ? { objectType } : {})
26
+ })
27
+ }
28
+ }
29
+
30
+ export class ObjectDoesNotExistError extends ProblemDetailsError {
31
+ constructor (detail, extra = {}) {
32
+ super(400, detail, {
33
+ type: `${FEP_C180}#object-does-not-exist`,
34
+ title: 'Object does not exist',
35
+ ...extra
36
+ })
37
+ }
38
+ }
39
+
40
+ export class DuplicateDeliveryError extends ProblemDetailsError {
41
+ constructor (detail, extra = {}) {
42
+ super(400, detail, {
43
+ type: `${FEP_C180}#duplicate-delivery`,
44
+ title: 'Duplicate delivery',
45
+ ...extra
46
+ })
47
+ }
48
+ }
49
+
50
+ export class RedundantActivityError extends ProblemDetailsError {
51
+ constructor (detail, extra = {}) {
52
+ super(400, detail, {
53
+ type: `${FEP_C180}#redundant-activity`,
54
+ title: 'Redundant activity',
55
+ ...extra
56
+ })
57
+ }
58
+ }
59
+
60
+ export class ApprovalRequiredError extends ProblemDetailsError {
61
+ constructor (detail, extra = {}) {
62
+ super(202, detail, {
63
+ type: `${FEP_C180}#approval-required`,
64
+ title: 'Approval required',
65
+ ...extra
66
+ })
67
+ }
68
+ }
69
+
70
+ export class NotAnActorError extends ProblemDetailsError {
71
+ constructor (detail, extra = {}) {
72
+ super(400, detail, {
73
+ type: `${FEP_C180}#not-an-actor`,
74
+ title: 'Not an actor',
75
+ ...extra
76
+ })
77
+ }
78
+ }
79
+
80
+ export class PrincipalActorMismatchError extends ProblemDetailsError {
81
+ constructor (detail, extra = {}) {
82
+ super(400, detail, {
83
+ type: `${FEP_C180}#principal-actor-mismatch`,
84
+ title: 'Principal-actor mismatch',
85
+ ...extra
86
+ })
87
+ }
88
+ }
89
+
90
+ export class ActorNotAuthorizedError extends ProblemDetailsError {
91
+ constructor (detail, extra = {}) {
92
+ super(403, detail, {
93
+ type: `${FEP_C180}#actor-not-authorized`,
94
+ title: 'Actor not authorized',
95
+ ...extra
96
+ })
97
+ }
98
+ }
99
+
100
+ export class PrincipalNotAuthorizedError extends ProblemDetailsError {
101
+ constructor (detail, extra = {}) {
102
+ super(403, detail, {
103
+ type: `${FEP_C180}#principal-not-authorized`,
104
+ title: 'Principal not authorized',
105
+ ...extra
106
+ })
107
+ }
108
+ }
109
+
110
+ export class ClientNotAuthorizedError extends ProblemDetailsError {
111
+ constructor (detail, extra = {}) {
112
+ super(403, detail, {
113
+ type: `${FEP_C180}#client-not-authorized`,
114
+ title: 'Client not authorized',
115
+ ...extra
116
+ })
117
+ }
118
+ }
119
+
120
+ export class UnsupportedMediaTypeError extends ProblemDetailsError {
121
+ constructor (detail, extra = {}) {
122
+ super(400, detail, {
123
+ type: `${FEP_C180}#unsupported-media-type`,
124
+ title: 'Unsupported media type',
125
+ ...extra
126
+ })
127
+ }
128
+ }
129
+
130
+ export class MediaTooLargeError extends ProblemDetailsError {
131
+ constructor (detail, extra = {}) {
132
+ super(413, detail, {
133
+ type: `${FEP_C180}#media-too-large`,
134
+ title: 'Media too large',
135
+ ...extra
136
+ })
137
+ }
138
+ }
139
+
140
+ export class NoApplicableAddresseesError extends ProblemDetailsError {
141
+ constructor (detail, extra = {}) {
142
+ super(400, detail, {
143
+ type: `${FEP_C180}#no-applicable-addressees`,
144
+ title: 'No applicable addressees',
145
+ ...extra
146
+ })
147
+ }
148
+ }
149
+
150
+ export class RateLimitExceededError extends ProblemDetailsError {
151
+ constructor (detail, extra = {}) {
152
+ super(429, detail, {
153
+ type: `${FEP_C180}#rate-limit-exceeded`,
154
+ title: 'Rate limit exceeded',
155
+ ...extra
156
+ })
157
+ }
158
+ }
@@ -15,7 +15,7 @@ export class HTTPMessageSignature {
15
15
  constructor (logger) {
16
16
  assert.ok(logger)
17
17
  assert.strictEqual(typeof logger, 'object')
18
- this.#logger = logger
18
+ this.#logger = logger.child({ class: this.constructor.name })
19
19
  }
20
20
 
21
21
  keyId (signatureInput) {
@@ -1,7 +1,7 @@
1
1
  import crypto from 'node:crypto'
2
2
  import assert from 'node:assert'
3
3
 
4
- import createHttpError from 'http-errors'
4
+ import { ProblemDetailsError } from './errors.js'
5
5
 
6
6
  export class HTTPSignature {
7
7
  static #maxDateDiff = 5 * 60 * 1000 // 5 minutes
@@ -13,48 +13,48 @@ export class HTTPSignature {
13
13
  keyId (signature) {
14
14
  const params = this.#parseSignatureHeader(signature)
15
15
  if (!params.keyId) {
16
- throw createHttpError(401, 'No keyId provided')
16
+ throw new ProblemDetailsError(401, 'No keyId provided')
17
17
  }
18
18
  return params.keyId
19
19
  }
20
20
 
21
21
  async validate (publicKeyPem, signature, method, path, headers) {
22
22
  if (!signature) {
23
- throw createHttpError(401, 'No signature provided')
23
+ throw new ProblemDetailsError(401, 'No signature provided')
24
24
  }
25
25
  if (!method) {
26
- throw createHttpError(400, 'No HTTP method provided')
26
+ throw new ProblemDetailsError(400, 'No HTTP method provided')
27
27
  }
28
28
  if (!path) {
29
- throw createHttpError(400, 'No URL path provided')
29
+ throw new ProblemDetailsError(400, 'No URL path provided')
30
30
  }
31
31
  if (!headers) {
32
- throw createHttpError(400, 'No request headers provided')
32
+ throw new ProblemDetailsError(400, 'No request headers provided')
33
33
  }
34
34
 
35
35
  const params = this.#parseSignatureHeader(signature)
36
36
 
37
37
  const keyId = params.keyId
38
38
  if (!keyId) {
39
- throw createHttpError(401, 'No keyId provided')
39
+ throw new ProblemDetailsError(401, 'No keyId provided')
40
40
  }
41
41
  this.#logger.debug({ keyId }, 'validating signature')
42
42
  const algorithm = params.algorithm
43
43
  if (!algorithm) {
44
- throw createHttpError(401, 'No algorithm provided')
44
+ throw new ProblemDetailsError(401, 'No algorithm provided')
45
45
  }
46
46
  this.#logger.debug({ algorithm }, 'validating signature')
47
47
  if (!(algorithm === 'rsa-sha256' || (algorithm === 'hs2019' && this.#isRSAKey(publicKeyPem)))) {
48
- throw createHttpError(401, 'Only rsa-sha256 or hs2019 with RSA supported')
48
+ throw new ProblemDetailsError(401, 'Only rsa-sha256 or hs2019 with RSA supported')
49
49
  }
50
50
  if (!params.headers) {
51
- throw createHttpError(401, 'No headers provided')
51
+ throw new ProblemDetailsError(401, 'No headers provided')
52
52
  }
53
53
  const signedHeaders = params.headers.split(' ')
54
54
  this.#logger.debug({ signedHeaders }, 'validating signature')
55
55
  const signatureString = params.signature
56
56
  if (!signatureString) {
57
- throw createHttpError(401, 'No signature field provided in signature header')
57
+ throw new ProblemDetailsError(401, 'No signature field provided in signature header')
58
58
  }
59
59
  this.#logger.debug({ signatureString }, 'validating signature')
60
60
  const signingString = this.#signingString({
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert'
2
2
 
3
- import createHttpError from 'http-errors'
3
+ import { ProblemDetailsError } from './errors.js'
4
4
 
5
5
  import BotMaker from './botmaker.js'
6
6
 
@@ -77,21 +77,21 @@ export class HTTPSignatureAuthenticator {
77
77
  this.#logger.debug({ reqId: req.id, signature }, 'Got signed request')
78
78
  const date = req.get('Date')
79
79
  if (!date) {
80
- throw createHttpError(400, 'No date provided')
80
+ throw new ProblemDetailsError(400, 'No date provided')
81
81
  }
82
82
  if (Math.abs(Date.parse(date) - Date.now()) >
83
83
  HTTPSignatureAuthenticator.#maxDateDiff) {
84
- throw createHttpError(400, 'Time skew too large')
84
+ throw new ProblemDetailsError(400, 'Time skew too large')
85
85
  }
86
86
  if (req.rawBodyText && req.rawBodyText.length > 0) {
87
87
  const digest = req.get('Digest')
88
88
  if (!digest) {
89
- throw createHttpError(400, 'No digest provided')
89
+ throw new ProblemDetailsError(400, 'No digest provided')
90
90
  }
91
91
  const calculated = await this.#digester.digest(req.rawBodyText)
92
92
  if (!this.#digester.equals(digest, calculated)) {
93
93
  this.#logger.debug({ reqId: req.id, calculated, digest }, 'Digest mismatch')
94
- throw createHttpError(400, 'Digest mismatch')
94
+ throw new ProblemDetailsError(400, 'Digest mismatch')
95
95
  }
96
96
  }
97
97
  const { method, headers } = req
@@ -100,7 +100,7 @@ export class HTTPSignatureAuthenticator {
100
100
  this.#logger.debug({ reqId: req.id, keyId }, 'Signed with keyId')
101
101
  const ok = await this.#remoteKeyStorage.getPublicKey(keyId)
102
102
  if (!ok) {
103
- throw createHttpError(400, 'public key not found')
103
+ throw new ProblemDetailsError(400, 'public key not found')
104
104
  }
105
105
  let owner = ok.owner
106
106
  let publicKeyPem = ok.publicKeyPem
@@ -125,7 +125,7 @@ export class HTTPSignatureAuthenticator {
125
125
  req.auth.subject = owner
126
126
  return next()
127
127
  } else {
128
- throw createHttpError(401, 'Unauthorized')
128
+ throw new ProblemDetailsError(401, 'Unauthorized')
129
129
  }
130
130
  }
131
131
 
@@ -137,25 +137,25 @@ export class HTTPSignatureAuthenticator {
137
137
  const date = req.get('Date')
138
138
  if (date && Math.abs(Date.parse(date) - Date.now()) >
139
139
  HTTPSignatureAuthenticator.#maxDateDiff) {
140
- throw createHttpError(400, 'Time skew too large')
140
+ throw new ProblemDetailsError(400, 'Time skew too large')
141
141
  }
142
142
  if (req.rawBodyText && req.rawBodyText.length > 0) {
143
143
  const digest = req.get('Content-Digest')
144
144
  if (!digest) {
145
- throw createHttpError(400, 'No digest provided')
145
+ throw new ProblemDetailsError(400, 'No digest provided')
146
146
  }
147
147
  const calculated = await this.#digester.contentDigest(req.rawBodyText)
148
148
  if (!this.#digester.equals(digest, calculated)) {
149
149
  this.#logger.debug({ reqId: req.id, calculated, digest }, 'Digest mismatch')
150
- throw createHttpError(400, 'Digest mismatch')
150
+ throw new ProblemDetailsError(400, 'Digest mismatch')
151
151
  }
152
152
  }
153
153
  const created = this.#messageSigner.created(signatureInput)
154
154
  if (!created) {
155
- throw createHttpError(400, 'No created timestamp provided')
155
+ throw new ProblemDetailsError(400, 'No created timestamp provided')
156
156
  }
157
157
  if (Math.abs(Date.now() - created * 1000) > HTTPSignatureAuthenticator.#maxDateDiff) {
158
- throw createHttpError(400, 'Time skew too large')
158
+ throw new ProblemDetailsError(400, 'Time skew too large')
159
159
  }
160
160
  const { method, headers } = req
161
161
  const { origin } = req.app.locals
@@ -163,12 +163,12 @@ export class HTTPSignatureAuthenticator {
163
163
 
164
164
  const keyId = this.#messageSigner.keyId(signatureInput)
165
165
  if (!keyId) {
166
- throw createHttpError(400, 'no public key provided')
166
+ throw new ProblemDetailsError(400, 'no public key provided')
167
167
  }
168
168
  this.#logger.debug({ reqId: req.id, keyId }, 'Signed with keyId')
169
169
  const ok = await this.#remoteKeyStorage.getPublicKey(keyId)
170
170
  if (!ok) {
171
- throw createHttpError(400, 'public key not found')
171
+ throw new ProblemDetailsError(400, 'public key not found')
172
172
  }
173
173
  let owner = ok.owner
174
174
  let publicKeyPem = ok.publicKeyPem
@@ -193,7 +193,7 @@ export class HTTPSignatureAuthenticator {
193
193
  req.auth.subject = owner
194
194
  return next()
195
195
  } else {
196
- throw createHttpError(401, 'Unauthorized')
196
+ throw new ProblemDetailsError(401, 'Unauthorized')
197
197
  }
198
198
  }
199
199
  }
@@ -0,0 +1,16 @@
1
+ export const id = '008-signature-policy'
2
+
3
+ export async function up (connection, queryOptions = {}) {
4
+ await connection.query(`
5
+ CREATE TABLE signature_policy (
6
+ origin varchar(256) NOT NULL PRIMARY KEY,
7
+ policy varchar(32) NOT NULL,
8
+ expiry TIMESTAMP NOT NULL,
9
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
10
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
11
+ );
12
+ `, queryOptions)
13
+ await connection.query(`
14
+ CREATE INDEX signature_policy_expiry on signature_policy (expiry);
15
+ `, queryOptions)
16
+ }