acme-client 0.5.5 → 0.6.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 +4 -4
- data/lib/acme/client.rb +15 -4
- data/lib/acme/client/faraday_middleware.rb +1 -9
- data/lib/acme/client/jwk.rb +21 -0
- data/lib/acme/client/jwk/base.rb +84 -0
- data/lib/acme/client/jwk/ecdsa.rb +104 -0
- data/lib/acme/client/jwk/rsa.rb +52 -0
- data/lib/acme/client/resources/challenges/base.rb +1 -5
- data/lib/acme/client/resources/challenges/dns01.rb +2 -1
- data/lib/acme/client/resources/challenges/tls_sni01.rb +2 -1
- data/lib/acme/client/self_sign_certificate.rb +1 -1
- data/lib/acme/client/util.rb +24 -0
- data/lib/acme/client/version.rb +1 -1
- metadata +8 -4
- data/lib/acme/client/crypto.rb +0 -98
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9afb2ecd4afa51f2a457c64ed33d656188154552
|
4
|
+
data.tar.gz: 737c7eabe999b57d4f3445e577a453911f7e52fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 369dbc069d504500b37d2b2bc36060c0be56e4c630be2e92242c88260619e099aba1e5fb2c416e036bb8456251a241e2bf2e248d045fe3bab2aabe693251ba4f
|
7
|
+
data.tar.gz: 0e989e2ab114ed13bd26e1a15885cb1d0027aa21bd76b0b3bf17b37164fae040632b3cb334098ea2aa6a3bd0d456793dcf5d7ce5818b8fa402df5b41fb26eb3f
|
data/lib/acme/client.rb
CHANGED
@@ -15,10 +15,11 @@ require 'acme/client/version'
|
|
15
15
|
require 'acme/client/certificate'
|
16
16
|
require 'acme/client/certificate_request'
|
17
17
|
require 'acme/client/self_sign_certificate'
|
18
|
-
require 'acme/client/crypto'
|
19
18
|
require 'acme/client/resources'
|
20
19
|
require 'acme/client/faraday_middleware'
|
20
|
+
require 'acme/client/jwk'
|
21
21
|
require 'acme/client/error'
|
22
|
+
require 'acme/client/util'
|
22
23
|
|
23
24
|
class Acme::Client
|
24
25
|
DEFAULT_ENDPOINT = 'http://127.0.0.1:4000'.freeze
|
@@ -29,13 +30,23 @@ class Acme::Client
|
|
29
30
|
'revoke-cert' => '/acme/revoke-cert'
|
30
31
|
}.freeze
|
31
32
|
|
32
|
-
def initialize(private_key
|
33
|
-
|
33
|
+
def initialize(jwk: nil, private_key: nil, endpoint: DEFAULT_ENDPOINT, directory_uri: nil, connection_options: {})
|
34
|
+
if jwk.nil? && private_key.nil?
|
35
|
+
raise ArgumentError, 'must specify jwk or private_key'
|
36
|
+
end
|
37
|
+
|
38
|
+
@jwk = if jwk
|
39
|
+
jwk
|
40
|
+
else
|
41
|
+
Acme::Client::JWK.from_private_key(private_key)
|
42
|
+
end
|
43
|
+
|
44
|
+
@endpoint, @directory_uri, @connection_options = endpoint, directory_uri, connection_options
|
34
45
|
@nonces ||= []
|
35
46
|
load_directory!
|
36
47
|
end
|
37
48
|
|
38
|
-
attr_reader :
|
49
|
+
attr_reader :jwk, :nonces, :endpoint, :directory_uri, :operation_endpoints
|
39
50
|
|
40
51
|
def register(contact:)
|
41
52
|
payload = {
|
@@ -14,7 +14,7 @@ class Acme::Client::FaradayMiddleware < Faraday::Middleware
|
|
14
14
|
def call(env)
|
15
15
|
@env = env
|
16
16
|
@env[:request_headers]['User-Agent'] = USER_AGENT
|
17
|
-
@env.body =
|
17
|
+
@env.body = client.jwk.jws(header: { nonce: pop_nonce }, payload: env.body)
|
18
18
|
@app.call(env).on_complete { |response_env| on_complete(response_env) }
|
19
19
|
rescue Faraday::TimeoutError
|
20
20
|
raise Acme::Client::Error::Timeout
|
@@ -116,12 +116,4 @@ class Acme::Client::FaradayMiddleware < Faraday::Middleware
|
|
116
116
|
def nonces
|
117
117
|
client.nonces
|
118
118
|
end
|
119
|
-
|
120
|
-
def private_key
|
121
|
-
client.private_key
|
122
|
-
end
|
123
|
-
|
124
|
-
def crypto
|
125
|
-
@crypto ||= Acme::Client::Crypto.new(private_key)
|
126
|
-
end
|
127
119
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Acme::Client::JWK
|
2
|
+
# Make a JWK from a private key.
|
3
|
+
#
|
4
|
+
# private_key - An OpenSSL::PKey::EC or OpenSSL::PKey::RSA instance.
|
5
|
+
#
|
6
|
+
# Returns a JWK::Base subclass instance.
|
7
|
+
def self.from_private_key(private_key)
|
8
|
+
case private_key
|
9
|
+
when OpenSSL::PKey::RSA
|
10
|
+
Acme::Client::JWK::RSA.new(private_key)
|
11
|
+
when OpenSSL::PKey::EC
|
12
|
+
Acme::Client::JWK::ECDSA.new(private_key)
|
13
|
+
else
|
14
|
+
raise ArgumentError, 'private_key must be EC or RSA'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'acme/client/jwk/base'
|
20
|
+
require 'acme/client/jwk/rsa'
|
21
|
+
require 'acme/client/jwk/ecdsa'
|
@@ -0,0 +1,84 @@
|
|
1
|
+
class Acme::Client::JWK::Base
|
2
|
+
THUMBPRINT_DIGEST = OpenSSL::Digest::SHA256
|
3
|
+
|
4
|
+
# Initialize a new JWK.
|
5
|
+
#
|
6
|
+
# Returns nothing.
|
7
|
+
def initialize
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
|
11
|
+
# Generate a JWS JSON web signature.
|
12
|
+
#
|
13
|
+
# header - A Hash of extra header fields to include.
|
14
|
+
# payload - A Hash of payload data.
|
15
|
+
#
|
16
|
+
# Returns a JSON String.
|
17
|
+
def jws(header: {}, payload: {})
|
18
|
+
header = jws_header.merge(header)
|
19
|
+
encoded_header = Acme::Client::Util.urlsafe_base64(header.to_json)
|
20
|
+
encoded_payload = Acme::Client::Util.urlsafe_base64(payload.to_json)
|
21
|
+
|
22
|
+
signature_data = "#{encoded_header}.#{encoded_payload}"
|
23
|
+
signature = sign(signature_data)
|
24
|
+
encoded_signature = Acme::Client::Util.urlsafe_base64(signature)
|
25
|
+
|
26
|
+
{
|
27
|
+
protected: encoded_header,
|
28
|
+
payload: encoded_payload,
|
29
|
+
signature: encoded_signature
|
30
|
+
}.to_json
|
31
|
+
end
|
32
|
+
|
33
|
+
# Serialize this JWK as JSON.
|
34
|
+
#
|
35
|
+
# Returns a JSON string.
|
36
|
+
def to_json
|
37
|
+
to_h.to_json
|
38
|
+
end
|
39
|
+
|
40
|
+
# Get this JWK as a Hash for JSON serialization.
|
41
|
+
#
|
42
|
+
# Returns a Hash.
|
43
|
+
def to_h
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
|
47
|
+
# JWK thumbprint as used for key authorization.
|
48
|
+
#
|
49
|
+
# Returns a String.
|
50
|
+
def thumbprint
|
51
|
+
Acme::Client::Util.urlsafe_base64(THUMBPRINT_DIGEST.digest(to_json))
|
52
|
+
end
|
53
|
+
|
54
|
+
# Header fields for a JSON web signature.
|
55
|
+
#
|
56
|
+
# typ: - Value for the `typ` field. Default 'JWT'.
|
57
|
+
#
|
58
|
+
# Returns a Hash.
|
59
|
+
def jws_header
|
60
|
+
{
|
61
|
+
typ: 'JWT',
|
62
|
+
alg: jwa_alg,
|
63
|
+
jwk: to_h
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
# The name of the algorithm as needed for the `alg` member of a JWS object.
|
68
|
+
#
|
69
|
+
# Returns a String.
|
70
|
+
def jwa_alg
|
71
|
+
raise NotImplementedError
|
72
|
+
end
|
73
|
+
|
74
|
+
# Sign a message with the private key.
|
75
|
+
#
|
76
|
+
# message - A String message to sign.
|
77
|
+
#
|
78
|
+
# Returns a String signature.
|
79
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
80
|
+
def sign(message)
|
81
|
+
raise NotImplementedError
|
82
|
+
end
|
83
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
84
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
class Acme::Client::JWK::ECDSA < Acme::Client::JWK::Base
|
2
|
+
# JWA parameters for supported OpenSSL curves.
|
3
|
+
# https://tools.ietf.org/html/rfc7518#section-3.1
|
4
|
+
KNWON_CURVES = {
|
5
|
+
'prime256v1' => {
|
6
|
+
jwa_crv: 'P-256',
|
7
|
+
jwa_alg: 'ES384',
|
8
|
+
digest: OpenSSL::Digest::SHA256
|
9
|
+
}.freeze,
|
10
|
+
'secp384r1' => {
|
11
|
+
jwa_crv: 'P-384',
|
12
|
+
jwa_alg: 'ES384',
|
13
|
+
digest: OpenSSL::Digest::SHA384
|
14
|
+
}.freeze,
|
15
|
+
'secp521r1' => {
|
16
|
+
jwa_crv: 'P-521',
|
17
|
+
jwa_alg: 'ES512',
|
18
|
+
digest: OpenSSL::Digest::SHA512
|
19
|
+
}.freeze
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
# Instantiate a new ECDSA JWK.
|
23
|
+
#
|
24
|
+
# private_key - A OpenSSL::PKey::EC instance.
|
25
|
+
#
|
26
|
+
# Returns nothing.
|
27
|
+
def initialize(private_key)
|
28
|
+
unless private_key.is_a?(OpenSSL::PKey::EC)
|
29
|
+
raise ArgumentError, 'private_key must be a OpenSSL::PKey::EC'
|
30
|
+
end
|
31
|
+
|
32
|
+
unless @curve_params = KNWON_CURVES[private_key.group.curve_name]
|
33
|
+
raise ArgumentError, 'Unknown EC curve'
|
34
|
+
end
|
35
|
+
|
36
|
+
@private_key = private_key
|
37
|
+
end
|
38
|
+
|
39
|
+
# The name of the algorithm as needed for the `alg` member of a JWS object.
|
40
|
+
#
|
41
|
+
# Returns a String.
|
42
|
+
def jwa_alg
|
43
|
+
@curve_params[:jwa_alg]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get this JWK as a Hash for JSON serialization.
|
47
|
+
#
|
48
|
+
# Returns a Hash.
|
49
|
+
def to_h
|
50
|
+
{
|
51
|
+
crv: @curve_params[:jwa_crv],
|
52
|
+
kty: 'EC',
|
53
|
+
x: Acme::Client::Util.urlsafe_base64(coordinates[:x].to_s(2)),
|
54
|
+
y: Acme::Client::Util.urlsafe_base64(coordinates[:y].to_s(2))
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
# Sign a message with the private key.
|
59
|
+
#
|
60
|
+
# message - A String message to sign.
|
61
|
+
#
|
62
|
+
# Returns a String signature.
|
63
|
+
def sign(message)
|
64
|
+
# DER encoded ASN.1 signature
|
65
|
+
der = @private_key.sign(@curve_params[:digest].new, message)
|
66
|
+
|
67
|
+
# ASN.1 SEQUENCE
|
68
|
+
seq = OpenSSL::ASN1.decode(der)
|
69
|
+
|
70
|
+
# ASN.1 INTs
|
71
|
+
ints = seq.value
|
72
|
+
|
73
|
+
# BigNumbers
|
74
|
+
bns = ints.map(&:value)
|
75
|
+
|
76
|
+
# Binary R/S values
|
77
|
+
r, s = bns.map { |bn| [bn.to_s(16)].pack('H*') }
|
78
|
+
|
79
|
+
# JWS wants raw R/S concatenated.
|
80
|
+
[r, s].join
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
# rubocop:disable Metrics/AbcSize
|
86
|
+
def coordinates
|
87
|
+
@coordinates ||= begin
|
88
|
+
hex = public_key.to_bn.to_s(16)
|
89
|
+
data_len = hex.length - 2
|
90
|
+
hex_x = hex[2, data_len / 2]
|
91
|
+
hex_y = hex[2 + data_len / 2, data_len / 2]
|
92
|
+
|
93
|
+
{
|
94
|
+
x: OpenSSL::BN.new([hex_x].pack('H*'), 2),
|
95
|
+
y: OpenSSL::BN.new([hex_y].pack('H*'), 2)
|
96
|
+
}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
# rubocop:enable Metrics/AbcSize
|
100
|
+
|
101
|
+
def public_key
|
102
|
+
@private_key.public_key
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class Acme::Client::JWK::RSA < Acme::Client::JWK::Base
|
2
|
+
# Digest algorithm to use when signing.
|
3
|
+
DIGEST = OpenSSL::Digest::SHA256
|
4
|
+
|
5
|
+
# Instantiate a new RSA JWK.
|
6
|
+
#
|
7
|
+
# private_key - A OpenSSL::PKey::RSA instance.
|
8
|
+
#
|
9
|
+
# Returns nothing.
|
10
|
+
def initialize(private_key)
|
11
|
+
unless private_key.is_a?(OpenSSL::PKey::RSA)
|
12
|
+
raise ArgumentError, 'private_key must be a OpenSSL::PKey::RSA'
|
13
|
+
end
|
14
|
+
|
15
|
+
@private_key = private_key
|
16
|
+
end
|
17
|
+
|
18
|
+
# Get this JWK as a Hash for JSON serialization.
|
19
|
+
#
|
20
|
+
# Returns a Hash.
|
21
|
+
def to_h
|
22
|
+
{
|
23
|
+
e: Acme::Client::Util.urlsafe_base64(public_key.e.to_s(2)),
|
24
|
+
kty: 'RSA',
|
25
|
+
n: Acme::Client::Util.urlsafe_base64(public_key.n.to_s(2))
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
# Sign a message with the private key.
|
30
|
+
#
|
31
|
+
# message - A String message to sign.
|
32
|
+
#
|
33
|
+
# Returns a String signature.
|
34
|
+
def sign(message)
|
35
|
+
@private_key.sign(DIGEST.new, message)
|
36
|
+
end
|
37
|
+
|
38
|
+
# The name of the algorithm as needed for the `alg` member of a JWS object.
|
39
|
+
#
|
40
|
+
# Returns a String.
|
41
|
+
def jwa_alg
|
42
|
+
# https://tools.ietf.org/html/rfc7518#section-3.1
|
43
|
+
# RSASSA-PKCS1-v1_5 using SHA-256
|
44
|
+
'RS256'
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def public_key
|
50
|
+
@private_key.public_key
|
51
|
+
end
|
52
|
+
end
|
@@ -34,10 +34,6 @@ class Acme::Client::Resources::Challenges::Base
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def authorization_key
|
37
|
-
"#{token}.#{
|
38
|
-
end
|
39
|
-
|
40
|
-
def crypto
|
41
|
-
@crypto ||= Acme::Client::Crypto.new(client.private_key)
|
37
|
+
"#{token}.#{client.jwk.thumbprint}"
|
42
38
|
end
|
43
39
|
end
|
@@ -4,6 +4,7 @@ class Acme::Client::Resources::Challenges::DNS01 < Acme::Client::Resources::Chal
|
|
4
4
|
CHALLENGE_TYPE = 'dns-01'.freeze
|
5
5
|
RECORD_NAME = '_acme-challenge'.freeze
|
6
6
|
RECORD_TYPE = 'TXT'.freeze
|
7
|
+
DIGEST = OpenSSL::Digest::SHA256
|
7
8
|
|
8
9
|
def record_name
|
9
10
|
RECORD_NAME
|
@@ -14,6 +15,6 @@ class Acme::Client::Resources::Challenges::DNS01 < Acme::Client::Resources::Chal
|
|
14
15
|
end
|
15
16
|
|
16
17
|
def record_content
|
17
|
-
|
18
|
+
Acme::Client::Util.urlsafe_base64(DIGEST.digest(authorization_key))
|
18
19
|
end
|
19
20
|
end
|
@@ -2,9 +2,10 @@
|
|
2
2
|
|
3
3
|
class Acme::Client::Resources::Challenges::TLSSNI01 < Acme::Client::Resources::Challenges::Base
|
4
4
|
CHALLENGE_TYPE = 'tls-sni-01'.freeze
|
5
|
+
DIGEST = OpenSSL::Digest::SHA256
|
5
6
|
|
6
7
|
def hostname
|
7
|
-
digest =
|
8
|
+
digest = DIGEST.hexdigest(authorization_key)
|
8
9
|
"#{digest[0..31]}.#{digest[32..64]}.acme.invalid"
|
9
10
|
end
|
10
11
|
|
@@ -46,7 +46,7 @@ class Acme::Client::SelfSignCertificate
|
|
46
46
|
certificate = OpenSSL::X509::Certificate.new
|
47
47
|
certificate.not_before = not_before
|
48
48
|
certificate.not_after = not_after
|
49
|
-
certificate
|
49
|
+
Acme::Client::Util.set_public_key(certificate, private_key)
|
50
50
|
certificate.version = 2
|
51
51
|
certificate.serial = 1
|
52
52
|
certificate
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Acme::Client::Util
|
2
|
+
def urlsafe_base64(data)
|
3
|
+
Base64.urlsafe_encode64(data).sub(/[\s=]*\z/, '')
|
4
|
+
end
|
5
|
+
|
6
|
+
# Sets public key on CSR or cert.
|
7
|
+
#
|
8
|
+
# obj - An OpenSSL::X509::Certificate or OpenSSL::X509::Request instance.
|
9
|
+
# priv - An OpenSSL::PKey::EC or OpenSSL::PKey::RSA instance.
|
10
|
+
#
|
11
|
+
# Returns nothing.
|
12
|
+
def set_public_key(obj, priv)
|
13
|
+
case priv
|
14
|
+
when OpenSSL::PKey::EC
|
15
|
+
obj.public_key = priv
|
16
|
+
when OpenSSL::PKey::RSA
|
17
|
+
obj.public_key = priv.public_key
|
18
|
+
else
|
19
|
+
raise ArgumentError, 'priv must be EC or RSA'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
extend self
|
24
|
+
end
|
data/lib/acme/client/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acme-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Charles Barbier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-06-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -148,9 +148,12 @@ files:
|
|
148
148
|
- lib/acme/client/certificate.rb
|
149
149
|
- lib/acme/client/certificate_request.rb
|
150
150
|
- lib/acme/client/certificate_request/ec_key_patch.rb
|
151
|
-
- lib/acme/client/crypto.rb
|
152
151
|
- lib/acme/client/error.rb
|
153
152
|
- lib/acme/client/faraday_middleware.rb
|
153
|
+
- lib/acme/client/jwk.rb
|
154
|
+
- lib/acme/client/jwk/base.rb
|
155
|
+
- lib/acme/client/jwk/ecdsa.rb
|
156
|
+
- lib/acme/client/jwk/rsa.rb
|
154
157
|
- lib/acme/client/resources.rb
|
155
158
|
- lib/acme/client/resources/authorization.rb
|
156
159
|
- lib/acme/client/resources/challenges.rb
|
@@ -160,6 +163,7 @@ files:
|
|
160
163
|
- lib/acme/client/resources/challenges/tls_sni01.rb
|
161
164
|
- lib/acme/client/resources/registration.rb
|
162
165
|
- lib/acme/client/self_sign_certificate.rb
|
166
|
+
- lib/acme/client/util.rb
|
163
167
|
- lib/acme/client/version.rb
|
164
168
|
homepage: http://github.com/unixcharles/acme-client
|
165
169
|
licenses:
|
@@ -181,7 +185,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
181
185
|
version: '0'
|
182
186
|
requirements: []
|
183
187
|
rubyforge_project:
|
184
|
-
rubygems_version: 2.6.
|
188
|
+
rubygems_version: 2.6.10
|
185
189
|
signing_key:
|
186
190
|
specification_version: 4
|
187
191
|
summary: Client for the ACME protocol.
|
data/lib/acme/client/crypto.rb
DELETED
@@ -1,98 +0,0 @@
|
|
1
|
-
class Acme::Client::Crypto
|
2
|
-
attr_reader :private_key
|
3
|
-
|
4
|
-
def initialize(private_key)
|
5
|
-
@private_key = private_key
|
6
|
-
end
|
7
|
-
|
8
|
-
def generate_signed_jws(header:, payload:)
|
9
|
-
header = { typ: 'JWT', alg: jws_alg, jwk: jwk }.merge(header)
|
10
|
-
|
11
|
-
encoded_header = urlsafe_base64(header.to_json)
|
12
|
-
encoded_payload = urlsafe_base64(payload.to_json)
|
13
|
-
signature_data = "#{encoded_header}.#{encoded_payload}"
|
14
|
-
|
15
|
-
signature = private_key.sign digest, signature_data
|
16
|
-
encoded_signature = urlsafe_base64(signature)
|
17
|
-
|
18
|
-
{
|
19
|
-
protected: encoded_header,
|
20
|
-
payload: encoded_payload,
|
21
|
-
signature: encoded_signature
|
22
|
-
}.to_json
|
23
|
-
end
|
24
|
-
|
25
|
-
def thumbprint
|
26
|
-
urlsafe_base64 digest.digest(jwk.to_json)
|
27
|
-
end
|
28
|
-
|
29
|
-
def digest
|
30
|
-
OpenSSL::Digest::SHA256.new
|
31
|
-
end
|
32
|
-
|
33
|
-
def urlsafe_base64(data)
|
34
|
-
Base64.urlsafe_encode64(data).sub(/[\s=]*\z/, '')
|
35
|
-
end
|
36
|
-
|
37
|
-
private
|
38
|
-
|
39
|
-
def jws_alg
|
40
|
-
{ 'RSA' => 'RS256', 'EC' => 'ES256' }.fetch(jwk[:kty])
|
41
|
-
end
|
42
|
-
|
43
|
-
def jwk
|
44
|
-
@jwk ||= case private_key
|
45
|
-
when OpenSSL::PKey::RSA
|
46
|
-
rsa_jwk
|
47
|
-
when OpenSSL::PKey::EC
|
48
|
-
ec_jwk
|
49
|
-
else
|
50
|
-
raise ArgumentError, "Can't handle #{private_key} as private key, only OpenSSL::PKey::RSA and OpenSSL::PKey::EC"
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def rsa_jwk
|
55
|
-
{
|
56
|
-
e: urlsafe_base64(public_key.e.to_s(2)),
|
57
|
-
kty: 'RSA',
|
58
|
-
n: urlsafe_base64(public_key.n.to_s(2))
|
59
|
-
}
|
60
|
-
end
|
61
|
-
|
62
|
-
def ec_jwk
|
63
|
-
{
|
64
|
-
crv: curve_name,
|
65
|
-
kty: 'EC',
|
66
|
-
x: urlsafe_base64(coordinates[:x].to_s(2)),
|
67
|
-
y: urlsafe_base64(coordinates[:y].to_s(2))
|
68
|
-
}
|
69
|
-
end
|
70
|
-
|
71
|
-
def curve_name
|
72
|
-
{
|
73
|
-
'prime256v1' => 'P-256',
|
74
|
-
'secp384r1' => 'P-384',
|
75
|
-
'secp521r1' => 'P-521'
|
76
|
-
}.fetch(private_key.group.curve_name) { raise ArgumentError, 'Unknown EC curve' }
|
77
|
-
end
|
78
|
-
|
79
|
-
# rubocop:disable Metrics/AbcSize
|
80
|
-
def coordinates
|
81
|
-
@coordinates ||= begin
|
82
|
-
hex = public_key.to_bn.to_s(16)
|
83
|
-
data_len = hex.length - 2
|
84
|
-
hex_x = hex[2, data_len / 2]
|
85
|
-
hex_y = hex[2 + data_len / 2, data_len / 2]
|
86
|
-
|
87
|
-
{
|
88
|
-
x: OpenSSL::BN.new([hex_x].pack('H*'), 2),
|
89
|
-
y: OpenSSL::BN.new([hex_y].pack('H*'), 2)
|
90
|
-
}
|
91
|
-
end
|
92
|
-
end
|
93
|
-
# rubocop:enable Metrics/AbcSize
|
94
|
-
|
95
|
-
def public_key
|
96
|
-
@public_key ||= private_key.public_key
|
97
|
-
end
|
98
|
-
end
|