Merge branch 'master' into 7478-anm-spot-instances
[arvados.git] / services / api / app / models / blob.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 require 'request_error'
6
7 class Blob
8   extend DbCurrentTime
9
10   def initialize locator
11     @locator = locator
12   end
13
14   def empty?
15     !!@locator.match(/^d41d8cd98f00b204e9800998ecf8427e(\+.*)?$/)
16   end
17
18   # In order to get a Blob from Keep, you have to prove either
19   # [a] you have recently written it to Keep yourself, or
20   # [b] apiserver has recently decided that you should be able to read it
21   #
22   # To ensure that the requestor of a blob is authorized to read it,
23   # Keep requires clients to timestamp the blob locator with an expiry
24   # time, and to sign the timestamped locator with their API token.
25   #
26   # A signed blob locator has the form:
27   #     locator_hash +A blob_signature @ timestamp
28   # where the timestamp is a Unix time expressed as a hexadecimal value,
29   # and the blob_signature is the signed locator_hash + API token + timestamp.
30   #
31   class InvalidSignatureError < RequestError
32   end
33
34   # Blob.sign_locator: return a signed and timestamped blob locator.
35   #
36   # The 'opts' argument should include:
37   #   [required] :api_token - API token (signatures only work for this token)
38   #   [optional] :key       - the Arvados server-side blobstore key
39   #   [optional] :ttl       - number of seconds before signature should expire
40   #   [optional] :expire    - unix timestamp when signature should expire
41   #
42   def self.sign_locator blob_locator, opts
43     # We only use the hash portion for signatures.
44     blob_hash = blob_locator.split('+').first
45
46     # Generate an expiry timestamp (seconds after epoch, base 16)
47     if opts[:expire]
48       if opts[:ttl]
49         raise "Cannot specify both :ttl and :expire options"
50       end
51       timestamp = opts[:expire]
52     else
53       timestamp = db_current_time.to_i +
54         (opts[:ttl] || Rails.configuration.blob_signature_ttl)
55     end
56     timestamp_hex = timestamp.to_s(16)
57     # => "53163cb4"
58     blob_signature_ttl = Rails.configuration.blob_signature_ttl.to_s(16)
59
60     # Generate a signature.
61     signature =
62       generate_signature((opts[:key] or Rails.configuration.blob_signing_key),
63                          blob_hash, opts[:api_token], timestamp_hex, blob_signature_ttl)
64
65     blob_locator + '+A' + signature + '@' + timestamp_hex
66   end
67
68   # Blob.verify_signature
69   #   Safely verify the signature on a blob locator.
70   #   Return value: true if the locator has a valid signature, false otherwise
71   #   Arguments: signed_blob_locator, opts
72   #
73   def self.verify_signature(*args)
74     begin
75       self.verify_signature!(*args)
76       true
77     rescue Blob::InvalidSignatureError
78       false
79     end
80   end
81
82   # Blob.verify_signature!
83   #   Verify the signature on a blob locator.
84   #   Return value: true if the locator has a valid signature
85   #   Arguments: signed_blob_locator, opts
86   #   Exceptions:
87   #     Blob::InvalidSignatureError if the blob locator does not include a
88   #     valid signature
89   #
90   def self.verify_signature! signed_blob_locator, opts
91     blob_hash = signed_blob_locator.split('+').first
92     given_signature, timestamp = signed_blob_locator.
93       split('+A').last.
94       split('+').first.
95       split('@')
96
97     if !timestamp
98       raise Blob::InvalidSignatureError.new 'No signature provided.'
99     end
100     unless timestamp =~ /^[\da-f]+$/
101       raise Blob::InvalidSignatureError.new 'Timestamp is not a base16 number.'
102     end
103     if timestamp.to_i(16) < (opts[:now] or db_current_time.to_i)
104       raise Blob::InvalidSignatureError.new 'Signature expiry time has passed.'
105     end
106     blob_signature_ttl = Rails.configuration.blob_signature_ttl.to_s(16)
107
108     my_signature =
109       generate_signature((opts[:key] or Rails.configuration.blob_signing_key),
110                          blob_hash, opts[:api_token], timestamp, blob_signature_ttl)
111
112     if my_signature != given_signature
113       raise Blob::InvalidSignatureError.new 'Signature is invalid.'
114     end
115
116     true
117   end
118
119   def self.generate_signature key, blob_hash, api_token, timestamp, blob_signature_ttl
120     OpenSSL::HMAC.hexdigest('sha1', key,
121                             [blob_hash,
122                              api_token,
123                              timestamp,
124                              blob_signature_ttl].join('@'))
125   end
126 end