acme-cli 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []