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