smaak 0.0.10 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a06ce9d26d15fda320f1c294b584a16c8cd1006d
4
- data.tar.gz: 9b04495475ec21d1ff6f33625f30978cc1e823b0
3
+ metadata.gz: 41115c26166ffbbd902b43707e6e219101155802
4
+ data.tar.gz: 6cb113261fc5c764ebf137eec743297a664d709a
5
5
  SHA512:
6
- metadata.gz: 165f5d90ce01a893cb9c7cf8464edebe13c3ffdb420efaed98ccc96c410fb1380b572988ec130f91749853b6136d0448fada87f196295a0fc487ae8b3b147d5d
7
- data.tar.gz: f50c7a2da3b77aab667bcca1081b6eae07392cea03a36050f9a119f3edd964859b3298267c9a172eb200abd9c0ea9a2fa0a8440a5e79963c90ae621630c2eeb1
6
+ metadata.gz: eee3f15e1cb880f556fadc7338900ca6ee0a856b70cd1b0db5ee0d1f8d7d6aeb2d9d3297cc354a840b1b5397af9136a9f603a1a9cddab250572c1c11d757ec71
7
+ data.tar.gz: 193ecdd1021ff1f8f5cd485196e383af674aa02d02d4552bc5448dc86c3ef1b465d6f1aa9ef45ae1450cdca0c41a4069f1cfc50ad01cf5e26e17641e733bece7
data/.gitignore CHANGED
@@ -2,3 +2,5 @@ coverage
2
2
  Gemfile.lock
3
3
  *swp
4
4
  *gem
5
+ backup
6
+ doc
data/README.md CHANGED
@@ -1,8 +1,117 @@
1
1
  # Smaak
2
2
 
3
- This gems caters for both client and server sides of a signed message interaction over HTTP or HTTPS implementing the RFC2617 Digest Access Authentication. The following compromises are protected against as specified: Man in the middle (header and payload signature- see 'Extending') / snooping (HTTPS turned on), Replay (nonce + expires), Forgery (signature), Masquerading (recipient pub key check), Clear-text password compromise (MD5 pre-shared key)
3
+ This gems caters for both client and server sides of a signed message interaction over HTTP implementing RFC2617 Digest Access Authentication as well as IETF draft-cavage-http-signatures-04, extended with 'x-smaak-recipient', 'x-smaak-identifier', 'x-smaak-psk', 'x-smaak-expires' and 'x-smaak-nonce' headers. The following compromises are protected against as specified: Man in the middle (header and payload signature, as well as body digest) / snooping (message body encryption), Replay (nonce + expiry), Forgery (signature), Masquerading (identifier and signature), Forwarding / Unintended recipient (recipient pub key check), Clear-text password compromise (MD5 pre-shared key, obfuscated), lack of password (pre-shared key), Message fabrication (associations are purpose-fully provisioned to known associates.)
4
+
5
+ ## Smaak mechanism:
6
+
7
+ When provisioning a Smaak::Server and a Smaak::Client, all associations these services should be aware of are provisioned by calling add_association. The associations are indexed by identifier (e.g. FQDN of the associate,) and remember the associate's public key, a pre-shared key and a boolean indicating whether the association expects data to encrypted.
8
+
9
+ Smaak appends 'x-smaak' headers to the HTTP request to convey a generated nonce, expiry, the requestor's identifier, the pre-shared key (obfuscated) and a digest of the request body. The headers are signed using the requestor (Smaak::Client)'s private key. If encryption is requested, the message body is encrypted using the receiver (Smaak::Server)'s public key. The message body for the response from the Smaak::Server to the Smaak::Client is also encrypted. RSA 4096 bit keys are recommended.
10
+
11
+ The signing of an HTTP request and the placement of the signature in an Authorization header is performed by a signing specification specified when signing. The algorithm implementing the signing specification is expected to embed a Smaak::AuthMessage in the signature and allow access to it when the signed header is interpreted. Currently IETF draft-cavage-http-signatures-04 is the only supported signature specification.
12
+
13
+ Smaak verifies an AuthMessage signed in the Authorization header by looking at nonce, expiry, recipient and pre-shared key. The order of headers signed is important for signature verification.
14
+
15
+ ### Example Server:
16
+
17
+ A Smaak::Server operates on an instance of an HTTP request received. The Smaak module can be told about different request technology implementations by providing an adaptor to a request technology (Smaak::add_request_adaptor). The gem ships with a Rack::Request adaptor. Call Smaak::create_adaptor with your request to get an instance of an adaptor.
18
+
19
+ A Smaak::Server needs to keep track of nonces received in the fresh-ness interval of requests. To make this easy, you can extend Smaak::SmaakService. Override the configure_service method to provide your server's public key, private key and associations. Smaak::SmaakService provides a cache of received nonces checked against the freshness interval.
20
+
21
+ When setting up a Smaak::Server, tell the server of your SmaakService and verify incoming request, so:
22
+
23
+ class SecureServer < Smaak::SmaakService
24
+ def configure_services
25
+ @smaak_server.set_public_key(File.read '/secure/server_public.pem')
26
+ @smaak_server.set_private_key(File.read '/secure/server_private.pem') # only required when encryption is specified
27
+ @smaak_server.add_association('client-facing-service-needing-back-end-data', File.read '/secure/client_public.pem', 'client-pre-shared-key')
28
+ end
29
+
30
+ class SecureService
31
+ def serve(request)
32
+ auth_message, body = SecureServer.get_instance.smaak_server.verify_signed_request(request)
33
+ return [200, "message from #{auth_message.identifier} verified!"] if auth_message.identifier
34
+ [401, "Insufficient proof!"]
35
+ end
36
+ end
37
+
38
+ ### Example Client:
39
+
40
+ A Smaak::Client operates on an instance of an HTTP request. The Smaak module can be told about different request technology implementations by providing an adaptor to a request technology (Smaak::add_request_adaptor). The gem ships with a Net::HTTP adaptor. Call Smaak::create_adaptor with your request to get an instance of an adaptor.
41
+
42
+ # The user requested some data which requires my service to talk to another, a Smaak::Server
43
+ class SecureController
44
+ def initialize
45
+ # We recommend this configuration be provisioned by a secure configuration service and/or by service boot-strapping
46
+ @client = Smaak::Client.new
47
+ @client.set_identifier('client-facing-service-needing-back-end-data')
48
+ @client.set_private_key(File.read '/secure/client_private.pem')
49
+ @client.add_association('service-provider', File.read('/secure/server_public.pem'), 'client-pre-shared-key', true) # encrypted
50
+ end
51
+
52
+ def serve(request)
53
+ response = @client.post('service-provider', 'http://service-provider.com:9393/backend', { 'index1' => 'data1', 'index2' => 'data2' }.to_json)
54
+ [200, response.body]
55
+ end
56
+
57
+ class SecureConsumer
58
+ def initialize
59
+ ...
60
+ end
61
+
62
+ def some_other(param)
63
+ response = @client.get('service-provider', 'http://service-provider.com:9393/query', { 'looking_for' => param }.to_json)
64
+ consume_response(response)
65
+ end
66
+ end
67
+
68
+ ## Identity management
69
+
70
+ During provisioning, we recommend that the key-pair that does the signing and verification has associated with it an X.509 certificate signed by a CA you trust that contains the identity of the signer. The association is provisioned with an 'identifier' that the Authorization header transports in the 'x-smaak-identifier' header. This identifier is used on the receiver end to look up the public key of the signer in the association list. Once the associated key successfully verifies the signature, that certificate's identity can be used for identity management and authorization. This allows multiple identifiers (e.g. multiple server heads) to represent a single service (identity) with separate signing certs for each head.
71
+
72
+ ## Example on-the-wire requests
73
+
74
+ ### Un-encrypted
75
+
76
+ POST http://service-provider-internal:9393/secure-service HTTP/1.1
77
+ Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3
78
+ Accept: */*
79
+ User-Agent: Ruby
80
+ Authorization: Signature keyId="rsa-key-1",algorithm="rsa-sha256", headers="host date digest x-smaak-recipient x-smaak-identifier x-smaak-psk x-smaak-expires x-smaak-nonce x-smaak-encrypt content-length", signature="DywQfsuJOzP7uQw/rZ3sEKtyBlzs+3Gqif/WEjjxjC1h6/vsMP6LHz1jeCQdBWRgPZ/NonM06NeSWei/YXpg9dtntoWWHQ8e8pvQsLVrx0BuMAyGhckuE9IcUSnaAOqCGCTcEV2cIE6a50tPSbBHS88jzIasliMrM8QIG2boIB9hMXbYNCPUzUKo7mOtda2NUrGwYflmLZ1cXuRGHeXuG/m/kYJOSSUawrneWH3uxuIhJTQiVtblYr7tHDQsAB6WJgCxLkrZJreALYyM62D6Cpvip3atMoKh+2b3/SqseSdt2BirrMiTdS+1+6Tyk+z/y9sGhEF0WZoIOZUmo1+7yfXe4hHaa7SQD8olsjoJTPaBsd39sb5xPZKsFS3k/eeWQxXEa7iLLumDgHncaIhyRkyb+XTG6/qk0XemBuc4LlU1JjFCdGYjYx0T9V9OUrt8Jpi6g2FKg8JLaJsd2Xk4sUBwHMrYwweutdCXxbHZn2VYF5BydDB+Eesc62PC8jh1xiAVperSUF60HdOLhcJp/eJz7VuyjaRx+EYVNqBHxIG9w9si/pcxy6tX2yA5Go+UJ37xG5E12P3QNDC6HBEEqq8tOW+YqnOacm+IbI2YSZ/ilEbSYhmE/KH7GZKxl1cSvswHcDrYRwFhviSPutQGBtGl6o/YjdmpYAVcZiEGUtE="
81
+ Host: service-provider-internal
82
+ Date: 2015-06-25 09:48:13 GMT
83
+ Digest: SHA-256=0190f465c943501984c4018bacdbb0be167979f261caf1fe50ce63e97d31dff2
84
+ X-Smaak-Recipient: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUFxL2tiYjdBNWllQWV1WlBBVnI3MAo5cjl1TkFzc2dmYkdjeGMzZTc3RDNndkY4U2tzbURNQmQyTUt5TUh0ZjBrM1pqSVdZemJJVG5jQXM1Nnd4cmRSClhiVHpIZnhjMll1dDMwd0ljR2YvUVk4ZTJXNmdMWko4aVM3MXlYb0JQNFpEc2lLSXd4ajFsenYyVFlXWnNSL3EKd28xSzBxZ1NzOXJJVEVkWDVqampycHBYWTdobHNPMGVKQ2JBRG0weEtnU1hMcFQycnJzUnJ2OFllRXFvZTRMaQpDOFd6RjZZRlh1U3RHR1E4SXlxbjdPaTN5aVU2WFc3OTl2cFpIeHJlaERYaytDalZuU0ZXWkVPUHg3cENpam9SCnlXb0gyUmR6QVpQczdVdVJWOUdGWWFQeHRudmttNVdVZDVTdWVCNlMxT2E4dVZ3UnpyeXl6WkRjdG0xdWs1VjIKUE0zLzFqbFJMbFJzTWxSeHdZUDRzaFMzVlhjTkdGYjkvbzkvTjkzbitKZUFpSGd4YU5pQjN6YVV0a05XWWs0Vgozang2d0psTythOUNxdGJJeXg2ZzdyTHhOanVqRFpRZTZGcUdsMzVkVDR5MHA2UmVuUWQ4b1p5aWw3dlpqSkJaCjluTWRJblMyU05wWUZFclBsb25rdXNZKzZsam9TbFNLMXVSRmd2S3dzeGE3RmROMXZWSnRJQk9qdVJzSk9DaHYKOTB2K0ZEQWwxSnNZVUNPUnByUmtMWXB2TWI4Q1BZaUlzb3JmTUdKNnI3NktYUEIzRS9xejRmaWJ1UmZVeWJxMgp5eGxRTVJKb216d1BPemUrbWRQUU5Hd3VTTjU0VnByYXhoNGFpcWtaUVBsSWpRb1dFaFVKRWxMb0NtQXZ4TmtxCmRBcVZJMXZ3cS9FRXFBTEh3amJKRXIwQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo=
85
+ X-Smaak-Identifier: service-provider-public
86
+ X-Smaak-Psk: 917e5f9bcf6d7c20a338d8a39bbf79ef
87
+ X-Smaak-Expires: 1435225695
88
+ X-Smaak-Nonce: 7211840395
89
+ X-Smaak-Encrypt: false
90
+ Content-Type: text/plain
91
+ Content-Length: 20
92
+ ... (redacted)
93
+ {"index1":"data1"}
94
+
95
+ ### Encrypted
96
+ POST http://service-provider-internal:9393/secure-service HTTP/1.1
97
+ Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3
98
+ Accept: */*
99
+ User-Agent: Ruby
100
+ Authorization: Signature keyId="rsa-key-1",algorithm="rsa-sha256", headers="host date digest x-smaak-recipient x-smaak-identifier x-smaak-psk x-smaak-expires x-smaak-nonce x-smaak-encrypt content-length", signature="Y9v95p/rUAp3mmrZHKKvc4FVcDkCQuBVqArvhu70REQrIuHjJ2HDdS2xcQc1t41Ff+lRhO1aWf60cd+Be+1Q/8qsm5G35S66R9sQVr79h/zXovsimWw+GWmHj/d2RecgvGC9SXLRLchPSibYWiV1H5UlkCSqZEYevwFf17LlAk6mRVvFzcB50F+mYglcAFMQhFQI68JMN6CJWUrp09q5DH44WlsaCwUdmbn7pVAnbO6z45OtHPBjVoHtSFkGeFqhndkSRiXWrd9joXPqCb4VRUNG+NZk/9gU17yxkg63cheurf29EmRjAMDkP3nd/VGseOjNzm6MHjdgF9qrQxoQLleNb/lZcZB/ldCimR3AV0thu21NcSpOr1dIlKZX0oyiUOijPXCjXiOEtfO2wqsRk42c8b8nLJJUnFoDvWLQrY7ZFWnzeuu6OPVZQELcSsXokssz8Wsa+RjWo4HoQzfRi12/P1fDZVgj5EkNfUK/R/3ROR11XqdRaIiSXU8SIkof7iCe/2nGOLQNDmhQB6DWKRbN6Sl6bKB00Wto0t1yeeDyLcTrCDmJpKGS3L8hC671cT2f8nv4zHeDZUCqEVdvbcpbOILh9BxoxtLkhOhoAamdebOeOESDQEHwPvXHg9e46cQjGkdMxNgO/CyhSzVM6I5P60Sbn6ppHgsZ5w8ymPA="
101
+ Host: service-provider-internal
102
+ Date: 2015-06-25 09:45:34 GMT
103
+ Digest: SHA-256=3f4502e658dd304d4cd1004a83935ede11692751011a410134ba861a1b55df92
104
+ X-Smaak-Recipient: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUFxL2tiYjdBNWllQWV1WlBBVnI3MAo5cjl1TkFzc2dmYkdjeGMzZTc3RDNndkY4U2tzbURNQmQyTUt5TUh0ZjBrM1pqSVdZemJJVG5jQXM1Nnd4cmRSClhiVHpIZnhjMll1dDMwd0ljR2YvUVk4ZTJXNmdMWko4aVM3MXlYb0JQNFpEc2lLSXd4ajFsenYyVFlXWnNSL3EKd28xSzBxZ1NzOXJJVEVkWDVqampycHBYWTdobHNPMGVKQ2JBRG0weEtnU1hMcFQycnJzUnJ2OFllRXFvZTRMaQpDOFd6RjZZRlh1U3RHR1E4SXlxbjdPaTN5aVU2WFc3OTl2cFpIeHJlaERYaytDalZuU0ZXWkVPUHg3cENpam9SCnlXb0gyUmR6QVpQczdVdVJWOUdGWWFQeHRudmttNVdVZDVTdWVCNlMxT2E4dVZ3UnpyeXl6WkRjdG0xdWs1VjIKUE0zLzFqbFJMbFJzTWxSeHdZUDRzaFMzVlhjTkdGYjkvbzkvTjkzbitKZUFpSGd4YU5pQjN6YVV0a05XWWs0Vgozang2d0psTythOUNxdGJJeXg2ZzdyTHhOanVqRFpRZTZGcUdsMzVkVDR5MHA2UmVuUWQ4b1p5aWw3dlpqSkJaCjluTWRJblMyU05wWUZFclBsb25rdXNZKzZsam9TbFNLMXVSRmd2S3dzeGE3RmROMXZWSnRJQk9qdVJzSk9DaHYKOTB2K0ZEQWwxSnNZVUNPUnByUmtMWXB2TWI4Q1BZaUlzb3JmTUdKNnI3NktYUEIzRS9xejRmaWJ1UmZVeWJxMgp5eGxRTVJKb216d1BPemUrbWRQUU5Hd3VTTjU0VnByYXhoNGFpcWtaUVBsSWpRb1dFaFVKRWxMb0NtQXZ4TmtxCmRBcVZJMXZ3cS9FRXFBTEh3amJKRXIwQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo=
105
+ X-Smaak-Identifier: service-provider-public
106
+ X-Smaak-Psk: 917e5f9bcf6d7c20a338d8a39bbf79ef
107
+ X-Smaak-Expires: 1435225536
108
+ X-Smaak-Nonce: 1443964335
109
+ X-Smaak-Encrypt: true
110
+ Content-Type: text/plain
111
+ Content-Length: 684
112
+ ... (redacted)
113
+ fBkLECsy+UXmfx08eoFf45dtRgzkOCsoH2yh3CjsnSpiPKuXz+O5KVYvM/VpWrtYs11h5rwl563wwDoLuSQLgWzeiNoyJ4jttqcawVLG+
4
114
 
5
- This gem is sponsored by Hetzner (Pty) Ltd - http://hetzner.co.za
6
115
 
7
116
  ## Installation
8
117
 
@@ -18,52 +127,6 @@ Or install it yourself as:
18
127
 
19
128
  $ gem install smaak
20
129
 
21
- ## Use cases
22
-
23
- This gem and mechanism attempts to alleviate the following attacks and concerns for inter-service communication. This is not a public client/server mechanism.
24
-
25
- Man-in-the-middle attack:
26
-
27
- Use this gem to communicate inside an HTTPS tunnel that you trust. For internal private networks, place a secure CA on your network.
28
-
29
- Masquerading:
30
-
31
- Each association made requires messages to be signed using that association's keypair.
32
-
33
- Forgery:
34
-
35
- The indentity of the requestor is signed. The HTTP headers, URL and body / querystring is not. Authorization is left to a service that understands the identity and its permissions.
36
-
37
- Replay:
38
-
39
- The request is signed with an expiry and a nonce.
40
-
41
- ## Extending
42
-
43
- Designing for future protocol security requirements. The nonce and expiry becomes an optional feature (timestamp header signed could replace these). Passing in a RequestData object for signing, and a RequestSigningValidator object that validates the data content includes all that is required for security, allows us to, in future, cater for man-in-the-middle attacks as well if, for example, the validator requires timestamp, url, form data/querystring, method, etc. to be in the data. If this is all signed, a device in the middle cannot modify the request without the source's private key, and man-in-the-middle is defeated.
44
-
45
- Though arbitrary data can be added for signature and an arbitrary validator provided to ensure the data and headers required are included for signing on request, on the server side the payload is not automatically checked by the Smaak::Server class. Ensure after verification of signature that you look in message_data['data'] and verify your payload has what you require to prove no man-in-the middle modification. This could be automated in a future release.
46
-
47
- ## Usage
48
-
49
- Inject your certs and pre-shared keys from the environment, or configuration files or configuration service, etc. User your HTTP library of choice.
50
-
51
- ### Client
52
-
53
- client = Smaak::Client.new
54
- client.set_identity('service-provider-public')
55
- client.set_private_key(File.read '/home/ubuntu/secure/service-provider-public.pem')
56
- client.add_association('service-provider-internal', File.read('/home/ubuntu/secure/service-provider-internal-pub.pem'), 'pre-shared-key')
57
-
58
- response = Unirest.get "http://service-provider-internal:9393/secure-service", headers:{ "Authorization" => client.build_auth_header('service-provider-internal') }, parameters:{ :someparam => 'somevalue'] }
59
-
60
- ### Server
61
-
62
- server = Smaak::Server.new
63
- server.set_public_key(File.read('/home/ubuntu/secure/service-provider-internal-pub.pem'))
64
- server.add_association('service-provider-public', File.read('/home/ubuntu/secure/service-provider-public-pub.pem'), 'pre-shared-key')
65
- requestor = server.verify_signed_request(request)
66
-
67
130
  ## Contributing
68
131
 
69
132
  Please send feedback and comments to the author at:
@@ -72,8 +135,4 @@ Inject your certs and pre-shared keys from the environment, or configuration fil
72
135
 
73
136
  Thanks to Sheldon Hearn for review and great ideas that unblocked complex challenges (https://rubygems.org/profiles/sheldonh).
74
137
 
75
- 1. Fork it
76
- 2. Create your feature branch (`git checkout -b my-new-feature`)
77
- 3. Commit your changes (`git commit -am 'Add some feature'`)
78
- 4. Push to the branch (`git push origin my-new-feature`)
79
- 5. Create new Pull Request
138
+ This gem is sponsored by Hetzner (Pty) Ltd - http://hetzner.co.za
@@ -0,0 +1,43 @@
1
+ require 'smaak'
2
+ require 'smaak/utils'
3
+ require 'uri'
4
+
5
+ module Smaak
6
+ class NetHttpAdaptor
7
+ attr_reader :request
8
+
9
+ def initialize(request)
10
+ raise ArgumentError.new("Must provide a Net::HTTPRequest") if not request.is_a? Net::HTTPRequest
11
+ @request = request
12
+ end
13
+
14
+ def set_header(header, value)
15
+ raise ArgumentError.new("Header must be a non-blank string") if not Smaak::Utils::non_blank_string?(header)
16
+ @request[header] = value
17
+ end
18
+
19
+ def each_header(&block)
20
+ @request.each_header(&block)
21
+ end
22
+
23
+ def host
24
+ URI.parse(@request.path).host
25
+ end
26
+
27
+ def path
28
+ URI.parse(@request.path).path
29
+ end
30
+
31
+ def method
32
+ @request.method
33
+ end
34
+
35
+ def body
36
+ @request.body
37
+ end
38
+
39
+ def body=(body)
40
+ @request.body = body
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,32 @@
1
+ require 'smaak'
2
+ require 'uri'
3
+
4
+ module Smaak
5
+ class RackAdaptor
6
+ attr_reader :request
7
+
8
+ def initialize(request)
9
+ raise ArgumentError.new("Must provide a Net::HTTPRequest") if not request.is_a? Rack::Request
10
+ @request = request
11
+ end
12
+
13
+ def header(header)
14
+ raise ArgumentError.new("Header must be a non-blank string") if not Smaak::Utils::non_blank_string?(header)
15
+ return value = @request.env["CONTENT_LENGTH"] if header == "content-length"
16
+ return value = @request.env["REQUEST_METHOD"] if header == "request-method"
17
+ return @request.env["HTTP_#{header.upcase.gsub("-", "_")}"]
18
+ end
19
+
20
+ def method
21
+ @request.env["REQUEST_METHOD"]
22
+ end
23
+
24
+ def path
25
+ @request.env["PATH_INFO"]
26
+ end
27
+
28
+ def body
29
+ @request.body
30
+ end
31
+ end
32
+ end
@@ -11,10 +11,8 @@ module Smaak
11
11
  @token_life = Smaak::DEFAULT_TOKEN_LIFE
12
12
  end
13
13
 
14
- def add_association(identity, key, psk)
15
- the_key = key.is_a?(String) ? OpenSSL::PKey::RSA.new(key) : key
16
- raise ArgumentError.new("Key needs to be valid") if not validate_key(the_key)
17
- @association_store[identity] = { 'public_key' => the_key, 'psk' => psk }
14
+ def set_key(key)
15
+ @key = adapt_rsa_key(key)
18
16
 
19
17
  rescue OpenSSL::PKey::RSAError
20
18
  raise ArgumentError.new("Key needs to be valid")
@@ -25,15 +23,23 @@ module Smaak
25
23
  @token_life = token_life
26
24
  end
27
25
 
28
- def set_key(key)
26
+ def add_association(identifier, key, psk, encrypt = false)
29
27
  the_key = key.is_a?(String) ? OpenSSL::PKey::RSA.new(key) : key
30
28
  raise ArgumentError.new("Key needs to be valid") if not validate_key(the_key)
31
- @key = the_key
29
+ @association_store[identifier] = { 'public_key' => the_key, 'psk' => psk, 'encrypt' => encrypt }
32
30
 
33
31
  rescue OpenSSL::PKey::RSAError
34
32
  raise ArgumentError.new("Key needs to be valid")
35
33
  end
36
34
 
35
+ protected
36
+
37
+ def adapt_rsa_key(key)
38
+ the_key = key.is_a?(String) ? OpenSSL::PKey::RSA.new(key) : key
39
+ raise ArgumentError.new("Key needs to be valid") if not validate_key(the_key)
40
+ the_key
41
+ end
42
+
37
43
  private
38
44
 
39
45
  def validate_key(key)
@@ -1,57 +1,69 @@
1
+ require 'smaak/crypto'
2
+
1
3
  module Smaak
2
4
  class AuthMessage
3
- attr_reader :message
4
- attr_reader :message_data
5
- attr_reader :identity
5
+ attr_reader :identifier
6
6
  attr_reader :nonce
7
+ attr_reader :recipient
8
+ attr_reader :psk
9
+ attr_reader :expires
10
+ attr_reader :encrypt
11
+
12
+ def self.create(recipient_public_key, psk, token_life, identifier, encrypt = false)
13
+ nonce = Smaak::Crypto::generate_nonce
14
+ expires = Time.now.to_i + token_life
15
+ #Must obfuscate PSK. AuthMessage must always have an obfuscated PSK
16
+ psk = Smaak::Crypto::obfuscate_psk(psk)
17
+ AuthMessage::build(recipient_public_key, psk, expires, identifier, nonce, encrypt)
18
+ end
7
19
 
8
- def initialize(message)
9
- raise ArgumentError.new("Message not specified") if message.nil?
10
- @message = message.dup
11
- @message.freeze
12
- begin
13
- @message_data = JSON.parse(Base64.decode64(message))
14
- @message_data.freeze
15
- rescue => ex
16
- raise ArgumentError.new("Message must have valid message data")
17
- end
18
- raise ArgumentError.new("Message must have a valid expiry set") if not validate_expiry
19
- @identity = @message_data['identity']
20
- @identity.freeze
21
- raise ArgumentError.new("Message must have a valid identity set") if @identity.nil? or @identity.empty?
22
- @nonce = @message_data['nonce']
20
+ def self.build(recipient_public_key, psk, expires, identifier, nonce, encrypt = false)
21
+ #No need to obfuscate PSK. Off the wire we should always expect an obfuscated PSK
22
+ AuthMessage.new(identifier, nonce, expires, psk, recipient_public_key, encrypt)
23
+ end
24
+
25
+ def initialize(identifier, nonce, expires, psk, recipient_public_key, encrypt)
26
+ raise ArgumentError.new("Message must have a valid identifier set") if identifier.nil? or identifier.empty?
27
+ @identifier = identifier
28
+ @identifier.freeze
29
+
30
+ raise ArgumentError.new("Message must have a valid nonce set") if not validate_nonce(nonce)
31
+ @nonce = nonce
23
32
  @nonce.freeze
24
- raise ArgumentError.new("Message must have a valid nonce") if not validate_nonce(@nonce)
33
+
34
+ @recipient = recipient_public_key
35
+ @psk = psk
36
+
37
+ raise ArgumentError.new("Message must have a valid expiry set") if not validate_expiry(expires)
38
+ @expires = expires
39
+ set_encrypt(encrypt)
25
40
  end
26
41
 
27
- def expired?
28
- @message_data['expires'].to_i < Time.now.to_i
42
+ def set_encrypt(encrypt)
43
+ @encrypt = false
44
+ @encrypt = true if encrypt == "true" or encrypt == true
29
45
  end
30
46
 
31
- def signature_ok?(signature, pubkey)
32
- return false if signature.nil?
33
- return false if pubkey.nil?
34
- digest = OpenSSL::Digest::SHA256.new
35
- pubkey.verify(digest, signature, @message)
47
+ def expired?
48
+ @expires.to_i < Time.now.to_i
36
49
  end
37
50
 
38
51
  def psk_match?(psk)
39
52
  return false if psk.nil?
40
- return false if @message_data['psk'].nil?
41
- @message_data['psk'] == psk
53
+ return false if @psk.nil?
54
+ @psk == Smaak::Crypto::obfuscate_psk(psk)
42
55
  end
43
56
 
44
57
  def intended_for_recipient?(pubkey)
45
58
  return false if pubkey.nil?
46
- return false if @message_data['recipient'].nil?
47
- @message_data['recipient'] == pubkey
59
+ return false if @recipient.nil?
60
+ @recipient == pubkey
48
61
  end
49
62
 
50
- def verify(signature, pubkey, psk)
63
+ def verify(psk)
51
64
  return false if expired?
52
- return false if not signature_ok?(signature, pubkey)
53
- return false if not psk_match?(Smaak::obfuscate_psk(psk))
54
- identity
65
+ return false if not psk_match?(psk)
66
+ true
55
67
  end
56
68
 
57
69
  private
@@ -60,14 +72,18 @@ module Smaak
60
72
  return false if nonce.nil?
61
73
  return false if nonce.to_i == 0
62
74
  true
75
+
76
+ rescue
77
+ false
63
78
  end
64
79
 
65
- def validate_expiry
66
- return false if @message_data.nil?
67
- return false if not @message_data.is_a? Hash
68
- return false if @message_data['expires'].nil?
69
- return false if not (@message_data['expires'].to_i > 0)
80
+ def validate_expiry(expires)
81
+ return false if expires.nil?
82
+ return false if not (expires.to_i > 0)
70
83
  true
84
+
85
+ rescue
86
+ false
71
87
  end
72
88
  end
73
89
  end
@@ -0,0 +1,84 @@
1
+ require 'smaak'
2
+
3
+ module Smaak
4
+ class Cavage04
5
+ SPECIFICATION = "https://datatracker.ietf.org/doc/draft-cavage-http-signatures/04/" unless defined? SPECIFICATION; SPECIFICATION.freeze
6
+ attr_reader :adaptor
7
+ attr_reader :headers_to_be_signed
8
+
9
+ def initialize(adaptor)
10
+ raise ArgumentError.new("Must provide a valid request adaptor") if adaptor.nil?
11
+ @adaptor = adaptor
12
+ @headers_to_be_signed = Smaak::Cavage04::headers_to_be_signed + Smaak::headers_to_be_signed
13
+ end
14
+
15
+ def self.headers_to_be_signed
16
+ [ "(request-target)",
17
+ "host",
18
+ "date",
19
+ "digest",
20
+ "content-length" ]
21
+ end
22
+
23
+ def compile_auth_header(signature)
24
+ raise ArgumentError.new("invalid signature") if not Smaak::Utils::non_blank_string?(signature)
25
+ ordered_headers = ""
26
+ @adaptor.each_header do |header, value|
27
+ ordered_headers = "#{ordered_headers} #{header}" if @headers_to_be_signed.include?(header)
28
+ end
29
+ ordered_headers = ordered_headers[1..ordered_headers.size]
30
+ @adaptor.set_header("authorization", "Signature keyId=\"rsa-key-1\",algorithm=\"rsa-sha256\", headers=\"#{ordered_headers}\", signature=\"#{signature}\"")
31
+ end
32
+
33
+ def compile_signature_headers(auth_message)
34
+ body = @adaptor.body.nil? ? "" : @adaptor.body
35
+ @adaptor.set_header("authorization", "")
36
+ @adaptor.set_header("host", "#{@adaptor.host}")
37
+ @adaptor.set_header("date", "#{gmt_now}")
38
+ @adaptor.set_header("digest", "SHA-256=#{Digest::SHA256.hexdigest(body)}")
39
+ @adaptor.set_header("x-smaak-recipient", "#{Smaak::Crypto::encode64(auth_message.recipient)}")
40
+ @adaptor.set_header("x-smaak-identifier", "#{auth_message.identifier}")
41
+ @adaptor.set_header("x-smaak-psk", "#{auth_message.psk}")
42
+ @adaptor.set_header("x-smaak-expires", "#{auth_message.expires}")
43
+ @adaptor.set_header("x-smaak-nonce", "#{auth_message.nonce}")
44
+ @adaptor.set_header("x-smaak-encrypt", "#{auth_message.encrypt}")
45
+ @adaptor.set_header("content-type", "text/plain")
46
+ @adaptor.set_header("content-length", "#{body.size}")
47
+
48
+ signature_headers = ""
49
+ @adaptor.each_header do |header, value|
50
+ signature_headers = append_header(signature_headers, "#{header}: #{value}") if @headers_to_be_signed.include? header
51
+ end
52
+ signature_headers = prepend_header("(request-target)", "#{@adaptor.method.downcase} #{@adaptor.path}", signature_headers)
53
+ end
54
+
55
+ def extract_signature_headers
56
+ @adaptor.header("authorization") =~ /headers=\"([^"]*)\",/
57
+ headers_order = $1.split(' ')
58
+
59
+ signature_headers = ""
60
+ headers_order.each do |header|
61
+ signature_headers = append_header(signature_headers, "#{header}: #{@adaptor.header(header)}")
62
+ end
63
+ signature_headers = prepend_header("(request-target)", "#{@adaptor.method.downcase} #{@adaptor.path}", signature_headers)
64
+ end
65
+
66
+ def extract_signature
67
+ @adaptor.header("authorization") =~ /signature=\"([^"]*)\"/
68
+ $1
69
+ end
70
+ private
71
+
72
+ def gmt_now
73
+ Time.now.gmtime.to_s.gsub("UTC", "GMT")
74
+ end
75
+
76
+ def append_header(header_list, header)
77
+ header_list = "#{header_list}\n#{header}"
78
+ end
79
+
80
+ def prepend_header(header, value, signature_headers)
81
+ "#{header}: #{value}#{signature_headers}"
82
+ end
83
+ end
84
+ end
data/lib/smaak/client.rb CHANGED
@@ -1,41 +1,58 @@
1
+ require 'uri'
1
2
  require 'smaak.rb'
2
3
  require 'smaak/associate.rb'
3
- require 'smaak/request_signing_validator.rb'
4
+ require 'smaak/auth_message.rb'
4
5
 
5
6
  module Smaak
6
7
  class Client < Associate
7
- attr_accessor :identity
8
- attr_accessor :private_key
8
+ attr_reader :identifier
9
9
 
10
10
  def set_private_key(key)
11
11
  set_key(key)
12
12
  end
13
13
 
14
- def set_identity(identity)
15
- @identity = identity
14
+ def set_identifier(identifier)
15
+ raise ArgumentError.new("Invalid identifier") if not Smaak::Utils::non_blank_string?(identifier)
16
+ @identifier = identifier
16
17
  end
17
18
 
18
- def build_auth_header(associate_identity,
19
- request_signing_data = {},
20
- request_signing_data_validator = RequestSigningValidator.new)
21
- raise ArgumentError.new("Associate invalid") if not validate_associate(associate_identity)
22
- request_signing_data_validator.validate(request_signing_data)
23
- associate = @association_store[associate_identity]
24
- message_data = Smaak::compile_auth_message_data(associate['public_key'], associate['psk'], @token_life, @identity, request_signing_data)
25
- signature = Smaak::sign_message_data(message_data, @key)
26
- #TBD add request_signing_data approved by validator here
27
- message = Smaak::build_message(message_data)
28
- auth_body = { 'message' => message,
29
- 'signature' => Base64.encode64(signature) }
30
- auth = auth_body.to_json
19
+ def sign_request(associate_identifier, adaptor)
20
+ raise ArgumentError.new("Associate invalid") if not validate_associate(associate_identifier)
21
+ associate = @association_store[associate_identifier]
22
+ raise ArgumentError.new("Invalid adaptor") if adaptor.nil?
23
+ auth_message = Smaak::AuthMessage.create(associate['public_key'].export, associate['psk'], @token_life, @identifier, associate['encrypt'])
24
+ adaptor.body = Smaak::Crypto::encrypt(adaptor.body, associate['public_key']) if auth_message.encrypt
25
+ adaptor = Smaak::sign_authorization_headers(@key, auth_message, adaptor, Smaak::Cavage04::SPECIFICATION)
26
+ end
27
+
28
+ def get(identifier, uri, body, ssl = false, ssl_verify = OpenSSL::SSL::VERIFY_PEER)
29
+ connect(Net::HTTP::Get, identifier, uri, body, ssl, ssl_verify)
30
+ end
31
+
32
+ def post(identifier, uri, body, ssl = false, ssl_verify = OpenSSL::SSL::VERIFY_PEER)
33
+ connect(Net::HTTP::Post, identifier, uri, body, ssl, ssl_verify)
31
34
  end
32
35
 
33
36
  private
34
37
 
35
- def validate_associate(associate_identity)
36
- return false if associate_identity.nil?
37
- return false if @association_store[associate_identity].nil?
38
+ def validate_associate(associate_identifier)
39
+ return false if associate_identifier.nil?
40
+ return false if @association_store[associate_identifier].nil?
38
41
  true
39
42
  end
43
+
44
+ def connect(connector, identifier, uri, body, ssl, ssl_verify)
45
+ url = URI.parse(uri)
46
+ http = Net::HTTP.new(url.host, url.port)
47
+ http.use_ssl = ssl
48
+ http.verify_mode = ssl_verify
49
+ req = connector.new(url.to_s)
50
+ req.body = body
51
+ adaptor = Smaak::create_adaptor(req)
52
+ req = (sign_request(identifier, adaptor)).request
53
+ response = http.request(req)
54
+ response.body = Smaak::Crypto::decrypt(response.body, @key) if @association_store[identifier]['encrypt']
55
+ response
56
+ end
40
57
  end
41
58
  end
@@ -0,0 +1,45 @@
1
+ module Smaak
2
+ class Crypto
3
+ def self.obfuscate_psk(psk)
4
+ Digest::MD5.hexdigest(psk.reverse)
5
+ end
6
+
7
+ def self.generate_nonce
8
+ SecureRandom::random_number(10000000000)
9
+ end
10
+
11
+ def self.encode64(data)
12
+ Base64.strict_encode64(data)
13
+ end
14
+
15
+ def self.decode64(data)
16
+ Base64.strict_decode64(data)
17
+ end
18
+
19
+ def self.sign_data(data, private_key)
20
+ digest = OpenSSL::Digest::SHA256.new
21
+ private_key.sign(digest, Smaak::Crypto::encode64(data))
22
+ end
23
+
24
+ def self.verify_signature(signature, data, public_key)
25
+ digest = OpenSSL::Digest::SHA256.new
26
+ public_key.verify(digest, signature, data)
27
+ end
28
+
29
+ def self.encrypt(data, public_key)
30
+ Base64.strict_encode64(public_key.public_encrypt(data))
31
+ end
32
+
33
+ def self.decrypt(data, private_key)
34
+ private_key.private_decrypt(Base64.strict_decode64(data))
35
+ end
36
+
37
+ def self.sink(stream)
38
+ data = ""
39
+ while t = stream.gets do
40
+ data = data + t
41
+ end
42
+ data
43
+ end
44
+ end
45
+ end