acme-client 0.2.1 → 0.2.2

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: fb284f691ef7182e7aa9ca56ff950373d92380b6
4
- data.tar.gz: 542ef5706c8265fecf34c3841742c673df8eb4fc
3
+ metadata.gz: 66b791acbcafdeb9a0cc7b1fc56977ff235771db
4
+ data.tar.gz: fcc5bdffc2e7278301de2ad97745a0dd1400915e
5
5
  SHA512:
6
- metadata.gz: 3453c7ea55e3b646eb313bee04277f14259197c6c7817924848e0635992c8b3a7edb17f176c4da64bdae26c51bf4f341380071b51721a71f144e3903dc759cda
7
- data.tar.gz: da85a2dc373d4b7eb361f1fe97b7cb0f3c77b52bae6862c3e7e11328e3a505e75536f3fcf5b5db5c53094cd0fcecc58eebf87a5f0c2e9274f367d04c87f48f5a
6
+ metadata.gz: 1dda0e5460db94b43231b033513b28c6a88b7af1be4c201bd360b5fda5dab159eb684305177636f6a3327a5ef6b54359045c1f12a87c62312a510dddae3b8345
7
+ data.tar.gz: c5d7b4f69f4455c56fdeb9611b66757e981ebd870582fca017656a6f9635bfd223cc1c2231c67a159682f66e369dad18e3ed81e259c9d931d4f0f5b11a3972de
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /vendor/bundle
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --format documentation
2
2
  --color
3
+ --order rand
data/README.md CHANGED
@@ -22,15 +22,15 @@ require 'acme-client'
22
22
  client = Acme::Client.new(private_key: private_key, endpoint: endpoint)
23
23
 
24
24
  # If the private key is not known to the server, we need to register it for the first time.
25
- registration = client.register(contact: 'mailto:unixcharles@gmail.com')
25
+ registration = client.register(contact: 'mailto:contact@example.com')
26
26
 
27
27
  # You'll may need to agree to the term (that's up the to the server to require it or not but boulder does by default)
28
28
  registration.agree_terms
29
29
 
30
- # Let's try to optain a certificate for yourdomain.com
30
+ # Let's try to optain a certificate for example.org
31
31
 
32
32
  # We need to prove that we control the domain using one of the challenges method.
33
- authorization = client.authorize(domain: 'yourdomain.com')
33
+ authorization = client.authorize(domain: 'example.org')
34
34
 
35
35
  # For now the only challenge method supprted by the client is http-01.
36
36
  challenge = authorization.http01
@@ -65,26 +65,18 @@ sleep(1)
65
65
 
66
66
  challenge.verify_status # => 'valid'
67
67
 
68
- # We're going to need a CSR, lets do this real quick with Ruby+OpenSSL.
69
- csr = OpenSSL::X509::Request.new
68
+ # We're going to need a certificate signing request. If not explicitly
69
+ # specified, the first name listed becomes the common name.
70
+ csr = Acme::CertificateRequest.new(names: %w[example.org www.example.org])
70
71
 
71
- # We need a private key for the certificate, not the same as the account key.
72
- certificate_private_key = OpenSSL::PKey::RSA.new(2048)
73
-
74
- # We just going to add the domain but normally you might want to provide more information.
75
- csr.subject = OpenSSL::X509::Name.new([
76
- ['CN', 'yourdomain.com', OpenSSL::ASN1::UTF8STRING]
77
- ])
78
-
79
- csr.public_key = certificate_private_key.public_key
80
- csr.sign(certificate_private_key, OpenSSL::Digest::SHA256.new)
81
-
82
- # We can now request a certificate
72
+ # We can now request a certificate, you can pass anything that returns
73
+ # a valid DER encoded CSR when calling to_der on it, for example a
74
+ # OpenSSL::X509::Request too.
83
75
  certificate = client.new_certificate(csr) # => #<Acme::Certificate ....>
84
76
 
85
77
  # Save the certificate and key
78
+ File.write("privkey.pem", certificate.request.private_key.to_pem)
86
79
  File.write("cert.pem", certificate.to_pem)
87
- File.write("key.pem", certificate_private_key.to_pem)
88
80
  File.write("chain.pem", certificate.chain_to_pem)
89
81
  File.write("fullchain.pem", certificate.fullchain_to_pem)
90
82
 
@@ -100,7 +92,7 @@ File.write("fullchain.pem", certificate.fullchain_to_pem)
100
92
  # Not implemented
101
93
 
102
94
  - Recovery methods are not implemented.
103
- - http-01 is the only challenge method implemented
95
+ - tls-sni-01 and proofOfPossession-01 are not implemented.
104
96
 
105
97
  ## Development
106
98
 
data/lib/acme-client.rb CHANGED
@@ -8,6 +8,7 @@ require 'digest'
8
8
  require 'forwardable'
9
9
 
10
10
  require 'acme/certificate'
11
+ require 'acme/certificate_request'
11
12
  require 'acme/crypto'
12
13
  require 'acme/client'
13
14
  require 'acme/resources'
@@ -1,13 +1,14 @@
1
1
  class Acme::Certificate
2
2
  extend Forwardable
3
3
 
4
- attr_reader :x509, :x509_chain
4
+ attr_reader :x509, :x509_chain, :request
5
5
 
6
6
  def_delegators :x509, :to_pem, :to_der
7
7
 
8
- def initialize(certificate, chain)
8
+ def initialize(certificate, chain, request)
9
9
  @x509 = certificate
10
10
  @x509_chain = chain
11
+ @request = request
11
12
  end
12
13
 
13
14
  def chain_to_pem
@@ -21,4 +22,8 @@ class Acme::Certificate
21
22
  def fullchain_to_pem
22
23
  x509_fullchain.map(&:to_pem).join
23
24
  end
25
+
26
+ def common_name
27
+ x509.subject.to_a.find {|name, _, _| name == "CN" }[1]
28
+ end
24
29
  end
@@ -0,0 +1,113 @@
1
+ class Acme::CertificateRequest
2
+ extend Forwardable
3
+
4
+ DEFAULT_KEY_LENGTH = 2048
5
+ DEFAULT_DIGEST = OpenSSL::Digest::SHA256
6
+ SUBJECT_KEYS = {
7
+ common_name: "CN",
8
+ country_name: "C",
9
+ organization_name: "O",
10
+ organizational_unit: "OU",
11
+ state_or_province: "ST",
12
+ locality_name: "L"
13
+ }.freeze
14
+
15
+ SUBJECT_TYPES = {
16
+ "CN" => OpenSSL::ASN1::UTF8STRING,
17
+ "C" => OpenSSL::ASN1::UTF8STRING,
18
+ "O" => OpenSSL::ASN1::UTF8STRING,
19
+ "OU" => OpenSSL::ASN1::UTF8STRING,
20
+ "ST" => OpenSSL::ASN1::UTF8STRING,
21
+ "L" => OpenSSL::ASN1::UTF8STRING
22
+ }.freeze
23
+
24
+ attr_reader :csr, :private_key, :common_name, :names, :subject
25
+
26
+ def_delegators :csr, :to_pem, :to_der
27
+
28
+ def initialize(common_name: nil,
29
+ names: [],
30
+ private_key: generate_private_key,
31
+ subject: {},
32
+ digest: DEFAULT_DIGEST.new)
33
+ raise ArgumentError.new("Digest must be a OpenSSL::Digest") unless digest.is_a?(OpenSSL::Digest)
34
+ @digest = digest
35
+
36
+ @private_key = private_key
37
+
38
+ @subject = normalize_subject(subject)
39
+ @common_name = common_name || @subject[SUBJECT_KEYS[:common_name]] || @subject[:common_name]
40
+
41
+ @names = names.to_a.dup
42
+ normalize_names
43
+
44
+ @subject[SUBJECT_KEYS[:common_name]] ||= @common_name
45
+ validate_subject
46
+
47
+ @csr = generate
48
+ end
49
+
50
+ private
51
+
52
+ def generate_private_key
53
+ OpenSSL::PKey::RSA.new(DEFAULT_KEY_LENGTH)
54
+ end
55
+
56
+ def normalize_subject(subject)
57
+ @subject = subject.each_with_object({}) {|(key, value), subject|
58
+ subject[SUBJECT_KEYS.fetch(key, key)] = value.to_s
59
+ }
60
+ end
61
+
62
+ def normalize_names
63
+ if @common_name
64
+ @names.unshift(@common_name) unless @names.include?(@common_name)
65
+ elsif @names.empty?
66
+ raise ArgumentError.new("No common name and no list of names given")
67
+ else
68
+ @common_name = @names.first
69
+ end
70
+ end
71
+
72
+ def validate_subject
73
+ extra_keys = @subject.keys - SUBJECT_KEYS.keys - SUBJECT_KEYS.values
74
+ unless extra_keys.empty?
75
+ raise ArgumentError.new("Unexpected subject attributes given: #{extra_keys.inspect}")
76
+ end
77
+
78
+ unless @common_name == @subject[SUBJECT_KEYS[:common_name]]
79
+ raise ArgumentError.new("Conflicting common name given in arguments and subject")
80
+ end
81
+ end
82
+
83
+ def generate
84
+ OpenSSL::X509::Request.new.tap do |csr|
85
+ csr.public_key = @private_key.public_key
86
+ csr.subject = generate_subject
87
+ add_extension(csr)
88
+ csr.sign @private_key, @digest
89
+ end
90
+ end
91
+
92
+ def generate_subject
93
+ OpenSSL::X509::Name.new(
94
+ @subject.map {|name, value|
95
+ [name, value, SUBJECT_TYPES[name]]
96
+ }
97
+ )
98
+ end
99
+
100
+ def add_extension(csr)
101
+ return if @names.size <= 1
102
+
103
+ extension = OpenSSL::X509::ExtensionFactory.new.create_extension(
104
+ "subjectAltName", @names.map {|name| "DNS:#{name}" }.join(", "), false
105
+ )
106
+ csr.add_attribute(
107
+ OpenSSL::X509::Attribute.new(
108
+ "extReq",
109
+ OpenSSL::ASN1::Set.new([OpenSSL::ASN1::Sequence.new([extension])])
110
+ )
111
+ )
112
+ end
113
+ end
data/lib/acme/client.rb CHANGED
@@ -44,7 +44,7 @@ class Acme::Client
44
44
  }
45
45
 
46
46
  response = connection.post(@operation_endpoints.fetch('new-cert'), payload)
47
- ::Acme::Certificate.new(OpenSSL::X509::Certificate.new(response.body), fetch_chain(response))
47
+ ::Acme::Certificate.new(OpenSSL::X509::Certificate.new(response.body), fetch_chain(response), csr)
48
48
  end
49
49
 
50
50
  def fetch_chain(response, limit=10)
data/lib/acme/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Acme
2
2
  class Client
3
- VERSION = '0.2.1'
3
+ VERSION = '0.2.2'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acme-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Charles Barbier
@@ -157,6 +157,7 @@ files:
157
157
  - bin/setup
158
158
  - lib/acme-client.rb
159
159
  - lib/acme/certificate.rb
160
+ - lib/acme/certificate_request.rb
160
161
  - lib/acme/client.rb
161
162
  - lib/acme/crypto.rb
162
163
  - lib/acme/error.rb