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.
- checksums.yaml +7 -0
- data/README.md +63 -0
- data/bin/lester +8 -0
- data/lib/lester.rb +22 -0
- data/lib/lester/authenticator.rb +42 -0
- data/lib/lester/cli.rb +101 -0
- data/lib/lester/command.rb +7 -0
- data/lib/lester/command/init.rb +18 -0
- data/lib/lester/command/renew.rb +63 -0
- data/lib/lester/factory.rb +70 -0
- data/lib/lester/private_key.rb +18 -0
- data/lib/lester/s3_store.rb +42 -0
- data/lib/lester/uploader.rb +28 -0
- data/lib/lester/version.rb +3 -0
- data/spec/acceptance/cli_init_spec.rb +47 -0
- data/spec/acceptance/cli_renew_spec.rb +149 -0
- data/spec/lester/authenticator_spec.rb +80 -0
- data/spec/lester/cli_spec.rb +102 -0
- data/spec/lester/command/init_spec.rb +31 -0
- data/spec/lester/command/renew_spec.rb +123 -0
- data/spec/lester/private_key_spec.rb +41 -0
- data/spec/lester/s3_store_spec.rb +73 -0
- data/spec/lester/uploader_spec.rb +116 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/support/acceptance_setup.rb +38 -0
- data/spec/support/cassettes/new-certificate-fail.yml +64 -0
- data/spec/support/cassettes/new-certificate.yml +432 -0
- data/spec/support/cassettes/verification-fail.yml +139 -0
- data/spec/support/fake_bucket.rb +54 -0
- data/spec/support/fake_cloudfront.rb +26 -0
- data/spec/support/fake_iam.rb +16 -0
- data/spec/support/parameter_validation.rb +60 -0
- data/spec/support/resources/privkey.json +1 -0
- data/spec/support/resources/privkey.pem +27 -0
- metadata +182 -0
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|