acme-cli 0.6.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2c369f88a826a9465922871deb4fbd8b3c232414
4
+ data.tar.gz: c706f4fa29eed7d87f3a3e421e4bcd7cea0fa43c
5
+ SHA512:
6
+ metadata.gz: bcd2a95fea94d400d9e118c8c68034e12ddf0dd9f6e768367c33f8e0d454ab2e187502191c593c820d558e34da91be8898ba5b4c03ec0e44b0d11b871eba3218
7
+ data.tar.gz: c9dec9b4a406dc85135d1d746e0bffde678d3fdcfe4fc354496bc6203104ff0e459daa18b3703977405f2c7e70195ec0ca61ecee85c1aaa5ddc397466d5507df
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.pem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,18 @@
1
+ language: ruby
2
+ sudo: false
3
+ cache: bundler
4
+
5
+ rvm:
6
+ - ruby-head
7
+ - 2.3.0
8
+ - 2.2.4
9
+ # - 2.1
10
+ # - 2.0
11
+
12
+ # before_script:
13
+ # - "bundle exec rake db:schema:load RAILS_ENV=test"
14
+
15
+ script:
16
+ - bundle exec rspec
17
+ # - ./exe/letsencrypt-cli
18
+
@@ -0,0 +1,83 @@
1
+ # Change Log
2
+
3
+ ## [v0.6.0](https://github.com/zealot128/ruby-acme-cli/tree/v0.6.0)
4
+
5
+ * Rename to acme-client for trademark reasons (#22)
6
+
7
+ ## [v0.5.0](https://github.com/zealot128/ruby-letsencrypt-cli/tree/v0.5.0)
8
+
9
+ * fix for CSR generated using a pre-1.0.2 OpenSSL with a client that doesn't properly specify the CSR version. See https://community.letsencrypt.org/t/openssl-bug-information/19591 (Acme::Client::Error::Malformed) https://github.com/zealot128/ruby-letsencrypt-cli/commit/b7fd1d592e9a74905f5067b64e0ac88a526cfeed
10
+ * explicitly require colorize Gem so color support does work https://github.com/zealot128/ruby-letsencrypt-cli/commit/b43510d1be1a495923ea8e27051b3a0bae4e23b0
11
+
12
+ ## [v0.4.1](https://github.com/zealot128/ruby-letsencrypt-cli/tree/v0.4.1)
13
+
14
+ * fix renewing via manage command when certificate is not expired https://github.com/zealot128/ruby-letsencrypt-cli/commit/8e6b9cd4a2b1d0caa7a85d6ead410b98555cb499
15
+ * require logger in beginning of file
16
+
17
+ ## [v0.4.0](https://github.com/zealot128/ruby-letsencrypt-cli/tree/v0.4.0)
18
+
19
+ * New ``--sub-directory`` option to use that instead of first domain name
20
+ * Solves issue #10 -- certificate_exists_and_valid_and_all_domains_included? returns true when cert is expired
21
+
22
+ ## [v0.3.0](https://github.com/zealot128/ruby-letsencrypt-cli/tree/v0.3.0)
23
+
24
+ * Certificate creation checks if existing certificate includes all requested domains. If at least one is missing, a new cert will be requested
25
+ * Added Ruby 2.3.0 and Ruby head to the build matrix
26
+
27
+ [Full Changelog](https://github.com/zealot128/ruby-letsencrypt-cli/compare/v0.2.0...v0.3.0)
28
+
29
+ ## [v0.2.0](https://github.com/zealot128/ruby-letsencrypt-cli/tree/v0.2.0)
30
+
31
+ [Full Changelog](https://github.com/zealot128/ruby-letsencrypt-cli/compare/v0.1.4...v0.2.0)
32
+
33
+ **Closed issues:**
34
+
35
+ - cf1e0d9 Exit code 2, if certificate is still valid
36
+
37
+ **Merged pull requests:**
38
+
39
+ - Apply strict permissions on private key [\#4](https://github.com/zealot128/ruby-letsencrypt-cli/pull/4) ([zygiss](https://github.com/zygiss))
40
+ - Fix typo in README [\#2](https://github.com/zealot128/ruby-letsencrypt-cli/pull/2) ([kenrick](https://github.com/kenrick))
41
+
42
+ ## [v0.1.4](https://github.com/zealot128/ruby-letsencrypt-cli/tree/v0.1.4) (2015-12-08)
43
+
44
+ * require higher acme-client version, that generated correct fullchain certs.
45
+ fullchain.pem is chain.pem + cert.pem, should be cert.pem + chain.pem [\#1](https://github.com/zealot128/ruby-letsencrypt-cli/issues/1)
46
+
47
+ [Full Changelog](https://github.com/zealot128/ruby-letsencrypt-cli/compare/v0.1.3...v0.1.4)
48
+
49
+ ## [v0.1.3](https://github.com/zealot128/ruby-letsencrypt-cli/tree/v0.1.3) (2015-12-06)
50
+
51
+ * Fixed registration
52
+ * Added various specs
53
+
54
+ [Full Changelog](https://github.com/zealot128/ruby-letsencrypt-cli/compare/v0.1.2...v0.1.3)
55
+
56
+ ## [v0.1.2](https://github.com/zealot128/ruby-letsencrypt-cli/tree/v0.1.2) (2015-12-05)
57
+
58
+ * Added manage command
59
+ * Improved nginx doc + Ruby installation
60
+
61
+ [Full Changelog](https://github.com/zealot128/ruby-letsencrypt-cli/compare/v0.1.1...v0.1.2)
62
+
63
+ ## [v0.1.1](https://github.com/zealot128/ruby-letsencrypt-cli/tree/v0.1.1) (2015-12-05)
64
+
65
+ [Full Changelog](https://github.com/zealot128/ruby-letsencrypt-cli/compare/v0.1.0...v0.1.1)
66
+
67
+ * b654469 new command: check PATH_TO_CERT
68
+
69
+ ## [v0.1.0](https://github.com/zealot128/ruby-letsencrypt-cli/tree/v0.1.0) (2015-12-05)
70
+
71
+ * released first public version
72
+ * added --version flag
73
+ * added explicit production server
74
+
75
+ [Full Changelog](https://github.com/zealot128/ruby-letsencrypt-cli/compare/v0.1.0.beta1...v0.1.0)
76
+
77
+ ## [v0.1.0.beta1](https://github.com/zealot128/ruby-letsencrypt-cli/tree/v0.1.0.beta1) (2015-12-05)
78
+ [Full Changelog](https://github.com/zealot128/ruby-letsencrypt-cli/compare/v0.1.0.pre...v0.1.0.beta1)
79
+
80
+ ## [v0.1.0.pre](https://github.com/zealot128/ruby-letsencrypt-cli/tree/v0.1.0.pre) (2015-12-05)
81
+
82
+
83
+ \* *This Change Log was (partially) automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in letsencrypt-cli.gemspec
4
+ gemspec :name => 'acme-cli'
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 TODO: Write your name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,170 @@
1
+ # ACME-Cli
2
+
3
+ [![Build Status](https://travis-ci.org/zealot128/ruby-acme-cli.svg?branch=travis)](https://travis-ci.org/zealot128/ruby-acme-cli)
4
+ [![Gem Version](https://badge.fury.io/rb/acme-cli.svg)](https://badge.fury.io/rb/acme-cli)
5
+
6
+ Yet another ACME client (e.g. to use together with Letsencrypt CA to issue TLS certs) for command lines using Ruby.
7
+
8
+ ## Installation
9
+
10
+ * This tool needs Ruby >= 2.1 (as the dependency ``acme-client`` needs that because of use of keyword arguments).
11
+ * OpenSSL bindings
12
+ * no sudo! (needs access to webserver-root ``/.well-known/acme-challenges`` alias for all domains - See later section for Nginx example)
13
+
14
+ ```
15
+ # check your ruby version:
16
+ $ ruby --version
17
+ ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-linux]
18
+
19
+ $ gem install acme-cli
20
+
21
+ $ acme-cli --version
22
+ 0.2.0
23
+ ```
24
+
25
+ ### Troubleshooting Ruby version
26
+
27
+ Unfortunately, most Linux distributions does not ship a current Ruby version (Version 1.9.3 or 2.0). Check, if your ruby version is at least 2.2. Otherwise you need to update the Ruby.
28
+
29
+ If you are installing this as a non-root user, you might want to try RVM. Installation itself needs no root, but needs some packages:
30
+
31
+ ```
32
+ sudo apt-get install curl bison build-essential zlib1g-dev libssl-dev libreadline6-dev libxml2-dev libgmp-dev git-core
33
+ ```
34
+
35
+ To install RVM:
36
+
37
+ ```
38
+ gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
39
+ \curl -sSL https://get.rvm.io | bash -s stable --autolibs=disable --auto-dotfiles
40
+
41
+ rvm install 2.2
42
+ source ~/.bashrc # or ~/.profile RVM tells you to reload your shell
43
+
44
+ ruby --version
45
+ ```
46
+
47
+ Notice: If you are using RVM, all your cronjobs must be run as a login shell, otherwise RVM does not work:
48
+
49
+ ```cron
50
+ * * * * * /bin/bash -l -c "acme-cli manage ..."
51
+ ```
52
+
53
+ Another way, e.g. on Ubuntu 14.04 might be to use the [Brightbox ppa](https://www.brightbox.com/blog/2015/01/05/ruby-2-2-0-packages-for-ubuntu/).
54
+
55
+ ## Usage
56
+
57
+ Specify ``-t`` to use Letsencrypt test server. Without it, all requests are called against the production server, that might have some more strict rate limiting. If you are just toying around, add the -t flag.
58
+
59
+ ```bash
60
+ # show all commands
61
+
62
+ acme-cli help
63
+
64
+ # show options for an individual command
65
+ acme-cli help cert
66
+
67
+ # creates account_key.json in current_dir
68
+ acme-cli register -t myemail@example.com
69
+
70
+ # authorize one or more domains/subdomains
71
+ acme-cli authorize -t --webroot-path /var/www/default example.com www.example.com somedir.example.com
72
+
73
+ # experimental: authorize all server_names in /etc/nginx/sites-enabled/*
74
+ acme-cli authorize_all -t --webroot-path /var/www/default
75
+
76
+ # create a certificate for domains that are already authorized within the last minutes (1h-2h I think)
77
+ # the first domain will be the cn subject. All other are subjectAlternateName
78
+ # if cert.pem already exists, will only create a new one if the old is expired
79
+ # (30 days before expiration) -> see full help
80
+ acme-cli help cert
81
+
82
+ acme-cli cert -t example.com www.example.com somdir.example.com
83
+ # will create key.pem fullchain.pem chain.pem and cert.pem in current directory
84
+
85
+ # checks validation date of given certificate.
86
+ # Exists non-zero if:
87
+ # * not exists (exit 1)
88
+ # * will expire in more than 30 days (exit code 2)
89
+ acme-cli check --days-valid 30 cert.pem
90
+ ```
91
+
92
+
93
+ And last but not least, the meta command ``manage`` that integrated check + authorize + cert (intended to be run as cronjob):
94
+
95
+ ```bash
96
+ $ acme-cli manage --days-valid 30 \
97
+ --account-key /home/acme/account_key.pem \
98
+ --webroot-path /home/acme/webroot/.well-known/acme-challenge \
99
+ --key-directory /home/acme/certs \
100
+ example.com www.example.com
101
+
102
+ 2015-12-05 23:40:04 +0100: Certificate /home/acme/certs/example.com/cert.pem does not exists
103
+ 2015-12-05 23:40:04 +0100: Authorizing example.com...
104
+ 2015-12-05 23:40:04 +0100: existing account key found
105
+ 2015-12-05 23:40:06 +0100: Authorization successful for example.com
106
+ 2015-12-05 23:40:06 +0100: Authorizing www.example.com
107
+ 2015-12-05 23:40:08 +0100: Authorization successful for www.example.com
108
+ 2015-12-05 23:40:08 +0100: creating new private key to /home/acme/certs/example.com/key.pem...
109
+ 2015-12-05 23:40:09 +0100: Certificate successfully created to /home/acme/certs/example.com/fullchain.pem /home/acme/certs/example.com/chain.pem and /home/acme/certs/example.com/cert.pem!
110
+ 2015-12-05 23:40:09 +0100: Certificate valid until: 2016-03-04 21:40:00 UTC
111
+
112
+ # Run command again exits immediately:
113
+ $ acme-cli manage --days-valid 30 --account-key /home/acme/account_key.pem --webroot-path /home/acme/webroot/.wel
114
+ l-known/acme-challenge --key-directory /home/acme/certs \
115
+ example.com www.example.com
116
+ 2015-12-05 23:40:17 +0100: Certificate '/home/acme/certs/example.com/cert.pem' valid until 2016-03-04.
117
+ $ echo $?
118
+ 1
119
+ ```
120
+
121
+ This had:
122
+
123
+ 1. check if /home/acme/certs/example.com/cert.pem exists and expires in less than 30 days (or exit 1 at this point)
124
+ 2. authorize all domains + subdomains
125
+ 3. issue one certificate with those domains and place it under /home/acme/certs/example.com/[key.pem,fullchain.pem,chain.pem,cert.pem]
126
+ 4. exit 0 -> so can be && with ``service nginx reload`` or mail deliver
127
+
128
+ For running as cron, reducing log level to fatal might be desirable: ``acme-cli manage --log-level fatal``.
129
+
130
+ ## Example integration Nginx:
131
+
132
+ ```nginx
133
+ server {
134
+ listen 80;
135
+ server_name example.com www.example.com somedir.example.com
136
+ location /.well-known/acme-challenge {
137
+ alias /home/acme/webroot/.well-known/acme-challenge;
138
+ default_type "text/plain";
139
+ try_files $uri =404;
140
+ }
141
+ ```
142
+
143
+ notice the location - alias. Use this dir with ``--webroot-path`` for authorization.
144
+
145
+ Afterwards, use the fullchain.pem and key.pem:
146
+
147
+ ```nginx
148
+ server {
149
+ listen 443 ssl;
150
+ server_name example.com www.example.com;
151
+ ssl on;
152
+ ssl_certificate_key /home/acme/certs/example.com/key.pem;
153
+ ssl_certificate /home/acme/certs/example.com/fullchain.pem;
154
+
155
+ # use the settings from: https://gist.github.com/konklone/6532544
156
+ ```
157
+
158
+ ## Development
159
+
160
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
161
+
162
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
163
+
164
+ ## Contributing
165
+
166
+ 1. Fork it ( https://github.com/zealot128/ruby-acme-cli/fork )
167
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
168
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
169
+ 4. Push to the branch (`git push origin my-new-feature`)
170
+ 5. Create a new Pull Request
@@ -0,0 +1,6 @@
1
+ require "rake/clean"
2
+ CLOBBER.include "pkg"
3
+
4
+ require "bundler/gem_helper"
5
+ Bundler::GemHelper.install_tasks name: 'acme-cli'
6
+
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'letsencrypt/cli/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "acme-cli"
8
+ spec.version = Letsencrypt::Cli::VERSION
9
+ spec.authors = ["Stefan Wienert"]
10
+ spec.email = ["stwienert@gmail.com"]
11
+
12
+ spec.summary = %q{slim ACME (e. g. letsencrypt) client for quickly authorizing (multiple) domains and issuing certificates}
13
+ spec.homepage = "https://github.com/zealot28/acme-cli"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = '>= 2.0.0'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_runtime_dependency 'acme-client', '>= 0.2.4'
23
+ spec.add_runtime_dependency 'thor'
24
+ spec.add_runtime_dependency 'colorize'
25
+
26
+ spec.add_development_dependency 'pry'
27
+ spec.add_development_dependency 'activesupport', '>= 3.0'
28
+ spec.add_development_dependency 'simplecov'
29
+ spec.add_development_dependency 'vcr', "~> 3.0"
30
+ spec.add_development_dependency 'webmock', "~> 1.22"
31
+ spec.add_development_dependency 'timecop', "~> 0.8"
32
+ spec.add_development_dependency "bundler", "~> 1.7"
33
+ spec.add_development_dependency "rake", "~> 10.0"
34
+ spec.add_development_dependency "rspec", "~> 3.0"
35
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "letsencrypt/cli"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "letsencrypt-cli"
4
+
5
+ Letsencrypt::Cli::App.start
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'letsencrypt/cli/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "letsencrypt-cli"
8
+ spec.version = Letsencrypt::Cli::VERSION
9
+ spec.authors = ["Stefan Wienert"]
10
+ spec.email = ["stwienert@gmail.com"]
11
+
12
+ spec.summary = %q{slim letsencrypt client for quickly authorizing (multiple) domains and issuing certificates}
13
+ spec.homepage = "https://github.com/zealot128/letsencrypt-cli"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = '>= 2.0.0'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_runtime_dependency 'acme-client', '>= 0.2.4'
23
+ spec.add_runtime_dependency 'thor'
24
+ spec.add_runtime_dependency 'colorize'
25
+
26
+ spec.add_development_dependency 'pry'
27
+ spec.add_development_dependency 'activesupport', '>= 3.0'
28
+ spec.add_development_dependency 'simplecov'
29
+ spec.add_development_dependency 'vcr', "~> 3.0"
30
+ spec.add_development_dependency 'webmock', "~> 1.22"
31
+ spec.add_development_dependency 'timecop', "~> 0.8"
32
+ spec.add_development_dependency "bundler", "~> 1.7"
33
+ spec.add_development_dependency "rake", "~> 10.0"
34
+ spec.add_development_dependency "rspec", "~> 3.0"
35
+ end
@@ -0,0 +1 @@
1
+ require 'letsencrypt/cli'
@@ -0,0 +1,10 @@
1
+ require "letsencrypt/cli/version"
2
+ require "letsencrypt/cli/acme_wrapper"
3
+ require "letsencrypt/cli/app"
4
+
5
+
6
+ module Letsencrypt
7
+ module Cli
8
+ # Your code goes here...
9
+ end
10
+ end
@@ -0,0 +1,177 @@
1
+ require 'json'
2
+ require 'acme-client'
3
+ require 'logger'
4
+ require 'colorize'
5
+
6
+ class AcmeWrapper
7
+ def initialize(options)
8
+ @options = options
9
+ if !@options[:color]
10
+ String.disable_colorization = true
11
+ end
12
+ end
13
+
14
+ def log(message, severity=:info)
15
+ @logger ||= Logger.new(STDOUT).tap {|logger|
16
+ logger.level = Logger::SEV_LABEL.index(@options[:log_level].upcase)
17
+ logger.formatter = proc do |sev, datetime, progname, msg|
18
+ "#{datetime.to_s.light_black}: #{msg}\n"
19
+ end
20
+ }
21
+ @logger.send(severity, message)
22
+ end
23
+
24
+ def client
25
+ @client ||= Acme::Client.new(private_key: account_key, endpoint: endpoint)
26
+ end
27
+
28
+ def authorize(domain)
29
+ FileUtils.mkdir_p(@options[:webroot_path])
30
+ log "Authorizing #{domain.blue}.."
31
+ authorization = client.authorize(domain: domain)
32
+
33
+ challenge = authorization.http01
34
+
35
+ challenge_file = File.join(@options[:webroot_path], challenge.filename.split('/').last)
36
+ log "Writing challenge to #{challenge_file}", :debug
37
+ File.write(challenge_file, challenge.file_content)
38
+
39
+ challenge.request_verification
40
+
41
+ 5.times do
42
+ log "Checking verification...", :debug
43
+ sleep 1
44
+ break if challenge.verify_status != 'pending'
45
+ end
46
+ if challenge.verify_status == 'valid'
47
+ log "Authorization successful for #{domain.green}"
48
+ File.unlink(challenge_file)
49
+ true
50
+ else
51
+ log "Authorization error for #{domain.red}", :error
52
+ log challenge.error['detail']
53
+ false
54
+ end
55
+ end
56
+
57
+ def cert(domains)
58
+ return if certificate_exists_and_valid_and_all_domains_included?(domains)
59
+ csr = OpenSSL::X509::Request.new
60
+ certificate_private_key = find_or_create_pkey(@options[:private_key_path], "private key", @options[:key_length] || 2048)
61
+
62
+ csr.subject = OpenSSL::X509::Name.new([
63
+ # ['C', options[:country], OpenSSL::ASN1::PRINTABLESTRING],
64
+ # ['ST', options[:state], OpenSSL::ASN1::PRINTABLESTRING],
65
+ # ['L', options[:city], OpenSSL::ASN1::PRINTABLESTRING],
66
+ # ['O', options[:organization], OpenSSL::ASN1::UTF8STRING],
67
+ # ['OU', options[:department], OpenSSL::ASN1::UTF8STRING],
68
+ # ['CN', options[:common_name], OpenSSL::ASN1::UTF8STRING],
69
+ # ['emailAddress', options[:email], OpenSSL::ASN1::UTF8STRING]
70
+ ['CN', domains.first, OpenSSL::ASN1::UTF8STRING]
71
+ ])
72
+ if domains.count > 1
73
+ ef = OpenSSL::X509::ExtensionFactory.new
74
+ exts = [ ef.create_extension( "subjectAltName", domains.map{|domain| "DNS:#{domain}"}.join(','), false ) ]
75
+ attrval = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence(exts)])
76
+ attrs = [
77
+ OpenSSL::X509::Attribute.new('extReq', attrval),
78
+ OpenSSL::X509::Attribute.new('msExtReq', attrval),
79
+ ]
80
+ attrs.each do |attr|
81
+ csr.add_attribute(attr)
82
+ end
83
+ end
84
+ csr.version = 2
85
+ csr.public_key = certificate_private_key.public_key
86
+ csr.sign(certificate_private_key, OpenSSL::Digest::SHA256.new)
87
+ certificate = client.new_certificate(csr)
88
+ File.write(@options[:fullchain_path], certificate.fullchain_to_pem)
89
+ File.write(@options[:chain_path], certificate.chain_to_pem)
90
+ File.write(@options[:certificate_path], certificate.to_pem)
91
+ log "Certificate successfully created to #{@options[:fullchain_path]} #{@options[:chain_path]} and #{@options[:certificate_path]}!".green
92
+ log "Certificate valid until: #{certificate.x509.not_after}"
93
+ end
94
+
95
+ def check_certificate(path)
96
+ unless File.exists?(path)
97
+ log "Certificate #{path} does not exists", :warn
98
+ return false
99
+ end
100
+ cert = OpenSSL::X509::Certificate.new(File.read(path))
101
+ renew_on = cert.not_after.to_date - @options[:days_valid]
102
+ log "Certificate '#{path}' valid until #{cert.not_after.to_date}.", :info
103
+ if Date.today >= renew_on
104
+ log "Certificate '#{path}' should be renewed!", :warn
105
+ return false
106
+ else
107
+ true
108
+ end
109
+ end
110
+
111
+ def revoke_certificate(path)
112
+ unless File.exists?(path)
113
+ log "Certificate #{path} does not exists", :warn
114
+ return false
115
+ end
116
+ cert = OpenSSL::X509::Certificate.new(File.read(path))
117
+ if client.revoke_certificate(cert)
118
+ log "Certificate '#{path}' was revoked", :info
119
+ end
120
+ true
121
+ rescue Acme::Client::Error::Malformed => e
122
+ log e.message, :error
123
+ return false
124
+ end
125
+
126
+ private
127
+
128
+ def certificate_exists_and_valid_and_all_domains_included?(domains)
129
+ return false if !File.exists?(@options[:certificate_path])
130
+ cert = OpenSSL::X509::Certificate.new(File.read(@options[:certificate_path]))
131
+ domains_in_cert = cert.extensions.map(&:to_h).select{|i| i['oid'] == 'subjectAltName' }.map{|i| i['value']}.join(', ').split(/, */).map{|i| i.sub(/^DNS:/, '') } +
132
+ [ cert.subject.to_s.sub(%r{/CN=}, '') ].uniq.sort
133
+ missing_domains = domains.sort.uniq - domains_in_cert
134
+ if missing_domains != []
135
+ log "Certificate '#{@options[:certificate_path]}' missing domains #{missing_domains.join(' ')}. Existing: #{domains_in_cert.join(' ')}", :warn
136
+ return false
137
+ end
138
+ expires_on = cert.not_after.to_date
139
+ if expires_on <= Date.today
140
+ log "Certificate '#{@options[:certificate_path]}' has expired on #{expires_on}.", :warn
141
+ return false
142
+ end
143
+ renew_on = expires_on - @options[:days_valid]
144
+ if renew_on > Date.today
145
+ log "Certificate '#{@options[:certificate_path]}' still valid till #{cert.not_after.to_date}.", :warn
146
+ log "Won't renew until #{renew_on} (#{@options[:days_valid]} days before)", :warn
147
+ exit 2
148
+ end
149
+
150
+ false
151
+ end
152
+
153
+ def endpoint
154
+ if @options[:test]
155
+ "https://acme-staging.api.letsencrypt.org"
156
+ else
157
+ "https://acme-v01.api.letsencrypt.org"
158
+ end
159
+ end
160
+
161
+ def account_key
162
+ @account_key ||= find_or_create_pkey(@options[:account_key], "account key", @options[:key_length] || 4096)
163
+ end
164
+
165
+ def find_or_create_pkey(file_path, name, length)
166
+ if File.exists?(file_path)
167
+ log "existing account key found"
168
+ OpenSSL::PKey::RSA.new File.read file_path
169
+ else
170
+ log "creating new private key to #{file_path}..."
171
+ private_key = OpenSSL::PKey::RSA.new(length)
172
+ File.write(file_path, private_key.to_s)
173
+ File.chmod(0400, file_path)
174
+ private_key
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,116 @@
1
+ require 'thor'
2
+ require 'colorize'
3
+ require 'fileutils'
4
+ module Letsencrypt
5
+ module Cli
6
+ class App < Thor
7
+ class_option :account_key, desc: "Path to private key file (will be created if not exists)", aliases: "-a", default: 'account_key.pem'
8
+ class_option :test, desc: "Use staging url of Letsencrypt instead of production server", aliases: "-t", type: :boolean
9
+ class_option :log_level, desc: "Log Level (debug, info, warn, error, fatal)", default: "info"
10
+ class_option :color, desc: "Disable colorize", default: true, type: :boolean
11
+
12
+ desc 'register EMAIL', 'Register account'
13
+ method_option :key_length, desc: "Length of generated private key", type: :numeric, default: 4096
14
+ def register(email)
15
+ if email.nil? || email == ""
16
+ wrapper.log "no E-Mail specified!", :fatal
17
+ exit 1
18
+ end
19
+ if !email[/.*@.*/]
20
+ wrapper.log "not an email", :fatal
21
+ exit 1
22
+ end
23
+ registration = wrapper.client.register(contact: "mailto:" + email)
24
+ registration.agree_terms
25
+ wrapper.log "Account created, Terms accepted"
26
+ end
27
+
28
+ desc 'authorize_all', "Verify all server_names in /etc/nginx/sites-enabled/* (needs read access)"
29
+ method_option :webroot_path, desc: "Path to mapped .acme-challenge folder (no subdir)", aliases: '-w', required: true
30
+ method_option :webserver_dir, desc: "Path to webserver configs", default: "/etc/nginx/sites-enabled"
31
+ def authorize_all
32
+ lines = Dir[ File.join(@options[:webserver_dir], "*")].map{|file| File.read(file).lines.grep(/^\s*server_name/) }.flatten
33
+ domains = lines.flatten.map{|i| i.strip.split(/[; ]/).drop(1) }.flatten.reject{|i| i.length < 3 }.uniq
34
+ authorize(*domains)
35
+ end
36
+
37
+ desc 'authorize [DOMAINS]', 'Authorize all domains'
38
+ method_option :webroot_path, desc: "Path to mapped .well-known/acme-challenge folder (no subdirs will be created)", aliases: '-w', required: true
39
+ def authorize(*domains)
40
+ rc = 0
41
+ domains.each do |domain|
42
+ if !wrapper.authorize(domain)
43
+ rc = 1
44
+ end
45
+ end
46
+ if rc != 0
47
+ exit rc
48
+ end
49
+ end
50
+
51
+ desc "cert [DOMAINS]", "create certificate and private key pair for domains. The first domain is the main CN domain, the reset will be added as SAN. If the given certificate-path already exists, script will exit non-zero if the certificate is still valid until the given number of days before."
52
+ method_option :private_key_path, desc: "Path to private key. Will be created if non existant", aliases: '-k', default: 'key.pem'
53
+ method_option :key_length, desc: "Length of private key", default: 2048, type: :numeric
54
+ method_option :fullchain_path, desc: "Path to fullchain certificate (Nginx) (will be overwritten if exists!)", aliases: '-f', default: 'fullchain.pem'
55
+ method_option :certificate_path, desc: "Path to certificate (Apache)", aliases: '-c', default: 'cert.pem'
56
+ method_option :chain_path, desc: "Path to chain (Apache)", aliases: '-n', default: 'chain.pem'
57
+ method_option :days_valid, desc: "If the --certificate-path already exists, only create new stuff, if that certificate isn't valid for less than the given number of days", default: 30, type: :numeric
58
+ def cert(*domains)
59
+ if domains.length == 0
60
+ wrapper.log "no domains given", :fatal
61
+ exit 1
62
+ end
63
+ wrapper.cert(domains)
64
+ end
65
+
66
+ desc "check PATH_TO_CERTIFICATE", "checks, if a given certificate exists and is valid until DAYS_VALID"
67
+ method_option :days_valid, desc: "If the --certificate-path already exists, only create new stuff, if that certificate isn't valid for less than the given number of days", default: 30, type: :numeric
68
+ def check(path)
69
+ if !wrapper.check_certificate(path)
70
+ exit 1
71
+ end
72
+ end
73
+
74
+ desc "revoke PATH_TO_CERTIFICATE", "revokes a given certificate"
75
+ def revoke(path)
76
+ if !wrapper.revoke_certificate(path)
77
+ exit 1
78
+ end
79
+ end
80
+
81
+ desc "manage DOMAINS", "meta command that will: check if cert already exists / still valid (exits zero if nothing todo, exits 2 if certificate is still valid) + authorize given domains + issue certificate for given domains"
82
+ method_option :key_length, desc: "Length of private key", default: 2048, type: :numeric
83
+ method_option :days_valid, desc: "If the --certificate-path already exists, only create new stuff, if that certificate isn't valid for less than the given number of days", default: 30, type: :numeric
84
+ method_option :webroot_path, desc: "Path to mapped .well-known/acme-challenge folder (no subdirs will be created)", aliases: '-w', required: true
85
+ method_option :key_directory, desc: "Base directory for certificate storage.", default: "~/certs/"
86
+ method_option :sub_directory, desc: "Sub-directory name in base directory where all certs + key are stored. If not set, the first domain name will be used", default: nil
87
+ def manage(*domains)
88
+ key_dir = File.join(@options[:key_directory], @options[:sub_directory] || domains.first)
89
+ FileUtils.mkdir_p(key_dir)
90
+ @options = @options.merge(
91
+ :private_key_path => File.join(key_dir, 'key.pem'),
92
+ :fullchain_path => File.join(key_dir, 'fullchain.pem'),
93
+ :certificate_path => File.join(key_dir, 'cert.pem'),
94
+ :chain_path => File.join(key_dir, 'chain.pem'),
95
+ )
96
+ if wrapper.check_certificate(@options[:certificate_path])
97
+ exit 2
98
+ end
99
+ authorize(*domains)
100
+ cert(*domains)
101
+ end
102
+
103
+ map %w[--version -v] => :__print_version
104
+ desc "--version, -v", "print the version"
105
+ def __print_version
106
+ puts Letsencrypt::Cli::VERSION
107
+ end
108
+
109
+ private
110
+
111
+ def wrapper
112
+ @wrapper ||= AcmeWrapper.new(options)
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,5 @@
1
+ module Letsencrypt
2
+ module Cli
3
+ VERSION = "0.6.1"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,232 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acme-cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.1
5
+ platform: ruby
6
+ authors:
7
+ - Stefan Wienert
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-02-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: acme-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: thor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: colorize
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: vcr
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: webmock
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.22'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.22'
125
+ - !ruby/object:Gem::Dependency
126
+ name: timecop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.8'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.8'
139
+ - !ruby/object:Gem::Dependency
140
+ name: bundler
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '1.7'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '1.7'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rake
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '10.0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '10.0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rspec
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '3.0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '3.0'
181
+ description:
182
+ email:
183
+ - stwienert@gmail.com
184
+ executables:
185
+ - acme-cli
186
+ extensions: []
187
+ extra_rdoc_files: []
188
+ files:
189
+ - ".gitignore"
190
+ - ".rspec"
191
+ - ".travis.yml"
192
+ - CHANGELOG.md
193
+ - Gemfile
194
+ - LICENSE.txt
195
+ - README.md
196
+ - Rakefile
197
+ - acme-cli.gemspec
198
+ - bin/console
199
+ - bin/setup
200
+ - exe/acme-cli
201
+ - letsencrypt-cli.gemspec
202
+ - lib/letsencrypt-cli.rb
203
+ - lib/letsencrypt/cli.rb
204
+ - lib/letsencrypt/cli/acme_wrapper.rb
205
+ - lib/letsencrypt/cli/app.rb
206
+ - lib/letsencrypt/cli/version.rb
207
+ homepage: https://github.com/zealot28/acme-cli
208
+ licenses:
209
+ - MIT
210
+ metadata: {}
211
+ post_install_message:
212
+ rdoc_options: []
213
+ require_paths:
214
+ - lib
215
+ required_ruby_version: !ruby/object:Gem::Requirement
216
+ requirements:
217
+ - - ">="
218
+ - !ruby/object:Gem::Version
219
+ version: 2.0.0
220
+ required_rubygems_version: !ruby/object:Gem::Requirement
221
+ requirements:
222
+ - - ">="
223
+ - !ruby/object:Gem::Version
224
+ version: '0'
225
+ requirements: []
226
+ rubyforge_project:
227
+ rubygems_version: 2.6.14
228
+ signing_key:
229
+ specification_version: 4
230
+ summary: slim ACME (e. g. letsencrypt) client for quickly authorizing (multiple) domains
231
+ and issuing certificates
232
+ test_files: []