acme-client 0.1.3 → 0.2.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: 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