kransekake 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +195 -0
- data/Rakefile +9 -0
- data/lib/kransekake/bundle.rb +27 -0
- data/lib/kransekake/caveat.rb +43 -0
- data/lib/kransekake/caveats.rb +29 -0
- data/lib/kransekake/crypto.rb +50 -0
- data/lib/kransekake/hex_display.rb +12 -0
- data/lib/kransekake/serializer/reader.rb +103 -0
- data/lib/kransekake/serializer/writer.rb +26 -0
- data/lib/kransekake/serializer.rb +64 -0
- data/lib/kransekake/trace/op.rb +34 -0
- data/lib/kransekake/trace.rb +11 -0
- data/lib/kransekake/verifier.rb +142 -0
- data/lib/kransekake/version.rb +5 -0
- data/lib/kransekake.rb +146 -0
- metadata +58 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0d88389afbc584e049d6d6e625861741bd28167c371ee950f506754c03517613
|
|
4
|
+
data.tar.gz: 856e4b4e1ec93f9a93a1bfb79ad53a2d8f7b3166595d0cf5751957a324adb8ac
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 13d63111914bcafc4de3f523a967c2ccc4143bd9f7ed03c101712d72f457b5fc82e74fae0a4b79d713f36d73c4d7159d6a00e200aa62cf4f6ea1ad8b5dfa8330
|
|
7
|
+
data.tar.gz: 3de3b29c89357829df4d148681b39a1a7930fe4a2a515ed7f9ff351a61f44b9d02e17b403a610ceb050db08487b51784209f20b2e606480c0b98de012980a719
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Shannon Skipper
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Kransekake
|
|
2
|
+
|
|
3
|
+
A pure Ruby [macaroons](https://research.google/pubs/macaroons-cookies-with-contextual-caveats-for-decentralized-authorization-in-the-cloud/) implementation. Interoperable with [go-macaroon](https://github.com/go-macaroon/macaroon) v2.
|
|
4
|
+
|
|
5
|
+
Named after the Norwegian ring cake, which is a type of macaroon.
|
|
6
|
+
|
|
7
|
+
Macaroons are bearer tokens that support attenuation (anyone can add restrictions, no one can remove them) and third-party delegation, making them well-suited for decentralized authorization.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
gem install kransekake
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or add to your Gemfile:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "kransekake"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For third-party caveats, also add [rbnacl](https://github.com/RubyCrypto/rbnacl) and install [libsodium](https://doc.libsodium.org/):
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem "rbnacl", "~> 7.0"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
First-party-only usage works without either.
|
|
28
|
+
|
|
29
|
+
## How it works
|
|
30
|
+
|
|
31
|
+
Macaroons use HMAC-SHA256 chaining. The root signature is derived from a secret key and each caveat extends the chain — caveats can be added but never removed, and tampering breaks the signature.
|
|
32
|
+
|
|
33
|
+
Instances are immutable `Data` objects. `add_caveat` returns a new macaroon, leaving the original unchanged.
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
base = Kransekake.new(key: "secret", identifier: "we used our secret key")
|
|
37
|
+
restricted = base.add_caveat("account = 3735928559")
|
|
38
|
+
|
|
39
|
+
base.caveats.empty? # => true
|
|
40
|
+
restricted.caveats.size # => 1
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
### Minting and attenuating
|
|
46
|
+
|
|
47
|
+
Mint a macaroon with a secret key. Anyone can add caveats but never remove them.
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
require "kransekake"
|
|
51
|
+
|
|
52
|
+
key = "this is our super secret key; only we should know it"
|
|
53
|
+
|
|
54
|
+
base = Kransekake.new(key:, identifier: "we used our secret key", location: "http://mybank/")
|
|
55
|
+
|
|
56
|
+
macaroon = base
|
|
57
|
+
.add_caveat("account = 3735928559")
|
|
58
|
+
.add_caveat("action = deposit")
|
|
59
|
+
.add_caveat("time < 2035-01-01T00:00")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Verifying
|
|
63
|
+
|
|
64
|
+
Every caveat must be satisfied. Pass a string for exact match or a block for a general predicate.
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
verifier = Kransekake::Verifier.new
|
|
68
|
+
verifier
|
|
69
|
+
.satisfy("account = 3735928559")
|
|
70
|
+
.satisfy("action = deposit")
|
|
71
|
+
.satisfy { |caveat| caveat.start_with?("time < ") }
|
|
72
|
+
|
|
73
|
+
verifier.verify(macaroon:, key:) # => true
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A wrong key, unsatisfied caveat, missing discharge or tampered signature raises a `Kransekake::VerificationError` subclass.
|
|
77
|
+
|
|
78
|
+
### Serialization
|
|
79
|
+
|
|
80
|
+
Binary (v2 format), base64 and JSON.
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
binary = macaroon.serialize
|
|
84
|
+
Kransekake.deserialize(binary)
|
|
85
|
+
|
|
86
|
+
base64 = macaroon.serialize(base64: true)
|
|
87
|
+
Kransekake.deserialize(base64, base64: true)
|
|
88
|
+
|
|
89
|
+
json = macaroon.to_json
|
|
90
|
+
Kransekake.from_json(json)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Third-party caveats
|
|
94
|
+
|
|
95
|
+
Delegate verification to an external service by adding a caveat only the third party can discharge. Third-party caveats use NaCl secretbox (XSalsa20-Poly1305) to encrypt caveat keys. Requires `rbnacl` and libsodium.
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
root_key = "this is our super secret key; only we should know it"
|
|
99
|
+
caveat_key = "this is a different super-secret key; never use the same secret twice"
|
|
100
|
+
|
|
101
|
+
# The authorizing service adds a third-party caveat
|
|
102
|
+
base = Kransekake.new(key: root_key, identifier: "we used our secret key", location: "http://mybank/")
|
|
103
|
+
|
|
104
|
+
macaroon = base
|
|
105
|
+
.add_caveat("account = 3735928559")
|
|
106
|
+
.add_third_party_caveat(key: caveat_key, identifier: "auth-session-id", location: "http://auth.mybank/")
|
|
107
|
+
|
|
108
|
+
# The third-party service mints a discharge macaroon
|
|
109
|
+
discharge = Kransekake.new(key: caveat_key, identifier: "auth-session-id", location: "http://auth.mybank/")
|
|
110
|
+
.add_caveat("email = alice@example.org")
|
|
111
|
+
|
|
112
|
+
# Bind the discharge to the authorizing macaroon before sending
|
|
113
|
+
bound = macaroon.bind(discharge)
|
|
114
|
+
|
|
115
|
+
# Verify with both the macaroon and its discharge
|
|
116
|
+
verifier = Kransekake::Verifier.new
|
|
117
|
+
verifier.satisfy("account = 3735928559")
|
|
118
|
+
verifier.satisfy("email = alice@example.org")
|
|
119
|
+
verifier.verify(macaroon:, key: root_key, discharges: [bound]) # => true
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Bundles
|
|
123
|
+
|
|
124
|
+
Package a macaroon with its bound discharges for transport. Supports binary, base64 and JSON.
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
bundle = macaroon.bundle(discharges: [bound])
|
|
128
|
+
restored = Kransekake::Bundle.deserialize(bundle.serialize)
|
|
129
|
+
|
|
130
|
+
verifier.verify(macaroon: restored.macaroon, key: root_key, discharges: restored.discharges)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Signature-only verification
|
|
134
|
+
|
|
135
|
+
Verify the HMAC chain without checking caveats. Returns first-party conditions.
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
macaroon = Kransekake.new(key: "secret", identifier: "we used our secret key")
|
|
139
|
+
.add_caveat("account = 3735928559")
|
|
140
|
+
|
|
141
|
+
verifier = Kransekake::Verifier.new
|
|
142
|
+
conditions = verifier.verify_signature(macaroon:, key: "secret")
|
|
143
|
+
# => ["account = 3735928559"]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Trace
|
|
147
|
+
|
|
148
|
+
Step-by-step trace for debugging the HMAC chain.
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
macaroon = Kransekake.new(key: "secret", identifier: "we used our secret key")
|
|
152
|
+
.add_caveat("account = 3735928559")
|
|
153
|
+
|
|
154
|
+
verifier = Kransekake::Verifier.new
|
|
155
|
+
trace = verifier.trace(macaroon:, key: "secret")
|
|
156
|
+
|
|
157
|
+
trace.ok? # => true
|
|
158
|
+
trace.map(&:type) # => [:derive_key, :hmac, :hmac, :verify_ok]
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Each `Kransekake::Trace::Op` has `type`, `input` and `output` attributes.
|
|
162
|
+
|
|
163
|
+
### Pattern matching
|
|
164
|
+
|
|
165
|
+
`Data` class, so deconstruction works:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
macaroon = Kransekake.new(key: "secret", identifier: "user:alice@example.org")
|
|
169
|
+
.add_caveat("account = 3735928559")
|
|
170
|
+
|
|
171
|
+
case macaroon
|
|
172
|
+
in identifier: /^user:/, caveats: {list: [Kransekake::Caveat => caveat]}
|
|
173
|
+
puts caveat.identifier # => "account = 3735928559"
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Choosing secrets
|
|
178
|
+
|
|
179
|
+
The examples above use human-readable keys for clarity. Random bytes are recommended in practice:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
key = SecureRandom.random_bytes(32)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Interoperability
|
|
186
|
+
|
|
187
|
+
Interoperable with [go-macaroon](https://github.com/go-macaroon/macaroon) v2. Binary and JSON serialization, first-party verification, third-party caveat encryption (NaCl secretbox) and discharge binding all work bidirectionally. The test suite includes golden vectors from `gopkg.in/macaroon.v2`.
|
|
188
|
+
|
|
189
|
+
## Requirements
|
|
190
|
+
|
|
191
|
+
- Ruby 4.0+
|
|
192
|
+
- [rbnacl](https://github.com/RubyCrypto/rbnacl) (~> 7.0) and [libsodium](https://doc.libsodium.org/) for third-party caveats (optional)
|
|
193
|
+
- macOS: `brew install libsodium`
|
|
194
|
+
- Debian: `apt install libsodium-dev`
|
|
195
|
+
- Arch: `pacman -S libsodium`
|
data/Rakefile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Kransekake
|
|
4
|
+
Bundle = Data.define(:macaroon, :discharges) do
|
|
5
|
+
class << self
|
|
6
|
+
def deserialize(data, base64: false)
|
|
7
|
+
macaroons = Serializer.deserialize_all(base64 ? data.unpack1("m0") : data)
|
|
8
|
+
new(macaroon: macaroons.first, discharges: macaroons.drop(1))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def from_json(json)
|
|
12
|
+
macaroons = JSON.parse(json).map { |hash| Kransekake.from_json_hash(hash) }
|
|
13
|
+
new(macaroon: macaroons.first, discharges: macaroons.drop(1))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(macaroon:, discharges: []) = super(macaroon:, discharges: discharges.freeze)
|
|
18
|
+
|
|
19
|
+
def serialize(base64: false)
|
|
20
|
+
buf = macaroon.serialize
|
|
21
|
+
discharges.each { |d| buf << d.serialize }
|
|
22
|
+
base64 ? [buf].pack("m0") : buf
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_json(*) = [macaroon, *discharges].map(&:to_json_hash).to_json(*)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Kransekake
|
|
4
|
+
Caveat = Data.define(:identifier, :vid, :location) do
|
|
5
|
+
include HexDisplay
|
|
6
|
+
|
|
7
|
+
def initialize(identifier:, vid: nil, location: nil) = super
|
|
8
|
+
|
|
9
|
+
def first_party? = vid.nil?
|
|
10
|
+
def third_party? = !first_party?
|
|
11
|
+
|
|
12
|
+
def to_json_hash
|
|
13
|
+
hash = {"i" => identifier}
|
|
14
|
+
hash["v64"] = [vid].pack("m0") if vid
|
|
15
|
+
hash["l"] = location if location
|
|
16
|
+
hash
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def inspect
|
|
20
|
+
fields = ["identifier=#{identifier.inspect}"]
|
|
21
|
+
fields << "vid=#{truncate_hex(vid)}" if vid
|
|
22
|
+
fields << "location=#{location.inspect}" if location
|
|
23
|
+
"#<data #{self.class} #{fields.join(", ")}>"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def pretty_print(pp)
|
|
27
|
+
pp.group(1, "#<data #{self.class}", ">") do
|
|
28
|
+
pp.breakable
|
|
29
|
+
pp.text "identifier=#{identifier.inspect}"
|
|
30
|
+
if vid
|
|
31
|
+
pp.text ","
|
|
32
|
+
pp.breakable
|
|
33
|
+
pp.text "vid=#{vid.unpack1("H*")}"
|
|
34
|
+
end
|
|
35
|
+
if location
|
|
36
|
+
pp.text ","
|
|
37
|
+
pp.breakable
|
|
38
|
+
pp.text "location=#{location.inspect}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Kransekake
|
|
4
|
+
Caveats = Data.define(:list) do
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
def initialize(list: []) = super(list: list.freeze)
|
|
8
|
+
|
|
9
|
+
def add(caveat) = with(list: [*list, caveat])
|
|
10
|
+
alias_method :<<, :add
|
|
11
|
+
|
|
12
|
+
def each(&) = list.each(&)
|
|
13
|
+
def size = list.size
|
|
14
|
+
def empty? = list.empty?
|
|
15
|
+
def [](index) = list[index]
|
|
16
|
+
|
|
17
|
+
def first_party = list.filter(&:first_party?)
|
|
18
|
+
def third_party = list.filter(&:third_party?)
|
|
19
|
+
|
|
20
|
+
def inspect = "#<data Kransekake::Caveats [#{list.map(&:inspect).join(", ")}]>"
|
|
21
|
+
|
|
22
|
+
def pretty_print(pp)
|
|
23
|
+
pp.group(2, "#<data Kransekake::Caveats [", "]>") do
|
|
24
|
+
pp.breakable ""
|
|
25
|
+
pp.seplist(list) { |caveat| pp.pp caveat }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Kransekake
|
|
4
|
+
module Crypto
|
|
5
|
+
class DecryptionError < StandardError
|
|
6
|
+
def initialize(message = "decryption failed") = super
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
NONCE_SIZE = 24
|
|
10
|
+
ZERO_KEY = 0.chr * 32
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def hmac(key, data) = OpenSSL::HMAC.digest("SHA256", key, data)
|
|
15
|
+
def hash2(key, d1, d2) = hmac(key, hmac(key, d1) + hmac(key, d2))
|
|
16
|
+
def derive_key(key) = hmac("macaroons-key-generator", key)
|
|
17
|
+
|
|
18
|
+
def encrypt(key, plaintext)
|
|
19
|
+
require_rbnacl
|
|
20
|
+
box = RbNaCl::SecretBox.new(key.b)
|
|
21
|
+
nonce = RbNaCl::Random.random_bytes(NONCE_SIZE)
|
|
22
|
+
nonce + box.encrypt(nonce, plaintext.b)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def decrypt(key, data)
|
|
26
|
+
require_rbnacl
|
|
27
|
+
data = data.b
|
|
28
|
+
box = RbNaCl::SecretBox.new(key.b)
|
|
29
|
+
box.decrypt(data.byteslice(0, NONCE_SIZE), data.byteslice(NONCE_SIZE..))
|
|
30
|
+
rescue ::RbNaCl::CryptoError, ::RbNaCl::LengthError => e
|
|
31
|
+
raise DecryptionError, e.message
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def require_rbnacl
|
|
35
|
+
require "rbnacl"
|
|
36
|
+
rescue LoadError
|
|
37
|
+
raise LoadError, <<~MESSAGE.chomp
|
|
38
|
+
Third-party caveats require the rbnacl gem and libsodium.
|
|
39
|
+
|
|
40
|
+
Add to your Gemfile:
|
|
41
|
+
gem "rbnacl", "~> 7.0"
|
|
42
|
+
|
|
43
|
+
Install libsodium:
|
|
44
|
+
macOS: brew install libsodium
|
|
45
|
+
Debian: apt install libsodium-dev
|
|
46
|
+
Arch: pacman -S libsodium
|
|
47
|
+
MESSAGE
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Kransekake
|
|
4
|
+
module Serializer
|
|
5
|
+
class Reader
|
|
6
|
+
attr_reader :pos
|
|
7
|
+
|
|
8
|
+
def initialize(data)
|
|
9
|
+
@data = data.b
|
|
10
|
+
@pos = 0
|
|
11
|
+
|
|
12
|
+
raise ArgumentError, "empty macaroon data" if @data.empty?
|
|
13
|
+
|
|
14
|
+
version = read_byte
|
|
15
|
+
raise ParseError, "unsupported macaroon version: #{version} (expected #{VERSION})" unless version == VERSION
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def section
|
|
19
|
+
location = nil
|
|
20
|
+
field_type, field_data = field
|
|
21
|
+
|
|
22
|
+
if field_type == LOCATION
|
|
23
|
+
location = field_data.force_encoding(Encoding::UTF_8)
|
|
24
|
+
field_type, field_data = field
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
raise ParseError, "expected field type `#{IDENTIFIER}` (identifier), got `#{field_type}`" unless field_type == IDENTIFIER
|
|
28
|
+
|
|
29
|
+
[location, field_data.force_encoding(Encoding::UTF_8)]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def caveat
|
|
33
|
+
location, identifier = section
|
|
34
|
+
|
|
35
|
+
vid = nil
|
|
36
|
+
field_type, field_data = field
|
|
37
|
+
|
|
38
|
+
if field_type == VID
|
|
39
|
+
vid = field_data
|
|
40
|
+
expect_eos
|
|
41
|
+
elsif field_type != EOS
|
|
42
|
+
raise ParseError, "unexpected field type `#{field_type}` in caveat"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
Caveat.new(identifier:, vid:, location:)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def signature
|
|
49
|
+
field_type, field_data = field
|
|
50
|
+
|
|
51
|
+
raise ParseError, "expected field type `#{SIGNATURE}` (signature), got `#{field_type}`" unless field_type == SIGNATURE
|
|
52
|
+
raise ParseError, "signature must be 32 bytes, got #{field_data.bytesize}" unless field_data.bytesize == 32
|
|
53
|
+
|
|
54
|
+
field_data
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def eos? = @data.getbyte(@pos) == EOS
|
|
58
|
+
def skip_eos = @pos += 1
|
|
59
|
+
|
|
60
|
+
def expect_eos
|
|
61
|
+
raise ParseError, "expected EOS" if @pos >= @data.bytesize || !eos?
|
|
62
|
+
|
|
63
|
+
skip_eos
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def read_byte
|
|
69
|
+
raise ParseError, "unexpected end of data" if @pos >= @data.bytesize
|
|
70
|
+
|
|
71
|
+
byte = @data.getbyte(@pos)
|
|
72
|
+
@pos += 1
|
|
73
|
+
byte
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def field
|
|
77
|
+
field_type = decode_varint
|
|
78
|
+
return [EOS, nil] if field_type == EOS
|
|
79
|
+
|
|
80
|
+
length = decode_varint
|
|
81
|
+
raise ParseError, "field extends past end of buffer" if @pos + length > @data.bytesize
|
|
82
|
+
|
|
83
|
+
field_data = @data.byteslice(@pos, length)
|
|
84
|
+
@pos += length
|
|
85
|
+
[field_type, field_data]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def decode_varint
|
|
89
|
+
value = 0
|
|
90
|
+
shift = 0
|
|
91
|
+
|
|
92
|
+
loop do
|
|
93
|
+
byte = read_byte
|
|
94
|
+
value |= (byte & 0x7f) << shift
|
|
95
|
+
return value if byte < 0x80
|
|
96
|
+
|
|
97
|
+
shift += 7
|
|
98
|
+
raise ParseError, "varint too large" if shift > 63
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Kransekake
|
|
4
|
+
module Serializer
|
|
5
|
+
class Writer
|
|
6
|
+
def initialize
|
|
7
|
+
@buf = String.new(VERSION.chr, encoding: Encoding::BINARY, capacity: 256)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def field(type, data) = @buf << encode_varint(type) << encode_varint(data.bytesize) << data.b
|
|
11
|
+
def eos = @buf << "\x00"
|
|
12
|
+
def to_s = @buf
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def encode_varint(value)
|
|
17
|
+
buf = String.new(encoding: Encoding::BINARY, capacity: 10)
|
|
18
|
+
while value > 0x7f
|
|
19
|
+
buf << ((value & 0x7f) | 0x80).chr
|
|
20
|
+
value >>= 7
|
|
21
|
+
end
|
|
22
|
+
buf << value.chr
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Kransekake
|
|
4
|
+
module Serializer
|
|
5
|
+
VERSION = 2
|
|
6
|
+
|
|
7
|
+
EOS = 0
|
|
8
|
+
LOCATION = 1
|
|
9
|
+
IDENTIFIER = 2
|
|
10
|
+
VID = 4
|
|
11
|
+
SIGNATURE = 6
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def serialize(macaroon)
|
|
15
|
+
writer = Writer.new
|
|
16
|
+
|
|
17
|
+
writer.field(LOCATION, macaroon.location) unless macaroon.location.empty?
|
|
18
|
+
writer.field(IDENTIFIER, macaroon.identifier)
|
|
19
|
+
writer.eos
|
|
20
|
+
|
|
21
|
+
macaroon.caveats.each do |caveat|
|
|
22
|
+
writer.field(LOCATION, caveat.location) if caveat.location
|
|
23
|
+
writer.field(IDENTIFIER, caveat.identifier)
|
|
24
|
+
writer.field(VID, caveat.vid) if caveat.vid
|
|
25
|
+
writer.eos
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
writer.eos
|
|
29
|
+
writer.field(SIGNATURE, macaroon.signature)
|
|
30
|
+
|
|
31
|
+
writer.to_s
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def deserialize(data) = read_one(Reader.new(data))
|
|
35
|
+
|
|
36
|
+
def deserialize_all(data)
|
|
37
|
+
data = data.b
|
|
38
|
+
macaroons = []
|
|
39
|
+
offset = 0
|
|
40
|
+
|
|
41
|
+
while offset < data.bytesize
|
|
42
|
+
reader = Reader.new(data.byteslice(offset..))
|
|
43
|
+
macaroons << read_one(reader)
|
|
44
|
+
offset += reader.pos
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
macaroons
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def read_one(reader)
|
|
53
|
+
location, identifier = reader.section
|
|
54
|
+
reader.expect_eos
|
|
55
|
+
|
|
56
|
+
caveats = Caveats.new
|
|
57
|
+
caveats <<= reader.caveat until reader.eos?
|
|
58
|
+
reader.skip_eos
|
|
59
|
+
|
|
60
|
+
Kransekake.new(location: location || "", identifier:, caveats:, signature: reader.signature)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Kransekake
|
|
4
|
+
class Trace
|
|
5
|
+
Op = Data.define(:type, :input, :output) do
|
|
6
|
+
include HexDisplay
|
|
7
|
+
|
|
8
|
+
def inspect
|
|
9
|
+
"#<data #{self.class} type=#{type.inspect}, input=#{format_field(input)}, output=#{format_field(output)}>"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def pretty_print(pp)
|
|
13
|
+
pp.group(1, "#<data #{self.class}", ">") do
|
|
14
|
+
pp.breakable
|
|
15
|
+
pp.text "type=#{type.inspect},"
|
|
16
|
+
pp.breakable
|
|
17
|
+
pp.text "input=#{format_field(input, truncate: false)},"
|
|
18
|
+
pp.breakable
|
|
19
|
+
pp.text "output=#{format_field(output, truncate: false)}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def format_field(value, truncate: true)
|
|
26
|
+
if value.encoding == Encoding::BINARY
|
|
27
|
+
truncate ? truncate_hex(value) : value.unpack1("H*")
|
|
28
|
+
else
|
|
29
|
+
value.inspect
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Kransekake
|
|
4
|
+
class Verifier
|
|
5
|
+
include Crypto
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@exact = Set.new
|
|
9
|
+
@general = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def satisfy(predicate = nil, &)
|
|
13
|
+
if block_given?
|
|
14
|
+
@general << proc(&)
|
|
15
|
+
else
|
|
16
|
+
@exact << predicate
|
|
17
|
+
end
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def verify(macaroon:, key:, discharges: [])
|
|
22
|
+
sig = hmac(derive_key(key), macaroon.identifier)
|
|
23
|
+
|
|
24
|
+
macaroon.caveats.each do |caveat|
|
|
25
|
+
sig = if caveat.third_party?
|
|
26
|
+
verify_third_party(caveat:, sig:, authorizing_signature: macaroon.signature, discharges:)
|
|
27
|
+
else
|
|
28
|
+
satisfy_caveat(predicate: caveat.identifier, sig:)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
compare_signatures!(expected: sig, actual: macaroon.signature)
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def verify_signature(macaroon:, key:, discharges: [])
|
|
37
|
+
conditions = []
|
|
38
|
+
sig = hmac(derive_key(key), macaroon.identifier)
|
|
39
|
+
|
|
40
|
+
macaroon.caveats.each do |caveat|
|
|
41
|
+
if caveat.third_party?
|
|
42
|
+
sig = verify_third_party_signature(caveat:, sig:, authorizing_signature: macaroon.signature,
|
|
43
|
+
discharges:, conditions:)
|
|
44
|
+
else
|
|
45
|
+
conditions << caveat.identifier
|
|
46
|
+
sig = hmac(sig, caveat.identifier)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
compare_signatures!(expected: sig, actual: macaroon.signature)
|
|
51
|
+
conditions
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def trace(macaroon:, key:, discharges: [])
|
|
55
|
+
ops = []
|
|
56
|
+
root_key = derive_key(key)
|
|
57
|
+
ops << Trace::Op.new(type: :derive_key, input: key, output: root_key)
|
|
58
|
+
|
|
59
|
+
sig = hmac(root_key, macaroon.identifier)
|
|
60
|
+
ops << Trace::Op.new(type: :hmac, input: macaroon.identifier, output: sig)
|
|
61
|
+
|
|
62
|
+
macaroon.caveats.each do |caveat|
|
|
63
|
+
if caveat.third_party?
|
|
64
|
+
caveat_key = decrypt(sig, caveat.vid)
|
|
65
|
+
ops << Trace::Op.new(type: :decrypt, input: caveat.identifier, output: caveat_key)
|
|
66
|
+
sig = hash2(sig, caveat.vid, caveat.identifier)
|
|
67
|
+
else
|
|
68
|
+
sig = hmac(sig, caveat.identifier)
|
|
69
|
+
end
|
|
70
|
+
ops << Trace::Op.new(type: :hmac, input: caveat.identifier, output: sig)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
matched = OpenSSL.fixed_length_secure_compare(sig, macaroon.signature)
|
|
74
|
+
ops << Trace::Op.new(type: matched ? :verify_ok : :verify_fail, input: sig, output: macaroon.signature)
|
|
75
|
+
|
|
76
|
+
Trace.new(ops:)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def compare_signatures!(expected:, actual:)
|
|
82
|
+
raise SignatureMismatchError unless OpenSSL.fixed_length_secure_compare(expected, actual)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def satisfied?(predicate) = @exact.include?(predicate) || @general.any? { it.call(predicate) }
|
|
86
|
+
|
|
87
|
+
def satisfy_caveat(predicate:, sig:)
|
|
88
|
+
raise UnsatisfiedCaveatError.new(predicate) unless satisfied?(predicate)
|
|
89
|
+
|
|
90
|
+
hmac(sig, predicate)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def verify_third_party(caveat:, sig:, authorizing_signature:, discharges:)
|
|
94
|
+
discharge = discharges.find { it.identifier == caveat.identifier }
|
|
95
|
+
raise MissingDischargeError.new(caveat.identifier) unless discharge
|
|
96
|
+
|
|
97
|
+
caveat_key = decrypt(sig, caveat.vid)
|
|
98
|
+
verify_discharge(discharge:, key: caveat_key, authorizing_signature:, discharges:)
|
|
99
|
+
|
|
100
|
+
hash2(sig, caveat.vid, caveat.identifier)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def verify_discharge(discharge:, key:, authorizing_signature:, discharges:)
|
|
104
|
+
sig = hmac(key, discharge.identifier)
|
|
105
|
+
|
|
106
|
+
discharge.caveats.each do |caveat|
|
|
107
|
+
sig = if caveat.third_party?
|
|
108
|
+
verify_third_party(caveat:, sig:, authorizing_signature:, discharges:)
|
|
109
|
+
else
|
|
110
|
+
satisfy_caveat(predicate: caveat.identifier, sig:)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
compare_signatures!(expected: hash2(ZERO_KEY, authorizing_signature, sig), actual: discharge.signature)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def verify_third_party_signature(caveat:, sig:, authorizing_signature:, discharges:, conditions:)
|
|
118
|
+
discharge = discharges.find { it.identifier == caveat.identifier }
|
|
119
|
+
raise MissingDischargeError.new(caveat.identifier) unless discharge
|
|
120
|
+
|
|
121
|
+
caveat_key = decrypt(sig, caveat.vid)
|
|
122
|
+
verify_discharge_signature(discharge:, key: caveat_key, authorizing_signature:, conditions:, discharges:)
|
|
123
|
+
|
|
124
|
+
hash2(sig, caveat.vid, caveat.identifier)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def verify_discharge_signature(discharge:, key:, authorizing_signature:, conditions:, discharges:)
|
|
128
|
+
sig = hmac(key, discharge.identifier)
|
|
129
|
+
|
|
130
|
+
discharge.caveats.each do |caveat|
|
|
131
|
+
if caveat.third_party?
|
|
132
|
+
sig = verify_third_party_signature(caveat:, sig:, authorizing_signature:, discharges:, conditions:)
|
|
133
|
+
else
|
|
134
|
+
conditions << caveat.identifier
|
|
135
|
+
sig = hmac(sig, caveat.identifier)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
compare_signatures!(expected: hash2(ZERO_KEY, authorizing_signature, sig), actual: discharge.signature)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
data/lib/kransekake.rb
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
Kransekake = Data.define(:location, :identifier, :caveats, :signature)
|
|
7
|
+
|
|
8
|
+
require_relative "kransekake/hex_display"
|
|
9
|
+
require_relative "kransekake/caveat"
|
|
10
|
+
require_relative "kransekake/caveats"
|
|
11
|
+
require_relative "kransekake/crypto"
|
|
12
|
+
require_relative "kransekake/serializer"
|
|
13
|
+
require_relative "kransekake/serializer/writer"
|
|
14
|
+
require_relative "kransekake/serializer/reader"
|
|
15
|
+
require_relative "kransekake/bundle"
|
|
16
|
+
require_relative "kransekake/trace"
|
|
17
|
+
require_relative "kransekake/trace/op"
|
|
18
|
+
require_relative "kransekake/verifier"
|
|
19
|
+
require_relative "kransekake/version"
|
|
20
|
+
|
|
21
|
+
class Kransekake
|
|
22
|
+
class ParseError < StandardError
|
|
23
|
+
def initialize(message = "failed to parse macaroon") = super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class VerificationError < StandardError
|
|
27
|
+
def initialize(message = "verification failed") = super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class MissingDischargeError < VerificationError
|
|
31
|
+
attr_reader :identifier
|
|
32
|
+
|
|
33
|
+
def initialize(identifier)
|
|
34
|
+
@identifier = identifier
|
|
35
|
+
super("missing discharge for caveat: #{identifier}")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class SignatureMismatchError < VerificationError
|
|
40
|
+
def initialize(message = "signature mismatch") = super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class UnsatisfiedCaveatError < VerificationError
|
|
44
|
+
attr_reader :caveat
|
|
45
|
+
|
|
46
|
+
def initialize(caveat)
|
|
47
|
+
@caveat = caveat
|
|
48
|
+
super("unsatisfied caveat: #{caveat}")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
include Crypto
|
|
53
|
+
include HexDisplay
|
|
54
|
+
|
|
55
|
+
class << self
|
|
56
|
+
def deserialize(data, base64: false) = Serializer.deserialize(base64 ? data.unpack1("m0") : data)
|
|
57
|
+
def from_json(json) = from_json_hash(JSON.parse(json))
|
|
58
|
+
|
|
59
|
+
def from_json_hash(hash)
|
|
60
|
+
location = hash.fetch("l", "")
|
|
61
|
+
identifier = hash["i"] || decode64(hash.fetch("i64"))
|
|
62
|
+
|
|
63
|
+
caveats = Caveats.new
|
|
64
|
+
hash.fetch("c", []).each do |c|
|
|
65
|
+
cav_id = c["i"] || decode64(c.fetch("i64"))
|
|
66
|
+
v64 = c["v64"]
|
|
67
|
+
cav_vid = decode64(v64) if v64
|
|
68
|
+
cav_loc = c["l"]
|
|
69
|
+
caveats <<= Caveat.new(identifier: cav_id, vid: cav_vid, location: cav_loc)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
signature = decode64(hash["s64"])
|
|
73
|
+
raise ParseError, "signature must be 32 bytes, got #{signature.bytesize}" unless signature.bytesize == 32
|
|
74
|
+
|
|
75
|
+
new(location:, identifier:, caveats:, signature:)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def decode64(data) = data.tr("-_", "+/").ljust((data.size + 3) & ~3, "=").unpack1("m0")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def initialize(identifier:, key: nil, location: "", caveats: Caveats.new, **)
|
|
84
|
+
if key
|
|
85
|
+
super(identifier:, location:, caveats:, signature: hmac(derive_key(key), identifier), **)
|
|
86
|
+
else
|
|
87
|
+
super(identifier:, location:, caveats:, **)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def add_caveat(predicate) = with(caveats: caveats.add(Caveat.new(identifier: predicate)), signature: hmac(signature, predicate))
|
|
92
|
+
|
|
93
|
+
def add_third_party_caveat(key:, identifier:, location:)
|
|
94
|
+
vid = encrypt(signature, derive_key(key))
|
|
95
|
+
sig = hash2(signature, vid, identifier)
|
|
96
|
+
|
|
97
|
+
with(caveats: caveats.add(Caveat.new(identifier:, vid:, location:)), signature: sig)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def bind(discharge) = discharge.with(signature: hash2(ZERO_KEY, signature, discharge.signature))
|
|
101
|
+
|
|
102
|
+
def inspect
|
|
103
|
+
fields = []
|
|
104
|
+
fields << "location=#{location.inspect}" unless location.empty?
|
|
105
|
+
fields << "identifier=#{identifier.inspect}"
|
|
106
|
+
fields << "caveats=#{caveats.inspect}" unless caveats.empty?
|
|
107
|
+
fields << "signature=#{truncate_hex(signature)}"
|
|
108
|
+
"#<data #{self.class} #{fields.join(", ")}>"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def pretty_print(pp)
|
|
112
|
+
pp.group(1, "#<data #{self.class}", ">") do
|
|
113
|
+
pp.breakable
|
|
114
|
+
pp.text "location=#{location.inspect}," unless location.empty?
|
|
115
|
+
pp.breakable unless location.empty?
|
|
116
|
+
pp.text "identifier=#{identifier.inspect},"
|
|
117
|
+
unless caveats.empty?
|
|
118
|
+
pp.breakable
|
|
119
|
+
pp.text "caveats="
|
|
120
|
+
pp.pp caveats
|
|
121
|
+
pp.text ","
|
|
122
|
+
end
|
|
123
|
+
pp.breakable
|
|
124
|
+
pp.text "signature=#{signature.unpack1("H*")}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def version = Serializer::VERSION
|
|
129
|
+
def bundle(discharges: []) = Bundle.new(macaroon: self, discharges:)
|
|
130
|
+
|
|
131
|
+
def serialize(base64: false)
|
|
132
|
+
data = Serializer.serialize(self)
|
|
133
|
+
base64 ? [data].pack("m0") : data
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def to_json(*) = to_json_hash.to_json(*)
|
|
137
|
+
|
|
138
|
+
def to_json_hash
|
|
139
|
+
hash = {}
|
|
140
|
+
hash["l"] = location unless location.empty?
|
|
141
|
+
hash["i"] = identifier
|
|
142
|
+
hash["c"] = caveats.map(&:to_json_hash) unless caveats.empty?
|
|
143
|
+
hash["s64"] = [signature].pack("m0")
|
|
144
|
+
hash
|
|
145
|
+
end
|
|
146
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: kransekake
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Shannon Skipper
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: A pure Ruby implementation of macaroons for flexible authorization credentials
|
|
13
|
+
email:
|
|
14
|
+
- shannonskipper@gmail.com
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- LICENSE.txt
|
|
20
|
+
- README.md
|
|
21
|
+
- Rakefile
|
|
22
|
+
- lib/kransekake.rb
|
|
23
|
+
- lib/kransekake/bundle.rb
|
|
24
|
+
- lib/kransekake/caveat.rb
|
|
25
|
+
- lib/kransekake/caveats.rb
|
|
26
|
+
- lib/kransekake/crypto.rb
|
|
27
|
+
- lib/kransekake/hex_display.rb
|
|
28
|
+
- lib/kransekake/serializer.rb
|
|
29
|
+
- lib/kransekake/serializer/reader.rb
|
|
30
|
+
- lib/kransekake/serializer/writer.rb
|
|
31
|
+
- lib/kransekake/trace.rb
|
|
32
|
+
- lib/kransekake/trace/op.rb
|
|
33
|
+
- lib/kransekake/verifier.rb
|
|
34
|
+
- lib/kransekake/version.rb
|
|
35
|
+
homepage: https://github.com/havenwood/kransekake
|
|
36
|
+
licenses:
|
|
37
|
+
- MIT
|
|
38
|
+
metadata:
|
|
39
|
+
source_code_uri: https://github.com/havenwood/kransekake
|
|
40
|
+
rubygems_mfa_required: 'true'
|
|
41
|
+
rdoc_options: []
|
|
42
|
+
require_paths:
|
|
43
|
+
- lib
|
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '4.0'
|
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
requirements: []
|
|
55
|
+
rubygems_version: 4.0.7
|
|
56
|
+
specification_version: 4
|
|
57
|
+
summary: A macaroon authorization credentials library
|
|
58
|
+
test_files: []
|