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 +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
|
+
[](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
|