class Slosilo::Key
Constants
- DEFAULT_EXPIRATION
- JWT_ALGORITHM
- SIGNATURE_LEN
Attributes
Public Class Methods
# 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
# File lib/slosilo/key.rb, line 164 def == other to_der == other.to_der end
# File lib/slosilo/key.rb, line 26 def cipher @cipher ||= Slosilo::Symmetric.new end
# File lib/slosilo/key.rb, line 42 def decrypt ciphertext, skey key = @key.private_decrypt skey cipher.decrypt ciphertext, key: key end
# File lib/slosilo/key.rb, line 47 def decrypt_message ciphertext k, c = ciphertext.unpack("A256A*") decrypt c, k end
# 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
# File lib/slosilo/key.rb, line 37 def encrypt_message plaintext c, k = encrypt plaintext k + c end
# File lib/slosilo/key.rb, line 141 def err msg raise Error::TokenValidationError, msg, caller end
# File lib/slosilo/key.rb, line 160 def fingerprint @fingerprint ||= OpenSSL::Digest::SHA256.hexdigest key.public_key.to_der end
# File lib/slosilo/key.rb, line 170 def hash to_der.hash end
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
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
checks if the keypair contains a private key
# File lib/slosilo/key.rb, line 180 def private? @key.private? end
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
# File lib/slosilo/key.rb, line 60 def sign value sign_string(stringify value) end
# File lib/slosilo/key.rb, line 155 def sign_string value salt = shake_salt key.private_encrypt(hash_function.digest(salt + value)) + salt end
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
# File lib/slosilo/key.rb, line 56 def to_der @to_der ||= @key.to_der end
# File lib/slosilo/key.rb, line 52 def to_s @key.public_key.to_pem end
# 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 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
# 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
# File lib/slosilo/key.rb, line 214 def hash_function @hash_function ||= OpenSSL::Digest::SHA256 end
# File lib/slosilo/key.rb, line 210 def shake_salt Slosilo::Random::salt end
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