acmesmith 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9b35c4efa4b6c3d0c52ef547957374b6b004c29f
4
+ data.tar.gz: 09aa0fe3f0ff3030189c79b530d082468950a2f7
5
+ SHA512:
6
+ metadata.gz: fe49168655a265f35c1db3769260036f903cb4df15b82bfe6363c6462c0e140c1199de3ccaf0e5a141eedd382e70dfe5b7ae2efbdbdc0c7608b92d876d7cb0b2
7
+ data.tar.gz: 500736e89be09e585d115391cfc08e999c8d4d6c435c900c639088b3808e369ff43cebc4892ee29882e14d64fea8c2a3c2a4bc54d688f1a01084301bcf071398
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ config.yml
11
+ config.dev.yml
12
+ /storage/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.4.0
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in acmesmith.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 sorah (Shota Fukumori)
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,250 @@
1
+ # Acmesmith: An effective ACME client to operate on multiple servers environment with the cloud
2
+
3
+ Acmesmith is an [ACME (Automatic Certificate Management Environment)](https://github.com/ietf-wg-acme/acme) client that works perfect on environment with multiple servers. This client saves certificate and keys on cloud services (e.g. AWS S3) securely, then allow to deploy issued certificates onto your servers smoothly. This works well on [Let's encrypt](https://letsencrypt.org).
4
+
5
+ This tool is written in Ruby, but this saves certificates in simple scheme, so you can fetch certificate by your own simple scripts.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'acmesmith'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install acmesmith
22
+
23
+ ## Usage
24
+
25
+ ```
26
+ $ acmesmith register CONTACT # Create account key (contact e.g. mailto:xxx@example.org)
27
+ ```
28
+
29
+ ```
30
+ $ acmesmith authorize DOMAIN # Get authz for DOMAIN.
31
+ $ acmesmith request COMMON_NAME [SAN] # request certificate for CN +COMMON_NAME+ with SANs +SAN+
32
+ ```
33
+
34
+ ```
35
+ $ acmesmith list [COMMON_NAME] # list certificates or its versions
36
+ $ acmesmith current COMMON_NAME # show current version for certificate
37
+ $ acmesmith show-certificate COMMON_NAME # show certificate
38
+ $ acmesmith show-private-key COMMON_NAME # show private key
39
+ ```
40
+
41
+ ## Configuration
42
+
43
+ See [config.sample.yml](./config.sample.yml) to start.
44
+
45
+ ``` yaml
46
+ endpoint: https://acme-staging.api.letsencrypt.org/
47
+ # endpoint: https://acme-v01.api.letsencrypt.org/ # productilon
48
+
49
+ storage:
50
+ # configure where to store keys and certificates; described later
51
+ challenge_responders:
52
+ # configure how to respond ACME challenges; described later
53
+
54
+ account_key_passphrase: password
55
+ certificate_key_passphrase: secret
56
+ ```
57
+
58
+ ### Storage
59
+
60
+ #### S3
61
+
62
+ ```
63
+ storage:
64
+ type: s3
65
+ region:
66
+ bucket:
67
+ # prefix:
68
+ # aws_access_key: # aws credentials (optional); If omit, default configuration of aws-sdk use will be used.
69
+ # access_key_id:
70
+ # secret_access_key:
71
+ # session_token:
72
+ # use_kms: true
73
+ # kms_key_id: # KMS key id (optional); if omit, default AWS managed key for S3 will be used
74
+ # kms_key_id_account: # KMS key id for account key (optional); This overrides kms_key_id
75
+ # kms_key_id_certificate_key: # KMS key id for private keys for certificates (optional); This oveerides kms_key_id
76
+ ```
77
+
78
+ This saves certificates and keys in the following S3 keys:
79
+
80
+ - `{prefix}/account.pem`: Account private key in pem
81
+ - `{prefix}/certs/{common_name}/current`: text file contains current version name
82
+ - `{prefix}/certs/{common_name}/{version}/cert.pem`: certificate in pem
83
+ - `{prefix}/certs/{common_name}/{version}/key.pem`: private key in pem
84
+ - `{prefix}/certs/{common_name}/{version}/chain.pem`: CA chain in pem
85
+ - `{prefix}/certs/{common_name}/{version}/fullchain.pem`: certificate + CA chain in pem. This is suitable for some server softwares like nginx.
86
+
87
+ #### Filesystem
88
+
89
+ This is not recommended. If you're planning to use this, make sure backing up the keys.
90
+
91
+ ```
92
+ storage:
93
+ type: filesystem
94
+ path: /path/to/directory/to/store/keys
95
+ ```
96
+
97
+ ### Challenge Responders
98
+
99
+ Challenge responders responds to ACME challenges to prove domain ownership to CA.
100
+
101
+ #### Route53
102
+
103
+ Route53 responder supports `dns-01` challenge type. This assumes domain NS are managed under Route53 hosted zone.
104
+
105
+ ```
106
+ challenge_responders:
107
+ - route53:
108
+ # aws_access_key: # aws credentials (optional); If omit, default configuration of aws-sdk use will be used.
109
+ # access_key_id:
110
+ # secret_access_key:
111
+ # session_token:
112
+ # hosted_zone_map: # hosted zone map (optional); This is to specify exactly one hosted zone to use. This will be required when there are multiple hosted zone with same domain name. Usually
113
+ # "example.org.": "/hostedzone/DEADBEEF"
114
+ ```
115
+
116
+ ## Vendor dependent notes
117
+
118
+ ### AWS
119
+
120
+ #### IAM policy
121
+
122
+ ##### All access (S3 + Route53 setup)
123
+
124
+ ``` json
125
+ {
126
+ "Version": "2012-10-17",
127
+ "Statement": [
128
+ {
129
+ "Effect": "Allow",
130
+ "Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket"],
131
+ "Resource": ["arn:aws:s3:::{BUCKET-NAME}", "arn:aws:s3:::{BUCKET-NAME}/*"]
132
+ },
133
+ {
134
+ "Effect": "Allow",
135
+ "Action": "route53:ListHostedZones",
136
+ "Resource": "*"
137
+ }
138
+ {
139
+ "Effect": "Allow",
140
+ "Action": "route53:ChangeResourceRecordSets",
141
+ "Resource": ["arn:aws:route53:::hostedzone/*"]
142
+ }
143
+ {
144
+ "Effect": "Allow",
145
+ "Action": "route53:GetChange",
146
+ "Resource": "*"
147
+ }
148
+ ]
149
+ }
150
+ ```
151
+
152
+ Note: You can limit allowed hosted zone by modifying `Resource` of `route53:ChangeResourceRecordSets`
153
+
154
+ ##### Only fetching certificates
155
+
156
+ ``` json
157
+ {
158
+ "Version": "2012-10-17",
159
+ "Statement": [
160
+ {
161
+ "Effect": "Allow",
162
+ "Action": ["s3:GetObject"],
163
+ "Resource": ["arn:aws:s3:::{BUCKET-NAME}/certs/*"]
164
+ },
165
+ {
166
+ "Effect": "Allow",
167
+ "Action": ["s3:ListBucket"],
168
+ "Resource": ["arn:aws:s3:::{BUCKET-NAME}"],
169
+ "Condition": {
170
+ "StringEquals": {
171
+ "s3:delimiter": "/"
172
+ },
173
+ "StringLike": {
174
+ "s3:prefix": "certs/*",
175
+ }
176
+ }
177
+ }
178
+ ]
179
+ }
180
+ ```
181
+
182
+ #### AWS KMS key policy for customer managed keys
183
+
184
+ If you're going to use `aws_kms_id` option to use customer managed keys instead of AWS managed default KMS key for Amazon S3, use the following policy as base:
185
+
186
+ Be sure to replace `{S3-REGION}` and `{YOUR-AWS-ACCOUNT-ID}` before applying it.
187
+
188
+ ``` json
189
+ {
190
+ "Version": "2012-10-17",
191
+ "Id": "kms-acmesmith-s3-policy",
192
+ "Statement": [
193
+ {
194
+ "Effect": "Allow",
195
+ "Principal": {
196
+ "AWS": "*"
197
+ },
198
+ "Action": [
199
+ "kms:Encrypt",
200
+ "kms:Decrypt",
201
+ "kms:ReEncrypt*",
202
+ "kms:GenerateDataKey*",
203
+ "kms:DescribeKey"
204
+ ],
205
+ "Resource": "*",
206
+ "Condition": {
207
+ "StringEquals": {
208
+ "kms:ViaService": "s3.{S3-REGION}.amazonaws.com",
209
+ "kms:CallerAccount": "{YOUR-AWS-ACCOUNT-ID}"
210
+ }
211
+ }
212
+ },
213
+ {
214
+ "Effect": "Allow",
215
+ "Principal": {
216
+ "AWS": "arn:aws:iam::{YOUR-AWS-ACCOUNT-ID}:root"
217
+ },
218
+ "Action": [
219
+ "kms:Describe*",
220
+ "kms:Get*",
221
+ "kms:List*",
222
+ "kms:Put*"
223
+ ],
224
+ "Resource": "*"
225
+ }
226
+ ]
227
+ }
228
+ ```
229
+
230
+ ## Development
231
+
232
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
233
+
234
+ 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`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
235
+
236
+ ### Todos
237
+
238
+ - Tests
239
+ - Support post actions (notifying servers, deploying to somewhere, etc...)
240
+ - Automated renewal command (request new certificates for existing certificates that expires soon)
241
+
242
+ ## Contributing
243
+
244
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/acmesmith.
245
+
246
+
247
+ ## License
248
+
249
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
250
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/acmesmith.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'acmesmith/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "acmesmith"
8
+ spec.version = Acmesmith::VERSION
9
+ spec.authors = ["sorah (Shota Fukumori)"]
10
+ spec.email = ["her@sorah.jp"]
11
+
12
+ spec.summary = %q{ACME client (Let's encrypt client) to manage certificate in multi server environment with cloud services (e.g. AWS)}
13
+ spec.description = <<-EOF
14
+ Acmesmith is an [ACME (Automatic Certificate Management Environment)](https://github.com/ietf-wg-acme/acme) client that works perfect on environment with multiple servers. This client saves certificate and keys on cloud services (e.g. AWS S3) securely, then allow to deploy issued certificates onto your servers smoothly. This works well on [Let's encrypt](https://letsencrypt.org).
15
+ EOF
16
+ spec.homepage = "https://github.com/sorah/acmesmith"
17
+ spec.license = "MIT"
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
+ spec.bindir = "bin"
21
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "acme-client"
25
+ spec.add_dependency "aws-sdk", "> 2"
26
+ spec.add_dependency "thor"
27
+
28
+ spec.add_development_dependency "bundler"
29
+ spec.add_development_dependency "rake"
30
+ spec.add_development_dependency "rspec"
31
+ end
data/bin/acmesmith ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'acmesmith/command'
3
+
4
+ Acmesmith::Command.start
data/config.sample.yml ADDED
@@ -0,0 +1,18 @@
1
+ endpoint: https://acme-staging.api.letsencrypt.org/
2
+ # endpoint: https://acme-v01.api.letsencrypt.org/
3
+ storage:
4
+ type: s3
5
+ region: 'ap-northeast-1'
6
+ bucket: '...'
7
+ # prefix: '...'
8
+ # kms_key_id: 'arn:aws:kms:...'
9
+
10
+ # storage:
11
+ # type: filesystem
12
+ # path: ./storage
13
+
14
+ challenge_responders:
15
+ - route53: {}
16
+
17
+ account_key_passphrase: password
18
+ certificate_key_passphrase: secret
data/lib/acmesmith.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "acmesmith/version"
2
+
3
+ module Acmesmith
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,53 @@
1
+ require 'openssl'
2
+
3
+ module Acmesmith
4
+ class AccountKey
5
+ class PassphraseRequired < StandardError; end
6
+
7
+ def self.generate(bit_length = 2048)
8
+ new OpenSSL::PKey::RSA.new(bit_length)
9
+ end
10
+
11
+ def initialize(private_key, passphrase = nil)
12
+ case private_key
13
+ when String
14
+ @raw_private_key = private_key
15
+ if passphrase
16
+ self.key_passphrase = passphrase
17
+ else
18
+ begin
19
+ @private_key = OpenSSL::PKey::RSA.new(@raw_private_key, '')
20
+ rescue OpenSSL::PKey::RSAError
21
+ # may be encrypted
22
+ end
23
+ end
24
+ when OpenSSL::PKey::RSA
25
+ @private_key = private_key
26
+ else
27
+ raise TypeError, 'private_key is expected to be a String or OpenSSL::PKey::RSA'
28
+ end
29
+ end
30
+
31
+ def key_passphrase=(pw)
32
+ raise 'private_key already given' if @private_key
33
+
34
+ @private_key = OpenSSL::PKey::RSA.new(@raw_private_key, pw)
35
+
36
+ @raw_private_key = nil
37
+ nil
38
+ end
39
+
40
+ def private_key
41
+ return @private_key if @private_key
42
+ raise PassphraseRequired, 'key_passphrase required'
43
+ end
44
+
45
+ def export(passphrase, cipher: OpenSSL::Cipher.new('aes-256-cbc'))
46
+ if passphrase
47
+ private_key.export(cipher, passphrase)
48
+ else
49
+ private_key.export
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,98 @@
1
+ require 'openssl'
2
+
3
+ module Acmesmith
4
+ class Certificate
5
+ class PassphraseRequired < StandardError; end
6
+
7
+ def self.from_acme_client_certificate(c)
8
+ new c.x509, c.chain_to_pem, c.request.private_key, nil, c.request.csr
9
+ end
10
+
11
+ def initialize(certificate, chain, private_key, key_passphrase = nil, csr = nil)
12
+ @certificate = case certificate
13
+ when OpenSSL::X509::Certificate
14
+ certificate
15
+ when String
16
+ OpenSSL::X509::Certificate.new(certificate)
17
+ else
18
+ raise TypeError, 'certificate is expected to be a String or OpenSSL::X509::Certificate'
19
+ end
20
+ @chain = case chain
21
+ when String
22
+ chain
23
+ when nil
24
+ chain
25
+ else
26
+ raise TypeError, 'chain is expected to be a String'
27
+ end
28
+
29
+ case private_key
30
+ when String
31
+ @raw_private_key = private_key
32
+ if key_passphrase
33
+ self.key_passphrase = key_passphrase
34
+ else
35
+ begin
36
+ @private_key = OpenSSL::PKey::RSA.new(@raw_private_key, '')
37
+ rescue OpenSSL::PKey::RSAError
38
+ # may be encrypted
39
+ end
40
+ end
41
+ when OpenSSL::PKey::RSA
42
+ @private_key = private_key
43
+ else
44
+ raise TypeError, 'private_key is expected to be a String or OpenSSL::PKey::RSA'
45
+ end
46
+
47
+ @csr = case csr
48
+ when nil
49
+ nil
50
+ when String
51
+ OpenSSL::X509::Request.new(csr)
52
+ when OpenSSL::X509::Request
53
+ csr
54
+ end
55
+ end
56
+
57
+ attr_reader :certificate, :chain, :csr
58
+
59
+ def key_passphrase=(pw)
60
+ raise 'private_key already given' if @private_key
61
+
62
+ @private_key = OpenSSL::PKey::RSA.new(@raw_private_key, pw)
63
+
64
+ @raw_private_key = nil
65
+ nil
66
+ end
67
+
68
+ def private_key
69
+ return @private_key if @private_key
70
+ raise PassphraseRequired, 'key_passphrase required'
71
+ end
72
+
73
+ def fullchain
74
+ "#{certificate.to_pem}\n#{chain}".gsub(/\n+/,?\n)
75
+ end
76
+
77
+ def common_name
78
+ certificate.subject.to_a.assoc('CN')[1]
79
+ end
80
+
81
+ def version
82
+ "#{certificate.not_before.utc.strftime('%Y%m%d-%H%M%S')}_#{certificate.serial.to_i.to_s(16)}"
83
+ end
84
+
85
+ def export(passphrase, cipher: OpenSSL::Cipher.new('aes-256-cbc'))
86
+ {}.tap do |h|
87
+ h[:certificate] = certificate.to_pem
88
+ h[:chain] = chain
89
+ h[:fullchain] = fullchain
90
+ h[:private_key] = if passphrase
91
+ private_key.export(cipher, passphrase)
92
+ else
93
+ private_key.export
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end