jwk 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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