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