diff --git a/lib/jose/jwk/kty.rb b/lib/jose/jwk/kty.rb index 5ca4b96..e84900c 100644 --- a/lib/jose/jwk/kty.rb +++ b/lib/jose/jwk/kty.rb @@ -2,11 +2,14 @@ module JOSE::JWK::KTY def self.from_key(object) object = object.__getobj__ if object.is_a?(JOSE::JWK::PKeyProxy) + case object + when OpenSSL::X509::Certificate + JOSE::JWK::KTY_X509.from_key(object) when OpenSSL::PKey::EC - return JOSE::JWK::KTY_EC.from_key(object) + JOSE::JWK::KTY_EC.from_key(object) when OpenSSL::PKey::RSA - return JOSE::JWK::KTY_RSA.from_key(object) + JOSE::JWK::KTY_RSA.from_key(object) else raise ArgumentError, "'object' is not a recognized key type: #{object.class.name}" end @@ -38,4 +41,5 @@ def self.key_encryptor(kty, fields, key) require 'jose/jwk/kty_okp_ed448ph' require 'jose/jwk/kty_okp_x25519' require 'jose/jwk/kty_okp_x448' +require 'jose/jwk/kty_x509' require 'jose/jwk/kty_rsa' diff --git a/lib/jose/jwk/kty_x509.rb b/lib/jose/jwk/kty_x509.rb new file mode 100644 index 0000000..c97501a --- /dev/null +++ b/lib/jose/jwk/kty_x509.rb @@ -0,0 +1,94 @@ +class JOSE::JWK::KTY_X509 < Struct.new(:key) + + def self.from_key(object) + object = object.__getobj__ if object.is_a?(JOSE::JWK::PKeyProxy) + case object + when OpenSSL::PKey::PKey + JOSE::JWK::KTY_X509.new(JOSE::JWK::PKeyProxy.new(object)) + when OpenSSL::X509::Certificate + JOSE::JWK::KTY_X509.new(JOSE::JWK::PKeyProxy.new(object.public_key)) + else + raise ArgumentError, "'object' is not a recognized key type: #{object.class.name}" + end + end + + def to_key + key.__getobj__ + end + + def to_map(fields) + { + 'kty' => 'RSA', + 'n' => JOSE.urlsafe_encode64(key.n.to_s(2)), + 'e' => JOSE.urlsafe_encode64(key.e.to_s(2)) + } + end + + def to_public_map(fields) + to_map(fields) + end + + def to_thumbprint_map(fields) + to_map(fields).slice('e', 'kty', 'n') + end + + def block_encryptor(fields = nil) + if fields && fields['use'] == 'enc' && !fields['alg'].nil? && !fields['enc'].nil? + JOSE::Map[ + 'alg' => fields['alg'], + 'enc' => fields['enc'] + ] + else + JOSE::Map[ + 'alg' => 'RSA-OAEP', + 'enc' => 'A128GCM' + ] + end + end + + def encrypt_public(plain_text, rsa_padding: :rsa_pkcs1_padding, rsa_oaep_md: nil) + case rsa_padding + when :rsa_pkcs1_padding + key.public_encrypt(plain_text, OpenSSL::PKey::RSA::PKCS1_PADDING) + when :rsa_pkcs1_oaep_padding + rsa_oaep_md ||= OpenSSL::Digest::SHA1 + key.public_encrypt(plain_text, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING) + else + raise ArgumentError, "unsupported RSA padding: #{rsa_padding.inspect}" + end + end + + def verify(message, digest_type, signature, padding: :rsa_pkcs1_padding) + case padding + when :rsa_pkcs1_padding + key.verify(digest_type.new, signature, message) + when :rsa_pkcs1_pss_padding + if key.respond_to?(:verify_pss) + digest_name = digest_type.new.name + key.verify_pss(digest_name, signature, message, salt_length: :digest, mgf1_hash: digest_name) + else + JOSE::JWA::PKCS1.rsassa_pss_verify(digest_type, message, signature, key) + end + else + raise ArgumentError, "unsupported RSA padding: #{padding.inspect}" + end + rescue OpenSSL::PKey::PKeyError + false + end + + def signer(fields = nil) + if fields && fields['use'] == 'sig' && !fields['alg'].nil? + JOSE::Map['alg' => fields['alg']] + else + JOSE::Map['alg' => 'RS256'] + end + end + + def verifier(fields) + if fields && fields['use'] == 'sig' && !fields['alg'].nil? + [fields['alg']] + else + ['PS256', 'PS384', 'PS512', 'RS256', 'RS384', 'RS512'] + end + end +end diff --git a/lib/jose/jwk/pem.rb b/lib/jose/jwk/pem.rb index a603d8a..9343566 100644 --- a/lib/jose/jwk/pem.rb +++ b/lib/jose/jwk/pem.rb @@ -1,19 +1,34 @@ module JOSE::JWK::PEM - extend self def from_binary(object, password = nil) - pkey = OpenSSL::PKey.read(object, password) - return JOSE::JWK::KTY.from_key(pkey) + begin + pkey = OpenSSL::PKey.read(object, password) + return JOSE::JWK::KTY.from_key(pkey) + rescue OpenSSL::PKey::PKeyError + begin + cert = OpenSSL::X509::Certificate.new(object) + return JOSE::JWK::KTY_X509.new(JOSE::JWK::PKeyProxy.new(cert.public_key)) + rescue OpenSSL::X509::CertificateError => e + raise RuntimeError, "Unsupported key type or incorrect password: #{e.message}" + end + end end def to_binary(key, password = nil) - if password - cipher = OpenSSL::Cipher.new('DES-EDE3-CBC') - return key.to_pem(cipher, password) - else + if key.is_a?(JOSE::JWK::PKeyProxy) + if password + cipher = OpenSSL::Cipher.new('DES-EDE3-CBC') + return key.to_pem(cipher, password) + else + return key.to_pem + end + elsif key.is_a?(OpenSSL::X509::Certificate) return key.to_pem + elsif key.is_a?(JOSE::JWK::PKeyProxy) + return key.__getobj__.to_pem + else + raise ArgumentError, "Unsupported key type: #{key.class}" end end - end diff --git a/test/jose/jwk/kty_x509_test.rb b/test/jose/jwk/kty_x509_test.rb new file mode 100644 index 0000000..f78aa90 --- /dev/null +++ b/test/jose/jwk/kty_x509_test.rb @@ -0,0 +1,72 @@ +require 'test_helper' + +class JOSE::JWK::KTY_X509Test < Minitest::Test + def test_from_key_and_to_key + x509_pem_data = <<~PEM + -----BEGIN CERTIFICATE----- + MIIDxzCCAq+gAwIBAgIUXm1i9UarQZwGQ3MaNarRSUZbwVAwDQYJKoZIhvcNAQEL + BQAwczELMAkGA1UEBhMCQlIxCzAJBgNVBAgMAlJTMQwwCgYDVQQHDANQT0ExDTAL + BgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxDTALBgNVBAMMBHRlc3QxHDAaBgkq + hkiG9w0BCQEWDXRlc3RAdGVzdC5jb20wHhcNMjQwNTMxMjIyNDI3WhcNMjUwNTMx + MjIyNDI3WjBzMQswCQYDVQQGEwJCUjELMAkGA1UECAwCUlMxDDAKBgNVBAcMA1BP + QTENMAsGA1UECgwEdGVzdDENMAsGA1UECwwEdGVzdDENMAsGA1UEAwwEdGVzdDEc + MBoGCSqGSIb3DQEJARYNdGVzdEB0ZXN0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD + ggEPADCCAQoCggEBALsMvAFvOJtRO8rwF6QwJ/UfhhJ+JHtLdycfLrR/wCW0acHr + MqMeoTKJcRsyQTTQCjO7r1QScv+xaEQohBE6Vq8O8rD38xZn4lesCq3mSf2mUi3k + etZv1pFKU/lN7SceUG65/vVAMoj9HEFZ43WfpDkYFvFDw2dR+vkcSp5SWQW8JxrA + nuGSP+1E57cAsoPHcWPgYBe5y8ndOQREikpOkKUbCcDN5mrg0Y0kUHboXm18jKeW + dCejOj9z0DS1mFqpE8sG4Khv0aL7kAzwQb8vNVoMog3R+qqgv61e3U6BAKyU3k0w + Xet9tyAgofHscO4QEo6ThELTFPgHnvD9DaclKZ8CAwEAAaNTMFEwHQYDVR0OBBYE + FE8ld9J8T8FPZtudE6emFcwOL1MiMB8GA1UdIwQYMBaAFE8ld9J8T8FPZtudE6em + FcwOL1MiMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIhtTzS3 + yoUnKYhIqUIRs5Dp+iwXjAjx9J5kR1cjfBuNGOKvJIqPTkcL7wu9fEG0eR8wsuWD + kYbwNk07fc03Gx/44COH0jIwn8XsDffj3ITA0gU8p9Ee6I/f2jBUQ2SvyC+lr6lc + YJKY3aNi+osMhXOVOguOl6DsxvvmGCI26BMpZueu1WXBEcMNCH/lgFwzNC6HHVKu + kxvYEOrhcgodA5kiOFltgZdwqb1Q7EBFFn1rKPQFvc2XVlJrubyiOXalcwWJ/REa + o0bTj132BSdfkPF2l1rZBQM2pzPg0U7DiTvEa6yMaj4IN8Gv140ogF0niyDegElt + ls8R0jfIBZj2N0I= + -----END CERTIFICATE----- + PEM + + x509_cert = OpenSSL::X509::Certificate.new(x509_pem_data) + x509_key = x509_cert.public_key + x509_jwk = JOSE::JWK::KTY_X509.new(JOSE::JWK::PKeyProxy.new(x509_key)) + assert_equal x509_key.to_pem.strip, x509_jwk.key.__getobj__.to_pem.strip + end + + def test_from_binary_and_to_binary + x509_pem_data = <<~PEM + -----BEGIN CERTIFICATE----- + MIIDxzCCAq+gAwIBAgIUXm1i9UarQZwGQ3MaNarRSUZbwVAwDQYJKoZIhvcNAQEL + BQAwczELMAkGA1UEBhMCQlIxCzAJBgNVBAgMAlJTMQwwCgYDVQQHDANQT0ExDTAL + BgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxDTALBgNVBAMMBHRlc3QxHDAaBgkq + hkiG9w0BCQEWDXRlc3RAdGVzdC5jb20wHhcNMjQwNTMxMjIyNDI3WhcNMjUwNTMx + MjIyNDI3WjBzMQswCQYDVQQGEwJCUjELMAkGA1UECAwCUlMxDDAKBgNVBAcMA1BP + QTENMAsGA1UECgwEdGVzdDENMAsGA1UECwwEdGVzdDENMAsGA1UEAwwEdGVzdDEc + MBoGCSqGSIb3DQEJARYNdGVzdEB0ZXN0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD + ggEPADCCAQoCggEBALsMvAFvOJtRO8rwF6QwJ/UfhhJ+JHtLdycfLrR/wCW0acHr + MqMeoTKJcRsyQTTQCjO7r1QScv+xaEQohBE6Vq8O8rD38xZn4lesCq3mSf2mUi3k + etZv1pFKU/lN7SceUG65/vVAMoj9HEFZ43WfpDkYFvFDw2dR+vkcSp5SWQW8JxrA + nuGSP+1E57cAsoPHcWPgYBe5y8ndOQREikpOkKUbCcDN5mrg0Y0kUHboXm18jKeW + dCejOj9z0DS1mFqpE8sG4Khv0aL7kAzwQb8vNVoMog3R+qqgv61e3U6BAKyU3k0w + Xet9tyAgofHscO4QEo6ThELTFPgHnvD9DaclKZ8CAwEAAaNTMFEwHQYDVR0OBBYE + FE8ld9J8T8FPZtudE6emFcwOL1MiMB8GA1UdIwQYMBaAFE8ld9J8T8FPZtudE6em + FcwOL1MiMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIhtTzS3 + yoUnKYhIqUIRs5Dp+iwXjAjx9J5kR1cjfBuNGOKvJIqPTkcL7wu9fEG0eR8wsuWD + kYbwNk07fc03Gx/44COH0jIwn8XsDffj3ITA0gU8p9Ee6I/f2jBUQ2SvyC+lr6lc + YJKY3aNi+osMhXOVOguOl6DsxvvmGCI26BMpZueu1WXBEcMNCH/lgFwzNC6HHVKu + kxvYEOrhcgodA5kiOFltgZdwqb1Q7EBFFn1rKPQFvc2XVlJrubyiOXalcwWJ/REa + o0bTj132BSdfkPF2l1rZBQM2pzPg0U7DiTvEa6yMaj4IN8Gv140ogF0niyDegElt + ls8R0jfIBZj2N0I= + -----END CERTIFICATE----- + PEM + + x509_cert = OpenSSL::X509::Certificate.new(x509_pem_data) + x509_key = x509_cert.public_key + x509_jwk = JOSE::JWK::KTY_X509.new(JOSE::JWK::PKeyProxy.new(x509_key)) + + from_binary_jwk = JOSE::JWK::KTY_X509.new(JOSE::JWK::PKeyProxy.new(OpenSSL::X509::Certificate.new(x509_pem_data).public_key)) + + assert_equal x509_jwk.key.__getobj__.to_pem.strip, from_binary_jwk.key.__getobj__.to_pem.strip + end +end diff --git a/test/jose/jwk/pem_test.rb b/test/jose/jwk/pem_test.rb index 979cafa..c47154f 100644 --- a/test/jose/jwk/pem_test.rb +++ b/test/jose/jwk/pem_test.rb @@ -1,6 +1,9 @@ require 'test_helper' class JOSE::JWK::PEMTest < Minitest::Test + def normalize_pem(pem) + pem.strip.gsub("\r\n", "\n").gsub("\n", "\n") + end def test_from_pem_and_to_pem ec_pem_data = \ @@ -55,4 +58,43 @@ def test_from_pem_and_to_pem assert_equal rsa_pem, JOSE::JWK.from_pem(encrypted_rsa_pem_data, rsa_pem_password) end + def sanitize_pem(pem) + pem.gsub(/\s+/, '').gsub(/\n/, '') + end + + def test_from_pem_and_to_pem_x509 + x509_pem_data = <<~PEM + -----BEGIN CERTIFICATE----- + MIIDxzCCAq+gAwIBAgIUXm1i9UarQZwGQ3MaNarRSUZbwVAwDQYJKoZIhvcNAQEL + BQAwczELMAkGA1UEBhMCQlIxCzAJBgNVBAgMAlJTMQwwCgYDVQQHDANQT0ExDTAL + BgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxDTALBgNVBAMMBHRlc3QxHDAaBgkq + hkiG9w0BCQEWDXRlc3RAdGVzdC5jb20wHhcNMjQwNTMxMjIyNDI3WhcNMjUwNTMx + MjIyNDI3WjBzMQswCQYDVQQGEwJCUjELMAkGA1UECAwCUlMxDDAKBgNVBAcMA1BP + QTENMAsGA1UECgwEdGVzdDENMAsGA1UECwwEdGVzdDENMAsGA1UEAwwEdGVzdDEc + MBoGCSqGSIb3DQEJARYNdGVzdEB0ZXN0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD + ggEPADCCAQoCggEBALsMvAFvOJtRO8rwF6QwJ/UfhhJ+JHtLdycfLrR/wCW0acHr + MqMeoTKJcRsyQTTQCjO7r1QScv+xaEQohBE6Vq8O8rD38xZn4lesCq3mSf2mUi3k + etZv1pFKU/lN7SceUG65/vVAMoj9HEFZ43WfpDkYFvFDw2dR+vkcSp5SWQW8JxrA + nuGSP+1E57cAsoPHcWPgYBe5y8ndOQREikpOkKUbCcDN5mrg0Y0kUHboXm18jKeW + dCejOj9z0DS1mFqpE8sG4Khv0aL7kAzwQb8vNVoMog3R+qqgv61e3U6BAKyU3k0w + Xet9tyAgofHscO4QEo6ThELTFPgHnvD9DaclKZ8CAwEAAaNTMFEwHQYDVR0OBBYE + FE8ld9J8T8FPZtudE6emFcwOL1MiMB8GA1UdIwQYMBaAFE8ld9J8T8FPZtudE6em + FcwOL1MiMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIhtTzS3 + yoUnKYhIqUIRs5Dp+iwXjAjx9J5kR1cjfBuNGOKvJIqPTkcL7wu9fEG0eR8wsuWD + kYbwNk07fc03Gx/44COH0jIwn8XsDffj3ITA0gU8p9Ee6I/f2jBUQ2SvyC+lr6lc + YJKY3aNi+osMhXOVOguOl6DsxvvmGCI26BMpZueu1WXBEcMNCH/lgFwzNC6HHVKu + kxvYEOrhcgodA5kiOFltgZdwqb1Q7EBFFn1rKPQFvc2XVlJrubyiOXalcwWJ/REa + o0bTj132BSdfkPF2l1rZBQM2pzPg0U7DiTvEa6yMaj4IN8Gv140ogF0niyDegElt + ls8R0jfIBZj2N0I= + -----END CERTIFICATE----- + PEM + + x509_cert = OpenSSL::X509::Certificate.new(x509_pem_data) + x509_key = x509_cert.public_key + x509_jwk = JOSE::JWK::KTY_X509.new(JOSE::JWK::PKeyProxy.new(x509_key)) + + from_binary_jwk = JOSE::JWK::KTY_X509.new(JOSE::JWK::PKeyProxy.new(OpenSSL::X509::Certificate.new(x509_pem_data).public_key)) + + assert_equal x509_jwk.key.__getobj__.to_pem.strip, from_binary_jwk.key.__getobj__.to_pem.strip + end end