lester 1.0.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,123 @@
1
+ require 'spec_helper'
2
+
3
+ module Lester
4
+ module Command
5
+ describe Renew do
6
+ let :command do
7
+ described_class.new(domain, acme_client, authenticator, uploader, store, options)
8
+ end
9
+
10
+ let :domain do
11
+ 'example.org'
12
+ end
13
+
14
+ let :acme_client do
15
+ double(:acme_client)
16
+ end
17
+
18
+ let :authenticator do
19
+ double(:authenticator)
20
+ end
21
+
22
+ let :uploader do
23
+ double(:uploader)
24
+ end
25
+
26
+ let :store do
27
+ double(:store)
28
+ end
29
+
30
+ let :options do
31
+ {
32
+ key_class: key_impl,
33
+ csr_class: csr_impl,
34
+ }
35
+ end
36
+
37
+ let :key_impl do
38
+ double(new: private_key)
39
+ end
40
+
41
+ let :csr_impl do
42
+ double(:csr_impl)
43
+ end
44
+
45
+ let :authorization do
46
+ double(:authorization)
47
+ end
48
+
49
+ let :http01_challenge do
50
+ double(:challenge)
51
+ end
52
+
53
+ let :certificate do
54
+ OpenSSL::X509::Certificate.new.tap do |cert|
55
+ cert.not_before = Time.utc(2016, 1, 1)
56
+ end
57
+ end
58
+
59
+ let :chain do
60
+ 2.times.map { OpenSSL::X509::Certificate.new }
61
+ end
62
+
63
+ let :private_key do
64
+ OpenSSL::PKey::RSA.new(2048)
65
+ end
66
+
67
+ let :new_certificate do
68
+ Acme::Certificate.new(certificate, chain, nil)
69
+ end
70
+
71
+ before do
72
+ allow(acme_client).to receive(:authorize).and_return(authorization)
73
+ allow(acme_client).to receive(:new_certificate).and_return(new_certificate)
74
+ allow(authorization).to receive(:http01).and_return(http01_challenge)
75
+ allow(authenticator).to receive(:authenticate).with(http01_challenge)
76
+ allow(uploader).to receive(:upload)
77
+ allow(csr_impl).to receive(:new) do |args|
78
+ Acme::CertificateRequest.new(args)
79
+ end
80
+ allow(store).to receive(:put)
81
+ end
82
+
83
+ describe '#run' do
84
+ before do
85
+ command.run
86
+ end
87
+
88
+ it 'authorizes using the ACME client' do
89
+ expect(acme_client).to have_received(:authorize).with(domain: 'example.org')
90
+ expect(acme_client).to have_received(:authorize).with(domain: 'www.example.org')
91
+ end
92
+
93
+ it 'requests a new certificate' do
94
+ expect(acme_client).to have_received(:new_certificate)
95
+ end
96
+
97
+ it 'uploads the new certificate with a timestamp suffix' do
98
+ expect(uploader).to have_received(:upload).with('example.org-201601010000', new_certificate, private_key)
99
+ end
100
+
101
+ it 'stores the certificate under a timestamp prefix' do
102
+ expect(store).to have_received(:put).with('201601010000/cert.pem', certificate.to_pem)
103
+ end
104
+
105
+ it 'stores the certificate request under a timestamp prefix' do
106
+ expect(store).to have_received(:put).with('201601010000/csr.pem', anything)
107
+ end
108
+
109
+ it 'stores the certificate chain under a timestamp prefix' do
110
+ expect(store).to have_received(:put).with('201601010000/chain.pem', chain.map(&:to_pem).join)
111
+ end
112
+
113
+ it 'stores the certificate fullchain under a timestamp prefix' do
114
+ expect(store).to have_received(:put).with('201601010000/fullchain.pem', certificate.to_pem + chain.map(&:to_pem).join)
115
+ end
116
+
117
+ it 'stores the certificate private key under a timestamp prefix' do
118
+ expect(store).to have_received(:put).with('201601010000/privkey.pem', private_key.to_pem)
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ module Lester
4
+ describe PrivateKey do
5
+ let :private_key do
6
+ described_class.new(Pathname.new(path))
7
+ end
8
+
9
+ describe '.load' do
10
+ context 'with a PEM encoded key' do
11
+ let :path do
12
+ File.expand_path('../../support/resources/privkey.pem', __FILE__)
13
+ end
14
+
15
+ it 'loads the key' do
16
+ expect(private_key.load).to be_a(OpenSSL::PKey::RSA)
17
+ end
18
+ end
19
+
20
+ context 'with a JWK encoded key' do
21
+ let :path do
22
+ File.expand_path('../../support/resources/privkey.json', __FILE__)
23
+ end
24
+
25
+ it 'loads the key' do
26
+ expect(private_key.load).to be_a(OpenSSL::PKey::RSA)
27
+ end
28
+ end
29
+
30
+ context 'with any other key type' do
31
+ let :path do
32
+ File.expand_path('../../support/resources/privkey.txt')
33
+ end
34
+
35
+ it 'raises an error' do
36
+ expect { private_key.load }.to raise_error(UnknownKeyFormatError)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+
3
+ module Lester
4
+ describe S3Store do
5
+ let :store do
6
+ described_class.new(bucket, prefix, options)
7
+ end
8
+
9
+ let :bucket do
10
+ double(:bucket)
11
+ end
12
+
13
+ let :prefix do
14
+ 'prefix'
15
+ end
16
+
17
+ let :options do
18
+ {'silly' => 'option'}
19
+ end
20
+
21
+ let :object do
22
+ double(:object)
23
+ end
24
+
25
+ before do
26
+ allow(bucket).to receive(:object).with('prefix/key.ext').and_return(object)
27
+ allow(bucket).to receive(:put_object)
28
+ allow(object).to receive(:key).and_return('prefix/key.ext')
29
+ allow(object).to receive(:get).and_return(object)
30
+ allow(object).to receive(:body).and_return(object)
31
+ allow(object).to receive(:read).and_return('content')
32
+ allow(object).to receive(:exists?).and_return(true)
33
+ end
34
+
35
+ describe '#put' do
36
+ it 'stores values under given prefix' do
37
+ store.put('key.ext', 'content')
38
+ expect(bucket).to have_received(:put_object).with(hash_including(key: 'prefix/key.ext'))
39
+ expect(bucket).to have_received(:put_object).with(hash_including(body: 'content'))
40
+ end
41
+
42
+ it 'includes given options' do
43
+ store.put('key.ext', 'content')
44
+ expect(bucket).to have_received(:put_object).with(hash_including('silly' => 'option'))
45
+ end
46
+ end
47
+
48
+ describe '#get' do
49
+ it 'retrieves values from a prefix' do
50
+ store.get('key.ext')
51
+ expect(bucket).to have_received(:object).with('prefix/key.ext')
52
+ end
53
+
54
+ context 'returns something that responds to' do
55
+ let :s3_object do
56
+ store.get('key.ext')
57
+ end
58
+
59
+ it 'exists?' do
60
+ expect(s3_object.exists?).to be true
61
+ end
62
+
63
+ it 'read' do
64
+ expect(s3_object.read).to eq('content')
65
+ end
66
+
67
+ it 'extname' do
68
+ expect(s3_object.extname).to eq('.ext')
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,116 @@
1
+ require 'spec_helper'
2
+
3
+ module Lester
4
+ describe Uploader do
5
+ let :uploader do
6
+ described_class.new(iam, cloudfront, distribution_id)
7
+ end
8
+
9
+ let :iam do
10
+ double(:iam)
11
+ end
12
+
13
+ let :cloudfront do
14
+ double(:cloudfront)
15
+ end
16
+
17
+ let :distribution_id do
18
+ 'distribution-id'
19
+ end
20
+
21
+ let :certificate_name do
22
+ 'name'
23
+ end
24
+
25
+ let :certificate do
26
+ double(:certificate)
27
+ end
28
+
29
+ let :private_key do
30
+ double(:private_key)
31
+ end
32
+
33
+ let :iam_response do
34
+ double(:iam_response)
35
+ end
36
+
37
+ let :distribution_config do
38
+ {
39
+ viewer_certificate: {
40
+ iam_certificate_id: 'old-certificate-id',
41
+ some: 'other-option',
42
+ },
43
+ top_level_key: {
44
+ hello: :world,
45
+ }
46
+ }
47
+ end
48
+
49
+ before do
50
+ allow(iam_response).to receive(:server_certificate_metadata) do
51
+ double(server_certificate_id: 'certificate-id')
52
+ end
53
+ allow(iam).to receive(:upload_server_certificate).and_return(iam_response)
54
+ allow(certificate).to receive(:to_pem).and_return('PEM ENCODED CERTIFICATE')
55
+ allow(certificate).to receive(:chain_to_pem).and_return('PEM ENCODED CHAIN')
56
+ allow(private_key).to receive(:to_pem).and_return('PEM ENCODED KEY')
57
+ allow(cloudfront).to receive(:get_distribution_config) do
58
+ double(distribution_config: distribution_config, etag: 'ETAG')
59
+ end
60
+ allow(cloudfront).to receive(:update_distribution)
61
+ end
62
+
63
+ describe '#upload' do
64
+ before do
65
+ uploader.upload(certificate_name, certificate, private_key)
66
+ end
67
+
68
+ it 'uploads the certificate to IAM' do
69
+ expect(iam).to have_received(:upload_server_certificate)
70
+ end
71
+
72
+ it 'uses `/cloudfront/` as path argument' do
73
+ expect(iam).to have_received(:upload_server_certificate).with(hash_including(path: '/cloudfront/'))
74
+ end
75
+
76
+ it 'uses given name for the certificate' do
77
+ expect(iam).to have_received(:upload_server_certificate).with(hash_including(server_certificate_name: 'name'))
78
+ end
79
+
80
+ it 'uses a PEM encoded certificate' do
81
+ expect(iam).to have_received(:upload_server_certificate).with(hash_including(certificate_body: 'PEM ENCODED CERTIFICATE'))
82
+ end
83
+
84
+ it 'uses a PEM encoded private key' do
85
+ expect(iam).to have_received(:upload_server_certificate).with(hash_including(private_key: 'PEM ENCODED KEY'))
86
+ end
87
+
88
+ it 'uses a PEM encoded certificate chain' do
89
+ expect(iam).to have_received(:upload_server_certificate).with(hash_including(certificate_chain: 'PEM ENCODED CHAIN'))
90
+ end
91
+
92
+ it 'fetches distribution config for the given distribution ID' do
93
+ expect(cloudfront).to have_received(:get_distribution_config).with(id: 'distribution-id')
94
+ end
95
+
96
+ it 'updates the IAM certificate ID' do
97
+ expect(cloudfront).to have_received(:update_distribution) do |args|
98
+ expect(args[:distribution_config][:viewer_certificate][:iam_certificate_id]).to eq('certificate-id')
99
+ end
100
+ end
101
+
102
+ it 'leaves all other configuration untouched' do
103
+ distribution_config[:viewer_certificate].delete(:iam_certificate_id)
104
+ expect(cloudfront).to have_received(:update_distribution).with(hash_including(distribution_config: hash_including(distribution_config)))
105
+ end
106
+
107
+ it 'includes etag from distribution config response when updating distribution' do
108
+ expect(cloudfront).to have_received(:update_distribution).with(hash_including(if_match: 'ETAG'))
109
+ end
110
+
111
+ it 'uses given distribution ID when updating distribution' do
112
+ expect(cloudfront).to have_received(:update_distribution).with(hash_including(id: 'distribution-id'))
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,33 @@
1
+ require 'simplecov'
2
+ require 'webmock/rspec'
3
+ require 'vcr'
4
+ require 'stringio'
5
+ require 'ostruct'
6
+ require 'digest/md5'
7
+
8
+ require 'support/fake_cloudfront'
9
+ require 'support/fake_bucket'
10
+ require 'support/fake_iam'
11
+ require 'support/acceptance_setup'
12
+ require 'support/parameter_validation'
13
+
14
+ RSpec.configure do |c|
15
+ c.extend(ParameterValidation)
16
+ end
17
+
18
+ VCR.configure do |c|
19
+ c.cassette_library_dir = 'spec/support/cassettes'
20
+ c.configure_rspec_metadata!
21
+ c.hook_into :webmock
22
+ c.ignore_localhost = false
23
+ c.default_cassette_options = {record: :none}
24
+ c.allow_http_connections_when_no_cassette = false
25
+ end
26
+
27
+ SimpleCov.start do
28
+ add_group 'Source', 'lib'
29
+ add_group 'Unit tests', 'spec/lester'
30
+ add_filter '/vendor/bundle/'
31
+ end
32
+
33
+ require 'lester'
@@ -0,0 +1,38 @@
1
+ shared_context 'acceptance setup' do
2
+ let :io do
3
+ StringIO.new
4
+ end
5
+
6
+ let :private_key_path do
7
+ File.expand_path('../../support/resources/privkey.json', __FILE__)
8
+ end
9
+
10
+ let :iam do
11
+ FakeIAM.new
12
+ end
13
+
14
+ let :buckets do
15
+ {}
16
+ end
17
+
18
+ let :site_bucket do
19
+ Aws::S3::Bucket.new('example-org-site')
20
+ end
21
+
22
+ let :storage_bucket do
23
+ Aws::S3::Bucket.new('example-org-backup')
24
+ end
25
+
26
+ let :cloudfront do
27
+ FakeCloudFront.new
28
+ end
29
+
30
+ before do
31
+ allow(Aws::S3::Bucket).to receive(:new) do |name, opts|
32
+ buckets[name] ||= FakeBucket.new(name)
33
+ buckets[name]
34
+ end
35
+ allow(Aws::IAM::Client).to receive(:new).and_return(iam)
36
+ allow(Aws::CloudFront::Client).to receive(:new).and_return(cloudfront)
37
+ end
38
+ end
@@ -0,0 +1,64 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: head
5
+ uri: http://127.0.0.1:4000/acme/new-authz
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ headers:
10
+ User-Agent:
11
+ - Faraday v0.9.2
12
+ Accept:
13
+ - "*/*"
14
+ response:
15
+ status:
16
+ code: 405
17
+ message: Method Not Allowed
18
+ headers:
19
+ Allow:
20
+ - POST
21
+ Content-Type:
22
+ - application/problem+json
23
+ Replay-Nonce:
24
+ - nivK3lvQemeqPlxezrNF6Teb7SBJIywII1UhUpyeUW8
25
+ Date:
26
+ - Sat, 12 Dec 2015 09:36:10 GMT
27
+ body:
28
+ encoding: UTF-8
29
+ string: ''
30
+ http_version:
31
+ recorded_at: Sat, 12 Dec 2015 09:36:10 GMT
32
+ - request:
33
+ method: post
34
+ uri: http://127.0.0.1:4000/acme/new-authz
35
+ body:
36
+ encoding: UTF-8
37
+ string: '{"protected":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIm5vbmNlIjoibml2SzNsdlFlbWVxUGx4ZXpyTkY2VGViN1NCSkl5d0lJMVVoVXB5ZVVXOCIsImp3ayI6eyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsIm4iOiI1QkZzQzFxUWxRR1RDLURiY1lfY21NaFFJSG1yd3VCYmZOeW1BMU9HYXpzWkliWVpvekVoVmotSmMtcXJCYzYzOE1uZHNRcUtwUF96NFpPQ0NMQ1Ryd05rM2JjX2pUXzFpaXJ4Y1g1RU82YmVrcU4tdEtzZFNkTnZFUTZwY2FFeWhuQTZnTlJma19VSTM1S0hON2ZlaWkxSU05d2RDRW5qRjBocDdGdXpCYU1aT05pYTlKYXB2SFE2aW1ydHZwMFJpNUNSSTBzbFVNZHRnZnY0UUVIOS13dHk1M1hsWnRLT1kzQV9kR255RmxNcEFJdThFVUQtMTFMUWxvR1ZzMi1ONVZfMy1hU2NrMzQ2X1pFNEE0M2ZVYkJjdElyZjRTUnlUNTd0Zm5qc0l1RE0xUmR2aVFkVDB4c2UxcGxVU2VUb0Ywa0FONU5iV3k3blNKMDZHTWg5clEifX0","payload":"eyJyZXNvdXJjZSI6Im5ldy1hdXRoeiIsImlkZW50aWZpZXIiOnsidHlwZSI6ImRucyIsInZhbHVlIjoiZXhhbXBsZS5vcmcifX0","signature":"44R4iCec1Wit09vBBGkKnUJw10sxvY5i-xj8T6ndy-gwXWO7N8-5h84epq32kEeGLcpLIX7JAbt6LKs9gJm5C1O_5vYR6chukZANlzQhVqRVtjTenTr1lbUT276suZ4bOmKuqjts5cPiz1tKvIYhXVwNd927QKudf0PjTvVvixTvubUfadghSWr3HbcRGPIt0FaWZchZhKN5FtX5E8vzevUpg1jMuaxY5PNIXUegqysj-ZW8y3ZkFLO-1ZNK8q7y63d8Qe0BI6zKrDEw0pujFEfMk4ybfqGxyPq9FdH3C7Q-N3D0KWDKwZlze6zLztLWSMN07Aurf9uGJGocWTPNKg"}'
38
+ headers:
39
+ User-Agent:
40
+ - Faraday v0.9.2
41
+ Accept-Encoding:
42
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
43
+ Accept:
44
+ - "*/*"
45
+ response:
46
+ status:
47
+ code: 403
48
+ message: Forbidden
49
+ headers:
50
+ Content-Type:
51
+ - application/problem+json
52
+ Replay-Nonce:
53
+ - r3cXZnYPrb2SBLE-Ly6v19BPZrvaN7KsA8Imn_5MvaE
54
+ Date:
55
+ - Sat, 12 Dec 2015 09:36:10 GMT
56
+ Content-Length:
57
+ - '107'
58
+ body:
59
+ encoding: UTF-8
60
+ string: '{"type":"urn:acme:error:unauthorized","detail":"No registration exists
61
+ matching provided key","status":403}'
62
+ http_version:
63
+ recorded_at: Sat, 12 Dec 2015 09:36:10 GMT
64
+ recorded_with: VCR 2.9.3