acme-client 0.2.1 → 0.2.2

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