jwk 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +15 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.rubocop.yml +16 -0
- data/.travis.yml +16 -0
- data/Gemfile +4 -0
- data/LICENSE.md +23 -0
- data/README.md +26 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/asn1.md +62 -0
- data/jwk.gemspec +23 -0
- data/lib/jwk.rb +15 -0
- data/lib/jwk/asn1.rb +101 -0
- data/lib/jwk/ec_key.rb +79 -0
- data/lib/jwk/key.rb +64 -0
- data/lib/jwk/oct_key.rb +38 -0
- data/lib/jwk/rsa_key.rb +91 -0
- data/lib/jwk/utils.rb +41 -0
- data/lib/jwk/version.rb +3 -0
- data/spec/jwk/asn1_spec.rb +70 -0
- data/spec/jwk/ec_key_spec.rb +84 -0
- data/spec/jwk/key_spec.rb +66 -0
- data/spec/jwk/oct_key_spec.rb +54 -0
- data/spec/jwk/rsa_key_spec.rb +92 -0
- data/spec/jwk_spec.rb +5 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/ec_private.json +7 -0
- data/spec/support/ec_private.pem +5 -0
- data/spec/support/ec_public.json +6 -0
- data/spec/support/oct.json +4 -0
- data/spec/support/rsa_partial.json +7 -0
- data/spec/support/rsa_private.json +11 -0
- data/spec/support/rsa_private.pem +27 -0
- data/spec/support/rsa_public.json +5 -0
- data/spec/support/rsa_public.pem +9 -0
- metadata +137 -0
data/lib/jwk/key.rb
ADDED
@@ -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
|
data/lib/jwk/oct_key.rb
ADDED
@@ -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
|
data/lib/jwk/rsa_key.rb
ADDED
@@ -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
|
data/lib/jwk/utils.rb
ADDED
@@ -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
|
data/lib/jwk/version.rb
ADDED
@@ -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
|