jwk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,64 @@
1
+ module JWK
2
+ class Key
3
+ class << self
4
+ def from_pem(pem)
5
+ key = OpenSSL::PKey.read(pem)
6
+ from_openssl(key)
7
+ end
8
+
9
+ def from_openssl(key)
10
+ if key.is_a?(OpenSSL::PKey::RSA)
11
+ RSAKey.from_openssl(key)
12
+ elsif key.is_a?(OpenSSL::PKey::EC)
13
+ ECKey.from_openssl(key)
14
+ end
15
+ end
16
+
17
+ def from_json(json)
18
+ key = JSON.parse(json)
19
+ validate_kty!(key['kty'])
20
+
21
+ case key['kty']
22
+ when 'EC'
23
+ ECKey.new(key)
24
+ when 'RSA'
25
+ RSAKey.new(key)
26
+ when 'oct'
27
+ OctKey.new(key)
28
+ end
29
+ end
30
+
31
+ def validate_kty!(kty)
32
+ unless %w[EC RSA oct].include?(kty)
33
+ raise JWK::InvalidKey, "The provided JWK has an unknown \"kty\" value: #{kty}."
34
+ end
35
+ end
36
+ end
37
+
38
+ def to_json
39
+ @key.to_json
40
+ end
41
+
42
+ %w[kty use key_ops alg kid x5u x5c x5t].each do |part|
43
+ define_method(part) do
44
+ @key[part]
45
+ end
46
+ end
47
+
48
+ def x5t_s256
49
+ @key['x5t#S256']
50
+ end
51
+
52
+ protected
53
+
54
+ def pem_base64(content)
55
+ Base64.strict_encode64(content).scan(/.{1,64}/).join("\n")
56
+ end
57
+
58
+ def generate_pem(header, asn)
59
+ "-----BEGIN #{header} KEY-----\n" +
60
+ pem_base64(asn) +
61
+ "\n-----END #{header} KEY-----\n"
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,38 @@
1
+ require 'jwk/key'
2
+
3
+ module JWK
4
+ class OctKey < Key
5
+ def initialize(key)
6
+ @key = key
7
+ validate
8
+ end
9
+
10
+ def public?
11
+ true
12
+ end
13
+
14
+ def private?
15
+ true
16
+ end
17
+
18
+ def validate
19
+ raise JWK::InvalidKey, 'Invalid RSA key.' unless @key['k']
20
+ end
21
+
22
+ def to_pem
23
+ raise NotImplementedError, 'Oct Keys cannot be converted to PEM.'
24
+ end
25
+
26
+ def to_openssl_key
27
+ raise NotImplementedError, 'Oct Keys cannot be converted to OpenSSL::PKey.'
28
+ end
29
+
30
+ def to_s
31
+ k
32
+ end
33
+
34
+ def k
35
+ Utils.decode_ub64(@key['k'])
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,91 @@
1
+ require 'jwk/key'
2
+
3
+ module JWK
4
+ class RSAKey < Key
5
+ def initialize(key)
6
+ @key = key
7
+ validate
8
+ end
9
+
10
+ def public?
11
+ true
12
+ end
13
+
14
+ def private?
15
+ !@key['d'].nil?
16
+ end
17
+
18
+ def validate
19
+ raise JWK::InvalidKey, 'Invalid RSA key.' unless @key['n'] && @key['e']
20
+ end
21
+
22
+ def to_pem
23
+ asn = to_asn
24
+
25
+ if private?
26
+ generate_pem('RSA PRIVATE', asn)
27
+ else
28
+ generate_pem('PUBLIC', asn)
29
+ end
30
+ end
31
+
32
+ def to_openssl_key
33
+ OpenSSL::PKey.read(to_pem)
34
+ end
35
+
36
+ def to_s
37
+ to_pem
38
+ end
39
+
40
+ %w[n e d p q dp dq qi].each do |part|
41
+ define_method(part) do
42
+ Utils.decode_ub64_int(@key[part]) if @key[part]
43
+ end
44
+ end
45
+
46
+ class << self
47
+ def from_openssl(k)
48
+ if k.private?
49
+ key = { 'kty' => 'RSA' }.merge(key_params(k, 'n', 'e', 'd', 'p', 'q', 'dmp1', 'dmq1', 'iqmp'))
50
+ key['dp'] = key.delete('dmp1')
51
+ key['dq'] = key.delete('dmq1')
52
+ key['qi'] = key.delete('iqmp')
53
+ else
54
+ key = { 'kty' => 'RSA' }.merge(key_params(k, 'n', 'e'))
55
+ end
56
+
57
+ new(key)
58
+ end
59
+
60
+ private
61
+
62
+ def key_params(key, *params)
63
+ Hash[params.map do |p|
64
+ [p, Utils.encode_ub64_int(key.params[p].to_i)]
65
+ end]
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def to_asn
72
+ if private?
73
+ unless full_private?
74
+ raise NotImplementedError, 'Cannot convert RSA private key to PEM. Missing key data.'
75
+ end
76
+
77
+ ASN1.rsa_private_key(*key_parts)
78
+ elsif public?
79
+ ASN1.rsa_public_key(n, e)
80
+ end
81
+ end
82
+
83
+ def full_private?
84
+ @key['d'] && @key['p'] && @key['q'] && @key['dp'] && @key['dq'] && @key['qi']
85
+ end
86
+
87
+ def key_parts
88
+ [n, e, d, p, q, dp, dq, qi]
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,41 @@
1
+ module JWK
2
+ module Utils
3
+ class << self
4
+ def hex_string_to_binary(s)
5
+ s.scan(/.{2}/).map { |n| n.to_i(16).chr }.join
6
+ end
7
+
8
+ def int_to_binary(n)
9
+ num_octets = (n.to_s(16).length / 2.0).ceil
10
+
11
+ shifted = n << 8
12
+ Array.new(num_octets) do
13
+ ((shifted >>= 8) & 0xFF).chr
14
+ end.join.reverse
15
+ end
16
+
17
+ def binary_to_int(s)
18
+ s.chars.inject(0) do |val, char|
19
+ (val << 8) | char[0].ord
20
+ end
21
+ end
22
+
23
+ def decode_ub64(data)
24
+ clean = data.gsub(/[[:space:]]/, '')
25
+
26
+ len = clean.length
27
+ padded = (len % 4).zero? ? clean : clean + '=' * (4 - len % 4)
28
+
29
+ Base64.urlsafe_decode64(padded)
30
+ end
31
+
32
+ def decode_ub64_int(data)
33
+ Utils.binary_to_int(decode_ub64(data))
34
+ end
35
+
36
+ def encode_ub64_int(n)
37
+ Base64.urlsafe_encode64(Utils.int_to_binary(n))
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module JWK
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,70 @@
1
+ describe JWK::ASN1 do
2
+ describe '.rsa_public_key' do
3
+ let(:known_asn) do
4
+ Base64.decode64('MBowDQYJKoZIhvcNAQEBBQADCQAwBgIBAQIBAg==')
5
+ end
6
+
7
+ let(:bignum) do
8
+ ([0x80FFFFFF] * 56).inject(0) { |a, n| (a << 32) | n }
9
+ end
10
+
11
+ let(:known_big_asn) do
12
+ Base64.decode64('MIH9MA0GCSqGSIb3DQEBAQUAA4HrADCB5wIBAQKB4QCA////gP///4D///+A
13
+ ////gP///4D///+A////gP///4D///+A////gP///4D///+A////gP///4D/
14
+ //+A////gP///4D///+A////gP///4D///+A////gP///4D///+A////gP//
15
+ /4D///+A////gP///4D///+A////gP///4D///+A////gP///4D///+A////
16
+ gP///4D///+A////gP///4D///+A////gP///4D///+A////gP///4D///+A
17
+ ////gP///4D///+A////gP///4D///+A////gP///w==')
18
+ end
19
+
20
+ it 'generates valid ASN1 for a Generic Public Key of type RSA' do
21
+ result = JWK::ASN1.rsa_public_key(1, 2)
22
+ expect(result).to eq(known_asn)
23
+ end
24
+
25
+ it 'handles big values numbers correctly' do
26
+ result = JWK::ASN1.rsa_public_key(1, bignum)
27
+ expect(result).to eq(known_big_asn)
28
+ end
29
+ end
30
+
31
+ describe '.rsa_private_key' do
32
+ let(:known_asn) do
33
+ Base64.decode64('MBwCAQACAQECAQICAQMCAQQCAQUCAQYCAQcCAgCA')
34
+ end
35
+
36
+ it 'generates valid ASN1 for an RSA Private Key' do
37
+ result = JWK::ASN1.rsa_private_key(1, 2, 3, 4, 5, 6, 7, 0x80)
38
+ expect(result).to eq(known_asn)
39
+ end
40
+ end
41
+
42
+ describe '.ec_private_key' do
43
+ let(:known_p256_asn) do
44
+ Base64.decode64('MBoCAQEEAaCgCgYIKoZIzj0DAQehBgMEAAQCAw==')
45
+ end
46
+
47
+ let(:known_p384_asn) do
48
+ Base64.decode64('MBcCAQEEAaCgBwYFK4EEACKhBgMEAAQCAw==')
49
+ end
50
+
51
+ let(:known_p521_asn) do
52
+ Base64.decode64('MBcCAQEEAaCgBwYFK4EEACOhBgMEAAQCAw==')
53
+ end
54
+
55
+ it 'generates valid ASN1 for a P-256 EC Private Key' do
56
+ result = JWK::ASN1.ec_private_key('P-256', 0xA0, 2, 3)
57
+ expect(result).to eq(known_p256_asn)
58
+ end
59
+
60
+ it 'generates valid ASN1 for a P-384 EC Private Key' do
61
+ result = JWK::ASN1.ec_private_key('P-384', 0xA0, 2, 3)
62
+ expect(result).to eq(known_p384_asn)
63
+ end
64
+
65
+ it 'generates valid ASN1 for a P-521 EC Private Key' do
66
+ result = JWK::ASN1.ec_private_key('P-521', 0xA0, 2, 3)
67
+ expect(result).to eq(known_p521_asn)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,84 @@
1
+ describe JWK::ECKey do
2
+ let(:private_jwk) do
3
+ File.read('spec/support/ec_private.json')
4
+ end
5
+
6
+ let(:public_jwk) do
7
+ File.read('spec/support/ec_public.json')
8
+ end
9
+
10
+ let(:private_pem) do
11
+ File.read('spec/support/ec_private.pem')
12
+ end
13
+
14
+ describe '#initialize' do
15
+ it 'raises with invalid parameters' do
16
+ expect { JWK::Key.from_json('{"kty":"EC","crv":"P-256"}') }.to raise_error(JWK::InvalidKey)
17
+ end
18
+ end
19
+
20
+ describe '#to_pem' do
21
+ it 'converts private keys to the right format' do
22
+ key = JWK::Key.from_json(private_jwk)
23
+ expect(key.to_pem).to eq private_pem
24
+ end
25
+
26
+ it 'raises with public keys' do
27
+ key = JWK::Key.from_json(public_jwk)
28
+ expect { key.to_pem }.to raise_error NotImplementedError
29
+ end
30
+ end
31
+
32
+ describe '#to_s' do
33
+ it 'converts to pem' do
34
+ key = JWK::Key.from_json(private_jwk)
35
+ expect(key.to_s).to eq(key.to_pem)
36
+ end
37
+ end
38
+
39
+ describe '#to_openssl_key' do
40
+ it 'converts the private key to an openssl object' do
41
+ key = JWK::Key.from_json(private_jwk)
42
+
43
+ begin
44
+ expect(key.to_openssl_key).to be_a OpenSSL::PKey::EC
45
+ rescue Exception => e
46
+ # This is expected to fail on old jRuby versions
47
+ raise e unless defined?(JRUBY_VERSION)
48
+ end
49
+ end
50
+ end
51
+
52
+ describe '#to_json' do
53
+ it 'responds with the JWK JSON key' do
54
+ key = JWK::Key.from_json(private_jwk)
55
+ expect(JSON.parse(key.to_json)).to eq JSON.parse(private_jwk)
56
+ end
57
+ end
58
+
59
+ describe '#kty' do
60
+ it 'equals EC' do
61
+ key = JWK::Key.from_json(private_jwk)
62
+ expect(key.kty).to eq 'EC'
63
+ end
64
+ end
65
+
66
+ describe '#public?' do
67
+ it 'is true' do
68
+ key = JWK::Key.from_json(private_jwk)
69
+ expect(key.public?).to be_truthy
70
+ end
71
+ end
72
+
73
+ describe '#private?' do
74
+ it 'is true for private keys' do
75
+ key = JWK::Key.from_json(private_jwk)
76
+ expect(key.private?).to be_truthy
77
+ end
78
+
79
+ it 'is false for public keys' do
80
+ key = JWK::Key.from_json(public_jwk)
81
+ expect(key.private?).to be_falsey
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,66 @@
1
+ describe JWK::Key do
2
+ describe '.from_json' do
3
+ it 'raises for invalid kty' do
4
+ expect { JWK::Key.from_json('{"kty":"my-key-type"}') }.to raise_error JWK::InvalidKey
5
+ end
6
+ end
7
+
8
+ describe '.from_openssl' do
9
+ it 'creates an RSAKey for RSA keys' do
10
+ key = OpenSSL::PKey::RSA.new(2048)
11
+ jwk = JWK::Key.from_openssl(key)
12
+
13
+ expect(jwk).to be_a JWK::RSAKey
14
+ end
15
+
16
+ it 'creates an RSAKey for RSA keys that resolves to the same parameters' do
17
+ key = OpenSSL::PKey::RSA.new(2048)
18
+ jwk = JWK::Key.from_openssl(key)
19
+
20
+ expect(jwk.to_pem).to eq key.to_pem
21
+ end
22
+
23
+ it 'creates an RSAKey for RSA public keys that resolves to the same parameters' do
24
+ key = OpenSSL::PKey::RSA.new(2048).public_key
25
+ jwk = JWK::Key.from_openssl(key)
26
+
27
+ expect(jwk.to_pem).to eq key.to_pem
28
+ end
29
+
30
+ it 'creates an ECKey for EC keys' do
31
+ begin
32
+ key = OpenSSL::PKey::EC.new('secp384r1')
33
+ key.generate_key
34
+ jwk = JWK::Key.from_openssl(key)
35
+
36
+ expect(jwk).to be_a JWK::ECKey
37
+ rescue NameError => e
38
+ raise e unless defined?(JRUBY_VERSION)
39
+ end
40
+ end
41
+
42
+ # jRuby 9k OpenSSL generates a bad PEM file with private key only, skipping
43
+ # the public part. This is in contrast with all other OpenSSL implementations.
44
+ # And it makes this test fail.
45
+ it 'creates an ECKey for EC keys that resolves to the same parameters' do
46
+ begin
47
+ key = OpenSSL::PKey::EC.new('secp384r1')
48
+ key.generate_key
49
+ jwk = JWK::Key.from_openssl(key)
50
+
51
+ expect(jwk.to_pem).to eq key.to_pem unless defined?(JRUBY_VERSION)
52
+ rescue NameError => e
53
+ raise e unless defined?(JRUBY_VERSION)
54
+ end
55
+ end
56
+ end
57
+
58
+ describe '.from_pem' do
59
+ it 'generates an RSAKey for RSA Keys' do
60
+ pem = OpenSSL::PKey::RSA.new(2048).to_pem
61
+ jwk = JWK::Key.from_pem(pem)
62
+
63
+ expect(jwk).to be_a JWK::RSAKey
64
+ end
65
+ end
66
+ end