-
Notifications
You must be signed in to change notification settings - Fork 0
/
verify-http-message-signature
executable file
·418 lines (346 loc) · 11.4 KB
/
verify-http-message-signature
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
#!/usr/bin/env ruby
#
# OVERVIEW
# --------
#
# Verify an HTTP message signature.
#
# USAGE
# -----
#
# verify-http-message-signature
# --key FILE # Public key in the JWK format.
# --signature SIGNATURE # HTTP message signature to verify.
# [--alg ALG] # Algorithm (RFC 9421 Section 3.3)
# [--baseline BASELINE] # signature-base-line (RFC 9421 Section 2.5)
# [--created TIME] # TIME is seconds since epoch, or 'now'.
# [--expires TIME] # TIME is seconds since epoch, or '+{seconds}'
# [--keyid KEYID] # Key ID
# [--nonce NONCE] # Nonce
# [--tag TAG] # Tag
# [--print-all]
# [--[no-]print-signature-base]
# [--[no-]print-signature-metadata]
# [--[no-]print-verification-result]
#
# EXAMPLE
# -------
#
# SIGNATURE=:qgdoRZrTtwyUNf5mNLyfLxyM3dipSIEc9OoM2931znzz1w9jhIwM0L9lBOJA2kU9OglMKwrbc1jW05iL1z+qRg==:
# TARGET_URI=https://fapidev-rs.authlete.net/api/fapi/accounts?key=value
# CONTENT_DIGEST=sha-256=:RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=:
# CREATED=1729607861
# KEYID=tsq5sQwuoADZ3iARLOreaYaIa9mG5TnV11zpRRjuA0k
#
# verify-http-message-signature \
# --key response-signing.jwk \
# --signature ${SIGNATURE} \
# --baseline "\"@method\";req: GET" \
# --baseline "\"@target-uri\";req: ${TARGET_URI}" \
# --baseline "\"@status\": 200" \
# --baseline "\"content-digest\": ${CONTENT_DIGEST}" \
# --created ${CREATED} \
# --keyid ${KEYID} \
# --tag fapi-2-response \
# --print-all
#
# output:
#
# "@method";req: GET
# "@target-uri";req: https://fapidev-rs.authlete.net/api/fapi/accounts?key=value
# "@status": 200
# "content-digest": sha-256=:RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=:
# "@signature-params": ("@method";req "@target-uri";req "@status" "content-digest");created=1729607861;# keyid="tsq5sQwuoADZ3iARLOreaYaIa9mG5TnV11zpRRjuA0k";tag="fapi-2-response"
# ("@method";req "@target-uri";req "@status" "content-digest");created=1729607861;# keyid="tsq5sQwuoADZ3iARLOreaYaIa9mG5TnV11zpRRjuA0k";tag="fapi-2-response"
# true
#
#
# NOTE
# ----
#
# This script does not provide flexibility to change the order of
# signature metadata parameters, such as `created` and `tag`.
# The parameters are ordered alphabetically.
#
# If the resource server implementation uses a different order,
# the signature verification will fail.
#
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'json-jwt'
gem 'optparse'
end
require 'base64'
require 'time'
#------------------------------------------------------------
# main
#------------------------------------------------------------
def main(args)
# Process the command line options.
options = Options.process(args)
# Build signature metadata.
signature_metadata = build_signature_metadata(options)
# Build a signature-params-line.
signature_params_line = build_signature_params_line(options, signature_metadata)
# Build a signature base.
signature_base = build_signature_base(options, signature_params_line)
# Verify the signature.
result = verify_signature(options, signature_base)
# Signature Base
if options.print_signature_base
puts signature_base
end
# Signature Metadata
if options.print_signature_metadata
puts signature_metadata
end
# Verification Result
if options.print_verification_result
puts result
end
end
#------------------------------------------------------------
# Command line options
#------------------------------------------------------------
class Options < OptionParser
DESC_ALG = "specifies the 'alg' parameter."
DESC_BASELINE = "specifies a signature-base-line (RFC 9421 Section 2.5)"
DESC_CREATED = "specifies the 'created' parameter. TIME is seconds since epoch, or 'now'."
DESC_EXPIRES = "specifies the 'expires' parameter. TIME is seconds since epoch, or '+{seconds}'."
DESC_KEY = "specifies a file containing a private key in the JWK format."
DESC_KEYID = "specifies the 'keyid' parameter."
DESC_NONCE = "specifies the 'nonce' parameter."
DESC_SIGNATURE = "specifies the HTTP message signature to verify."
DESC_TAG = "specifies the 'tag' parameter."
attr_reader :alg, :baselines, :created, :expires, :key, :keyid, :nonce, :signature, :tag
attr_reader :print_signature_base, :print_signature_metadata, :print_verification_result
def initialize
super
@alg = nil
@baselines = []
@created = nil
@expires = nil
@key = nil
@keyid = nil
@nonce = nil
@signature = nil
@tag = nil
@print_signature_base = false
@print_signature_metadata = false
@print_verification_result = true
# For the 'tag' parameter.
self.on('--alg ALG', DESC_ALG) do |alg|
@alg = alg
end
# For signature-base-line's.
self.on('--baseline BASELINE', DESC_BASELINE) do |baseline|
@baselines << baseline
end
# For the 'created' parameter.
self.on('--created TIME', DESC_CREATED) do |time|
# If the specified value is 'now' (string)
if time == 'now'
# The current time is used as the creation time.
@created = Time.now.to_i
else
# The value is interpreted as seconds since the Unix epoch.
@created = time.to_i
end
end
# For the 'expires' parameter.
self.on('--expires TIME', DESC_EXPIRES) do |time|
if time.start_with?('+')
# Convert the substring after '+' to an integer.
seconds = time[1 .. -1].to_i
# If the --created option has already been used.
if @created.nil?
# The specified value is interpreted as a difference from the current time.
@expires = Time.now.to_i + seconds
else
# The specified value is interpreted as a difference from the creation time.
@expires = @created + seconds
end
else
# The value is interpreted as seconds since the Unix epoch.
@expires = time.to_i
end
end
# Private key for signing.
self.on('--key FILE', DESC_KEY) do |file|
@key = read_jwk(file)
end
# For the 'keyid' parameter.
self.on('--keyid KEYID', DESC_KEYID) do |id|
@keyid = id
end
# For the 'nonce' parameter.
self.on('--nonce NONCE', DESC_NONCE) do |nonce|
@nonce = nonce
end
# The HTTP message signature to verify.
self.on('--signature SIGNATURE', DESC_SIGNATURE) do |signature|
@signature = signature
end
# For the 'tag' parameter.
self.on('--tag TAG', DESC_TAG) do |tag|
@tag = tag
end
# Print the signature, and signature base.
self.on('--print-all') do
@print_signature_base = true
@print_signature_metadata = true
@print_verification_result = true
end
# Whether to print the signature base.
self.on('--[no-]print-signature-base') do |bool|
@print_signature_base = bool
end
# Whether to print the signature metadata.
self.on('--[no-]print-signature-metadata') do |bool|
@print_signature_metadata = bool
end
# Whether to print the signature verification result.
self.on('--[no-]print-verification-result') do |bool|
@print_verification_result = bool
end
end
private
def read_jwk(file)
json = File.read(file)
hash = JSON.parse(json, {symbolize_names: true})
JSON::JWK.new(hash)
end
def error_if_missing(value, option)
if value.nil?
raise OptionParser::ParseError.new "'#{option}' is missing."
end
end
public
def verify
error_if_missing(@key, '--key FILE')
error_if_missing(@signature, '--signature SIGNATURE');
end
def self.process(args)
options = Options.new
options.parse(args)
options.verify()
return options
end
end
#------------------------------------------------------------
# signature metadata
#------------------------------------------------------------
def build_signature_metadata(options)
metadata = '('
# Extract component identifiers from the baselines.
component_identifiers = options.baselines.map { |s| s.split(':')[0] }
metadata << component_identifiers.join(' ')
metadata << ')'
# alg
if !options.alg.nil?
metadata << ";alg=\"#{options.alg}\""
end
# created
if !options.created.nil?
metadata << ";created=#{options.created}"
end
# expires
if !options.expires.nil?
metadata << ";expires=#{options.expires}"
end
# keyid
if !options.keyid.nil?
metadata << ";keyid=\"#{options.keyid}\""
end
# nonce
if !options.nonce.nil?
metadata << ";nonce=\"#{options.nonce}\""
end
# tag
if !options.tag.nil?
metadata << ";tag=\"#{options.tag}\""
end
return metadata
end
#------------------------------------------------------------
# signature-params-line
#------------------------------------------------------------
def build_signature_params_line(options, signature_metadata)
# RFC 9421 HTTP Message Signatures
# 2.5. Creating the Signature Base
#
# signature-params-line = DQUOTE "@signature-params" DQUOTE
# ":" SP inner-list
#
"\"@signature-params\": #{signature_metadata}"
end
#------------------------------------------------------------
# signature-base
#------------------------------------------------------------
def build_signature_base(options, signature_params_line)
# RFC 9421 HTTP Message Signatures
# 2.5. Creating the Signature Base
#
# signature-base = *( signature-base-line LF ) signature-params-line
#
base = ''
options.baselines.each do |baseline|
base << baseline
base << "\n"
end
base << signature_params_line
return base
end
#------------------------------------------------------------
# verification
#------------------------------------------------------------
def verify_signature(options, signature_base)
# Verifier
verifier = HttpVerifier.new(options.key)
# Verify the signature.
verifier.http_verify(signature_base, options.signature)
end
#------------------------------------------------------------
# HTTP Verifier
#------------------------------------------------------------
class HttpVerifier < JSON::JWS
def initialize(public_key)
self.alg = autodetected_algorithm_from(public_key)
@public_key = public_key
end
def http_verify(signature_base, signature)
# RFC 9421 HTTP Message Signatures
# 3.3.7. JSON Web Signature (JWS) Algorithms
#
# For both signing and verification, the HTTP message's
# signature base (Section 2.5) is used as the entire
# "JWS Signing Input". The JOSE Header [JWS] [RFC7517]
# is not used, and the signature base is not first
# encoded in Base64 before applying the algorithm.
# The output of the JWS Signature is taken as a byte
# array prior to the Base64url encoding used in JOSE.
#
self.signature = decode_signature(signature)
self.signature_base_string = signature_base
begin
self.verify! @public_key
true
rescue
false
end
end
private
def decode_signature(signature)
# Remove a leading colon, if any.
signature = signature.delete_prefix(":")
# Remove a trailing colon, if any.
signature = signature.delete_suffix(":")
# Decode the base64 string.
Base64.strict_decode64(signature)
end
end
#------------------------------------------------------------
# Entry Point
#------------------------------------------------------------
main(ARGV)