acme-cli 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +18 -0
- data/CHANGELOG.md +83 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +170 -0
- data/Rakefile +6 -0
- data/acme-cli.gemspec +35 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/exe/acme-cli +5 -0
- data/letsencrypt-cli.gemspec +35 -0
- data/lib/letsencrypt-cli.rb +1 -0
- data/lib/letsencrypt/cli.rb +10 -0
- data/lib/letsencrypt/cli/acme_wrapper.rb +177 -0
- data/lib/letsencrypt/cli/app.rb +116 -0
- data/lib/letsencrypt/cli/version.rb +5 -0
- metadata +232 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -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
|
+
|
data/CHANGELOG.md
ADDED
@@ -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
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
data/acme-cli.gemspec
ADDED
@@ -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
|
data/bin/console
ADDED
@@ -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
|
data/bin/setup
ADDED
data/exe/acme-cli
ADDED
@@ -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,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
|
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: []
|