class Slosilo::Key

Constants

DEFAULT_EXPIRATION
JWT_ALGORITHM
SIGNATURE_LEN

Attributes

key[R]

Public Class Methods

new(raw_key = nil) click to toggle source
# 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

Public Instance Methods

==(other) click to toggle source
# File lib/slosilo/key.rb, line 164
def == other
  to_der == other.to_der
end
Also aliased as: eql?
cipher() click to toggle source
# File lib/slosilo/key.rb, line 26
def cipher
  @cipher ||= Slosilo::Symmetric.new
end
decrypt(ciphertext, skey) click to toggle source
# 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) click to toggle source
# File lib/slosilo/key.rb, line 47
def decrypt_message ciphertext
  k, c = ciphertext.unpack("A256A*")
  decrypt c, k
end
encrypt(plaintext) click to toggle source
# 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) click to toggle source
# File lib/slosilo/key.rb, line 37
def encrypt_message plaintext
  c, k = encrypt plaintext
  k + c
end
eql?(other)
Alias for: ==
err(msg) click to toggle source
# File lib/slosilo/key.rb, line 141
def err msg
  raise Error::TokenValidationError, msg, caller
end
fingerprint() click to toggle source
# File lib/slosilo/key.rb, line 160
def fingerprint
  @fingerprint ||= OpenSSL::Digest::SHA256.hexdigest key.public_key.to_der
end
hash() click to toggle source
# File lib/slosilo/key.rb, line 170
def hash
  to_der.hash
end
issue_jwt(claims) click to toggle source

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

# 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) click to toggle source

Validate a JWT.

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

@param token [JWT] pre-parsed token to verify @return [Boolean]

# File lib/slosilo/key.rb, line 117
def jwt_valid? token
  validate_jwt token
  true
rescue
  false
end
private?() click to toggle source

checks if the keypair contains a private key

# File lib/slosilo/key.rb, line 180
def private?
  @key.private?
end
public() click to toggle source

return a new key with just the public part of this

# File lib/slosilo/key.rb, line 175
def public
  Key.new(@key.public_key)
end
sign(value) click to toggle source
# File lib/slosilo/key.rb, line 60
def sign value
  sign_string(stringify value)
end
sign_string(value) click to toggle source
# 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) click to toggle source

create a new timestamped and signed token carrying data

# 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_der() click to toggle source
# File lib/slosilo/key.rb, line 56
def to_der
  @to_der ||= @key.to_der
end
to_s() click to toggle source
# File lib/slosilo/key.rb, line 52
def to_s
  @key.public_key.to_pem
end
token_valid?(token, expiry = DEFAULT_EXPIRATION) click to toggle source
# 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) click to toggle source

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.

@raises TokenValidationError if any of the checks fail.

@note It's the responsibility of the caller to examine other claims included in the token; consideration needs to be given to handling unrecognized claims.

@param token [JWT] pre-parsed token to verify

# 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) click to toggle source
# 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

Private Instance Methods

hash_function() click to toggle source
# File lib/slosilo/key.rb, line 214
def hash_function
  @hash_function ||= OpenSSL::Digest::SHA256
end
shake_salt() click to toggle source
# File lib/slosilo/key.rb, line 210
def shake_salt
  Slosilo::Random::salt
end
stringify(value) click to toggle source

Note that this is currently somewhat shallow stringification – to implement originating tokens we may need to make it deeper.

# File lib/slosilo/key.rb, line 188
def stringify value
  string = case value
  when Hash
    value.to_a.sort.to_json
  when String
    value
  else
    value.to_json
  end

  # Make sure that the string is ascii_8bit (i.e. raw bytes), and represents
  # the utf-8 encoding of the string.  This accomplishes two things: it normalizes
  # the representation of the string at the byte level (so we don't have an error if
  # one username is submitted as ISO-whatever, and the next as UTF-16), and it prevents
  # an incompatible encoding error when we concatenate it with the salt.
  if string.encoding != Encoding::ASCII_8BIT
    string.encode(Encoding::UTF_8).force_encoding(Encoding::ASCII_8BIT)
  else
    string
  end
end