Merge branch '2620-keep-serialize-io' (closes #2620)
[arvados.git] / services / api / app / models / blob.rb
1 class Blob
2
3   # In order to get a Blob from Keep, you have to prove either
4   # [a] you have recently written it to Keep yourself, or
5   # [b] apiserver has recently decided that you should be able to read it
6   #
7   # To ensure that the requestor of a blob is authorized to read it,
8   # Keep requires clients to timestamp the blob locator with an expiry
9   # time, and to sign the timestamped locator with their API token.
10   #
11   # A signed blob locator has the form:
12   #     locator_hash +A blob_signature @ timestamp
13   # where the timestamp is a Unix time expressed as a hexadecimal value,
14   # and the blob_signature is the signed locator_hash + API token + timestamp.
15   # 
16   class InvalidSignatureError < StandardError
17   end
18
19   # Blob.sign_locator: return a signed and timestamped blob locator.
20   #
21   # The 'opts' argument should include:
22   #   [required] :key       - the Arvados server-side blobstore key
23   #   [required] :api_token - user's API token
24   #   [optional] :ttl       - number of seconds before this request expires
25   #
26   def self.sign_locator blob_locator, opts
27     # We only use the hash portion for signatures.
28     blob_hash = blob_locator.split('+').first
29
30     # Generate an expiry timestamp (seconds since epoch, base 16)
31     timestamp = (Time.now.to_i + (opts[:ttl] || 600)).to_s(16)
32     # => "53163cb4"
33
34     # Generate a signature.
35     signature =
36       generate_signature opts[:key], blob_hash, opts[:api_token], timestamp
37
38     blob_locator + '+A' + signature + '@' + timestamp
39   end
40
41   # Blob.verify_signature
42   #   Safely verify the signature on a blob locator.
43   #   Return value: true if the locator has a valid signature, false otherwise
44   #   Arguments: signed_blob_locator, opts
45   #
46   def self.verify_signature *args
47     begin
48       self.verify_signature! *args
49       true
50     rescue Blob::InvalidSignatureError
51       false
52     end
53   end
54
55   # Blob.verify_signature!
56   #   Verify the signature on a blob locator.
57   #   Return value: true if the locator has a valid signature
58   #   Arguments: signed_blob_locator, opts
59   #   Exceptions:
60   #     Blob::InvalidSignatureError if the blob locator does not include a
61   #     valid signature
62   #
63   def self.verify_signature! signed_blob_locator, opts
64     blob_hash = signed_blob_locator.split('+').first
65     given_signature, timestamp = signed_blob_locator.
66       split('+A').last.
67       split('+').first.
68       split('@')
69
70     if !timestamp
71       raise Blob::InvalidSignatureError.new 'No signature provided.'
72     end
73     if !timestamp.match /^[\da-f]+$/
74       raise Blob::InvalidSignatureError.new 'Timestamp is not a base16 number.'
75     end
76     if timestamp.to_i(16) < Time.now.to_i
77       raise Blob::InvalidSignatureError.new 'Signature expiry time has passed.'
78     end
79
80     my_signature =
81       generate_signature opts[:key], blob_hash, opts[:api_token], timestamp
82
83     if my_signature != given_signature
84       raise Blob::InvalidSignatureError.new 'Signature is invalid.'
85     end
86
87     true
88   end
89
90   def self.generate_signature key, blob_hash, api_token, timestamp
91     OpenSSL::HMAC.hexdigest('sha1', key,
92                             [blob_hash,
93                              api_token,
94                              timestamp].join('@'))
95   end
96 end