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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1575c0b94d636319a7465b52c562595fb9879116
|
4
|
+
data.tar.gz: 3a75443a7e7f83443309de1746c661a26c351f4a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dd408f90856457824da0a2f522c0609e404ea794825ffd3354318a0568fad0739c068b860f6d7cd839db456d5b61526940ffed13982211ee2f5958721d9bd976
|
7
|
+
data.tar.gz: dc057987055f26dc8fbc3c533eb93e9321a8023d13825c75b40dcfab0a3b586dc620f20522f34f23d4c034bf7cb94468ff2019aacb6ab44c6c1bffbff44e889f
|
data/.codeclimate.yml
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
language: ruby
|
2
|
+
dist: trusty
|
3
|
+
rvm:
|
4
|
+
- jruby-1.7.26
|
5
|
+
- jruby-9.1.9.0
|
6
|
+
- 2.0.0-p648
|
7
|
+
- 2.1.10
|
8
|
+
- 2.2.8
|
9
|
+
- 2.3.5
|
10
|
+
|
11
|
+
matrix:
|
12
|
+
include:
|
13
|
+
- rvm: 2.4.2
|
14
|
+
env: CODECLIMATE_REPO_TOKEN=5d5a47900abd78d4b430435e4948a78c9b065c20e2f4be4ecb0a084413d75ca0
|
15
|
+
after_script:
|
16
|
+
- bundle exec codeclimate-test-reporter
|
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
=====================
|
3
|
+
|
4
|
+
* Copyright © 2017 Francesco Boffa
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
a copy of this software and associated documentation files (the
|
8
|
+
"Software"), to deal in the Software without restriction, including
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
20
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
21
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
22
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
23
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# JWK
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/aomega08/jwk.svg)](https://travis-ci.org/aomega08/jwk)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/aomega08/jwk/badges/gpa.svg)](https://codeclimate.com/github/aomega08/jwk)
|
5
|
+
[![Test Coverage](https://codeclimate.com/github/aomega08/jwk/badges/coverage.svg)](https://codeclimate.com/github/aomega08/jwk/coverage)
|
6
|
+
|
7
|
+
A Ruby implementation of the RFC 7517 JSON Web Keys (JWK) standard.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
$ gem install jwk
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
TODO: Write usage instructions here
|
16
|
+
|
17
|
+
## License
|
18
|
+
|
19
|
+
The MIT License
|
20
|
+
|
21
|
+
Copyright © 2017 Francesco Boffa
|
22
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
23
|
+
|
24
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
25
|
+
|
26
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'jwk'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/docs/asn1.md
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# DER Encoded ASN.1
|
2
|
+
|
3
|
+
* Everything is Big Endian.
|
4
|
+
* Every encoded item is prefixed by a tag that specifies its nature followed by its length and data.
|
5
|
+
* If lengths are < 128 they are just added to the stream as a single byte. Lengths > 127 are prefixed by (0x80 | bytelength of length).
|
6
|
+
* If Tags have 0x80 bit set, they're "custom". If 0xA0, they're custom, but structured like a SEQUENCE.
|
7
|
+
|
8
|
+
### Notable tags:
|
9
|
+
|
10
|
+
* **0x30** SEQUENCE (array of non-uniform objects)
|
11
|
+
* **0x02** INTEGER
|
12
|
+
* **0x03** BIT STRING (for some reason prefixed by 00)
|
13
|
+
* **0x04** OCTET STRING (similar to BIT STRING but not prefixed by 00)
|
14
|
+
* **0x05** NULL
|
15
|
+
* **0x06** OBJECT IDENTIFIER
|
16
|
+
|
17
|
+
## Generic Public Key format:
|
18
|
+
|
19
|
+
SEQUENCE
|
20
|
+
SEQUENCE
|
21
|
+
OBJECT IDENTIFIER
|
22
|
+
NULL
|
23
|
+
BIT STRING
|
24
|
+
(Key Type dependent)
|
25
|
+
|
26
|
+
## RSA Public Key format:
|
27
|
+
|
28
|
+
Can be found in the wild, but it is usually embedded in the Generic Public Key format.
|
29
|
+
|
30
|
+
SEQUENCE
|
31
|
+
INTEGER n
|
32
|
+
INTEGER e
|
33
|
+
|
34
|
+
## RSA Private Key format:
|
35
|
+
|
36
|
+
SEQUENCE (0x30)
|
37
|
+
INTEGER 0
|
38
|
+
INTEGER n
|
39
|
+
INTEGER e
|
40
|
+
INTEGER d
|
41
|
+
INTEGER p
|
42
|
+
INTEGER q
|
43
|
+
INTEGER dp
|
44
|
+
INTEGER dq
|
45
|
+
INTEGER qi
|
46
|
+
|
47
|
+
## EC Private Key format:
|
48
|
+
|
49
|
+
SEQUENCE
|
50
|
+
INTEGER 1
|
51
|
+
OCTET STRING d
|
52
|
+
(custom structure, 0xA0)
|
53
|
+
OBJECT IDENTIFIER
|
54
|
+
(custom structure, 0xA1)
|
55
|
+
BIT STRING (0x04 followed by x and y)
|
56
|
+
|
57
|
+
## Common Object IDs
|
58
|
+
|
59
|
+
RSA: 1.2.840.113549.1.1.1
|
60
|
+
EC P-256: 1.2.840.10045.3.1.7
|
61
|
+
EC P-384: 1.3.132.0.34
|
62
|
+
EC P-521: 1.3.132.0.35
|
data/jwk.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'jwk/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'jwk'
|
7
|
+
spec.version = JWK::VERSION
|
8
|
+
spec.authors = ['Francesco Boffa']
|
9
|
+
spec.email = ['fra.boffa@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'JSON Web Keys implementation in Ruby'
|
12
|
+
spec.description = 'A Ruby implementation of the RFC 7517 JSON Web Keys (JWK) standard'
|
13
|
+
spec.homepage = 'https://github.com/aomega08/jwk'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split("\n")
|
17
|
+
spec.require_paths = ['lib']
|
18
|
+
|
19
|
+
spec.add_development_dependency 'rspec'
|
20
|
+
spec.add_development_dependency 'rake'
|
21
|
+
spec.add_development_dependency 'simplecov'
|
22
|
+
spec.add_development_dependency 'codeclimate-test-reporter'
|
23
|
+
end
|
data/lib/jwk.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'json'
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
require 'jwk/asn1'
|
6
|
+
require 'jwk/ec_key'
|
7
|
+
require 'jwk/key'
|
8
|
+
require 'jwk/oct_key'
|
9
|
+
require 'jwk/rsa_key'
|
10
|
+
require 'jwk/utils'
|
11
|
+
require 'jwk/version'
|
12
|
+
|
13
|
+
module JWK
|
14
|
+
class InvalidKey < StandardError; end
|
15
|
+
end
|
data/lib/jwk/asn1.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
module JWK
|
2
|
+
class ASN1
|
3
|
+
class << self
|
4
|
+
def rsa_public_key(n, e)
|
5
|
+
pubkey = bit_string(sequence(integer(n), integer(e)))
|
6
|
+
sequence(rsa_header, pubkey)
|
7
|
+
end
|
8
|
+
|
9
|
+
def rsa_private_key(*args)
|
10
|
+
raise ArgumentError('Some pieces missing for RSA Private Key') unless args.length == 8
|
11
|
+
sequence(integer(0), *args.map { |n| integer(n) })
|
12
|
+
end
|
13
|
+
|
14
|
+
def ec_private_key(crv, d, x, y)
|
15
|
+
_, raw_x = raw_integer_encoding(x)
|
16
|
+
_, raw_y = raw_integer_encoding(y)
|
17
|
+
|
18
|
+
object_id = object_id_for_crv(crv)
|
19
|
+
|
20
|
+
sequence(
|
21
|
+
integer(1),
|
22
|
+
integer_octet_string(d),
|
23
|
+
context_specific(true, 0, object_id),
|
24
|
+
context_specific(true, 1, bit_string("\x04#{raw_x}#{raw_y}"))
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def object_id_for_crv(crv)
|
31
|
+
case crv
|
32
|
+
when 'P-256'
|
33
|
+
"\x06\x08\x2A\x86\x48\xCE\x3D\x03\x01\x07"
|
34
|
+
when 'P-384'
|
35
|
+
"\x06\x05\x2B\x81\x04\x00\x22"
|
36
|
+
when 'P-521'
|
37
|
+
"\x06\x05\x2B\x81\x04\x00\x23"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def rsa_header
|
42
|
+
"\x30\x0D\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01\x05\x00".force_encoding('ASCII-8BIT')
|
43
|
+
end
|
44
|
+
|
45
|
+
def bit_string(data)
|
46
|
+
"\x03" + asn_length(data.length + 1) + "\x00" + data
|
47
|
+
end
|
48
|
+
|
49
|
+
def sequence(*items)
|
50
|
+
data = items.join
|
51
|
+
"\x30" + asn_length(data.length) + data
|
52
|
+
end
|
53
|
+
|
54
|
+
def integer(n)
|
55
|
+
len, data = raw_integer_encoding(n)
|
56
|
+
|
57
|
+
if data[0].ord & 0x80 != 0
|
58
|
+
data = "\x00" + data
|
59
|
+
len += 1
|
60
|
+
end
|
61
|
+
|
62
|
+
"\x02" + asn_length(len) + data
|
63
|
+
end
|
64
|
+
|
65
|
+
def integer_octet_string(n)
|
66
|
+
len, data = raw_integer_encoding(n)
|
67
|
+
"\x04" + asn_length(len) + data
|
68
|
+
end
|
69
|
+
|
70
|
+
def asn_length(n)
|
71
|
+
if n > 127
|
72
|
+
self_len, data = raw_integer_encoding(n)
|
73
|
+
self_len |= 0x80
|
74
|
+
self_len.chr + data
|
75
|
+
else
|
76
|
+
n.chr
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def raw_integer_encoding(n)
|
81
|
+
# find out how many octets are required to encode it
|
82
|
+
num_octets = (n.to_s(16).length / 2.0).ceil
|
83
|
+
|
84
|
+
# encode the low num_octets bytes of the integer.
|
85
|
+
shifted = n << 8
|
86
|
+
data = Array.new(num_octets) do
|
87
|
+
((shifted >>= 8) & 0xFF).chr
|
88
|
+
end.join.reverse
|
89
|
+
|
90
|
+
[num_octets, data]
|
91
|
+
end
|
92
|
+
|
93
|
+
def context_specific(structured, tag, content)
|
94
|
+
tag |= 0x80
|
95
|
+
tag |= 0x20 if structured
|
96
|
+
|
97
|
+
tag.chr + asn_length(content.length) + content.force_encoding('ASCII-8BIT')
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/jwk/ec_key.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'jwk/key'
|
2
|
+
|
3
|
+
module JWK
|
4
|
+
class ECKey < 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
|
+
unless @key['x'] && @key['y'] && ['P-256', 'P-384', 'P-521'].include?(@key['crv'])
|
20
|
+
raise JWK::InvalidKey, 'Invalid EC key.'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_pem
|
25
|
+
raise NotImplementedError, 'Cannot convert an EC public key to PEM.' unless private?
|
26
|
+
|
27
|
+
asn = ASN1.ec_private_key(crv, d, x, y)
|
28
|
+
generate_pem('EC PRIVATE', asn)
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_openssl_key
|
32
|
+
OpenSSL::PKey.read(to_pem)
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
to_pem
|
37
|
+
end
|
38
|
+
|
39
|
+
def crv
|
40
|
+
@key['crv']
|
41
|
+
end
|
42
|
+
|
43
|
+
%w[d x y].each do |part|
|
44
|
+
define_method(part) do
|
45
|
+
Utils.decode_ub64_int(@key[part]) if @key[part]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class << self
|
50
|
+
def from_openssl(k)
|
51
|
+
x, y = coords_from_key(k)
|
52
|
+
|
53
|
+
names = { 'secp256r1' => 'P-256', 'secp384r1' => 'P-384', 'secp521r1' => 'P-521' }
|
54
|
+
crv = names[k.group.curve_name]
|
55
|
+
|
56
|
+
raise NotImplementedError, "Unsupported EC curve type #{k.group.curve_name}" unless crv
|
57
|
+
|
58
|
+
new('kty' => 'EC',
|
59
|
+
'crv' => crv,
|
60
|
+
'd' => Utils.encode_ub64_int(k.private_key.to_i), 'x' => x, 'y' => y)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def coords_from_key(key)
|
66
|
+
pb = key.public_key.to_bn.to_s(16)
|
67
|
+
|
68
|
+
raise NotImplementedError, 'Cannot convert EC compressed public key' unless pb[0..1] == '04'
|
69
|
+
|
70
|
+
decode_uncompressed_coords(pb[2..-1])
|
71
|
+
end
|
72
|
+
|
73
|
+
def decode_uncompressed_coords(pb)
|
74
|
+
coords = [pb[0...pb.length / 2], pb[pb.length / 2..-1]]
|
75
|
+
coords.map { |c| Base64.urlsafe_encode64(Utils.hex_string_to_binary(c)) }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|