Class: Slosilo::Key

Inherits:
Object
  • Object
show all
Defined in:
lib/slosilo/key.rb

Constant Summary collapse

SIGNATURE_LEN =
256
JWT_ALGORITHM =
'conjur.org/slosilo/v2'.freeze
DEFAULT_EXPIRATION =
8 * 60

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(raw_key = nil) ⇒ Key

Returns a new instance of Key.



10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/slosilo/key.rb', line 10

def initialize raw_key = nil
  @key = if raw_key.is_a? OpenSSL::PKey::RSA
    raw_key
  elsif !raw_key.nil?
    OpenSSL::PKey.read raw_key
  else
    OpenSSL::PKey::RSA.new 2048
  end
rescue OpenSSL::PKey::PKeyError => e
  # old openssl versions used to report ArgumentError
  # which arguably makes more sense here, so reraise as that
  raise ArgumentError, e, e.backtrace
end

Instance Attribute Details

#keyObject (readonly)

Returns the value of attribute key.



24
25
26
# File 'lib/slosilo/key.rb', line 24

def key
  @key
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?



164
165
166
# File 'lib/slosilo/key.rb', line 164

def == other
  to_der == other.to_der
end

#cipherObject



26
27
28
# File 'lib/slosilo/key.rb', line 26

def cipher
  @cipher ||= Slosilo::Symmetric.new
end

#decrypt(ciphertext, skey) ⇒ Object



42
43
44
45
# File 'lib/slosilo/key.rb', line 42

def decrypt ciphertext, skey
  key = @key.private_decrypt skey
  cipher.decrypt ciphertext, key: key
end

#decrypt_message(ciphertext) ⇒ Object



47
48
49
50
# File 'lib/slosilo/key.rb', line 47

def decrypt_message ciphertext
  k, c = ciphertext.unpack("A256A*")
  decrypt c, k
end

#encrypt(plaintext) ⇒ Object



30
31
32
33
34
35
# File 'lib/slosilo/key.rb', line 30

def encrypt plaintext
  key = cipher.random_key
  ctxt = cipher.encrypt plaintext, key: key
  key = @key.public_encrypt key
  [ctxt, key]
end

#encrypt_message(plaintext) ⇒ Object



37
38
39
40
# File 'lib/slosilo/key.rb', line 37

def encrypt_message plaintext
  c, k = encrypt plaintext
  k + c
end

#err(msg) ⇒ Object



141
142
143
# File 'lib/slosilo/key.rb', line 141

def err msg
  raise Error::TokenValidationError, msg, caller
end

#fingerprintObject



160
161
162
# File 'lib/slosilo/key.rb', line 160

def fingerprint
  @fingerprint ||= OpenSSL::Digest::SHA256.hexdigest key.public_key.to_der
end

#hashObject



170
171
172
# File 'lib/slosilo/key.rb', line 170

def hash
  to_der.hash
end

#issue_jwt(claims) ⇒ Object

Issue a JWT with the given claims. ‘iat` (issued at) claim is automatically added. Other interesting claims you can give are:

  • ‘sub` - token subject, for example a user name;

  • ‘exp` - expiration time (absolute);

  • ‘cidr` (Conjur extension) - array of CIDR masks that are accepted to make requests that bear this token



90
91
92
93
94
95
96
97
# File 'lib/slosilo/key.rb', line 90

def issue_jwt claims
  token = Slosilo::JWT.new claims
  token.add_signature \
      alg: JWT_ALGORITHM,
      kid: fingerprint,
      &method(:sign)
  token.freeze
end

#jwt_valid?(token) ⇒ Boolean

Validate a JWT.

Convenience method calling #validate_jwt and returning false if an exception is raised.

Parameters:

  • token (JWT)

    pre-parsed token to verify

Returns:

  • (Boolean)


117
118
119
120
121
122
# File 'lib/slosilo/key.rb', line 117

def jwt_valid? token
  validate_jwt token
  true
rescue
  false
end

#private?Boolean

checks if the keypair contains a private key

Returns:

  • (Boolean)


180
181
182
# File 'lib/slosilo/key.rb', line 180

def private?
  @key.private?
end

#publicObject

return a new key with just the public part of this



175
176
177
# File 'lib/slosilo/key.rb', line 175

def public
  Key.new(@key.public_key)
end

#sign(value) ⇒ Object



60
61
62
# File 'lib/slosilo/key.rb', line 60

def sign value
  sign_string(stringify value)
end

#sign_string(value) ⇒ Object



155
156
157
158
# File 'lib/slosilo/key.rb', line 155

def sign_string value
  salt = shake_salt
  key.private_encrypt(hash_function.digest(salt + value)) + salt
end

#signed_token(data) ⇒ Object

create a new timestamped and signed token carrying data



74
75
76
77
78
79
# File 'lib/slosilo/key.rb', line 74

def signed_token data
  token = { "data" => data, "timestamp" => Time.new.utc.to_s }
  token["signature"] = Base64::urlsafe_encode64(sign token)
  token["key"] = fingerprint
  token
end

#to_derObject



56
57
58
# File 'lib/slosilo/key.rb', line 56

def to_der
  @to_der ||= @key.to_der
end

#to_sObject



52
53
54
# File 'lib/slosilo/key.rb', line 52

def to_s
  @key.public_key.to_pem
end

#token_valid?(token, expiry = DEFAULT_EXPIRATION) ⇒ Boolean

Returns:

  • (Boolean)


101
102
103
104
105
106
107
108
# File 'lib/slosilo/key.rb', line 101

def token_valid? token, expiry = DEFAULT_EXPIRATION
  return jwt_valid? token if token.respond_to? :header
  token = token.clone
  expected_key = token.delete "key"
  return false if (expected_key and (expected_key != fingerprint))
  signature = Base64::urlsafe_decode64(token.delete "signature")
  (Time.parse(token["timestamp"]) + expiry > Time.now) && verify_signature(token, signature)
end

#validate_jwt(token) ⇒ Object

Note:

It’s the responsibility of the caller to examine other claims

Validate a JWT.

First checks whether algorithm is ‘conjur.org/slosilo/v2’ and the key id matches this key’s fingerprint. Then verifies if the token is not expired, as indicated by the ‘exp` claim; in its absence tokens are assumed to expire in `iat` + 8 minutes.

If those checks pass, finally the signature is verified.

included in the token; consideration needs to be given to handling unrecognized claims.

Parameters:

  • token (JWT)

    pre-parsed token to verify



140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/slosilo/key.rb', line 140

def validate_jwt token
  def err msg
    raise Error::TokenValidationError, msg, caller
  end

  header = token.header
  err 'unrecognized algorithm' unless header['alg'] == JWT_ALGORITHM
  err 'mismatched key' if (kid = header['kid']) && kid != fingerprint
  iat = Time.at token.claims['iat'] || err('unknown issuing time')
  exp = Time.at token.claims['exp'] || (iat + DEFAULT_EXPIRATION)
  err 'token expired' if exp <= Time.now
  err 'invalid signature' unless verify_signature token.string_to_sign, token.signature
  true
end

#verify_signature(data, signature) ⇒ Object



66
67
68
69
70
71
# File 'lib/slosilo/key.rb', line 66

def verify_signature data, signature
  signature, salt = signature.unpack("a#{SIGNATURE_LEN}a*")
  key.public_decrypt(signature) == hash_function.digest(salt + stringify(data))
rescue
  false
end