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 +4 -4
- data/.travis.yml +2 -2
- data/README.md +40 -13
- data/acme-client.gemspec +1 -1
- data/lib/acme-client.rb +2 -0
- data/lib/acme/certificate.rb +20 -0
- data/lib/acme/client.rb +10 -1
- data/lib/acme/resources/authorization.rb +3 -1
- data/lib/acme/resources/challenges.rb +1 -0
- data/lib/acme/resources/challenges/dns01.rb +21 -0
- data/lib/acme/version.rb +1 -1
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5ee8283817ef99669367cba5c6e5d62095aa5015
|
4
|
+
data.tar.gz: 62f8588bfd710561c626064825b7bd0a678b3a49
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4cd8d3dcdeb48bd21acc1212bc04f65fb0a37ce797d3553d63dc3730370bce723a68e5683fcf7745eb14916be610dd8009e284c911f75d50d76a7be0135a1dcf
|
7
|
+
data.tar.gz: 7bb4ff248144d4cbc9f43b752cca79773b6529a0140c81db433cdb2c8653d063ee7933287c0071fdb50daa0817b105c930e4f51a80e6eddcc7d98c3b2898cc14
|
data/.travis.yml
CHANGED
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
|
33
|
-
|
35
|
+
# For now the only challenge method supprted by the client is http-01.
|
36
|
+
challenge = authorization.http01
|
34
37
|
|
35
|
-
# The
|
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
|
-
|
41
|
+
challenge.filename # => ".well-known/acme-challenge/:some_token"
|
39
42
|
|
40
43
|
# You can generate the body of the expected response.
|
41
|
-
|
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
|
-
|
48
|
-
|
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
|
-
|
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',
|
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) # => #<
|
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
|
-
-
|
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', '
|
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
@@ -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
|
@@ -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
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.
|
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
|
+
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:
|
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:
|
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
|