slosilo 2.0.1 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.dockerignore +2 -0
- data/CHANGELOG.md +4 -1
- data/Jenkinsfile +39 -0
- data/lib/slosilo.rb +1 -0
- data/lib/slosilo/errors.rb +3 -0
- data/lib/slosilo/jwt.rb +122 -0
- data/lib/slosilo/key.rb +73 -1
- data/lib/slosilo/keystore.rb +13 -2
- data/lib/slosilo/version.rb +1 -1
- data/publish-rubygem.sh +11 -0
- data/slosilo.gemspec +9 -1
- data/spec/file_adapter_spec.rb +1 -1
- data/spec/jwt_spec.rb +103 -0
- data/spec/key_spec.rb +48 -2
- data/spec/sequel_adapter_spec.rb +1 -1
- data/spec/slosilo_spec.rb +32 -0
- data/spec/symmetric_spec.rb +4 -4
- data/test.sh +25 -0
- metadata +42 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1b7ca2d8d2bbd323237e1ff513bd8d00af684da0
|
4
|
+
data.tar.gz: 152b2735c0d053e414baeebd40ce229761514a2a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a09dd7a7607809467e9f77c0400c85e3c8336722058de27e00f5aad6cda1577db3f747f3b30b2ecdf677ef679a2a78061f0dbd292de253f02d3f6c14a3ee2495
|
7
|
+
data.tar.gz: 56e9ee6c12c3419d32b6e6260a6889cad2d38e9822cd66788950b17bf3f559e1a8c660cd9519d1c05c070dc4873fc7446e3cda58809d535886ce62835032db34
|
data/.dockerignore
ADDED
data/CHANGELOG.md
CHANGED
data/Jenkinsfile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env groovy
|
2
|
+
|
3
|
+
pipeline {
|
4
|
+
agent { label 'executor-v2' }
|
5
|
+
|
6
|
+
options {
|
7
|
+
timestamps()
|
8
|
+
buildDiscarder(logRotator(daysToKeepStr: '30'))
|
9
|
+
}
|
10
|
+
|
11
|
+
stages {
|
12
|
+
stage('Test') {
|
13
|
+
steps {
|
14
|
+
sh './test.sh'
|
15
|
+
|
16
|
+
junit 'spec/reports/*.xml'
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
stage('Publish to RubyGems') {
|
21
|
+
agent { label 'releaser-v2' }
|
22
|
+
when {
|
23
|
+
branch 'master'
|
24
|
+
}
|
25
|
+
|
26
|
+
steps {
|
27
|
+
checkout scm
|
28
|
+
sh './publish-rubygem.sh'
|
29
|
+
deleteDir()
|
30
|
+
}
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
post {
|
35
|
+
always {
|
36
|
+
cleanupAndNotify(currentBuild.currentResult)
|
37
|
+
}
|
38
|
+
}
|
39
|
+
}
|
data/lib/slosilo.rb
CHANGED
data/lib/slosilo/errors.rb
CHANGED
data/lib/slosilo/jwt.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Slosilo
|
4
|
+
# A JWT-formatted Slosilo token.
|
5
|
+
# @note This is not intended to be a general-purpose JWT implementation.
|
6
|
+
class JWT
|
7
|
+
# Create a new unsigned token with the given claims.
|
8
|
+
# @param claims [#to_h] claims to embed in this token.
|
9
|
+
def initialize claims = {}
|
10
|
+
@claims = JSONHash[claims]
|
11
|
+
end
|
12
|
+
|
13
|
+
# Parse a token in compact representation
|
14
|
+
def self.parse_compact raw
|
15
|
+
load *raw.split('.', 3).map(&Base64.method(:urlsafe_decode64))
|
16
|
+
end
|
17
|
+
|
18
|
+
# Parse a token in JSON representation.
|
19
|
+
# @note only single signature is currently supported.
|
20
|
+
def self.parse_json raw
|
21
|
+
raw = JSON.load raw unless raw.respond_to? :to_h
|
22
|
+
parts = raw.to_h.values_at(*%w(protected payload signature))
|
23
|
+
fail ArgumentError, "input not a complete JWT" unless parts.all?
|
24
|
+
load *parts.map(&Base64.method(:urlsafe_decode64))
|
25
|
+
end
|
26
|
+
|
27
|
+
# Add a signature.
|
28
|
+
# @note currently only a single signature is handled;
|
29
|
+
# the token will be frozen after this operation.
|
30
|
+
def add_signature header, &sign
|
31
|
+
@claims = canonicalize_claims.freeze
|
32
|
+
@header = JSONHash[header].freeze
|
33
|
+
@signature = sign[string_to_sign].freeze
|
34
|
+
freeze
|
35
|
+
end
|
36
|
+
|
37
|
+
def string_to_sign
|
38
|
+
[header, claims].map(&method(:encode)).join '.'
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the JSON serialization of this JWT.
|
42
|
+
def to_json *a
|
43
|
+
{
|
44
|
+
protected: encode(header),
|
45
|
+
payload: encode(claims),
|
46
|
+
signature: encode(signature)
|
47
|
+
}.to_json *a
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the compact serialization of this JWT.
|
51
|
+
def to_s
|
52
|
+
[header, claims, signature].map(&method(:encode)).join('.')
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_accessor :claims, :header, :signature
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# Create a JWT token object from existing header, payload, and signature strings.
|
60
|
+
# @param header [#to_s] URLbase64-encoded representation of the protected header
|
61
|
+
# @param payload [#to_s] URLbase64-encoded representation of the token payload
|
62
|
+
# @param signature [#to_s] URLbase64-encoded representation of the signature
|
63
|
+
def self.load header, payload, signature
|
64
|
+
self.new(JSONHash.load payload).tap do |token|
|
65
|
+
token.header = JSONHash.load header
|
66
|
+
token.signature = signature.to_s.freeze
|
67
|
+
token.freeze
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def canonicalize_claims
|
72
|
+
claims[:iat] = Time.now unless claims.include? :iat
|
73
|
+
claims[:iat] = claims[:iat].to_time.to_i
|
74
|
+
claims[:exp] = claims[:exp].to_time.to_i if claims.include? :exp
|
75
|
+
JSONHash[claims.to_a]
|
76
|
+
end
|
77
|
+
|
78
|
+
# Convenience method to make the above code clearer.
|
79
|
+
# Converts to string and urlbase64-encodes.
|
80
|
+
def encode s
|
81
|
+
Base64.urlsafe_encode64 s.to_s
|
82
|
+
end
|
83
|
+
|
84
|
+
# a hash with a possibly frozen JSON stringification
|
85
|
+
class JSONHash < Hash
|
86
|
+
def to_s
|
87
|
+
@repr || to_json
|
88
|
+
end
|
89
|
+
|
90
|
+
def freeze
|
91
|
+
@repr = to_json.freeze
|
92
|
+
super
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.load raw
|
96
|
+
self[JSON.load raw.to_s].tap do |h|
|
97
|
+
h.send :repr=, raw
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def repr= raw
|
104
|
+
@repr = raw.freeze
|
105
|
+
freeze
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Try to convert by detecting token representation and parsing
|
111
|
+
def self.JWT raw
|
112
|
+
if raw.is_a? JWT
|
113
|
+
raw
|
114
|
+
elsif raw.respond_to?(:to_h) || raw =~ /\A\s*\{/
|
115
|
+
JWT.parse_json raw
|
116
|
+
else
|
117
|
+
JWT.parse_compact raw
|
118
|
+
end
|
119
|
+
rescue
|
120
|
+
raise ArgumentError, "invalid value for JWT(): #{raw.inspect}"
|
121
|
+
end
|
122
|
+
end
|
data/lib/slosilo/key.rb
CHANGED
@@ -3,6 +3,8 @@ require 'json'
|
|
3
3
|
require 'base64'
|
4
4
|
require 'time'
|
5
5
|
|
6
|
+
require 'slosilo/errors'
|
7
|
+
|
6
8
|
module Slosilo
|
7
9
|
class Key
|
8
10
|
def initialize raw_key = nil
|
@@ -13,6 +15,10 @@ module Slosilo
|
|
13
15
|
else
|
14
16
|
OpenSSL::PKey::RSA.new 2048
|
15
17
|
end
|
18
|
+
rescue OpenSSL::PKey::PKeyError => e
|
19
|
+
# old openssl versions used to report ArgumentError
|
20
|
+
# which arguably makes more sense here, so reraise as that
|
21
|
+
raise ArgumentError, e, e.backtrace
|
16
22
|
end
|
17
23
|
|
18
24
|
attr_reader :key
|
@@ -71,14 +77,80 @@ module Slosilo
|
|
71
77
|
token["key"] = fingerprint
|
72
78
|
token
|
73
79
|
end
|
80
|
+
|
81
|
+
JWT_ALGORITHM = 'conjur.org/slosilo/v2'.freeze
|
82
|
+
|
83
|
+
# Issue a JWT with the given claims.
|
84
|
+
# `iat` (issued at) claim is automatically added.
|
85
|
+
# Other interesting claims you can give are:
|
86
|
+
# - `sub` - token subject, for example a user name;
|
87
|
+
# - `exp` - expiration time (absolute);
|
88
|
+
# - `cidr` (Conjur extension) - array of CIDR masks that are accepted to
|
89
|
+
# make requests that bear this token
|
90
|
+
def issue_jwt claims
|
91
|
+
token = Slosilo::JWT.new claims
|
92
|
+
token.add_signature \
|
93
|
+
alg: JWT_ALGORITHM,
|
94
|
+
kid: fingerprint,
|
95
|
+
&method(:sign)
|
96
|
+
token.freeze
|
97
|
+
end
|
98
|
+
|
99
|
+
DEFAULT_EXPIRATION = 8 * 60
|
74
100
|
|
75
|
-
def token_valid? token, expiry =
|
101
|
+
def token_valid? token, expiry = DEFAULT_EXPIRATION
|
102
|
+
return jwt_valid? token if token.respond_to? :header
|
76
103
|
token = token.clone
|
77
104
|
expected_key = token.delete "key"
|
78
105
|
return false if (expected_key and (expected_key != fingerprint))
|
79
106
|
signature = Base64::urlsafe_decode64(token.delete "signature")
|
80
107
|
(Time.parse(token["timestamp"]) + expiry > Time.now) && verify_signature(token, signature)
|
81
108
|
end
|
109
|
+
|
110
|
+
# Validate a JWT.
|
111
|
+
#
|
112
|
+
# Convenience method calling #validate_jwt and returning false if an
|
113
|
+
# exception is raised.
|
114
|
+
#
|
115
|
+
# @param token [JWT] pre-parsed token to verify
|
116
|
+
# @return [Boolean]
|
117
|
+
def jwt_valid? token
|
118
|
+
validate_jwt token
|
119
|
+
true
|
120
|
+
rescue
|
121
|
+
false
|
122
|
+
end
|
123
|
+
|
124
|
+
# Validate a JWT.
|
125
|
+
#
|
126
|
+
# First checks whether algorithm is 'conjur.org/slosilo/v2' and the key id
|
127
|
+
# matches this key's fingerprint. Then verifies if the token is not expired,
|
128
|
+
# as indicated by the `exp` claim; in its absence tokens are assumed to
|
129
|
+
# expire in `iat` + 8 minutes.
|
130
|
+
#
|
131
|
+
# If those checks pass, finally the signature is verified.
|
132
|
+
#
|
133
|
+
# @raises TokenValidationError if any of the checks fail.
|
134
|
+
#
|
135
|
+
# @note It's the responsibility of the caller to examine other claims
|
136
|
+
# included in the token; consideration needs to be given to handling
|
137
|
+
# unrecognized claims.
|
138
|
+
#
|
139
|
+
# @param token [JWT] pre-parsed token to verify
|
140
|
+
def validate_jwt token
|
141
|
+
def err msg
|
142
|
+
raise Error::TokenValidationError, msg, caller
|
143
|
+
end
|
144
|
+
|
145
|
+
header = token.header
|
146
|
+
err 'unrecognized algorithm' unless header['alg'] == JWT_ALGORITHM
|
147
|
+
err 'mismatched key' if (kid = header['kid']) && kid != fingerprint
|
148
|
+
iat = Time.at token.claims['iat'] || err('unknown issuing time')
|
149
|
+
exp = Time.at token.claims['exp'] || (iat + DEFAULT_EXPIRATION)
|
150
|
+
err 'token expired' if exp <= Time.now
|
151
|
+
err 'invalid signature' unless verify_signature token.string_to_sign, token.signature
|
152
|
+
true
|
153
|
+
end
|
82
154
|
|
83
155
|
def sign_string value
|
84
156
|
salt = shake_salt
|
data/lib/slosilo/keystore.rb
CHANGED
@@ -59,15 +59,26 @@ module Slosilo
|
|
59
59
|
keystore.any? { |k| k.token_valid? token }
|
60
60
|
end
|
61
61
|
|
62
|
+
# Looks up the signer by public key fingerprint and checks the validity
|
63
|
+
# of the signature. If the token is JWT, exp and/or iat claims are also
|
64
|
+
# verified; the caller is responsible for validating any other claims.
|
62
65
|
def token_signer token
|
63
|
-
|
66
|
+
begin
|
67
|
+
# see if maybe it's a JWT
|
68
|
+
token = JWT token
|
69
|
+
fingerprint = token.header['kid']
|
70
|
+
rescue ArgumentError
|
71
|
+
fingerprint = token['key']
|
72
|
+
end
|
73
|
+
|
74
|
+
key, id = keystore.get_by_fingerprint fingerprint
|
64
75
|
if key && key.token_valid?(token)
|
65
76
|
return id
|
66
77
|
else
|
67
78
|
return nil
|
68
79
|
end
|
69
80
|
end
|
70
|
-
|
81
|
+
|
71
82
|
attr_accessor :adapter
|
72
83
|
|
73
84
|
private
|
data/lib/slosilo/version.rb
CHANGED
data/publish-rubygem.sh
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/bin/bash -e
|
2
|
+
|
3
|
+
docker pull registry.tld/conjurinc/publish-rubygem
|
4
|
+
|
5
|
+
docker run -i --rm -v $PWD:/src -w /src alpine/git clean -fxd
|
6
|
+
|
7
|
+
summon --yaml "RUBYGEMS_API_KEY: !var rubygems/api-key" \
|
8
|
+
docker run --rm --env-file @SUMMONENVFILE -v "$(pwd)":/opt/src \
|
9
|
+
registry.tld/conjurinc/publish-rubygem slosilo
|
10
|
+
|
11
|
+
docker run -i --rm -v $PWD:/src -w /src alpine/git clean -fxd
|
data/slosilo.gemspec
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
|
2
|
+
begin
|
3
|
+
require File.expand_path('../lib/slosilo/version', __FILE__)
|
4
|
+
rescue LoadError
|
5
|
+
# so that bundle can be run without the app code
|
6
|
+
module Slosilo
|
7
|
+
VERSION = '0.0.0'
|
8
|
+
end
|
9
|
+
end
|
3
10
|
|
4
11
|
Gem::Specification.new do |gem|
|
5
12
|
gem.authors = ["Rafa\305\202 Rzepecki"]
|
@@ -24,4 +31,5 @@ Gem::Specification.new do |gem|
|
|
24
31
|
gem.add_development_dependency 'io-grab', '~> 0.0.1'
|
25
32
|
gem.add_development_dependency 'sequel' # for sequel tests
|
26
33
|
gem.add_development_dependency 'sqlite3' # for sequel tests
|
34
|
+
gem.add_development_dependency 'activesupport' # for convenience in specs
|
27
35
|
end
|
data/spec/file_adapter_spec.rb
CHANGED
@@ -22,7 +22,7 @@ describe Slosilo::Adapters::FileAdapter do
|
|
22
22
|
context "unacceptable id" do
|
23
23
|
let(:id) { "foo.bar" }
|
24
24
|
it "isn't accepted" do
|
25
|
-
expect { subject.put_key id, key }.to raise_error
|
25
|
+
expect { subject.put_key id, key }.to raise_error /id should not contain a period/
|
26
26
|
end
|
27
27
|
end
|
28
28
|
context "acceptable id" do
|
data/spec/jwt_spec.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# (Mostly) integration tests for JWT token format
|
4
|
+
describe Slosilo::Key do
|
5
|
+
include_context "with example key"
|
6
|
+
|
7
|
+
describe '#issue_jwt' do
|
8
|
+
it 'issues an JWT token with given claims' do
|
9
|
+
allow(Time).to receive(:now) { DateTime.parse('2014-06-04 23:22:32 -0400').to_time }
|
10
|
+
|
11
|
+
tok = key.issue_jwt sub: 'host/example', cidr: %w(fec0::/64)
|
12
|
+
|
13
|
+
expect(tok).to be_frozen
|
14
|
+
|
15
|
+
expect(tok.header).to eq \
|
16
|
+
alg: 'conjur.org/slosilo/v2',
|
17
|
+
kid: key_fingerprint
|
18
|
+
expect(tok.claims).to eq \
|
19
|
+
iat: 1401938552,
|
20
|
+
sub: 'host/example',
|
21
|
+
cidr: ['fec0::/64']
|
22
|
+
|
23
|
+
expect(key.verify_signature tok.string_to_sign, tok.signature).to be_truthy
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe Slosilo::JWT do
|
29
|
+
context "with a signed token" do
|
30
|
+
let(:signature) { 'very signed, such alg' }
|
31
|
+
subject(:token) { Slosilo::JWT.new test: "token" }
|
32
|
+
before do
|
33
|
+
allow(Time).to receive(:now) { DateTime.parse('2014-06-04 23:22:32 -0400').to_time }
|
34
|
+
token.add_signature(alg: 'test-sig') { signature }
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'allows conversion to JSON representation with #to_json' do
|
38
|
+
json = JSON.load token.to_json
|
39
|
+
expect(JSON.load Base64.urlsafe_decode64 json['protected']).to eq \
|
40
|
+
'alg' => 'test-sig'
|
41
|
+
expect(JSON.load Base64.urlsafe_decode64 json['payload']).to eq \
|
42
|
+
'iat' => 1401938552, 'test' => 'token'
|
43
|
+
expect(Base64.urlsafe_decode64 json['signature']).to eq signature
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'allows conversion to compact representation with #to_s' do
|
47
|
+
h, c, s = token.to_s.split '.'
|
48
|
+
expect(JSON.load Base64.urlsafe_decode64 h).to eq \
|
49
|
+
'alg' => 'test-sig'
|
50
|
+
expect(JSON.load Base64.urlsafe_decode64 c).to eq \
|
51
|
+
'iat' => 1401938552, 'test' => 'token'
|
52
|
+
expect(Base64.urlsafe_decode64 s).to eq signature
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#to_json' do
|
57
|
+
it "passes any parameters" do
|
58
|
+
token = Slosilo::JWT.new
|
59
|
+
allow(token).to receive_messages \
|
60
|
+
header: :header,
|
61
|
+
claims: :claims,
|
62
|
+
signature: :signature
|
63
|
+
expect_any_instance_of(Hash).to receive(:to_json).with :testing
|
64
|
+
expect(token.to_json :testing)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '()' do
|
69
|
+
include_context "with example key"
|
70
|
+
|
71
|
+
it 'understands both serializations' do
|
72
|
+
[COMPACT_TOKEN, JSON_TOKEN].each do |token|
|
73
|
+
token = Slosilo::JWT token
|
74
|
+
expect(token.header).to eq \
|
75
|
+
'typ' => 'JWT',
|
76
|
+
'alg' => 'conjur.org/slosilo/v2',
|
77
|
+
'kid' => key_fingerprint
|
78
|
+
expect(token.claims).to eq \
|
79
|
+
'sub' => 'host/example',
|
80
|
+
'iat' => 1401938552,
|
81
|
+
'exp' => 1401938552 + 60*60,
|
82
|
+
'cidr' => ['fec0::/64']
|
83
|
+
|
84
|
+
expect(key.verify_signature token.string_to_sign, token.signature).to be_truthy
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'is a noop if already parsed' do
|
89
|
+
token = Slosilo::JWT COMPACT_TOKEN
|
90
|
+
expect(Slosilo::JWT token).to eq token
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'raises ArgumentError on failure to convert' do
|
94
|
+
expect { Slosilo::JWT "foo bar" }.to raise_error ArgumentError
|
95
|
+
expect { Slosilo::JWT elite: 31337 }.to raise_error ArgumentError
|
96
|
+
expect { Slosilo::JWT "foo.bar.xyzzy" }.to raise_error ArgumentError
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
COMPACT_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25qdXIub3JnL3Nsb3NpbG8vdjIiLCJraWQiOiJkMjhlM2EzNDdlMzY4NDE2YjMxMjlhNDBjMWI4ODdmZSJ9.eyJzdWIiOiJob3N0L2V4YW1wbGUiLCJpYXQiOjE0MDE5Mzg1NTIsImV4cCI6MTQwMTk0MjE1MiwiY2lkciI6WyJmZWMwOjovNjQiXX0=.cw9S8Oxu8BmvDgEotBlNiZoJNkAGDpvGIuhCCnG-nMq80dy0ECjC4xERYXHx3bcadEJ8jWfqDB90d7CGvJyepbMhC1hEdsb8xNWGkkqTvQOh33cnZiEJjjfbORpKnOpcc8QySmB9Eb_zKl5WaM6Sjm-uINJri3djuVo_n_S7I43YvKw7gbd5u2gVttgaWnqlnJoeXZnnmXSYH8_66Lr__BqO4tCedShQIf4gA0R_dljrzVSZtJsFTKvwuNOuCvBqO8dQkhp8vplOkKynDkdip-H2nDBQb9Y3bQ8K0NVtSJatBy-d1HvPHSwFZrH4K7P_J2OgJtw9GtckT43QasliXoibdk__Hyvy4HJIIM44rSm7JUuyZWl8e8svRqLujBP7".freeze
|
101
|
+
|
102
|
+
JSON_TOKEN = "{\"protected\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25qdXIub3JnL3Nsb3NpbG8vdjIiLCJraWQiOiJkMjhlM2EzNDdlMzY4NDE2YjMxMjlhNDBjMWI4ODdmZSJ9\",\"payload\":\"eyJzdWIiOiJob3N0L2V4YW1wbGUiLCJpYXQiOjE0MDE5Mzg1NTIsImV4cCI6MTQwMTk0MjE1MiwiY2lkciI6WyJmZWMwOjovNjQiXX0=\",\"signature\":\"cw9S8Oxu8BmvDgEotBlNiZoJNkAGDpvGIuhCCnG-nMq80dy0ECjC4xERYXHx3bcadEJ8jWfqDB90d7CGvJyepbMhC1hEdsb8xNWGkkqTvQOh33cnZiEJjjfbORpKnOpcc8QySmB9Eb_zKl5WaM6Sjm-uINJri3djuVo_n_S7I43YvKw7gbd5u2gVttgaWnqlnJoeXZnnmXSYH8_66Lr__BqO4tCedShQIf4gA0R_dljrzVSZtJsFTKvwuNOuCvBqO8dQkhp8vplOkKynDkdip-H2nDBQb9Y3bQ8K0NVtSJatBy-d1HvPHSwFZrH4K7P_J2OgJtw9GtckT43QasliXoibdk__Hyvy4HJIIM44rSm7JUuyZWl8e8svRqLujBP7\"}".freeze
|
103
|
+
end
|
data/spec/key_spec.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext/numeric/time'
|
5
|
+
|
3
6
|
describe Slosilo::Key do
|
4
7
|
include_context "with example key"
|
5
8
|
|
@@ -120,7 +123,7 @@ describe Slosilo::Key do
|
|
120
123
|
context "when given something else" do
|
121
124
|
subject { Slosilo::Key.new "foo" }
|
122
125
|
it "fails early" do
|
123
|
-
expect { subject }.to raise_error
|
126
|
+
expect { subject }.to raise_error ArgumentError
|
124
127
|
end
|
125
128
|
end
|
126
129
|
end
|
@@ -175,7 +178,50 @@ describe Slosilo::Key do
|
|
175
178
|
subject { key.signed_token data }
|
176
179
|
it { is_expected.to eq(expected_token) }
|
177
180
|
end
|
178
|
-
|
181
|
+
|
182
|
+
describe "#validate_jwt" do
|
183
|
+
let(:token) do
|
184
|
+
instance_double Slosilo::JWT,
|
185
|
+
header: { 'alg' => 'conjur.org/slosilo/v2' },
|
186
|
+
claims: { 'iat' => Time.now.to_i },
|
187
|
+
string_to_sign: double("string to sign"),
|
188
|
+
signature: double("signature")
|
189
|
+
end
|
190
|
+
|
191
|
+
before do
|
192
|
+
allow(key).to receive(:verify_signature).with(token.string_to_sign, token.signature) { true }
|
193
|
+
end
|
194
|
+
|
195
|
+
it "verifies the signature" do
|
196
|
+
expect { key.validate_jwt token }.not_to raise_error
|
197
|
+
end
|
198
|
+
|
199
|
+
it "rejects unknown algorithm" do
|
200
|
+
token.header['alg'] = 'HS256' # we're not supporting standard algorithms
|
201
|
+
expect { key.validate_jwt token }.to raise_error /algorithm/
|
202
|
+
end
|
203
|
+
|
204
|
+
it "rejects bad signature" do
|
205
|
+
allow(key).to receive(:verify_signature).with(token.string_to_sign, token.signature) { false }
|
206
|
+
expect { key.validate_jwt token }.to raise_error /signature/
|
207
|
+
end
|
208
|
+
|
209
|
+
it "rejects expired token" do
|
210
|
+
token.claims['exp'] = 1.hour.ago.to_i
|
211
|
+
expect { key.validate_jwt token }.to raise_error /expired/
|
212
|
+
end
|
213
|
+
|
214
|
+
it "accepts unexpired token with implicit expiration" do
|
215
|
+
token.claims['iat'] = 5.minutes.ago
|
216
|
+
expect { key.validate_jwt token }.to_not raise_error
|
217
|
+
end
|
218
|
+
|
219
|
+
it "rejects token expired with implicit expiration" do
|
220
|
+
token.claims['iat'] = 10.minutes.ago.to_i
|
221
|
+
expect { key.validate_jwt token }.to raise_error /expired/
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
179
225
|
describe "#token_valid?" do
|
180
226
|
let(:data) { { "foo" => :bar } }
|
181
227
|
let(:signature) { Base64::urlsafe_encode64 "\xB0\xCE{\x9FP\xEDV\x9C\xE7b\x8B[\xFAil\x87^\x96\x17Z\x97\x1D\xC2?B\x96\x9C\x8Ep-\xDF_\x8F\xC21\xD9^\xBC\n\x16\x04\x8DJ\xF6\xAF-\xEC\xAD\x03\xF9\xEE:\xDF\xB5\x8F\xF9\xF6\x81m\xAB\x9C\xAB1\x1E\x837\x8C\xFB\xA8P\xA8<\xEA\x1Dx\xCEd\xED\x84f\xA7\xB5t`\x96\xCC\x0F\xA9t\x8B\x9Fo\xBF\x92K\xFA\xFD\xC5?\x8F\xC68t\xBC\x9F\xDE\n$\xCA\xD2\x8F\x96\x0EtX2\x8Cl\x1E\x8Aa\r\x8D\xCAi\x86\x1A\xBD\x1D\xF7\xBC\x8561j\x91YlO\xFA(\x98\x10iq\xCC\xAF\x9BV\xC6\v\xBC\x10Xm\xCD\xFE\xAD=\xAA\x95,\xB4\xF7\xE8W\xB8\x83;\x81\x88\xE6\x01\xBA\xA5F\x91\x17\f\xCE\x80\x8E\v\x83\x9D<\x0E\x83\xF6\x8D\x03\xC0\xE8A\xD7\x90i\x1D\x030VA\x906D\x10\xA0\xDE\x12\xEF\x06M\xD8\x8B\xA9W\xC8\x9DTc\x8AJ\xA4\xC0\xD3!\xFA\x14\x89\xD1p\xB4J7\xA5\x04\xC2l\xDC8<\x04Y\xD8\xA4\xFB[\x89\xB1\xEC\xDA\xB8\xD7\xEA\x03Ja pinch of salt".force_encoding("ASCII-8BIT") }
|
data/spec/sequel_adapter_spec.rb
CHANGED
@@ -60,7 +60,7 @@ describe Slosilo::Adapters::SequelAdapter do
|
|
60
60
|
let(:db) { Sequel.sqlite }
|
61
61
|
before do
|
62
62
|
allow(subject).to receive(:create_model).and_call_original
|
63
|
-
Sequel.cache_anonymous_models = false
|
63
|
+
Sequel::Model.cache_anonymous_models = false
|
64
64
|
Sequel::Model.db = db
|
65
65
|
end
|
66
66
|
end
|
data/spec/slosilo_spec.rb
CHANGED
@@ -88,5 +88,37 @@ describe Slosilo do
|
|
88
88
|
expect(subject.token_signer(token)).not_to be
|
89
89
|
end
|
90
90
|
end
|
91
|
+
|
92
|
+
context "with JWT token" do
|
93
|
+
before do
|
94
|
+
expect(key).to receive(:validate_jwt) do |jwt|
|
95
|
+
expect(jwt.header).to eq 'kid' => key.fingerprint
|
96
|
+
expect(jwt.claims).to eq({})
|
97
|
+
expect(jwt.signature).to eq 'sig'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
it "accepts pre-parsed JSON serialization" do
|
102
|
+
expect(Slosilo.token_signer(
|
103
|
+
'protected' => 'eyJraWQiOiJkMjhlM2EzNDdlMzY4NDE2YjMxMjlhNDBjMWI4ODdmZSJ9',
|
104
|
+
'payload' => 'e30=',
|
105
|
+
'signature' => 'c2ln'
|
106
|
+
)).to eq 'test'
|
107
|
+
end
|
108
|
+
|
109
|
+
it "accepts pre-parsed JWT token" do
|
110
|
+
expect(Slosilo.token_signer(Slosilo::JWT(
|
111
|
+
'protected' => 'eyJraWQiOiJkMjhlM2EzNDdlMzY4NDE2YjMxMjlhNDBjMWI4ODdmZSJ9',
|
112
|
+
'payload' => 'e30=',
|
113
|
+
'signature' => 'c2ln'
|
114
|
+
))).to eq 'test'
|
115
|
+
end
|
116
|
+
|
117
|
+
it "accepts compact serialization" do
|
118
|
+
expect(Slosilo.token_signer(
|
119
|
+
'eyJraWQiOiJkMjhlM2EzNDdlMzY4NDE2YjMxMjlhNDBjMWI4ODdmZSJ9.e30=.c2ln'
|
120
|
+
)).to eq 'test'
|
121
|
+
end
|
122
|
+
end
|
91
123
|
end
|
92
124
|
end
|
data/spec/symmetric_spec.rb
CHANGED
@@ -24,12 +24,12 @@ describe Slosilo::Symmetric do
|
|
24
24
|
context "when the ciphertext has been messed with" do
|
25
25
|
let(:ciphertext) { "pwnd!" } # maybe we should do something more realistic like add some padding?
|
26
26
|
it "raises an exception" do
|
27
|
-
expect{ subject.decrypt(ciphertext, key: key, aad: auth_data)}.to raise_exception
|
27
|
+
expect{ subject.decrypt(ciphertext, key: key, aad: auth_data)}.to raise_exception /Invalid version/
|
28
28
|
end
|
29
29
|
context "by adding a trailing 0" do
|
30
30
|
let(:new_ciphertext){ ciphertext + '\0' }
|
31
31
|
it "raises an exception" do
|
32
|
-
expect{ subject.decrypt(new_ciphertext, key: key, aad: auth_data) }.to raise_exception
|
32
|
+
expect{ subject.decrypt(new_ciphertext, key: key, aad: auth_data) }.to raise_exception /Invalid version/
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
@@ -44,7 +44,7 @@ describe Slosilo::Symmetric do
|
|
44
44
|
|
45
45
|
context "and the ciphertext has been messed with" do
|
46
46
|
it "raises an exception" do
|
47
|
-
expect{ subject.decrypt(ciphertext + "\0\0\0", key: key, aad: auth_data)}.to raise_exception
|
47
|
+
expect{ subject.decrypt(ciphertext + "\0\0\0", key: key, aad: auth_data)}.to raise_exception OpenSSL::Cipher::CipherError
|
48
48
|
end
|
49
49
|
end
|
50
50
|
end
|
@@ -52,7 +52,7 @@ describe Slosilo::Symmetric do
|
|
52
52
|
context "when the auth data doesn't match" do
|
53
53
|
let(:auth_data){ "asdf" }
|
54
54
|
it "raises an exception" do
|
55
|
-
expect{ subject.decrypt(ciphertext, key: key, aad: auth_data)}.to raise_exception
|
55
|
+
expect{ subject.decrypt(ciphertext, key: key, aad: auth_data)}.to raise_exception OpenSSL::Cipher::CipherError
|
56
56
|
end
|
57
57
|
end
|
58
58
|
end
|
data/test.sh
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/bin/bash -xe
|
2
|
+
|
3
|
+
iid=slosilo-test-$(date +%s)
|
4
|
+
|
5
|
+
docker build -t $iid -f - . << EOF
|
6
|
+
FROM ruby
|
7
|
+
WORKDIR /app
|
8
|
+
COPY Gemfile slosilo.gemspec ./
|
9
|
+
RUN bundle
|
10
|
+
COPY . ./
|
11
|
+
RUN bundle
|
12
|
+
EOF
|
13
|
+
|
14
|
+
cidfile=$(mktemp -u)
|
15
|
+
docker run --cidfile $cidfile -v /app/spec/reports $iid bundle exec rake jenkins || :
|
16
|
+
|
17
|
+
cid=$(cat $cidfile)
|
18
|
+
|
19
|
+
docker cp $cid:/app/spec/reports spec/
|
20
|
+
docker rm $cid
|
21
|
+
|
22
|
+
# untag, will use cache next time if available but no junk will be left
|
23
|
+
docker rmi $iid
|
24
|
+
|
25
|
+
rm $cidfile
|
metadata
CHANGED
@@ -1,111 +1,125 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: slosilo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rafał Rzepecki
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-10-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '0'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rspec
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - ~>
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '3.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - ~>
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '3.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: ci_reporter_rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: simplecov
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- -
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- -
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: io-grab
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- - ~>
|
73
|
+
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: 0.0.1
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- - ~>
|
80
|
+
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: 0.0.1
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: sequel
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- -
|
87
|
+
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
89
|
version: '0'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- -
|
94
|
+
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: sqlite3
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
|
-
- -
|
101
|
+
- - ">="
|
102
102
|
- !ruby/object:Gem::Version
|
103
103
|
version: '0'
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
|
-
- -
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: activesupport
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
109
123
|
- !ruby/object:Gem::Version
|
110
124
|
version: '0'
|
111
125
|
description: This gem provides an easy way of storing and retrieving encryption keys
|
@@ -116,10 +130,12 @@ executables: []
|
|
116
130
|
extensions: []
|
117
131
|
extra_rdoc_files: []
|
118
132
|
files:
|
119
|
-
- .
|
120
|
-
- .
|
133
|
+
- ".dockerignore"
|
134
|
+
- ".gitignore"
|
135
|
+
- ".kateproject"
|
121
136
|
- CHANGELOG.md
|
122
137
|
- Gemfile
|
138
|
+
- Jenkinsfile
|
123
139
|
- LICENSE
|
124
140
|
- README.md
|
125
141
|
- Rakefile
|
@@ -132,15 +148,18 @@ files:
|
|
132
148
|
- lib/slosilo/adapters/sequel_adapter/migration.rb
|
133
149
|
- lib/slosilo/attr_encrypted.rb
|
134
150
|
- lib/slosilo/errors.rb
|
151
|
+
- lib/slosilo/jwt.rb
|
135
152
|
- lib/slosilo/key.rb
|
136
153
|
- lib/slosilo/keystore.rb
|
137
154
|
- lib/slosilo/random.rb
|
138
155
|
- lib/slosilo/symmetric.rb
|
139
156
|
- lib/slosilo/version.rb
|
140
157
|
- lib/tasks/slosilo.rake
|
158
|
+
- publish-rubygem.sh
|
141
159
|
- slosilo.gemspec
|
142
160
|
- spec/encrypted_attributes_spec.rb
|
143
161
|
- spec/file_adapter_spec.rb
|
162
|
+
- spec/jwt_spec.rb
|
144
163
|
- spec/key_spec.rb
|
145
164
|
- spec/keystore_spec.rb
|
146
165
|
- spec/random_spec.rb
|
@@ -148,6 +167,7 @@ files:
|
|
148
167
|
- spec/slosilo_spec.rb
|
149
168
|
- spec/spec_helper.rb
|
150
169
|
- spec/symmetric_spec.rb
|
170
|
+
- test.sh
|
151
171
|
homepage: ''
|
152
172
|
licenses:
|
153
173
|
- MIT
|
@@ -158,23 +178,24 @@ require_paths:
|
|
158
178
|
- lib
|
159
179
|
required_ruby_version: !ruby/object:Gem::Requirement
|
160
180
|
requirements:
|
161
|
-
- -
|
181
|
+
- - ">="
|
162
182
|
- !ruby/object:Gem::Version
|
163
183
|
version: 1.9.3
|
164
184
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
165
185
|
requirements:
|
166
|
-
- -
|
186
|
+
- - ">="
|
167
187
|
- !ruby/object:Gem::Version
|
168
188
|
version: '0'
|
169
189
|
requirements: []
|
170
190
|
rubyforge_project:
|
171
|
-
rubygems_version: 2.
|
191
|
+
rubygems_version: 2.6.14
|
172
192
|
signing_key:
|
173
193
|
specification_version: 4
|
174
194
|
summary: Store SSL keys in a database
|
175
195
|
test_files:
|
176
196
|
- spec/encrypted_attributes_spec.rb
|
177
197
|
- spec/file_adapter_spec.rb
|
198
|
+
- spec/jwt_spec.rb
|
178
199
|
- spec/key_spec.rb
|
179
200
|
- spec/keystore_spec.rb
|
180
201
|
- spec/random_spec.rb
|
@@ -182,4 +203,3 @@ test_files:
|
|
182
203
|
- spec/slosilo_spec.rb
|
183
204
|
- spec/spec_helper.rb
|
184
205
|
- spec/symmetric_spec.rb
|
185
|
-
has_rdoc:
|