http_signatures 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +72 -0
- data/Rakefile +1 -0
- data/http_signatures.gemspec +23 -0
- data/lib/http_signatures/algorithm/hmac.rb +22 -0
- data/lib/http_signatures/algorithm/null.rb +15 -0
- data/lib/http_signatures/algorithm.rb +20 -0
- data/lib/http_signatures/context.rb +19 -0
- data/lib/http_signatures/header_list.rb +49 -0
- data/lib/http_signatures/key.rb +19 -0
- data/lib/http_signatures/key_store.rb +20 -0
- data/lib/http_signatures/signature_parameters.rb +33 -0
- data/lib/http_signatures/signer.rb +51 -0
- data/lib/http_signatures/signing_string.rb +37 -0
- data/lib/http_signatures/version.rb +3 -0
- data/lib/http_signatures.rb +14 -0
- data/spec/algorithm_spec.rb +33 -0
- data/spec/context_spec.rb +30 -0
- data/spec/header_list_spec.rb +36 -0
- data/spec/key_store_spec.rb +23 -0
- data/spec/signer_spec.rb +85 -0
- data/spec/signing_string_spec.rb +42 -0
- data/spec/spec_helper.rb +12 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d470a2f4d0e20f91439770b757fec53dfe6e0cf7
|
4
|
+
data.tar.gz: 44e3fc97e52f403f4a71c7c0d9aead5440128409
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6ae9785147e0d0d07deec0af632caafdc67ea901eea51ead27c990ef0bc98e9c39b0b465e7961898271f231aa035fd823ff2a9b026f0ed6c6a3a2344d5142fec
|
7
|
+
data.tar.gz: 69334e7da16049f64d3ccba567a4737c75071290c698699111bfae112449d20bdd570295f9a8053948b6dca023502d395df37f435475f682f407907b21b600f3
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 99designs
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# HTTP Signatures
|
2
|
+
|
3
|
+
Ruby implementation of [HTTP Signatures][draft03] draft specification;
|
4
|
+
cryptographically sign and verify HTTP requests and responses.
|
5
|
+
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
Configure a context with your algorithm, keys, headers to sign. In Rails,
|
10
|
+
this is best placed in an initializer.
|
11
|
+
|
12
|
+
```rb
|
13
|
+
require "http_signatures"
|
14
|
+
|
15
|
+
$context = HttpSignatures::Context.new(
|
16
|
+
keys: {"examplekey" => "secret-key-here"},
|
17
|
+
algorithm: "hmac-sha256",
|
18
|
+
headers: %w{(request-target) Date Content-Length},
|
19
|
+
)
|
20
|
+
```
|
21
|
+
|
22
|
+
### Messages
|
23
|
+
|
24
|
+
A message is an HTTP request or response. A subset of the interface of
|
25
|
+
Ruby's Net::HTTPRequest and Net::HTTPResponse is expected; the ability to
|
26
|
+
set/read headers via `message["name"]`, and for requests, the presence
|
27
|
+
of `message#method` and `message#path` for `(request-target)` support.
|
28
|
+
|
29
|
+
```rb
|
30
|
+
require "net/http"
|
31
|
+
require "time"
|
32
|
+
message = Net::HTTP::Get.new(
|
33
|
+
"/path?query=123",
|
34
|
+
"Date" => Time.now.rfc822,
|
35
|
+
"Content-Length" => "0",
|
36
|
+
)
|
37
|
+
```
|
38
|
+
|
39
|
+
### Signing a message
|
40
|
+
|
41
|
+
```rb
|
42
|
+
$context.signer("examplekey").sign(message)
|
43
|
+
```
|
44
|
+
|
45
|
+
Now `message` contains the signature headers:
|
46
|
+
|
47
|
+
```rb
|
48
|
+
message["Signature"]
|
49
|
+
# keyId="examplekey",algorithm="hmac-sha256",headers="...",signature="..."
|
50
|
+
|
51
|
+
message["Authorization"]
|
52
|
+
# Signature keyId="examplekey",algorithm="hmac-sha256",headers="...",signature="..."
|
53
|
+
```
|
54
|
+
|
55
|
+
### Verifying a signed message
|
56
|
+
|
57
|
+
Message verification is not implemented, but will look like this:
|
58
|
+
|
59
|
+
* The key ID, algorithm name, header list and provided signature will be parsed
|
60
|
+
from the `Signature` and/or `Authorization` header.
|
61
|
+
* The signing string will be derived by selecting the listed headers from the
|
62
|
+
message.
|
63
|
+
* The valid signature will be derived by applying the algorithm and secret key.
|
64
|
+
* The message is valid if the provided signature matches the valid signature.
|
65
|
+
|
66
|
+
|
67
|
+
## Contributing
|
68
|
+
|
69
|
+
Pull Requests are welcome.
|
70
|
+
|
71
|
+
|
72
|
+
[draft03]: http://tools.ietf.org/html/draft-cavage-http-signatures-03
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'http_signatures/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "http_signatures"
|
8
|
+
spec.version = HttpSignatures::VERSION
|
9
|
+
spec.authors = ["Paul Annesley"]
|
10
|
+
spec.email = ["paul@annesley.cc"]
|
11
|
+
spec.summary = "Sign and verify HTTP messages"
|
12
|
+
spec.homepage = "https://github.com/99designs/http-signatures-ruby"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
21
|
+
spec.add_development_dependency "rake"
|
22
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
23
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "openssl"
|
2
|
+
|
3
|
+
module HttpSignatures
|
4
|
+
module Algorithm
|
5
|
+
class Hmac
|
6
|
+
|
7
|
+
def initialize(digest_name)
|
8
|
+
@digest_name = digest_name
|
9
|
+
@digest = OpenSSL::Digest.new(digest_name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def name
|
13
|
+
"hmac-#{@digest_name}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def sign(key, data)
|
17
|
+
OpenSSL::HMAC.digest(@digest, key, data)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module HttpSignatures
|
2
|
+
module Algorithm
|
3
|
+
|
4
|
+
def self.create(name)
|
5
|
+
case name
|
6
|
+
when "null" then Null.new
|
7
|
+
when "hmac-sha1" then Hmac.new("sha1")
|
8
|
+
when "hmac-sha256" then Hmac.new("sha256")
|
9
|
+
else raise UnknownAlgorithm.new(name)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class UnknownAlgorithm < StandardError
|
14
|
+
def initialize(name)
|
15
|
+
super("Unknown algorithm name '#{name}'")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module HttpSignatures
|
2
|
+
class Context
|
3
|
+
|
4
|
+
def initialize(keys: {}, algorithm: nil, headers: nil)
|
5
|
+
@key_store = KeyStore.new(keys)
|
6
|
+
@algorithm_name = algorithm
|
7
|
+
@headers = headers
|
8
|
+
end
|
9
|
+
|
10
|
+
def signer(key_id)
|
11
|
+
Signer.new(
|
12
|
+
key: @key_store.fetch(key_id),
|
13
|
+
algorithm: Algorithm.create(@algorithm_name),
|
14
|
+
header_list: HeaderList.new(@headers),
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module HttpSignatures
|
2
|
+
class HeaderList
|
3
|
+
|
4
|
+
# cannot sign the signature headers
|
5
|
+
ILLEGAL = ["authorization", "signature"]
|
6
|
+
|
7
|
+
def self.from_string(string)
|
8
|
+
new(string.split(" "))
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(names)
|
12
|
+
@names = names.map(&:downcase)
|
13
|
+
validate_names!
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_a
|
17
|
+
@names.dup
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
@names.join(" ")
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def validate_names!
|
27
|
+
if @names.empty?
|
28
|
+
raise EmptyHeaderList
|
29
|
+
end
|
30
|
+
if illegal_headers_present.any?
|
31
|
+
raise IllegalHeader, illegal_headers_present
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def illegal_headers_present
|
36
|
+
ILLEGAL & @names
|
37
|
+
end
|
38
|
+
|
39
|
+
class IllegalHeader < StandardError
|
40
|
+
def initialize(names)
|
41
|
+
names_string = names.map { |n| "'#{n}'" }.join(", ")
|
42
|
+
super("Header #{names_string} not permitted")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class EmptyHeaderList < StandardError; end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module HttpSignatures
|
2
|
+
class Key
|
3
|
+
|
4
|
+
def initialize(id:, secret:)
|
5
|
+
@id = id
|
6
|
+
@secret = secret
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :id
|
10
|
+
attr_reader :secret
|
11
|
+
|
12
|
+
def ==(other)
|
13
|
+
self.class == other.class &&
|
14
|
+
self.id == other.id &&
|
15
|
+
self.secret == other.secret
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module HttpSignatures
|
2
|
+
class KeyStore
|
3
|
+
|
4
|
+
def initialize(key_hash)
|
5
|
+
@keys = {}
|
6
|
+
key_hash.each { |id, secret| self[id] = secret }
|
7
|
+
end
|
8
|
+
|
9
|
+
def fetch(id)
|
10
|
+
@keys.fetch(id)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def []=(id, secret)
|
16
|
+
@keys[id] = Key.new(id: id, secret: secret)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "base64"
|
2
|
+
|
3
|
+
module HttpSignatures
|
4
|
+
class SignatureParameters
|
5
|
+
|
6
|
+
def initialize(key:, algorithm:, header_list:, signature:)
|
7
|
+
@key = key
|
8
|
+
@algorithm = algorithm
|
9
|
+
@header_list = header_list
|
10
|
+
@signature = signature
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_str
|
14
|
+
parameter_components.join(",")
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def parameter_components
|
20
|
+
pc = []
|
21
|
+
pc << 'keyId="%s"' % @key.id
|
22
|
+
pc << 'algorithm="%s"' % @algorithm.name
|
23
|
+
pc << 'headers="%s"' % @header_list.to_s
|
24
|
+
pc << 'signature="%s"' % signature_base64
|
25
|
+
pc
|
26
|
+
end
|
27
|
+
|
28
|
+
def signature_base64
|
29
|
+
Base64.strict_encode64(@signature)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module HttpSignatures
|
2
|
+
class Signer
|
3
|
+
|
4
|
+
AUTHORIZATION_SCHEME = "Signature"
|
5
|
+
|
6
|
+
def initialize(key:, algorithm:, header_list:)
|
7
|
+
@key = key
|
8
|
+
@algorithm = algorithm
|
9
|
+
@header_list = header_list
|
10
|
+
end
|
11
|
+
|
12
|
+
def sign(message)
|
13
|
+
message.tap do |m|
|
14
|
+
signature = signature_parameters_for_message(message).to_str
|
15
|
+
m["Signature"] = [signature]
|
16
|
+
m["Authorization"] = [AUTHORIZATION_SCHEME + " " + signature]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def signature_parameters_for_message(message)
|
23
|
+
SignatureParameters.new(
|
24
|
+
key: @key,
|
25
|
+
algorithm: @algorithm,
|
26
|
+
header_list: @header_list,
|
27
|
+
signature: signature_for_message(message),
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def signature_for_message(message)
|
32
|
+
@algorithm.sign(@key.secret, signing_string_for_message(message))
|
33
|
+
end
|
34
|
+
|
35
|
+
def signing_string_for_message(message)
|
36
|
+
SigningString.new(
|
37
|
+
header_list: @header_list,
|
38
|
+
message: message,
|
39
|
+
).to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
class EmptyHeaderNames < StandardError; end
|
43
|
+
|
44
|
+
class MessageMissingHeader < StandardError
|
45
|
+
def initialize(name)
|
46
|
+
super("Message missing header '#{name}'")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module HttpSignatures
|
2
|
+
class SigningString
|
3
|
+
|
4
|
+
REQUEST_TARGET = "(request-target)"
|
5
|
+
|
6
|
+
def initialize(header_list:, message:)
|
7
|
+
@header_list = header_list
|
8
|
+
@message = message
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
@header_list.to_a.map do |header|
|
13
|
+
"%s: %s" % [header, header_value(header)]
|
14
|
+
end.join("\n")
|
15
|
+
end
|
16
|
+
|
17
|
+
def header_value(header)
|
18
|
+
if header == REQUEST_TARGET
|
19
|
+
request_target
|
20
|
+
else
|
21
|
+
@message.fetch(header) { raise HeaderNotInMessage, header }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def request_target
|
26
|
+
"%s %s" % [@message.method.downcase, @message.path]
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
class HeaderNotInMessage < StandardError
|
32
|
+
def initialize(name)
|
33
|
+
super("Header '#{name}' not in message")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "http_signatures/algorithm"
|
2
|
+
require "http_signatures/algorithm/hmac"
|
3
|
+
require "http_signatures/algorithm/null"
|
4
|
+
require "http_signatures/context"
|
5
|
+
require "http_signatures/header_list"
|
6
|
+
require "http_signatures/key"
|
7
|
+
require "http_signatures/key_store"
|
8
|
+
require "http_signatures/signature_parameters"
|
9
|
+
require "http_signatures/signer"
|
10
|
+
require "http_signatures/signing_string"
|
11
|
+
require "http_signatures/version"
|
12
|
+
|
13
|
+
module HttpSignatures
|
14
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "base64"
|
2
|
+
|
3
|
+
RSpec.describe HttpSignatures::Algorithm do
|
4
|
+
|
5
|
+
let(:key) { "the-key" }
|
6
|
+
let(:input) { "the string\nto sign" }
|
7
|
+
|
8
|
+
{
|
9
|
+
"null" => "bnVsbA==", # "null"
|
10
|
+
"hmac-sha1" => "bXPeVc5ySIyeUapN7mpMsJRnxVg=",
|
11
|
+
"hmac-sha256" => "hRQ5zpbGudR1hokS4PqeAkveKmz2dd8SCgV8OHcramI=",
|
12
|
+
}.each do |name, base64_signature|
|
13
|
+
|
14
|
+
describe ".create('#{name}')" do
|
15
|
+
let(:algorithm) { HttpSignatures::Algorithm.create(name) }
|
16
|
+
it "has #name == '#{name}'" do
|
17
|
+
expect(algorithm.name).to eq(name)
|
18
|
+
end
|
19
|
+
it "produces known-good signature" do
|
20
|
+
signature = algorithm.sign(key, input)
|
21
|
+
expect(signature).to eq(Base64.strict_decode64(base64_signature))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
it "raises error for unknown algorithm" do
|
28
|
+
expect {
|
29
|
+
HttpSignatures::Algorithm.create(name: "nope", key: nil)
|
30
|
+
}.to raise_error(HttpSignatures::Algorithm::UnknownAlgorithm)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "net/http"
|
2
|
+
|
3
|
+
RSpec.describe HttpSignatures::Context do
|
4
|
+
|
5
|
+
subject(:context) do
|
6
|
+
HttpSignatures::Context.new(
|
7
|
+
keys: {"hello" => "world"},
|
8
|
+
algorithm: "null",
|
9
|
+
headers: %w{(request-target) date content-length},
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:message) { Net::HTTP::Get.new("/", "date" => "x", "content-length" => "0") }
|
14
|
+
|
15
|
+
describe "#signer" do
|
16
|
+
it "instantiates Signer with key, algorithm, headers" do
|
17
|
+
expect(HttpSignatures::Signer).to receive(:new) do |args|
|
18
|
+
expect(args[:key]).to eq(HttpSignatures::Key.new(id: "hello", secret: "world"))
|
19
|
+
expect(args[:algorithm].name).to eq("null")
|
20
|
+
expect(args[:header_list].to_a).to eq(%w{(request-target) date content-length})
|
21
|
+
end
|
22
|
+
context.signer("hello")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "signs without errors" do
|
26
|
+
context.signer("hello").sign(message)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
RSpec.describe HttpSignatures::HeaderList do
|
2
|
+
|
3
|
+
describe ".from_string" do
|
4
|
+
it "loads and normalizes header names" do
|
5
|
+
expect(HttpSignatures::HeaderList).to receive(:new).with(
|
6
|
+
["(request-target)", "Date", "Content-Type"]
|
7
|
+
)
|
8
|
+
HttpSignatures::HeaderList.from_string(
|
9
|
+
"(request-target) Date Content-Type"
|
10
|
+
)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe ".new" do
|
15
|
+
it "normalizes header names (downcase)" do
|
16
|
+
list = HttpSignatures::HeaderList.new(["(request-target)", "Date", "Content-Type"])
|
17
|
+
expect(list.to_a).to eq(["(request-target)", "date", "content-type"])
|
18
|
+
end
|
19
|
+
|
20
|
+
["Authorization", "Signature"].each do |header|
|
21
|
+
it "raises IllegalHeader for #{header} header" do
|
22
|
+
expect {
|
23
|
+
HttpSignatures::HeaderList.new([header])
|
24
|
+
}.to raise_error(HttpSignatures::HeaderList::IllegalHeader)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#to_s" do
|
30
|
+
it "joins normalized header names with spaces" do
|
31
|
+
list = HttpSignatures::HeaderList.new(["(request-target)", "Date", "Content-Type"])
|
32
|
+
expect(list.to_s).to eq("(request-target) date content-type")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
RSpec.describe HttpSignatures::KeyStore do
|
2
|
+
|
3
|
+
subject(:store) do
|
4
|
+
HttpSignatures::KeyStore.new(
|
5
|
+
"hello" => "world",
|
6
|
+
"another" => "key",
|
7
|
+
)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "#fetch" do
|
11
|
+
it "retrieves keys" do
|
12
|
+
expect(store.fetch("hello")).to eq(
|
13
|
+
HttpSignatures::Key.new(id: "hello", secret: "world")
|
14
|
+
)
|
15
|
+
end
|
16
|
+
it "raises KeyError" do
|
17
|
+
expect {
|
18
|
+
store.fetch("nope")
|
19
|
+
}.to raise_error(KeyError)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
data/spec/signer_spec.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require "net/http"
|
2
|
+
|
3
|
+
RSpec.describe HttpSignatures::Signer do
|
4
|
+
|
5
|
+
EXAMPLE_DATE = "Mon, 28 Jul 2014 15:39:13 -0700"
|
6
|
+
|
7
|
+
subject(:signer) do
|
8
|
+
HttpSignatures::Signer.new(key: key, algorithm: algorithm, header_list: header_list)
|
9
|
+
end
|
10
|
+
let(:key) { HttpSignatures::Key.new(id: "pda", secret: "sh") }
|
11
|
+
let(:algorithm) { HttpSignatures::Algorithm::Null.new }
|
12
|
+
let(:header_list) { HttpSignatures::HeaderList.new(["date", "content-type"]) }
|
13
|
+
|
14
|
+
let(:message) do
|
15
|
+
Net::HTTP::Get.new(
|
16
|
+
"/path?query=123",
|
17
|
+
"Date" => EXAMPLE_DATE,
|
18
|
+
"Content-Type" => "text/plain",
|
19
|
+
"Content-Length" => "123",
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
let(:authorization_structure_pattern) do
|
24
|
+
%r{
|
25
|
+
\A
|
26
|
+
Signature
|
27
|
+
\s
|
28
|
+
keyId="[\w-]+",
|
29
|
+
algorithm="[\w-]+",
|
30
|
+
(?:headers=".*",)?
|
31
|
+
signature="[a-zA-Z0-9/+=]+"
|
32
|
+
\z
|
33
|
+
}x
|
34
|
+
end
|
35
|
+
|
36
|
+
let(:signature_structure_pattern) do
|
37
|
+
%r{
|
38
|
+
\A
|
39
|
+
keyId="[\w-]+",
|
40
|
+
algorithm="[\w-]+",
|
41
|
+
(?:headers=".*",)?
|
42
|
+
signature="[a-zA-Z0-9/+=]+"
|
43
|
+
\z
|
44
|
+
}x
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#sign" do
|
48
|
+
it "passes correct signing string to algorithm" do
|
49
|
+
expect(algorithm).to receive(:sign).with(
|
50
|
+
"sh",
|
51
|
+
[
|
52
|
+
"date: #{EXAMPLE_DATE}",
|
53
|
+
"content-type: text/plain",
|
54
|
+
].join("\n")
|
55
|
+
).and_return("null")
|
56
|
+
signer.sign(message)
|
57
|
+
end
|
58
|
+
it "returns reference to the mutated input" do
|
59
|
+
expect(signer.sign(message)).to eq(message)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "after signing" do
|
64
|
+
before { signer.sign(message) }
|
65
|
+
it "has valid Authorization header structure" do
|
66
|
+
expect(message["Authorization"]).to match(authorization_structure_pattern)
|
67
|
+
end
|
68
|
+
it "has valid Signature header structure" do
|
69
|
+
expect(message["Signature"]).to match(signature_structure_pattern)
|
70
|
+
end
|
71
|
+
it "matches expected Authorization header" do
|
72
|
+
expect(message["Authorization"]).to eq(
|
73
|
+
'Signature keyId="pda",algorithm="null",' +
|
74
|
+
'headers="date content-type",signature="bnVsbA=="'
|
75
|
+
)
|
76
|
+
end
|
77
|
+
it "matches expected Signature header" do
|
78
|
+
expect(message["Signature"]).to eq(
|
79
|
+
'keyId="pda",algorithm="null",' +
|
80
|
+
'headers="date content-type",signature="bnVsbA=="'
|
81
|
+
)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "net/http"
|
2
|
+
|
3
|
+
RSpec.describe HttpSignatures::SigningString do
|
4
|
+
|
5
|
+
DATE = "Tue, 29 Jul 2014 14:17:02 -0700"
|
6
|
+
|
7
|
+
subject(:signing_string) do
|
8
|
+
HttpSignatures::SigningString.new(
|
9
|
+
header_list: header_list,
|
10
|
+
message: message,
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:header_list) do
|
15
|
+
HttpSignatures::HeaderList.from_string("(request-target) date")
|
16
|
+
end
|
17
|
+
|
18
|
+
let(:message) do
|
19
|
+
Net::HTTP::Get.new("/path?query=123", "date" => DATE, "x-herring" => "red")
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "#to_s" do
|
23
|
+
|
24
|
+
it "returns correct signing string" do
|
25
|
+
expect(signing_string.to_s).to eq(
|
26
|
+
"(request-target): get /path?query=123\n" +
|
27
|
+
"date: #{DATE}"
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
context "for header not in message" do
|
32
|
+
let(:header_list) { HttpSignatures::HeaderList.from_string("nope") }
|
33
|
+
it "raises HeaderNotInMessage" do
|
34
|
+
expect {
|
35
|
+
signing_string.to_s
|
36
|
+
}.to raise_error(HttpSignatures::HeaderNotInMessage)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: http_signatures
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Paul Annesley
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-07-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.5'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- paul@annesley.cc
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- ".rspec"
|
64
|
+
- Gemfile
|
65
|
+
- LICENSE.txt
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- http_signatures.gemspec
|
69
|
+
- lib/http_signatures.rb
|
70
|
+
- lib/http_signatures/algorithm.rb
|
71
|
+
- lib/http_signatures/algorithm/hmac.rb
|
72
|
+
- lib/http_signatures/algorithm/null.rb
|
73
|
+
- lib/http_signatures/context.rb
|
74
|
+
- lib/http_signatures/header_list.rb
|
75
|
+
- lib/http_signatures/key.rb
|
76
|
+
- lib/http_signatures/key_store.rb
|
77
|
+
- lib/http_signatures/signature_parameters.rb
|
78
|
+
- lib/http_signatures/signer.rb
|
79
|
+
- lib/http_signatures/signing_string.rb
|
80
|
+
- lib/http_signatures/version.rb
|
81
|
+
- spec/algorithm_spec.rb
|
82
|
+
- spec/context_spec.rb
|
83
|
+
- spec/header_list_spec.rb
|
84
|
+
- spec/key_store_spec.rb
|
85
|
+
- spec/signer_spec.rb
|
86
|
+
- spec/signing_string_spec.rb
|
87
|
+
- spec/spec_helper.rb
|
88
|
+
homepage: https://github.com/99designs/http-signatures-ruby
|
89
|
+
licenses:
|
90
|
+
- MIT
|
91
|
+
metadata: {}
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
requirements: []
|
107
|
+
rubyforge_project:
|
108
|
+
rubygems_version: 2.2.2
|
109
|
+
signing_key:
|
110
|
+
specification_version: 4
|
111
|
+
summary: Sign and verify HTTP messages
|
112
|
+
test_files:
|
113
|
+
- spec/algorithm_spec.rb
|
114
|
+
- spec/context_spec.rb
|
115
|
+
- spec/header_list_spec.rb
|
116
|
+
- spec/key_store_spec.rb
|
117
|
+
- spec/signer_spec.rb
|
118
|
+
- spec/signing_string_spec.rb
|
119
|
+
- spec/spec_helper.rb
|