lester 1.0.0.pre1

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