secure_headers 3.4.1 → 3.5.0

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.
@@ -16,7 +16,7 @@ module SecureHeaders
16
16
 
17
17
  it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do
18
18
  expect do
19
- SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY)
19
+ SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY)
20
20
  end.to raise_error(Configuration::NotYetConfiguredError)
21
21
  end
22
22
 
@@ -29,8 +29,12 @@ module SecureHeaders
29
29
 
30
30
  describe "#header_hash_for" do
31
31
  it "allows you to opt out of individual headers via API" do
32
- Configuration.default
33
- SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY)
32
+ Configuration.default do |config|
33
+ config.csp = { default_src: %w('self')}
34
+ config.csp_report_only = config.csp
35
+ end
36
+ SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY)
37
+ SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY)
34
38
  SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY)
35
39
  hash = SecureHeaders.header_hash_for(request)
36
40
  expect(hash['Content-Security-Policy-Report-Only']).to be_nil
@@ -56,7 +60,21 @@ module SecureHeaders
56
60
  end
57
61
 
58
62
  it "allows you to opt out entirely" do
59
- Configuration.default
63
+ # configure the disabled-by-default headers to ensure they also do not get set
64
+ Configuration.default do |config|
65
+ config.csp = { :default_src => ["example.com"] }
66
+ config.csp_report_only = config.csp
67
+ config.hpkp = {
68
+ report_only: false,
69
+ max_age: 10000000,
70
+ include_subdomains: true,
71
+ report_uri: "https://report-uri.io/example-hpkp",
72
+ pins: [
73
+ {sha256: "abc"},
74
+ {sha256: "123"}
75
+ ]
76
+ }
77
+ end
60
78
  SecureHeaders.opt_out_of_all_protection(request)
61
79
  hash = SecureHeaders.header_hash_for(request)
62
80
  ALL_HEADER_CLASSES.each do |klass|
@@ -82,7 +100,7 @@ module SecureHeaders
82
100
  SecureHeaders.override_content_security_policy_directives(request, default_src: %w(https:), script_src: %w('self'))
83
101
 
84
102
  hash = SecureHeaders.header_hash_for(request)
85
- expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
103
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
86
104
  expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
87
105
  end
88
106
 
@@ -96,14 +114,14 @@ module SecureHeaders
96
114
  firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
97
115
 
98
116
  # append an unsupported directive
99
- SecureHeaders.override_content_security_policy_directives(firefox_request, plugin_types: %w(flash))
117
+ SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(flash)})
100
118
  # append a supported directive
101
- SecureHeaders.override_content_security_policy_directives(firefox_request, script_src: %w('self'))
119
+ SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')})
102
120
 
103
121
  hash = SecureHeaders.header_hash_for(firefox_request)
104
122
 
105
123
  # child-src is translated to frame-src
106
- expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
124
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
107
125
  end
108
126
 
109
127
  it "produces a hash of headers with default config" do
@@ -152,7 +170,7 @@ module SecureHeaders
152
170
 
153
171
  SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
154
172
  hash = SecureHeaders.header_hash_for(request)
155
- expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
173
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
156
174
  end
157
175
 
158
176
  it "supports named appends" do
@@ -174,7 +192,7 @@ module SecureHeaders
174
192
  SecureHeaders.use_content_security_policy_named_append(request, :how_about_a_script_src_too)
175
193
  hash = SecureHeaders.header_hash_for(request)
176
194
 
177
- expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' https: 'unsafe-inline'")
195
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' https: 'unsafe-inline'")
178
196
  end
179
197
 
180
198
  it "appends a nonce to a missing script-src value" do
@@ -186,7 +204,7 @@ module SecureHeaders
186
204
 
187
205
  SecureHeaders.content_security_policy_script_nonce(request) # should add the value to the header
188
206
  hash = SecureHeaders.header_hash_for(chrome_request)
189
- expect(hash[CSP::HEADER_NAME]).to match /\Adefault-src 'self'; script-src 'self' 'nonce-.*'\z/
207
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match /\Adefault-src 'self'; script-src 'self' 'nonce-.*'\z/
190
208
  end
191
209
 
192
210
  it "appends a hash to a missing script-src value" do
@@ -198,48 +216,7 @@ module SecureHeaders
198
216
 
199
217
  SecureHeaders.append_content_security_policy_directives(request, script_src: %w('sha256-abc123'))
200
218
  hash = SecureHeaders.header_hash_for(chrome_request)
201
- expect(hash[CSP::HEADER_NAME]).to match /\Adefault-src 'self'; script-src 'self' 'sha256-abc123'\z/
202
- end
203
-
204
- it "dups global configuration just once when overriding n times and only calls idempotent_additions? once" do
205
- Configuration.default do |config|
206
- config.csp = {
207
- default_src: %w('self')
208
- }
209
- end
210
-
211
- expect(CSP).to receive(:idempotent_additions?).once
212
-
213
- # before an override occurs, the env is empty
214
- expect(request.env[SECURE_HEADERS_CONFIG]).to be_nil
215
-
216
- SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
217
- new_config = SecureHeaders.config_for(request)
218
- expect(new_config).to_not be(Configuration.get)
219
-
220
- SecureHeaders.override_content_security_policy_directives(request, script_src: %w(yet.anothercdn.com))
221
- current_config = SecureHeaders.config_for(request)
222
- expect(current_config).to be(new_config)
223
-
224
- SecureHeaders.header_hash_for(request)
225
- end
226
-
227
- it "doesn't allow you to muck with csp configs when a dynamic policy is in use" do
228
- default_config = Configuration.default
229
- expect { default_config.csp = {} }.to raise_error(NoMethodError)
230
-
231
- # config is frozen
232
- expect { default_config.send(:csp=, {}) }.to raise_error(RuntimeError)
233
-
234
- SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
235
- new_config = SecureHeaders.config_for(request)
236
- expect { new_config.send(:csp=, {}) }.to raise_error(Configuration::IllegalPolicyModificationError)
237
-
238
- expect do
239
- new_config.instance_eval do
240
- new_config.csp = {}
241
- end
242
- end.to raise_error(Configuration::IllegalPolicyModificationError)
219
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match /\Adefault-src 'self'; script-src 'self' 'sha256-abc123'\z/
243
220
  end
244
221
 
245
222
  it "overrides individual directives" do
@@ -250,14 +227,19 @@ module SecureHeaders
250
227
  end
251
228
  SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none'))
252
229
  hash = SecureHeaders.header_hash_for(request)
253
- expect(hash[CSP::HEADER_NAME]).to eq("default-src 'none'")
230
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'")
254
231
  end
255
232
 
256
233
  it "overrides non-existant directives" do
257
- Configuration.default
234
+ Configuration.default do |config|
235
+ config.csp = {
236
+ default_src: %w(https:)
237
+ }
238
+ end
258
239
  SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL])
259
240
  hash = SecureHeaders.header_hash_for(request)
260
- expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; img-src data:")
241
+ expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil
242
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; img-src data:")
261
243
  end
262
244
 
263
245
  it "does not append a nonce when the browser does not support it" do
@@ -272,7 +254,7 @@ module SecureHeaders
272
254
  safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
273
255
  nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
274
256
  hash = SecureHeaders.header_hash_for(safari_request)
275
- expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
257
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
276
258
  end
277
259
 
278
260
  it "appends a nonce to the script-src when used" do
@@ -294,6 +276,216 @@ module SecureHeaders
294
276
  hash = SecureHeaders.header_hash_for(chrome_request)
295
277
  expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'")
296
278
  end
279
+
280
+ it "uses a nonce for safari 10+" do
281
+ Configuration.default do |config|
282
+ config.csp = {
283
+ default_src: %w('self'),
284
+ script_src: %w(mycdn.com)
285
+ }
286
+ end
287
+
288
+ safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari10]))
289
+ nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
290
+ hash = SecureHeaders.header_hash_for(safari_request)
291
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'")
292
+ end
293
+
294
+ it "supports the deprecated `report_only: true` format" do
295
+ expect(Kernel).to receive(:warn).once
296
+
297
+ Configuration.default do |config|
298
+ config.csp = {
299
+ default_src: %w('self'),
300
+ report_only: true
301
+ }
302
+ end
303
+
304
+ expect(Configuration.get.csp).to eq(OPT_OUT)
305
+ expect(Configuration.get.csp_report_only).to be_a(ContentSecurityPolicyReportOnlyConfig)
306
+
307
+ hash = SecureHeaders.header_hash_for(request)
308
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to be_nil
309
+ expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to eq("default-src 'self'")
310
+ end
311
+
312
+ it "Raises an error if csp_report_only is used with `report_only: false`" do
313
+ expect do
314
+ Configuration.default do |config|
315
+ config.csp_report_only = {
316
+ default_src: %w('self'),
317
+ report_only: false
318
+ }
319
+ end
320
+ end.to raise_error(ContentSecurityPolicyConfigError)
321
+ end
322
+
323
+ context "setting two headers" do
324
+ before(:each) do
325
+ Configuration.default do |config|
326
+ config.csp = {
327
+ default_src: %w('self')
328
+ }
329
+ config.csp_report_only = config.csp
330
+ end
331
+ end
332
+
333
+ it "sets identical values when the configs are the same" do
334
+ Configuration.default do |config|
335
+ config.csp = {
336
+ default_src: %w('self')
337
+ }
338
+ config.csp_report_only = {
339
+ default_src: %w('self')
340
+ }
341
+ end
342
+
343
+ hash = SecureHeaders.header_hash_for(request)
344
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'")
345
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
346
+ end
347
+
348
+ it "sets different headers when the configs are different" do
349
+ Configuration.default do |config|
350
+ config.csp = {
351
+ default_src: %w('self')
352
+ }
353
+ config.csp_report_only = config.csp.merge({script_src: %w('self')})
354
+ end
355
+
356
+ hash = SecureHeaders.header_hash_for(request)
357
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'")
358
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self'")
359
+ end
360
+
361
+ it "allows you to opt-out of enforced CSP" do
362
+ Configuration.default do |config|
363
+ config.csp = SecureHeaders::OPT_OUT
364
+ config.csp_report_only = {
365
+ default_src: %w('self')
366
+ }
367
+ end
368
+
369
+ hash = SecureHeaders.header_hash_for(request)
370
+ expect(hash['Content-Security-Policy']).to be_nil
371
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
372
+ end
373
+
374
+ it "opts-out of enforced CSP when only csp_report_only is set" do
375
+ expect(Kernel).to receive(:warn).once
376
+ Configuration.default do |config|
377
+ config.csp_report_only = {
378
+ default_src: %w('self')
379
+ }
380
+ end
381
+
382
+ hash = SecureHeaders.header_hash_for(request)
383
+ expect(hash['Content-Security-Policy']).to be_nil
384
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
385
+ end
386
+
387
+ it "allows you to set csp_report_only before csp" do
388
+ expect(Kernel).to receive(:warn).once
389
+ Configuration.default do |config|
390
+ config.csp_report_only = {
391
+ default_src: %w('self')
392
+ }
393
+ config.csp = config.csp_report_only.merge({script_src: %w('unsafe-inline')})
394
+ end
395
+
396
+ hash = SecureHeaders.header_hash_for(request)
397
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' 'unsafe-inline'")
398
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
399
+ end
400
+
401
+ it "allows appending to the enforced policy" do
402
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced)
403
+ hash = SecureHeaders.header_hash_for(request)
404
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
405
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
406
+ end
407
+
408
+ it "allows appending to the report only policy" do
409
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only)
410
+ hash = SecureHeaders.header_hash_for(request)
411
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'")
412
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
413
+ end
414
+
415
+ it "allows appending to both policies" do
416
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both)
417
+ hash = SecureHeaders.header_hash_for(request)
418
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
419
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
420
+ end
421
+
422
+ it "allows overriding the enforced policy" do
423
+ SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced)
424
+ hash = SecureHeaders.header_hash_for(request)
425
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src anothercdn.com")
426
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'")
427
+ end
428
+
429
+ it "allows overriding the report only policy" do
430
+ SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only)
431
+ hash = SecureHeaders.header_hash_for(request)
432
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'")
433
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src anothercdn.com")
434
+ end
435
+
436
+ it "allows overriding both policies" do
437
+ SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both)
438
+ hash = SecureHeaders.header_hash_for(request)
439
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src anothercdn.com")
440
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src anothercdn.com")
441
+ end
442
+
443
+ context "when inferring which config to modify" do
444
+ it "updates the enforced header when configured" do
445
+ Configuration.default do |config|
446
+ config.csp = {
447
+ default_src: %w('self')
448
+ }
449
+ end
450
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
451
+
452
+ hash = SecureHeaders.header_hash_for(request)
453
+ expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
454
+ expect(hash['Content-Security-Policy-Report-Only']).to be_nil
455
+ end
456
+
457
+ it "updates the report only header when configured" do
458
+ Configuration.default do |config|
459
+ config.csp = OPT_OUT
460
+ config.csp_report_only = {
461
+ default_src: %w('self')
462
+ }
463
+ end
464
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
465
+
466
+ hash = SecureHeaders.header_hash_for(request)
467
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com")
468
+ expect(hash['Content-Security-Policy']).to be_nil
469
+ end
470
+
471
+ it "updates both headers if both are configured" do
472
+ Configuration.default do |config|
473
+ config.csp = {
474
+ default_src: %w(enforced.com)
475
+ }
476
+ config.csp_report_only = {
477
+ default_src: %w(reportonly.com)
478
+ }
479
+ end
480
+ SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)})
481
+
482
+ hash = SecureHeaders.header_hash_for(request)
483
+ expect(hash['Content-Security-Policy']).to eq("default-src enforced.com; script-src enforced.com anothercdn.com")
484
+ expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src reportonly.com; script-src reportonly.com anothercdn.com")
485
+ end
486
+
487
+ end
488
+ end
297
489
  end
298
490
  end
299
491
 
@@ -309,7 +501,7 @@ module SecureHeaders
309
501
  it "validates your csp config upon configuration" do
310
502
  expect do
311
503
  Configuration.default do |config|
312
- config.csp = { CSP::DEFAULT_SRC => '123456' }
504
+ config.csp = { ContentSecurityPolicy::DEFAULT_SRC => '123456' }
313
505
  end
314
506
  end.to raise_error(ContentSecurityPolicyConfigError)
315
507
  end
data/spec/spec_helper.rb CHANGED
@@ -21,11 +21,12 @@ USER_AGENTS = {
21
21
  ios6: "Mozilla/5.0 (iPhone; CPU iPhone OS 614 like Mac OS X) AppleWebKit/536.26 (KHTML like Gecko) Version/6.0 Mobile/10B350 Safari/8536.25",
22
22
  safari5: "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko ) Version/5.1 Mobile/9B176 Safari/7534.48.3",
23
23
  safari5_1: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10",
24
- safari6: "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1"
24
+ safari6: "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1",
25
+ safari10: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/602.2.11 (KHTML, like Gecko) Version/10.0.1 Safari/602.2.11"
25
26
  }
26
27
 
27
28
  def expect_default_values(hash)
28
- expect(hash[SecureHeaders::CSP::HEADER_NAME]).to eq(SecureHeaders::CSP::DEFAULT_VALUE)
29
+ expect(hash[SecureHeaders::ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'")
29
30
  expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE)
30
31
  expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE)
31
32
  expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: secure_headers
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.1
4
+ version: 3.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Neil Matatall
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-09-08 00:00:00.000000000 Z
11
+ date: 2016-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -62,6 +62,7 @@ files:
62
62
  - lib/secure_headers/configuration.rb
63
63
  - lib/secure_headers/hash_helper.rb
64
64
  - lib/secure_headers/headers/content_security_policy.rb
65
+ - lib/secure_headers/headers/content_security_policy_config.rb
65
66
  - lib/secure_headers/headers/cookie.rb
66
67
  - lib/secure_headers/headers/policy_management.rb
67
68
  - lib/secure_headers/headers/public_key_pins.rb