acme-client 0.5.5 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c5269e4586e1ba3dfa359b635a0ae311bed2d5ee
4
- data.tar.gz: 36eb213b6bb4f6fa4f940718acf89f7a080d3b8c
3
+ metadata.gz: 9afb2ecd4afa51f2a457c64ed33d656188154552
4
+ data.tar.gz: 737c7eabe999b57d4f3445e577a453911f7e52fa
5
5
  SHA512:
6
- metadata.gz: 39adb3b34f5eebfe7e718ef942270166a34844f50658698c800c112880f6485e65e7e08a6cba94b20c6ac613ea165df449a5118166f424e742c864c02998cd79
7
- data.tar.gz: ad0eb8878dcea3eca909f88049d62aac40381727a51b521f8e06fa49b48195c552939adc8dcc28c0ec0e784267cc15cd147c6f87368a7c3922bc4622a5101672
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:, endpoint: DEFAULT_ENDPOINT, directory_uri: nil, connection_options: {})
33
- @endpoint, @private_key, @directory_uri, @connection_options = endpoint, private_key, directory_uri, connection_options
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 :private_key, :nonces, :endpoint, :directory_uri, :operation_endpoints
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 = crypto.generate_signed_jws(header: { nonce: pop_nonce }, payload: 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}.#{crypto.thumbprint}"
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
- crypto.urlsafe_base64(crypto.digest.digest(authorization_key))
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 = crypto.digest.hexdigest(authorization_key)
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.public_key = private_key.public_key
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Acme
4
4
  class Client
5
- VERSION = '0.5.5'.freeze
5
+ VERSION = '0.6.0'.freeze
6
6
  end
7
7
  end
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.5.5
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-03-07 00:00:00.000000000 Z
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.8
188
+ rubygems_version: 2.6.10
185
189
  signing_key:
186
190
  specification_version: 4
187
191
  summary: Client for the ACME protocol.
@@ -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