acme-client 0.1.3 → 0.2.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: db4de0115967af2794cdf73fbc564ba3207d07bb
4
- data.tar.gz: a6e4dbafa02042fa3ebdf0c171a5f700770d000c
3
+ metadata.gz: 5ee8283817ef99669367cba5c6e5d62095aa5015
4
+ data.tar.gz: 62f8588bfd710561c626064825b7bd0a678b3a49
5
5
  SHA512:
6
- metadata.gz: e54827c5a1b93c079476b4ee7061d42d8a69c668063afd571887a040b063c362fc68f279fff2ebc9260b0bde0dd3d4af6b86b8f950385419f715bb6bb5f74f42
7
- data.tar.gz: 368afee114cd978a13f764869f84c74fbd7df1177bed9ed8a25c3e675b2bb8e4f0de80c97bc3f3f6d061e4bd9c6d0c4a938f78cd1f16789e48907f721499a0a7
6
+ metadata.gz: 4cd8d3dcdeb48bd21acc1212bc04f65fb0a37ce797d3553d63dc3730370bce723a68e5683fcf7745eb14916be610dd8009e284c911f75d50d76a7be0135a1dcf
7
+ data.tar.gz: 7bb4ff248144d4cbc9f43b752cca79773b6529a0140c81db433cdb2c8653d063ee7933287c0071fdb50daa0817b105c930e4f51a80e6eddcc7d98c3b2898cc14
data/.travis.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.1.0
4
- before_install: gem install bundler -v 1.10.6
3
+ - 2.1
4
+ - 2.2
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # Acme::Client
2
+ [![Build Status](https://travis-ci.org/unixcharles/acme-client.svg?branch=master)](https://travis-ci.org/unixcharles/acme-client)
2
3
 
3
4
  `acme-client` is a client implementation of the [ACME](https://letsencrypt.github.io/acme-spec) protocol in Ruby.
4
5
 
@@ -10,12 +11,14 @@ ACME is part of the [Letsencrypt](https://letsencrypt.org/) project, that are wo
10
11
 
11
12
  ```ruby
12
13
  # We're going to need a private key.
14
+ require 'openssl'
13
15
  private_key = OpenSSL::PKey::RSA.new(2048)
14
16
 
15
17
  # We need an ACME server to talk to, see github.com/letsencrypt/boulder
16
18
  endpoint = 'https://acme-staging.api.letsencrypt.org'
17
19
 
18
20
  # Initialize the client
21
+ require 'acme-client'
19
22
  client = Acme::Client.new(private_key: private_key, endpoint: endpoint)
20
23
 
21
24
  # If the private key is not known to the server, we need to register it for the first time.
@@ -29,28 +32,38 @@ registration.agree_terms
29
32
  # We need to prove that we control the domain using one of the challenges method.
30
33
  authorization = client.authorize(domain: 'yourdomain.com')
31
34
 
32
- # For now the only challenge method supprted by the client is simple_http.
33
- simple_http = authorization.simple_http
35
+ # For now the only challenge method supprted by the client is http-01.
36
+ challenge = authorization.http01
34
37
 
35
- # The SimpleHTTP method will require you to response to an HTTP request.
38
+ # The http-01 method will require you to response to an HTTP request.
36
39
 
37
40
  # You can retrieve the expected path for the file.
38
- simple_http.filename # => ".well-known/acme-challenge/:some_token"
41
+ challenge.filename # => ".well-known/acme-challenge/:some_token"
39
42
 
40
43
  # You can generate the body of the expected response.
41
- simple_http.file_content # => 'string of JWS signed json'
44
+ challenge.file_content # => 'string token and JWK thumbprint'
45
+
46
+ # You can send no Content-Type at all but if you send one it has to be 'text/plain'.
47
+ challenge.content_type
48
+
49
+ # Save the file. We'll create a public directory to serve it from, and we'll creating the challenge directory.
50
+ FileUtils.mkdir_p( File.join( 'public', File.dirname( challenge.filename ) ) )
51
+
52
+ # Then writing the file
53
+ File.write( File.join( 'public', challenge.filename), challenge.file_content )
54
+
55
+ # The challenge file can be server with a Ruby webserver such as run a webserver in another console. You may need to forward ports on your router
56
+ #ruby -run -e httpd public -p 8080 --bind-address 0.0.0.0
42
57
 
43
- # You can send no Content-Type at all but if you send one it has to be 'application/jose+json'.
44
- simple_http.content_type
45
58
 
46
59
  # Once you are ready to serve the confirmation request you can proceed.
47
- simple_http.request_verification # => true
48
- simple_http.verify_status # => 'pending'
60
+ challenge.request_verification # => true
61
+ challenge.verify_status # => 'pending'
49
62
 
50
63
  # Wait a bit for the server to make the request, or really just blink, it should be fast.
51
64
  sleep(1)
52
65
 
53
- simple_http.verify_status # => 'valid'
66
+ challenge.verify_status # => 'valid'
54
67
 
55
68
  # We're going to need a CSR, lets do this real quick with Ruby+OpenSSL.
56
69
  csr = OpenSSL::X509::Request.new
@@ -60,20 +73,34 @@ certificate_private_key = OpenSSL::PKey::RSA.new(2048)
60
73
 
61
74
  # We just going to add the domain but normally you might want to provide more information.
62
75
  csr.subject = OpenSSL::X509::Name.new([
63
- ['CN', common_name, OpenSSL::ASN1::UTF8STRING]
76
+ ['CN', 'yourdomain.com', OpenSSL::ASN1::UTF8STRING]
64
77
  ])
65
78
 
66
79
  csr.public_key = certificate_private_key.public_key
67
80
  csr.sign(certificate_private_key, OpenSSL::Digest::SHA256.new)
68
81
 
69
82
  # We can now request a certificate
70
- client.new_certificate(csr) # => #<OpenSSL::X509::Certificate ....>
83
+ certificate = client.new_certificate(csr) # => #<Acme::Certificate ....>
84
+
85
+ # Save the certificate and key
86
+ File.write("cert.pem", certificate.to_pem)
87
+ File.write("key.pem", certificate_private_key.to_pem)
88
+ File.write("chain.pem", certificate.chain_to_pem)
89
+ File.write("fullchain.pem", certificate.fullchain_to_pem)
90
+
91
+ # Start a webserver, using your shiny new certificate
92
+ # ruby -r openssl -r webrick -r 'webrick/https' -e "s = WEBrick::HTTPServer.new(
93
+ # :Port => 8443,
94
+ # :DocumentRoot => Dir.pwd,
95
+ # :SSLEnable => true,
96
+ # :SSLPrivateKey => OpenSSL::PKey::RSA.new( File.read('key.pem') ),
97
+ # :SSLCertificate => OpenSSL::X509::Certificate.new( File.read('cert.pem') )); trap('INT') { s.shutdown }; s.start"
71
98
  ```
72
99
 
73
100
  # Not implemented
74
101
 
75
102
  - Recovery methods are not implemented.
76
- - SimpleHTTP is the only challenge method implemented
103
+ - http-01 is the only challenge method implemented
77
104
 
78
105
  ## Development
79
106
 
data/acme-client.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
15
15
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16
16
  spec.require_paths = ['lib']
17
17
 
18
- spec.add_development_dependency 'bundler', '~> 1.10'
18
+ spec.add_development_dependency 'bundler', '>= 1.6.9'
19
19
  spec.add_development_dependency 'rake', '~> 10.0'
20
20
  spec.add_development_dependency 'rspec', '~> 3.3', '>= 3.3.0'
21
21
  spec.add_development_dependency 'vcr', '~> 2.9', '>= 2.9.3'
data/lib/acme-client.rb CHANGED
@@ -6,7 +6,9 @@ require 'json'
6
6
  require 'json/jwt'
7
7
  require 'openssl'
8
8
  require 'digest'
9
+ require 'forwardable'
9
10
 
11
+ require 'acme/certificate'
10
12
  require 'acme/crypto'
11
13
  require 'acme/client'
12
14
  require 'acme/resources'
@@ -0,0 +1,20 @@
1
+ class Acme::Certificate
2
+ extend Forwardable
3
+
4
+ attr_reader :x509, :x509_chain
5
+
6
+ def_delegators :x509, :to_pem, :to_der
7
+
8
+ def initialize(certificate, chain)
9
+ @x509 = certificate
10
+ @x509_chain = chain
11
+ end
12
+
13
+ def chain_to_pem
14
+ x509_chain.map(&:to_pem).join
15
+ end
16
+
17
+ def fullchain_to_pem
18
+ [*x509_chain, x509].map(&:to_pem).join
19
+ end
20
+ end
data/lib/acme/client.rb CHANGED
@@ -44,7 +44,16 @@ class Acme::Client
44
44
  }
45
45
 
46
46
  response = connection.post(@operation_endpoints.fetch('new-cert'), payload)
47
- OpenSSL::X509::Certificate.new(response.body)
47
+ ::Acme::Certificate.new(OpenSSL::X509::Certificate.new(response.body), fetch_chain(response).reverse)
48
+ end
49
+
50
+ def fetch_chain(response, limit=10)
51
+ if limit == 0 || response.headers["link"].nil? || response.headers["link"]["up"].nil?
52
+ []
53
+ else
54
+ issuer = connection.get(response.headers["link"]["up"])
55
+ [OpenSSL::X509::Certificate.new(issuer.body), *fetch_chain(issuer, limit-1)]
56
+ end
48
57
  end
49
58
 
50
59
  def connection
@@ -1,7 +1,8 @@
1
1
  class Acme::Resources::Authorization
2
2
  HTTP01 = Acme::Resources::Challenges::HTTP01
3
+ DNS01 = Acme::Resources::Challenges::DNS01
3
4
 
4
- attr_reader :domain, :status, :http01
5
+ attr_reader :domain, :status, :http01, :dns01
5
6
 
6
7
  def initialize(client, response)
7
8
  @client = client
@@ -15,6 +16,7 @@ class Acme::Resources::Authorization
15
16
  challenges.each do |attributes|
16
17
  case attributes.fetch('type')
17
18
  when 'http-01' then @http01 = HTTP01.new(@client, attributes)
19
+ when 'dns-01' then @dns01 = DNS01.new(@client, attributes)
18
20
  else
19
21
  # no supported
20
22
  end
@@ -1,3 +1,4 @@
1
1
  module Acme::Resources::Challenges; end
2
2
  require 'acme/resources/challenges/base'
3
3
  require 'acme/resources/challenges/http01'
4
+ require 'acme/resources/challenges/dns01'
@@ -0,0 +1,21 @@
1
+ class Acme::Resources::Challenges::DNS01 < Acme::Resources::Challenges::Base
2
+ RECORD_NAME = '_acme-challenge'
3
+ RECORD_TYPE = 'TXT'
4
+
5
+ def record_name
6
+ RECORD_NAME
7
+ end
8
+
9
+ def record_type
10
+ RECORD_TYPE
11
+ end
12
+
13
+ def record_content
14
+ crypto.digest.hexdigest(authorization_key)
15
+ end
16
+
17
+ def request_verification
18
+ response = @client.connection.post(@uri, { resource: 'challenge', type: 'dns-01', keyAuthorization: authorization_key })
19
+ response.success?
20
+ end
21
+ end
data/lib/acme/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Acme
2
2
  class Client
3
- VERSION = '0.1.3'
3
+ VERSION = '0.2.0'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acme-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.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: 2015-11-27 00:00:00.000000000 Z
11
+ date: 2015-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.10'
19
+ version: 1.6.9
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.10'
26
+ version: 1.6.9
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -176,6 +176,7 @@ files:
176
176
  - bin/console
177
177
  - bin/setup
178
178
  - lib/acme-client.rb
179
+ - lib/acme/certificate.rb
179
180
  - lib/acme/client.rb
180
181
  - lib/acme/crypto.rb
181
182
  - lib/acme/error.rb
@@ -184,6 +185,7 @@ files:
184
185
  - lib/acme/resources/authorization.rb
185
186
  - lib/acme/resources/challenges.rb
186
187
  - lib/acme/resources/challenges/base.rb
188
+ - lib/acme/resources/challenges/dns01.rb
187
189
  - lib/acme/resources/challenges/http01.rb
188
190
  - lib/acme/resources/registration.rb
189
191
  - lib/acme/version.rb