macaroons 0.4.0

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 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