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.
- 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
|