# encoding: binary # typed: strict # frozen_string_literal: true module Paseto module V3 # PASETOv3 `public` token interface providing asymmetric signature signing and verification of tokens. class Public < AsymmetricKey extend T::Sig extend T::Helpers final! # Size of (r || s) in an ECDSA secp384r1 signature SIGNATURE_BYTE_LEN = 96 # Size of r | s in an ECDSA secp384r1 signature SIGNATURE_PART_LEN = T.let(SIGNATURE_BYTE_LEN / 2, Integer) sig(:final) { override.returns(Protocol::Version3) } attr_reader :protocol # Create a new Public instance with a brand new EC key. sig(:final) { returns(T.attached_class) } def self.generate OpenSSL::PKey::EC.generate('secp384r1') .then(&:to_der) .then { |der| new(der) } end sig(:final) { params(bytes: String).returns(T.attached_class) } def self.from_public_bytes(bytes) ASN1.p384_public_bytes_to_spki_der(bytes) .then { |der| new(der) } end sig(:final) { params(bytes: String).returns(T.attached_class) } def self.from_scalar_bytes(bytes) ASN1.p384_scalar_bytes_to_oak_der(bytes) .then { |der| new(der) } end # `key` must be either a DER or PEM encoded secp384r1 key. # Encrypted PEM inputs are not supported. sig(:final) { params(key: String).void } def initialize(key) @key = T.let(OpenSSL::PKey::EC.new(key), OpenSSL::PKey::EC) @private = T.let(@key.private?, T::Boolean) raise LucidityError unless @key.group.curve_name == 'secp384r1' raise InvalidKeyPair unless custom_check_key @protocol = T.let(Protocol::Version3.new, Protocol::Version3) super rescue OpenSSL::PKey::ECError => e raise CryptoError, e.message end # Sign `message` and optional non-empty `footer` and return a Token. # The resulting token may be bound to a particular use by passing a non-empty `implicit_assertion`. sig(:final) { override.params(message: String, footer: String, implicit_assertion: String).returns(Token) } def sign(message:, footer: '', implicit_assertion: '') raise ArgumentError, 'no private key available' unless private? Util.pre_auth_encode(public_bytes, pae_header, message, footer, implicit_assertion) .then { |m2| protocol.digest(m2) } .then { |data| @key.sign_raw(nil, data) } .then { |sig_asn| ASN1::ECDSASignature.from_asn1(sig_asn) } .then { |ecdsa_sig| ecdsa_sig.to_rs(SIGNATURE_PART_LEN) } .then { |sig| Token.new(payload: "#{message}#{sig}", purpose: purpose, version: version, footer: footer) } rescue Encoding::CompatibilityError raise ParseError, 'invalid message encoding, must be UTF-8' end # Verify the signature of `token`, with an optional binding `implicit_assertion`. `token` must be a `v3.public` type Token. # Returns the verified payload if successful, otherwise raises an exception. sig(:final) { override.params(token: Token, implicit_assertion: String).returns(String) } def verify(token:, implicit_assertion: '') # rubocop:disable Metrics/AbcSize raise LucidityError unless header == token.header payload = token.raw_payload raise ParseError, 'message too short' if payload.bytesize < SIGNATURE_BYTE_LEN m = T.must(payload.slice(0, payload.bytesize - SIGNATURE_BYTE_LEN)) s = T.must(payload.slice(-SIGNATURE_BYTE_LEN, SIGNATURE_BYTE_LEN)) .then { |bytes| ASN1::ECDSASignature.from_rs(bytes, SIGNATURE_PART_LEN).to_der } Util.pre_auth_encode(public_bytes, pae_header, m, token.raw_footer, implicit_assertion) .then { |m2| protocol.digest(m2) } .then { |data| @key.verify_raw(nil, s, data) } .then { |valid| raise InvalidSignature unless valid } .then { m.encode(Encoding::UTF_8) } rescue Encoding::UndefinedConversionError raise ParseError, 'invalid payload encoding' end sig(:final) { override.returns(String) } def public_to_pem = @key.public_to_pem sig(:final) { override.returns(String) } def private_to_pem raise ArgumentError, 'no private key available' unless private? @key.to_pem end sig(:final) { override.returns(String) } def to_bytes raise ArgumentError, 'no private key available' unless private? @key.private_key.to_s(2).rjust(48, "\x00") end sig(:final) { override.returns(T::Boolean) } def private? = @private sig(:final) { override.returns(String) } def public_bytes = @key.public_key.to_octet_string(:compressed) sig(:final) { override.params(other: T.any(OpenSSL::PKey::EC, OpenSSL::PKey::EC::Point)).returns(String) } def ecdh(other) case other when OpenSSL::PKey::EC::Point @key.dh_compute_key(other) when OpenSSL::PKey::EC other.dh_compute_key(@key.public_key) end end private # TODO: Figure out how to get SimpleCov to cover this consistently. With OSSL1.1.1, most of # this doesn't run. With OSSL3, check_key never raises... # :nocov: # The openssl gem as of 3.0.0 will prefer EVP_PKEY_public_check over EC_KEY_check_key # whenever the EVP api is available, which is always for the library here as we're requiring # 3.0.0 or greater. However, this has some problems. # # The behavior of EVP_PKEY_public_check is different between 1.1.1 and 3.x. Specifically, # it no longer calls the custom verifier method in EVP_PKEY_METHOD, and only checks the # correctness of the public component. This leads to a problem when calling EC#key_check, # as the private component is NEVER verified for an ECDSA key through the APIs that the gem # makes available to us. # # Until this is fixed in ruby/openssl, I am working around this by implementing the algorithm # used by EVP_PKEY_pairwise_check through the OpenSSL API. # # BUG: https://github.com/ruby/openssl/issues/563 # https://www.openssl.org/docs/man1.1.1/man3/EVP_PKEY_public_check.html # https://www.openssl.org/docs/man3.0/man3/EVP_PKEY_public_check.html sig(:final) { returns(T::Boolean) } def custom_check_key begin @key.check_key rescue StandardError return false end return true unless private? && Util.openssl?(3) priv_key = @key.private_key group = @key.group # int ossl_ec_key_private_check(const EC_KEY *eckey) # { # ... # if (BN_cmp(eckey->priv_key, BN_value_one()) < 0 # || BN_cmp(eckey->priv_key, eckey->group->order) >= 0) { # ERR_raise(ERR_LIB_EC, EC_R_INVALID_PRIVATE_KEY); # return 0; # } # ... # } # # https://github.com/openssl/openssl/blob/5ac7cfb56211d18596e3c35baa942542f3c0189a/crypto/ec/ec_key.c#L510 # private keys must be in range [1, order-1] return false if priv_key < OpenSSL::BN.new(1) || priv_key > group.order # int ossl_ec_key_pairwise_check(const EC_KEY *eckey, BN_CTX *ctx) # { # ... # if (!EC_POINT_mul(eckey->group, point, eckey->priv_key, NULL, NULL, ctx)) { # ERR_raise(ERR_LIB_EC, ERR_R_EC_LIB); # goto err; # } # if (EC_POINT_cmp(eckey->group, point, eckey->pub_key, ctx) != 0) { # ERR_raise(ERR_LIB_EC, EC_R_INVALID_PRIVATE_KEY); # goto err; # } # ... # } # # https://github.com/openssl/openssl/blob/5ac7cfb56211d18596e3c35baa942542f3c0189a/crypto/ec/ec_key.c#L529 # Check generator * priv_key = pub_key @key.public_key == group.generator.mul(priv_key) end # :nocov: end end end