-
Notifications
You must be signed in to change notification settings - Fork 0
/
comakery_api_signature.rb
141 lines (118 loc) · 4.9 KB
/
comakery_api_signature.rb
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
require 'ed25519'
require 'securerandom'
require 'base64'
require 'json/canonicalization'
require 'httparty'
module Comakery
# Raised during unsuccessfull signature verification
APISignatureError = Class.new(StandardError)
# Verifying and generating signature
class APISignature
# Proof type identifier according Linked Data Cryptographic Suite Registry
PROOF_TYPE = 'Ed25519Signature2018'.freeze
# Allowed expiration time for timestamp
TIMESTAMP_EXPIRATION_SECONDS = 60
# Allowed ahead of time for timestamp
TIMESTAMP_AHEAD_SECONDS = 3
# Creates a new request to be verified
#
# @param request [Hash] request including body and optioanl proof
# @param http_url [String] url of the request (optional)
# @param http_method [String] HTTP method of the request (optional)
# @param is_nonce_unique [Proc] lambda function to verify unqiuness of the nonce for the given timewindow (optional)
def initialize(request, http_url = '', http_method = '', is_nonce_unique = ->(nonce) { true if nonce })
@request = request
@http_url = http_url
@http_method = http_method
@is_nonce_unique = is_nonce_unique
end
# A convenience method for signing and sending requests
def self.signed_request(api_key, private_key, request_payload)
signed_query = Comakery::APISignature.new(request_payload).sign(private_key)
method = request_payload["body"]["method"].downcase
api_endpoint = request_payload["body"]["url"]
response = HTTParty.send(method, api_endpoint,
query: signed_query,
headers: {
'Api-Key': api_key
})
return {signed_query: signed_query, response: response}
end
# Signs the request with given private key, appending nonce, timestamp and proof
#
# @param private_key [String] 64-bit keypair strict-encoded in base64
#
# @return [Hash] signed request
def sign(private_key)
signing_key = Ed25519::SigningKey.from_keypair(Base64.decode64(private_key))
@request['body']['nonce'] = nonce
@request['body']['timestamp'] = timestamp.to_s
@request['proof'] = {}
@request['proof']['type'] = PROOF_TYPE
@request['proof']['verificationMethod'] = Base64.strict_encode64(
signing_key.verify_key.to_bytes
)
@request['proof']['signature'] = Base64.strict_encode64(
signing_key.sign(@request['body'].to_json_c14n)
)
@request
end
# Verifies that:
# – signature of request by serializing body (data, http_url, http_method, nonce and timestamp)
# - timestamp fits into defined window (between TIMESTAMP_EXPIRATION_SECONDS and TIMESTAMP_AHEAD_SECONDS)
# - nonce is unique for provided nonce_history (see #new)
# - http_url matches provided one (see #new)
# - http_method matches provided one (see #new)
#
# @param public_key [String] 32-bit key strict-encoded in base64
#
# @return [Boolean] result of verification
def verify(public_key)
verify_http_url
verify_http_method
verify_type
verify_timestamp
verify_nonce
verify_method(public_key)
verify_signature(public_key)
true
end
private
def nonce
SecureRandom.hex
end
def timestamp
Time.now.utc.to_i
end
def verify_http_url
raise Comakery::APISignatureError, 'Invalid URL' unless @request.fetch('body', {}).fetch('url', '') == @http_url
end
def verify_http_method
raise Comakery::APISignatureError, 'Invalid HTTP method' unless @request.fetch('body', {}).fetch('method', '') == @http_method
end
def verify_nonce
raise Comakery::APISignatureError, 'Invalid nonce' unless @is_nonce_unique.call(@request.fetch('body', {}).fetch('nonce', ''))
end
def verify_timestamp
raise Comakery::APISignatureError, 'Invalid timestamp' unless @request.fetch('body', {}).fetch('timestamp', 0).to_i.between?(timestamp - TIMESTAMP_EXPIRATION_SECONDS, timestamp + TIMESTAMP_AHEAD_SECONDS)
end
def verify_type
raise Comakery::APISignatureError, 'Invalid proof type' unless @request.fetch('proof', {}).fetch('type', '') == PROOF_TYPE
end
def verify_method(public_key)
raise Comakery::APISignatureError, 'Invalid proof verificationMethod' unless (@request.fetch('proof', {}).fetch('verificationMethod', nil) || @request.fetch('proof', {}).fetch('verification_method', nil)) == public_key
end
def verify_signature(public_key)
Ed25519::VerifyKey.new(
Base64.strict_decode64(public_key)
).verify(
Base64.strict_decode64(
@request.fetch('proof', {}).fetch('signature', '')
),
@request.fetch('body', {}).to_json_c14n
)
rescue Ed25519::VerifyError, ArgumentError
raise Comakery::APISignatureError, 'Invalid proof signature'
end
end
end