macaroons 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1b8c0a4138d4382ec9066b5c95912acba6d473cd
4
+ data.tar.gz: d7c04a35aa9e133a51cc87538ef5eb93b0210e20
5
+ SHA512:
6
+ metadata.gz: 581dba3fd34883934f9cec9c7ea0bc85657471df394b15ed3a79ecfed7aa1bf508a4d40f3665512b7ad01720446c305a7378d1cfe4586591b3f093471000e3d5
7
+ data.tar.gz: 67bc07973505ed6ba9853622274c23725143bc9ae539877cf480233cabd5ddcfce5418f8a023bdab9e25fe9a1ce467f9b6047bb157f05aaa409f608c7706c0b8
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ Gemfile.lock
4
+ /.config
5
+ /coverage/
6
+ /InstalledFiles
7
+ /pkg/
8
+ /spec/reports/
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ ## Documentation cache and generated files:
14
+ /.yardoc/
15
+ /_yardoc/
16
+ /doc/
17
+ /rdoc/
18
+ api.txt
19
+
20
+ ## Environment normalisation:
21
+ /.bundle/
22
+ /lib/bundler/man/
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --require spec_helper
2
+ --color
3
+ --format doc
4
+ -b
data/.travis.yml ADDED
@@ -0,0 +1,15 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.0
4
+ - 2.0.0
5
+ - ruby-head
6
+ before_install:
7
+ - wget https://github.com/jedisct1/libsodium/releases/download/0.7.0/libsodium-0.7.0.tar.gz
8
+ - tar xzvf libsodium-0.7.0.tar.gz
9
+ - cd libsodium-0.7.0
10
+ - ./configure && make && make check && sudo make install
11
+ - sudo ldconfig
12
+ - cd ..
13
+ install: "bundle install"
14
+ script: rspec .
15
+ after_success: coveralls
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'coveralls', require: false
4
+
5
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2014 LocalMed, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # Macaroons
2
+ [![Build Status](https://travis-ci.org/localmed/ruby-macaroons.svg?branch=master)](https://travis-ci.org/localmed/ruby-macaroons)
3
+ [![Coverage Status](https://img.shields.io/coveralls/localmed/ruby-macaroons.svg)](https://coveralls.io/r/localmed/ruby-macaroons?branch=master)
4
+
5
+ This is a Ruby implementation of Macaroons. It is still under active development but is in a useable state - please report any bugs in the issue tracker.
6
+
7
+ ## What is a Macaroon?
8
+ Macaroons, like cookies, are a form of bearer credential. Unlike opaque tokens, macaroons embed *caveats* that define specific authorization requirements for the *target service*, the service that issued the root macaroon and which is capable of verifying the integrity of macaroons it recieves.
9
+
10
+ Macaroons allow for delegation and attenuation of authorization. They are simple and fast to verify, and decouple authorization policy from the enforcement of that policy.
11
+
12
+ Simple examples are outlined below. For more in-depth examples check out the [functional tests](https://github.com/localmed/ruby-macaroons/blob/master/spec/integration_spec.rb) and [references](#references).
13
+
14
+ ## Installing
15
+
16
+ Macaroons requires a sodium library like libsodium or tweetnacl to be installed on the host system.
17
+
18
+ To install [libsodium](https://github.com/jedisct1/libsodium):
19
+
20
+ For OS X users, libsodium is available via homebrew and can be installed with:
21
+
22
+ brew install libsodium
23
+
24
+ For other systems, please see the [libsodium documentation](http://doc.libsodium.org/).
25
+
26
+ ### Macaroons gem
27
+
28
+ Once you have libsodium installed, add this line to your application's Gemfile:
29
+
30
+ gem 'macaroons'
31
+
32
+ And then execute:
33
+
34
+ $ bundle
35
+
36
+ Or install it manually:
37
+
38
+ $ gem install macaroons
39
+
40
+ Inside of your Ruby program:
41
+
42
+ require 'macaroons'
43
+
44
+ ## Quickstart
45
+
46
+ key => Very secret key used to sign the macaroon
47
+ identifier => An identifier, to remind you which key was used to sign the macaroon
48
+ location => The location at which the macaroon is created
49
+
50
+ # Construct a Macaroon.
51
+ m = Macaroon.new(key: key, identifier: identifier, location: 'http://foo.com')
52
+
53
+ # Add first party caveat
54
+ m.add_first_party_caveat('caveat_1')
55
+
56
+ # List all first party caveats
57
+ m.first_party_caveats
58
+
59
+ # Add third party caveat
60
+ m.add_third_party_caveat('caveat_key', 'caveat_id', 'http://foo.com')
61
+
62
+ # List all third party caveats
63
+ m.third_party_caveats
64
+
65
+ ## Example with first- and third-party caveats
66
+
67
+ ```ruby
68
+
69
+ # Create macaroon. Sign with a key and identifier (a way to remember which key was used)
70
+ m = Macaroon.new(
71
+ location: 'http://mybank/',
72
+ identifier: 'we used our other secret key',
73
+ key: 'this is a different super-secret key; never use the same secret twice'
74
+ )
75
+
76
+ # Add a first party caveat
77
+ m.add_first_party_caveat('account = 3735928559')
78
+
79
+ # Add a third party caveat
80
+ caveat_key = '4; guaranteed random by a fair toss of the dice'
81
+ identifier = 'this was how we remind auth of key/pred'
82
+ m.add_third_party_caveat(caveat_key, identifier, 'http://auth.mybank/')
83
+
84
+ # User collects a discharge macaroon (likely from a separate service), that proves the claims in the third-party caveat and which may add additional caveats of its own
85
+ discharge = Macaroon.new(
86
+ location: 'http://auth.mybank/',
87
+ identifier: identifier,
88
+ caveat: Ocaveat_key
89
+ )
90
+ discharge.add_first_party_caveat('time < 2015-01-01T00:00')
91
+
92
+ # discharge macaroons are bound to the root macaroon so they cannot be reused
93
+ protected_discharge = m.prepare_for_request(discharge)
94
+
95
+ # The user sends their macaroon along with their discharge macaroons, and we verify them
96
+ v = Macaroon::Verifier.new()
97
+ v.satisfy_exact('account = 3735928559')
98
+ v.satisfy_exact('time < 2015-01-01T00:00')
99
+ verified = v.verify(
100
+ macaroon: m,
101
+ key: 'this is a different super-secret key; never use the same secret twice',
102
+ discharge_macaroons: [protected_discharge]
103
+ )
104
+ ```
105
+
106
+ ## More Macaroons
107
+
108
+ [PyMacaroons](https://github.com/ecordell/pymacaroons) is available for Python. PyMacaroons and Ruby-Macaroons are completely compatible (they can be used interchangibly within the same target service).
109
+
110
+ The [libmacaroons library](https://github.com/rescrv/libmacaroons) comes with Python and Go bindings.
111
+
112
+ PyMacaroons, libmacaroons, and Ruby-Macaroons all use the same underlying cryptographic library (libsodium).
113
+
114
+ ## References
115
+
116
+ - [The Macaroon Paper](http://research.google.com/pubs/pub41892.html)
117
+ - [Mozilla Macaroon Tech Talk](https://air.mozilla.org/macaroons-cookies-with-contextual-caveats-for-decentralized-authorization-in-the-cloud/)
118
+ - [libmacaroons](https://github.com/rescrv/libmacaroons)
119
+ - [PyMacaroons](https://github.com/ecordell/pymacaroons)
120
+ - [libnacl](https://github.com/saltstack/libnacl)
@@ -0,0 +1,26 @@
1
+ module Macaroons
2
+ class Caveat
3
+ def initialize(caveat_id, verification_id=nil, caveat_location=nil)
4
+ @caveat_id = caveat_id
5
+ @verification_id = verification_id
6
+ @caveat_location = caveat_location
7
+ end
8
+
9
+ attr_accessor :caveat_id
10
+ attr_accessor :verification_id
11
+ attr_accessor :caveat_location
12
+
13
+ def first_party?
14
+ verification_id.nil?
15
+ end
16
+
17
+ def third_party?
18
+ !first_party?
19
+ end
20
+
21
+ def to_h
22
+ {'cid' => @caveat_id, 'vid' => @verification_id, 'cl' => @caveat_location}
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ class SignatureMismatchError < StandardError
2
+ end
3
+
4
+ class CaveatUnsatisfiedError < StandardError
5
+ end
@@ -0,0 +1,63 @@
1
+ require 'macaroons/raw_macaroon'
2
+
3
+ module Macaroons
4
+ class Macaroon
5
+ def initialize(key: nil, identifier: nil, location: nil, raw_macaroon: nil)
6
+ @raw_macaroon = raw_macaroon || RawMacaroon.new(key: key, identifier: identifier, location: location)
7
+ end
8
+
9
+ def identifier
10
+ @raw_macaroon.identifier
11
+ end
12
+
13
+ def location
14
+ @raw_macaroon.location
15
+ end
16
+
17
+ def signature
18
+ @raw_macaroon.signature
19
+ end
20
+
21
+ def caveats
22
+ @raw_macaroon.caveats
23
+ end
24
+
25
+ def self.from_binary(serialized)
26
+ raw_macaroon = RawMacaroon.from_binary(serialized: serialized)
27
+ macaroon = Macaroons::Macaroon.new(raw_macaroon: raw_macaroon)
28
+ end
29
+
30
+ def self.from_json(serialized)
31
+ raw_macaroon = RawMacaroon.from_json(serialized: serialized)
32
+ macaroon = Macaroons::Macaroon.new(raw_macaroon: raw_macaroon)
33
+ end
34
+
35
+ def serialize
36
+ @raw_macaroon.serialize()
37
+ end
38
+
39
+ def serialize_json
40
+ @raw_macaroon.serialize_json()
41
+ end
42
+
43
+ def add_first_party_caveat(predicate)
44
+ @raw_macaroon.add_first_party_caveat(predicate)
45
+ end
46
+
47
+ def first_party_caveats
48
+ caveats.select(&:first_party?)
49
+ end
50
+
51
+ def add_third_party_caveat(caveat_key, caveat_id, caveat_location)
52
+ @raw_macaroon.add_third_party_caveat(caveat_key, caveat_id, caveat_location)
53
+ end
54
+
55
+ def third_party_caveats
56
+ caveats.select(&:third_party?)
57
+ end
58
+
59
+ def prepare_for_request(macaroon)
60
+ @raw_macaroon.prepare_for_request(macaroon)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,90 @@
1
+ require 'base64'
2
+
3
+ require 'rbnacl'
4
+
5
+ require 'macaroons/caveat'
6
+ require 'macaroons/utils'
7
+ require 'macaroons/serializers/binary'
8
+ require 'macaroons/serializers/json'
9
+
10
+ module Macaroons
11
+ class RawMacaroon
12
+
13
+ def initialize(key: nil, identifier: nil, location: nil)
14
+ if key.nil? || identifier.nil? || location.nil?
15
+ raise ArgumentError, 'Must provide all three: (key, id, location)'
16
+ end
17
+
18
+ @key = key
19
+ @identifier = identifier
20
+ @location = location
21
+ @signature = create_initial_macaroon_signature(key, identifier)
22
+ @caveats = []
23
+ end
24
+
25
+ def self.from_binary(serialized: serialized)
26
+ Macaroons::BinarySerializer.new().deserialize(serialized)
27
+ end
28
+
29
+ def self.from_json(serialized: serialized)
30
+ Macaroons::JsonSerializer.new().deserialize(serialized)
31
+ end
32
+
33
+ attr_reader :identifier
34
+ attr_reader :key
35
+ attr_reader :location
36
+ attr_accessor :caveats
37
+ attr_accessor :signature
38
+
39
+ def signature
40
+ Utils.hexlify(@signature).downcase
41
+ end
42
+
43
+ def add_first_party_caveat(predicate)
44
+ caveat = Caveat.new(predicate)
45
+ @caveats << caveat
46
+ @signature = Utils.sign_first_party_caveat(@signature, predicate)
47
+ end
48
+
49
+ def add_third_party_caveat(caveat_key, caveat_id, caveat_location)
50
+ derived_caveat_key = Utils.truncate_or_pad(Utils.hmac('macaroons-key-generator', caveat_key))
51
+ truncated_or_padded_signature = Utils.truncate_or_pad(@signature)
52
+ box = RbNaCl::SimpleBox.from_secret_key(truncated_or_padded_signature)
53
+ ciphertext = box.encrypt(derived_caveat_key)
54
+ verification_id = Base64.strict_encode64(ciphertext)
55
+ caveat = Caveat.new(caveat_id, verification_id, caveat_location)
56
+ @caveats << caveat
57
+ @signature = Utils.sign_third_party_caveat(@signature, verification_id, caveat_id)
58
+ end
59
+
60
+ def serialize
61
+ Macaroons::BinarySerializer.new().serialize(self)
62
+ end
63
+
64
+ def serialize_json
65
+ Macaroons::JsonSerializer.new().serialize(self)
66
+ end
67
+
68
+ def prepare_for_request(macaroon)
69
+ bound_macaroon = Marshal.load( Marshal.dump( macaroon ) )
70
+ raw = bound_macaroon.instance_variable_get(:@raw_macaroon)
71
+ raw.signature = bind_signature(macaroon.signature)
72
+ bound_macaroon
73
+ end
74
+
75
+ def bind_signature(signature)
76
+ key = Utils.truncate_or_pad('0')
77
+ hash1 = Utils.hmac(key, Utils.unhexlify(self.signature))
78
+ hash2 = Utils.hmac(key, Utils.unhexlify(signature))
79
+ Utils.hmac(key, hash1 + hash2)
80
+ end
81
+
82
+ private
83
+
84
+ def create_initial_macaroon_signature(key, identifier)
85
+ derived_key = Utils.generate_derived_key(key)
86
+ Utils.hmac(derived_key, identifier)
87
+ end
88
+
89
+ end
90
+ end
@@ -0,0 +1,13 @@
1
+ module Macaroons
2
+ class BaseSerializer
3
+
4
+ def serialize(macaroon)
5
+ raise NotImplementedError
6
+ end
7
+
8
+ def deserialize(serialized)
9
+ raise NotImplementedError
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,89 @@
1
+ require 'base64'
2
+
3
+ require 'macaroons/serializers/base'
4
+
5
+ module Macaroons
6
+ class BinarySerializer < BaseSerializer
7
+ PACKET_PREFIX_LENGTH = 4
8
+
9
+ def serialize(macaroon)
10
+ combined = packetize('location', macaroon.location)
11
+ combined += packetize('identifier', macaroon.identifier)
12
+
13
+ for caveat in macaroon.caveats
14
+ combined += packetize('cid', caveat.caveat_id)
15
+
16
+ if caveat.verification_id and caveat.caveat_location
17
+ combined += packetize('vid', caveat.verification_id)
18
+ combined += packetize('cl', caveat.caveat_location)
19
+ end
20
+ end
21
+
22
+ combined += packetize(
23
+ 'signature',
24
+ Utils.unhexlify(macaroon.signature)
25
+ )
26
+ Base64.urlsafe_encode64(combined)
27
+ end
28
+
29
+ def deserialize(serialized)
30
+ caveats = []
31
+ decoded = Base64.urlsafe_decode64(serialized)
32
+
33
+ index = 0
34
+
35
+ while index < decoded.length
36
+ packet_length = decoded[index..(index + PACKET_PREFIX_LENGTH - 1)].to_i(16)
37
+ stripped_packet = decoded[index + PACKET_PREFIX_LENGTH..(index + packet_length - 2)]
38
+
39
+ key, value = depacketize(stripped_packet)
40
+
41
+ case key
42
+ when 'location'
43
+ location = value
44
+ when 'identifier'
45
+ identifier = value
46
+ when 'cid'
47
+ caveats << Caveat.new(value)
48
+ when 'vid'
49
+ caveats[-1].verification_id = value
50
+ when 'cl'
51
+ caveats[-1].caveat_location = value
52
+ when 'signature'
53
+ signature = value
54
+ else
55
+ raise KeyError, 'Invalid key in binary macaroon. Macaroon may be corrupted.'
56
+ end
57
+
58
+ index = index + packet_length
59
+ end
60
+ macaroon = Macaroons::RawMacaroon.new(key: 'no_key', identifier: identifier, location: location)
61
+ macaroon.caveats = caveats
62
+ macaroon.signature = signature
63
+ macaroon
64
+ end
65
+
66
+ private
67
+
68
+ def packetize(key, data)
69
+ # The 2 covers the space and the newline
70
+ packet_size = PACKET_PREFIX_LENGTH + 2 + key.length + data.length
71
+ if packet_size > 65535
72
+ # Due to packet structure, length of packet must be less than 0xFFFF
73
+ raise ArgumentError, 'Data is too long for a binary packet.'
74
+ end
75
+ packet_size_hex = packet_size.to_s(16)
76
+ header = packet_size_hex.to_s.rjust(4, '0')
77
+ packet_content = "#{key} #{data}\n"
78
+ packet = "#{header}#{packet_content}"
79
+ packet
80
+ end
81
+
82
+ def depacketize(packet)
83
+ key = packet.split(" ")[0]
84
+ value = packet[key.length + 1..-1]
85
+ [key, value]
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,28 @@
1
+ require 'json'
2
+
3
+ module Macaroons
4
+ class JsonSerializer
5
+
6
+ def serialize(macaroon)
7
+ serialized = {
8
+ location: macaroon.location,
9
+ identifier: macaroon.identifier,
10
+ caveats: macaroon.caveats.map!(&:to_h),
11
+ signature: macaroon.signature
12
+ }
13
+ return serialized.to_json
14
+ end
15
+
16
+ def deserialize(serialized)
17
+ deserialized = JSON.parse(serialized)
18
+ macaroon = Macaroons::RawMacaroon.new(key: 'no_key', identifier: deserialized['identifier'], location: deserialized['location'])
19
+ deserialized['caveats'].each do |c|
20
+ caveat = Macaroons::Caveat.new(c['cid'], c['vid'], c['cl'])
21
+ macaroon.caveats << c
22
+ end
23
+ macaroon.signature = Utils.unhexlify(deserialized['signature'])
24
+ macaroon
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,49 @@
1
+ require 'openssl'
2
+
3
+ module Macaroons
4
+ module Utils
5
+
6
+ def self.convert_to_bytes(string)
7
+ string.encode('us-ascii') unless string.nil?
8
+ end
9
+
10
+ def self.hexlify(value)
11
+ value.unpack('C*').map { |byte| '%02X' % byte }.join('')
12
+ end
13
+
14
+ def self.unhexlify(value)
15
+ [value].pack('H*')
16
+ end
17
+
18
+ def self.truncate_or_pad(string, size=nil)
19
+ size = size.nil? ? 32 : size
20
+ if string.length > size
21
+ string[0, size]
22
+ elsif string.length > size
23
+ string + '\0'*(size-string.length)
24
+ else
25
+ string
26
+ end
27
+ end
28
+
29
+ def self.hmac(key, data, digest=nil)
30
+ digest = OpenSSL::Digest.new('sha256') if digest.nil?
31
+ OpenSSL::HMAC.digest(digest, key, data)
32
+ end
33
+
34
+ def self.sign_first_party_caveat(signature, predicate)
35
+ Utils.hmac(signature, predicate)
36
+ end
37
+
38
+ def self.sign_third_party_caveat(signature, verification_id, caveat_id)
39
+ verification_id_hash = Utils.hmac(signature, verification_id)
40
+ caveat_id_hash = Utils.hmac(signature, caveat_id)
41
+ combined = verification_id_hash + caveat_id_hash
42
+ Utils.hmac(signature, combined)
43
+ end
44
+
45
+ def self.generate_derived_key(key)
46
+ Utils.hmac('macaroons-key-generator', key)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,116 @@
1
+ require 'rbnacl'
2
+
3
+ require 'macaroons/errors'
4
+
5
+ module Macaroons
6
+ class Verifier
7
+ attr_accessor :predicates
8
+ attr_accessor :callbacks
9
+
10
+ def initialize
11
+ @predicates = []
12
+ @callbacks = []
13
+ @calculated_signature = nil
14
+ end
15
+
16
+ def satisfy_exact(predicate)
17
+ raise ArgumentError, 'Must provide predicate' unless predicate
18
+ @predicates << predicate
19
+ end
20
+
21
+ def satisfy_general(callback = nil, &block)
22
+ raise ArgumentError, 'Must provide callback or block' unless callback || block_given?
23
+ callback = block if block_given?
24
+ @callbacks << callback
25
+ end
26
+
27
+ def verify(macaroon: nil, key: nil, discharge_macaroons: nil)
28
+ raise ArgumentError, 'Macaroon and Key required' if macaroon.nil? || key.nil?
29
+ key = Utils.generate_derived_key(key)
30
+ verify_discharge(root: macaroon, macaroon: macaroon, key: key, discharge_macaroons:discharge_macaroons)
31
+ end
32
+
33
+ def verify_discharge(root: root, macaroon: macaroon, key: key, discharge_macaroons: [])
34
+ @calculated_signature = Utils.hmac(key, macaroon.identifier)
35
+
36
+ verify_caveats(macaroon, discharge_macaroons)
37
+
38
+ if root != macaroon
39
+ raw = root.instance_variable_get(:@raw_macaroon)
40
+ @calculated_signature = raw.bind_signature(Utils.hexlify(@calculated_signature).downcase)
41
+ end
42
+
43
+ raise SignatureMismatchError, 'Signatures do not match.' unless signatures_match(Utils.unhexlify(macaroon.signature), @calculated_signature)
44
+
45
+ return true
46
+ end
47
+
48
+ private
49
+
50
+ def verify_caveats(macaroon, discharge_macaroons)
51
+ for caveat in macaroon.caveats
52
+ if caveat.first_party?
53
+ caveat_met = verify_first_party_caveat(caveat)
54
+ else
55
+ caveat_met = verify_third_party_caveat(caveat, macaroon, discharge_macaroons)
56
+ end
57
+ raise CaveatUnsatisfiedError, "Caveat not met. Unable to satisfy: #{caveat.caveat_id}" unless caveat_met
58
+ end
59
+ end
60
+
61
+ def verify_first_party_caveat(caveat)
62
+ caveat_met = false
63
+ if @predicates.include? caveat.caveat_id
64
+ caveat_met = true
65
+ else
66
+ @callbacks.each do |callback|
67
+ caveat_met = true if callback.call(caveat.caveat_id)
68
+ end
69
+ end
70
+ @calculated_signature = Utils.sign_first_party_caveat(@calculated_signature, caveat.caveat_id) if caveat_met
71
+ return caveat_met
72
+ end
73
+
74
+ def verify_third_party_caveat(caveat, root_macaroon, discharge_macaroons)
75
+ caveat_met = false
76
+
77
+ caveat_macaroon = discharge_macaroons.find { |m| m.identifier == caveat.caveat_id }
78
+ raise CaveatUnsatisfiedError, "Caveat not met. No discharge macaroon found for identifier: #{caveat.caveat_id}" unless caveat_macaroon
79
+
80
+ caveat_key = extract_caveat_key(@calculated_signature, caveat)
81
+ caveat_macaroon_verifier = Verifier.new()
82
+ caveat_macaroon_verifier.predicates = @predicates
83
+ caveat_macaroon_verifier.callbacks = @callbacks
84
+
85
+ caveat_met = caveat_macaroon_verifier.verify_discharge(
86
+ root: root_macaroon,
87
+ macaroon: caveat_macaroon,
88
+ key: caveat_key,
89
+ discharge_macaroons: discharge_macaroons
90
+ )
91
+ if caveat_met
92
+ @calculated_signature = Utils.sign_third_party_caveat(@calculated_signature, caveat.verification_id, caveat.caveat_id)
93
+ end
94
+ return caveat_met
95
+ end
96
+
97
+ def extract_caveat_key(signature, caveat)
98
+ key = Utils.truncate_or_pad(signature)
99
+ box = RbNaCl::SimpleBox.from_secret_key(key)
100
+ decoded_vid = Base64.strict_decode64(caveat.verification_id)
101
+ box.decrypt(decoded_vid)
102
+ end
103
+
104
+ def signatures_match(a, b)
105
+ # Constant time compare, taken from Rack
106
+ return false unless a.bytesize == b.bytesize
107
+
108
+ l = a.unpack("C*")
109
+
110
+ r, i = 0, -1
111
+ b.each_byte { |v| r |= v ^ l[i+=1] }
112
+ r == 0
113
+ end
114
+
115
+ end
116
+ end
@@ -0,0 +1,3 @@
1
+ module Macaroons
2
+ VERSION = '0.4.0'
3
+ end
data/lib/macaroons.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'macaroons/macaroons'
2
+ require 'macaroons/verifier'
3
+
4
+ module Macaroon
5
+ class << self
6
+ def new(location: location, identifier: identifier, key: key)
7
+ Macaroons::Macaroon.new(location:location, identifier:identifier, key:key)
8
+ end
9
+
10
+ def from_binary(serialized)
11
+ Macaroons::Macaroon.from_binary(serialized)
12
+ end
13
+
14
+ def from_json(serialized)
15
+ Macaroons::Macaroon.from_json(serialized)
16
+ end
17
+ end
18
+
19
+ class Verifier
20
+ def self.new()
21
+ Macaroons::Verifier.new()
22
+ end
23
+ end
24
+ end
data/macaroons.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'macaroons/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'macaroons'
8
+ spec.version = Macaroons::VERSION
9
+ spec.authors = ["Evan Cordell", "Peter Browne", "Joel James"]
10
+ spec.email = ["ecordell@localmed.com", "pete@localmed.com", "joel.james@localmed.com"]
11
+ spec.summary = "Macaroons library in Ruby"
12
+ spec.description = "Macaroons library in Ruby"
13
+
14
+ spec.files = `git ls-files`.split($/)
15
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
+ spec.require_paths = ["lib"]
18
+ spec.required_ruby_version = "~> 2.0"
19
+ spec.add_dependency "json"
20
+ spec.add_dependency "rbnacl", "~> 3.1.2"
21
+
22
+ spec.add_development_dependency "bundler", "> 1.3"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "rspec", "~> 3.1.0"
25
+ spec.add_development_dependency "pry"
26
+ spec.add_development_dependency "pry-stack_explorer"
27
+ spec.add_development_dependency "rspec_junit_formatter"
28
+ end
@@ -0,0 +1,262 @@
1
+ require 'spec_helper'
2
+ require 'macaroons'
3
+ require 'macaroons/errors'
4
+
5
+ describe 'Macaroon' do
6
+ context 'without caveats' do
7
+ it 'should have correct signature' do
8
+ m = Macaroon.new(
9
+ location: 'http://mybank/',
10
+ identifier: 'we used our secret key',
11
+ key: 'this is our super secret key; only we should know it'
12
+ )
13
+ expect(m.signature).to eql('e3d9e02908526c4c0039ae15114115d97fdd68bf2ba379b342aaf0f617d0552f')
14
+ end
15
+ end
16
+
17
+ context 'with first party caveat' do
18
+ it 'should have correct signature' do
19
+ m = Macaroon.new(
20
+ location: 'http://mybank/',
21
+ identifier: 'we used our secret key',
22
+ key: 'this is our super secret key; only we should know it'
23
+ )
24
+ m.add_first_party_caveat('test = caveat')
25
+ expect(m.signature).to eql('197bac7a044af33332865b9266e26d493bdd668a660e44d88ce1a998c23dbd67')
26
+ end
27
+ end
28
+
29
+ context 'when serilizing as binary' do
30
+ it 'should serialize properly' do
31
+ m = Macaroon.new(
32
+ location: 'http://mybank/',
33
+ identifier: 'we used our secret key',
34
+ key: 'this is our super secret key; only we should know it'
35
+ )
36
+ m.add_first_party_caveat('test = caveat')
37
+ expect(m.serialize()).to eql('MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMjZpZGVudGlmaWVyIHdlIHVzZWQgb3VyIHNlY3JldCBrZXkKMDAxNmNpZCB0ZXN0ID0gY2F2ZWF0CjAwMmZzaWduYXR1cmUgGXusegRK8zMyhluSZuJtSTvdZopmDkTYjOGpmMI9vWcK')
38
+ end
39
+ end
40
+
41
+ context 'when deserializing binary' do
42
+ it 'should deserialize properly' do
43
+ m = Macaroon.from_binary(
44
+ 'MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMjZpZGVudGlmaWVyIHdlIHVzZWQgb3VyIHNlY3JldCBrZXkKMDAxNmNpZCB0ZXN0ID0gY2F2ZWF0CjAwMmZzaWduYXR1cmUgGXusegRK8zMyhluSZuJtSTvdZopmDkTYjOGpmMI9vWcK'
45
+ )
46
+ expect(m.signature).to eql('197bac7a044af33332865b9266e26d493bdd668a660e44d88ce1a998c23dbd67')
47
+ end
48
+ end
49
+
50
+ context 'when serilizing as json' do
51
+ it 'should serialize properly' do
52
+ m = Macaroon.new(
53
+ location: 'http://mybank/',
54
+ identifier: 'we used our secret key',
55
+ key: 'this is our super secret key; only we should know it'
56
+ )
57
+ m.add_first_party_caveat('test = caveat')
58
+ expect(m.serialize_json()).to eql('{"location":"http://mybank/","identifier":"we used our secret key","caveats":[{"cid":"test = caveat","vid":null,"cl":null}],"signature":"197bac7a044af33332865b9266e26d493bdd668a660e44d88ce1a998c23dbd67"}')
59
+ end
60
+ end
61
+
62
+ context 'when deserializing json' do
63
+ it 'should deserialize properly' do
64
+ m = Macaroon.from_json(
65
+ '{"location":"http://mybank/","identifier":"we used our secret key","caveats":[{"cid":"test = caveat","vid":null,"cl":null}],"signature":"197bac7a044af33332865b9266e26d493bdd668a660e44d88ce1a998c23dbd67"}'
66
+ )
67
+ expect(m.signature).to eql('197bac7a044af33332865b9266e26d493bdd668a660e44d88ce1a998c23dbd67')
68
+ end
69
+ end
70
+
71
+ context 'when serializing/deserializing binary with first and third caveats' do
72
+ it 'should serialize/deserialize properly' do
73
+ m = Macaroon.new(
74
+ location: 'http://mybank/',
75
+ identifier: 'we used our other secret key',
76
+ key: 'this is a different super-secret key; never use the same secret twice'
77
+ )
78
+ m.add_first_party_caveat('account = 3735928559')
79
+ caveat_key = '4; guaranteed random by a fair toss of the dice'
80
+ identifier = 'this was how we remind auth of key/pred'
81
+ m.add_third_party_caveat(caveat_key, identifier, 'http://auth.mybank/')
82
+ n = Macaroon.from_binary(m.serialize())
83
+ expect(m.signature).to eql(n.signature)
84
+ end
85
+ end
86
+
87
+ context 'when serializing/deserializing json with first and third caveats' do
88
+ it 'should serialize/deserialize properly' do
89
+ m = Macaroon.new(
90
+ location: 'http://mybank/',
91
+ identifier: 'we used our other secret key',
92
+ key: 'this is a different super-secret key; never use the same secret twice'
93
+ )
94
+ m.add_first_party_caveat('account = 3735928559')
95
+ caveat_key = '4; guaranteed random by a fair toss of the dice'
96
+ identifier = 'this was how we remind auth of key/pred'
97
+ m.add_third_party_caveat(caveat_key, identifier, 'http://auth.mybank/')
98
+ n = Macaroon.from_json(m.serialize_json())
99
+ expect(m.signature).to eql(n.signature)
100
+ end
101
+ end
102
+
103
+ context 'when preparing a macaroon for request' do
104
+ it 'should bind the signature to the root' do
105
+ m = Macaroon.new(
106
+ location: 'http://mybank/',
107
+ identifier: 'we used our other secret key',
108
+ key: 'this is a different super-secret key; never use the same secret twice'
109
+ )
110
+ m.add_first_party_caveat('account = 3735928559')
111
+ caveat_key = '4; guaranteed random by a fair toss of the dice'
112
+ identifier = 'this was how we remind auth of key/pred'
113
+ m.add_third_party_caveat(caveat_key, identifier, 'http://auth.mybank/')
114
+
115
+ discharge = Macaroon.new(
116
+ location: 'http://auth.mybank/',
117
+ identifier: identifier,
118
+ key: caveat_key
119
+ )
120
+ discharge.add_first_party_caveat('time < 2015-01-01T00:00')
121
+ protected_discharge = m.prepare_for_request(discharge)
122
+
123
+ expect(discharge.signature).not_to eql(protected_discharge.signature)
124
+ end
125
+ end
126
+ end
127
+
128
+ describe 'Verifier' do
129
+ context 'verifying first party exact caveats' do
130
+ before(:all) do
131
+ @m = Macaroon.new(
132
+ location: 'http://mybank/',
133
+ identifier: 'we used our secret key',
134
+ key: 'this is our super secret key; only we should know it'
135
+ )
136
+ @m.add_first_party_caveat('test = caveat')
137
+ end
138
+
139
+ context 'all caveats met' do
140
+ it 'should verify the macaroon' do
141
+ v = Macaroon::Verifier.new()
142
+ v.satisfy_exact('test = caveat')
143
+ verified = v.verify(
144
+ macaroon: @m,
145
+ key: 'this is our super secret key; only we should know it'
146
+ )
147
+ expect(verified).to be(true)
148
+ end
149
+ end
150
+ context 'not all caveats met' do
151
+ it 'should raise an error' do
152
+ v = Macaroon::Verifier.new()
153
+ expect {
154
+ v.verify(
155
+ macaroon: @m,
156
+ key: 'this is our super secret key; only we should know it'
157
+ )
158
+ }.to raise_error
159
+ end
160
+ end
161
+ end
162
+
163
+ context 'verifying first party general caveats' do
164
+ before(:all) do
165
+ @m = Macaroon.new(
166
+ location: 'http://mybank/',
167
+ identifier: 'we used our secret key',
168
+ key: 'this is our super secret key; only we should know it'
169
+ )
170
+ @m.add_first_party_caveat('general caveat')
171
+ end
172
+
173
+ context 'all caveats met' do
174
+ it 'should verify the macaroon' do
175
+ v = Macaroon::Verifier.new()
176
+ v.satisfy_general { |predicate| predicate == 'general caveat' }
177
+ verified = v.verify(
178
+ macaroon: @m,
179
+ key: 'this is our super secret key; only we should know it'
180
+ )
181
+ expect(verified).to be(true)
182
+ end
183
+ end
184
+ context 'not all caveats met' do
185
+ it 'should raise an error' do
186
+ v = Macaroon::Verifier.new()
187
+ v.satisfy_general { |predicate| predicate == 'unmet' }
188
+ expect {
189
+ v.verify(
190
+ macaroon: @m,
191
+ key: 'this is our super secret key; only we should know it'
192
+ )
193
+ }.to raise_error
194
+ end
195
+ end
196
+ end
197
+
198
+ context 'verifying third party caveats' do
199
+ before(:all) do
200
+ @m = Macaroon.new(
201
+ location: 'http://mybank/',
202
+ identifier: 'we used our other secret key',
203
+ key: 'this is a different super-secret key; never use the same secret twice'
204
+ )
205
+ @m.add_first_party_caveat('account = 3735928559')
206
+ caveat_key = '4; guaranteed random by a fair toss of the dice'
207
+ identifier = 'this was how we remind auth of key/pred'
208
+ @m.add_third_party_caveat(caveat_key, identifier, 'http://auth.mybank/')
209
+
210
+ discharge = Macaroon.new(
211
+ location: 'http://auth.mybank/',
212
+ identifier: identifier,
213
+ key: caveat_key
214
+ )
215
+ discharge.add_first_party_caveat('time < 2015-01-01T00:00')
216
+ @protected_discharge = @m.prepare_for_request(discharge)
217
+ end
218
+
219
+ context 'all caveats met and discharges provided' do
220
+ it 'should verify the macaroon' do
221
+ v = Macaroon::Verifier.new()
222
+ v.satisfy_exact('account = 3735928559')
223
+ v.satisfy_exact('time < 2015-01-01T00:00')
224
+ verified = v.verify(
225
+ macaroon: @m,
226
+ key: 'this is a different super-secret key; never use the same secret twice',
227
+ discharge_macaroons: [@protected_discharge]
228
+ )
229
+ end
230
+ end
231
+
232
+ context 'not all caveats met' do
233
+ it 'should raise an error' do
234
+ v = Macaroon::Verifier.new()
235
+ v.satisfy_exact('account = 3735928559')
236
+ expect {
237
+ v.verify(
238
+ macaroon: @m,
239
+ key: 'this is a different super-secret key; never use the same secret twice',
240
+ discharge_macaroons: [@protected_discharge]
241
+ )
242
+ }.to raise_error
243
+ end
244
+ end
245
+
246
+ context 'not all discharges provided' do
247
+ it 'should raise an error' do
248
+ v = Macaroon::Verifier.new()
249
+ v.satisfy_exact('account = 3735928559')
250
+ v.satisfy_exact('time < 2015-01-01T00:00')
251
+ expect {
252
+ v.verify(
253
+ macaroon: @m,
254
+ key: 'this is a different super-secret key; never use the same secret twice',
255
+ discharge_macaroons: []
256
+ )
257
+ }.to raise_error
258
+ end
259
+ end
260
+ end
261
+
262
+ end
@@ -0,0 +1,8 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
4
+ RSpec.configure do |config|
5
+ config.expect_with :rspec do |c|
6
+ c.syntax = :expect
7
+ end
8
+ end
metadata ADDED
@@ -0,0 +1,181 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: macaroons
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Evan Cordell
8
+ - Peter Browne
9
+ - Joel James
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2014-10-17 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: json
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - '>='
27
+ - !ruby/object:Gem::Version
28
+ version: '0'
29
+ - !ruby/object:Gem::Dependency
30
+ name: rbnacl
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ~>
34
+ - !ruby/object:Gem::Version
35
+ version: 3.1.2
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ~>
41
+ - !ruby/object:Gem::Version
42
+ version: 3.1.2
43
+ - !ruby/object:Gem::Dependency
44
+ name: bundler
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - '>'
48
+ - !ruby/object:Gem::Version
49
+ version: '1.3'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - '>'
55
+ - !ruby/object:Gem::Version
56
+ version: '1.3'
57
+ - !ruby/object:Gem::Dependency
58
+ name: rake
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ type: :development
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ - !ruby/object:Gem::Dependency
72
+ name: rspec
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 3.1.0
78
+ type: :development
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ~>
83
+ - !ruby/object:Gem::Version
84
+ version: 3.1.0
85
+ - !ruby/object:Gem::Dependency
86
+ name: pry
87
+ requirement: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - '>='
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ type: :development
93
+ prerelease: false
94
+ version_requirements: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ - !ruby/object:Gem::Dependency
100
+ name: pry-stack_explorer
101
+ requirement: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ type: :development
107
+ prerelease: false
108
+ version_requirements: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - '>='
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ - !ruby/object:Gem::Dependency
114
+ name: rspec_junit_formatter
115
+ requirement: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ type: :development
121
+ prerelease: false
122
+ version_requirements: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - '>='
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ description: Macaroons library in Ruby
128
+ email:
129
+ - ecordell@localmed.com
130
+ - pete@localmed.com
131
+ - joel.james@localmed.com
132
+ executables: []
133
+ extensions: []
134
+ extra_rdoc_files: []
135
+ files:
136
+ - .gitignore
137
+ - .rspec
138
+ - .travis.yml
139
+ - Gemfile
140
+ - LICENSE
141
+ - README.md
142
+ - lib/macaroons.rb
143
+ - lib/macaroons/caveat.rb
144
+ - lib/macaroons/errors.rb
145
+ - lib/macaroons/macaroons.rb
146
+ - lib/macaroons/raw_macaroon.rb
147
+ - lib/macaroons/serializers/base.rb
148
+ - lib/macaroons/serializers/binary.rb
149
+ - lib/macaroons/serializers/json.rb
150
+ - lib/macaroons/utils.rb
151
+ - lib/macaroons/verifier.rb
152
+ - lib/macaroons/version.rb
153
+ - macaroons.gemspec
154
+ - spec/integration_spec.rb
155
+ - spec/spec_helper.rb
156
+ homepage:
157
+ licenses: []
158
+ metadata: {}
159
+ post_install_message:
160
+ rdoc_options: []
161
+ require_paths:
162
+ - lib
163
+ required_ruby_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ~>
166
+ - !ruby/object:Gem::Version
167
+ version: '2.0'
168
+ required_rubygems_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - '>='
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ requirements: []
174
+ rubyforge_project:
175
+ rubygems_version: 2.2.2
176
+ signing_key:
177
+ specification_version: 4
178
+ summary: Macaroons library in Ruby
179
+ test_files:
180
+ - spec/integration_spec.rb
181
+ - spec/spec_helper.rb