fido_metadata 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7feb05dd5eac0b351f74073c80c16f1c7c7f6701a88be33eac7d6877af2a0478
4
- data.tar.gz: bfd36220edd79a54cccea8a1b4ebbfa303a067d86e75cc6c3942a64b090009f4
3
+ metadata.gz: 2881c4ebdb1827ac7d037d7e4460bd8708451c7a1c5e0501d5144a6119b64a1e
4
+ data.tar.gz: b01d388d2710097a8e84319963c528c4933ce0ceb4a2fee2ee68ee63d0d6b38c
5
5
  SHA512:
6
- metadata.gz: 7774a0c247e959267e946d09aeda636690ddf446f2120335133b76ad4c77bed62718ce83182f5cbf161577b2596fda2c31bdb34ca5d5b40aa888c2af61eae3f1
7
- data.tar.gz: 365630d2b485aebbb372e47b4d9bcbc8c804e275ecf436af56b3b91de1876bfa5085a7c803881183c1f0704a7c8b1d70e0f0330f45a6d052699f7144533873cc
6
+ metadata.gz: 01e1e3c685c0b88708067a451f5236607ee6bdc0a3d7f5b20bb5ff50bc053a24e49da71247d72e722cc46b302cf996f576f77acf71a47ad8d131af8f71a4a605
7
+ data.tar.gz: e28acf6f1afacf9f39a8309a9b0eff3f4222049a0c6ea6fb3e56b367e403753d9ae068fe867862dd2348d43d382d54495b5d523b2929a9a3377233d06562573e
data/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.0] - 2019-11-16
10
+ ### Added
11
+ - This CHANGELOG.md file.
12
+ - Add helper method to build a `OpenSSL::X509::Store` from a metadata statement root certificates
13
+
14
+ ### Changed
15
+ - JWT verification to match implementation [submitted upstream](https://github.com/jwt/ruby-jwt/pull/338) to `ruby-jwt`
16
+
17
+ ### Removed
18
+ - Drop `securecompare` gem for OpenSSL gem 2.2's implementation, with a Ruby fallback for older versions
19
+
20
+ ## [0.1.0] - 2019-11-13
21
+ ### Added
22
+ - Extracted from [webauthn-ruby PR 208](https://github.com/cedarcode/webauthn-ruby/pull/208) after discussion with the maintainers. Thanks for the feedback @grzuy and @brauliomartinezlm!
23
+
24
+ [0.1.0]: https://github.com/bdewater/fido_metadata/releases/tag/v0.1.0
data/Gemfile.lock CHANGED
@@ -1,9 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fido_metadata (0.1.0)
4
+ fido_metadata (0.2.0)
5
5
  jwt (~> 2.0)
6
- securecompare (~> 1.0)
7
6
 
8
7
  GEM
9
8
  remote: https://rubygems.org/
@@ -54,7 +53,6 @@ GEM
54
53
  unicode-display_width (>= 1.4.0, < 1.7)
55
54
  ruby-progressbar (1.10.1)
56
55
  safe_yaml (1.0.5)
57
- securecompare (1.0.0)
58
56
  unicode-display_width (1.6.0)
59
57
  webmock (3.7.6)
60
58
  addressable (>= 2.3.6)
data/README.md CHANGED
@@ -22,10 +22,12 @@ Or install it yourself as:
22
22
 
23
23
  ## Usage
24
24
 
25
- First, you need to [register for an access token](https://mds2.fidoalliance.org/tokens/). You can configure the gem as follows:
25
+ First, you need to [register for an access token](https://mds2.fidoalliance.org/tokens/) and configure a cache backend.
26
+ The cache interface is compatible with Rails' [`ActiveSupport::Cache::Store`](https://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html), which means you can configure the gem to use your existing cache or a separate one:
26
27
  ```ruby
27
28
  FidoMetadata.configure do |config|
28
29
  config.metadata_token = "your token"
30
+ config.cache_backend = Rails.cache # or something like `ActiveSupport::Cache::FileStore.new(...)`
29
31
  end
30
32
  ```
31
33
 
@@ -33,9 +35,8 @@ Then you can query the table of contents (TOC):
33
35
  ```ruby
34
36
  store = FidoMetadata::Store.new
35
37
  toc = store.table_of_contents
36
- # `toc.entries` returns an array of FidoMetadata::Entry objects, see
38
+ # returns a FidoMetadata::TableOfContents object. `toc.entries` returns an array of FidoMetadata::Entry objects, see
37
39
  # https://fidoalliance.org/specs/fido-v2.0-ps-20170927/fido-metadata-service-v2.0-ps-20170927.html#metadata-toc-payload-entry-dictionary
38
-
39
40
  ```
40
41
 
41
42
  Retrieve metadata statement via the authenticator `aaguid` (FIDO2) or `attestation_certificate_key_id` (U2F):
@@ -45,23 +46,16 @@ store.fetch_statement(aaguid: "0132d110-bf4e-4208-a403-ab4f5f12efe5")
45
46
  # https://fidoalliance.org/specs/fido-v2.0-ps-20170927/fido-metadata-statement-v2.0-ps-20170927.html#types
46
47
  ```
47
48
 
48
- ### Integrating the cache backend
49
-
50
- The cache interface is compatible with Rails' [`ActiveSupport::Cache::Store`](https://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html), which means you can configure the gem to use your existing cache or a separate one:
49
+ ### Custom cache backend
51
50
 
52
- ```ruby
53
- FidoMetadata.configure do |config|
54
- config.cache_backend = Rails.cache # or something like `ActiveSupport::Cache::FileStore.new(...)`
55
- end
56
- ```
57
-
58
- It is also possible to implement your own backend for using any datastore you'd like, such as your database. The interface you need to implement is as follows:
51
+ It is possible to implement your own backend for using any datastore you'd like, such as your database. The interface you need to implement is as follows:
59
52
 
60
53
  ```ruby
61
54
  class CustomMetadataCacheStore
62
55
  def read(name, _options = nil)
63
56
  # deserialize and return `value`
64
57
  end
58
+
65
59
  def write(name, value, _options = nil)
66
60
  # serialize and store `value` so it can be looked up using `name`
67
61
  end
data/bin/console CHANGED
@@ -12,10 +12,10 @@ FidoMetadata.configure do |config|
12
12
  end
13
13
 
14
14
  unless FidoMetadata.configuration.metadata_token
15
- puts <<~EOS
15
+ puts <<~TOKEN_HINT
16
16
  No MDS token configured via the MDS_TOKEN environment variable.
17
17
  Set one for this session: FidoMetadata.configuration.metadata_token = 'your token'
18
- EOS
18
+ TOKEN_HINT
19
19
  end
20
20
  puts "Reset the cache via: FidoMetadata.configuration.cache_backend.clear"
21
21
 
@@ -32,7 +32,6 @@ Gem::Specification.new do |spec|
32
32
  spec.required_ruby_version = ">= 2.3"
33
33
 
34
34
  spec.add_dependency "jwt", "~> 2.0"
35
- spec.add_dependency "securecompare", "~> 1.0"
36
35
  spec.add_development_dependency "bundler", "~> 1.17"
37
36
  spec.add_development_dependency "pry-byebug"
38
37
  spec.add_development_dependency "rake", "~> 10.0"
@@ -3,7 +3,8 @@
3
3
  require "jwt"
4
4
  require "net/http"
5
5
  require "openssl"
6
- require "securecompare"
6
+ require "fido_metadata/refinement/fixed_length_secure_compare"
7
+ require "fido_metadata/x5c_key_finder"
7
8
 
8
9
  module FidoMetadata
9
10
  class Client
@@ -11,34 +12,41 @@ module FidoMetadata
11
12
  class InvalidHashError < DataIntegrityError; end
12
13
  class UnverifiedSigningKeyError < DataIntegrityError; end
13
14
 
15
+ using Refinement::FixedLengthSecureCompare
16
+
14
17
  DEFAULT_HEADERS = {
15
18
  "Content-Type" => "application/json",
16
19
  "User-Agent" => "fido_metadata/#{FidoMetadata::VERSION} (Ruby)"
17
20
  }.freeze
18
-
19
- def self.fido_trust_store
20
- store = OpenSSL::X509::Store.new
21
- store.purpose = OpenSSL::X509::PURPOSE_ANY
22
- store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
23
- file = File.read(File.join(__dir__, "..", "Root.cer"))
24
- store.add_cert(OpenSSL::X509::Certificate.new(file))
25
- end
21
+ FIDO_ROOT_CERTIFICATES = [OpenSSL::X509::Certificate.new(
22
+ File.read(File.join(__dir__, "..", "Root.cer"))
23
+ )].freeze
26
24
 
27
25
  def initialize(token)
28
26
  @token = token
29
27
  end
30
28
 
31
- def download_toc(uri, trust_store: self.class.fido_trust_store)
29
+ def download_toc(uri, trusted_certs: FIDO_ROOT_CERTIFICATES)
32
30
  response = get_with_token(uri)
33
31
  payload, _ = JWT.decode(response, nil, true, algorithms: ["ES256"]) do |headers|
34
- verified_public_key(headers["x5c"], trust_store)
32
+ jwt_certificates = headers["x5c"].map do |encoded|
33
+ OpenSSL::X509::Certificate.new(Base64.strict_decode64(encoded))
34
+ end
35
+ crls = download_crls(jwt_certificates)
36
+
37
+ begin
38
+ X5cKeyFinder.from(jwt_certificates, trusted_certs, crls)
39
+ rescue JWT::VerificationError => e
40
+ raise(UnverifiedSigningKeyError, e.message)
41
+ end
35
42
  end
36
43
  payload
37
44
  end
38
45
 
39
46
  def download_entry(uri, expected_hash:)
40
47
  response = get_with_token(uri)
41
- unless SecureCompare.compare(OpenSSL::Digest::SHA256.digest(response), Base64.urlsafe_decode64(expected_hash))
48
+ decoded_hash = Base64.urlsafe_decode64(expected_hash)
49
+ unless OpenSSL.fixed_length_secure_compare(OpenSSL::Digest::SHA256.digest(response), decoded_hash)
42
50
  raise(InvalidHashError)
43
51
  end
44
52
 
@@ -74,25 +82,6 @@ module FidoMetadata
74
82
  end
75
83
  end
76
84
 
77
- def verified_public_key(x5c, trust_store)
78
- certificates = x5c.map do |encoded|
79
- OpenSSL::X509::Certificate.new(Base64.strict_decode64(encoded))
80
- end
81
- leaf_certificate = certificates[0]
82
- chain_certificates = certificates[1..-1]
83
-
84
- crls = download_crls(certificates)
85
- crls.each do |crl|
86
- trust_store.add_crl(crl)
87
- end
88
-
89
- if trust_store.verify(leaf_certificate, chain_certificates)
90
- leaf_certificate.public_key
91
- else
92
- raise(UnverifiedSigningKeyError, "OpenSSL error #{trust_store.error} (#{trust_store.error_string})")
93
- end
94
- end
95
-
96
85
  def download_crls(certificates)
97
86
  uris = extract_crl_distribution_points(certificates)
98
87
 
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module FidoMetadata
6
+ module Refinement
7
+ module FixedLengthSecureCompare
8
+ unless OpenSSL.singleton_class.method_defined?(:fixed_length_secure_compare)
9
+ refine OpenSSL.singleton_class do
10
+ def fixed_length_secure_compare(a, b) # rubocop:disable Naming/UncommunicativeMethodParamName
11
+ raise ArgumentError, "inputs must be of equal length" unless a.bytesize == b.bytesize
12
+
13
+ # borrowed from Rack::Utils
14
+ l = a.unpack("C*")
15
+ r, i = 0, -1
16
+ b.each_byte { |v| r |= v ^ l[i += 1] }
17
+ r == 0
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -53,5 +53,13 @@ module FidoMetadata
53
53
  def attestation_root_certificates
54
54
  Coercer::Certificates.coerce(@attestation_root_certificates)
55
55
  end
56
+
57
+ def trust_store
58
+ trust_store = OpenSSL::X509::Store.new
59
+ attestation_root_certificates.each do |certificate|
60
+ trust_store.add_cert(certificate)
61
+ end
62
+ trust_store
63
+ end
56
64
  end
57
65
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FidoMetadata
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "jwt/error"
5
+
6
+ module FidoMetadata
7
+ class VerificationError < StandardError; end
8
+
9
+ # If the x5c header certificate chain can be validated by trusted root
10
+ # certificates, and none of the certificates are revoked, returns the public
11
+ # key from the first certificate.
12
+ # See https://tools.ietf.org/html/rfc7515#section-4.1.6
13
+ class X5cKeyFinder
14
+ def self.from(x5c_header_or_certificates, trusted_certificates, crls)
15
+ store = build_store(trusted_certificates, crls)
16
+ signing_certificate, *certificate_chain = parse_certificates(x5c_header_or_certificates)
17
+ store_context = OpenSSL::X509::StoreContext.new(store, signing_certificate, certificate_chain)
18
+
19
+ if store_context.verify
20
+ signing_certificate.public_key
21
+ else
22
+ error = "Certificate verification failed: #{store_context.error_string}."
23
+ error = "#{error} Certificate subject: #{store_context.current_cert.subject}." if store_context.current_cert
24
+
25
+ raise JWT::VerificationError, error
26
+ end
27
+ end
28
+
29
+ def self.parse_certificates(x5c_header_or_certificates)
30
+ if x5c_header_or_certificates.all? { |obj| obj.is_a?(OpenSSL::X509::Certificate) }
31
+ x5c_header_or_certificates
32
+ else
33
+ x5c_header_or_certificates.map do |encoded|
34
+ OpenSSL::X509::Certificate.new(::Base64.strict_decode64(encoded))
35
+ end
36
+ end
37
+ end
38
+ private_class_method :parse_certificates
39
+
40
+ def self.build_store(trusted_certificates, crls)
41
+ store = OpenSSL::X509::Store.new
42
+ store.purpose = OpenSSL::X509::PURPOSE_ANY
43
+ store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
44
+ trusted_certificates.each { |certificate| store.add_cert(certificate) }
45
+ crls && crls.each { |crl| store.add_crl(crl) }
46
+ store
47
+ end
48
+ private_class_method :build_store
49
+ end
50
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fido_metadata
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart de Water
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-11-13 00:00:00.000000000 Z
11
+ date: 2019-11-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jwt
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
- - !ruby/object:Gem::Dependency
28
- name: securecompare
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '1.0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '1.0'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: bundler
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -132,6 +118,7 @@ files:
132
118
  - ".gitignore"
133
119
  - ".rubocop.yml"
134
120
  - ".travis.yml"
121
+ - CHANGELOG.md
135
122
  - CODE_OF_CONDUCT.md
136
123
  - Gemfile
137
124
  - Gemfile.lock
@@ -161,12 +148,14 @@ files:
161
148
  - lib/fido_metadata/constants.rb
162
149
  - lib/fido_metadata/entry.rb
163
150
  - lib/fido_metadata/pattern_accuracy_descriptor.rb
151
+ - lib/fido_metadata/refinement/fixed_length_secure_compare.rb
164
152
  - lib/fido_metadata/statement.rb
165
153
  - lib/fido_metadata/status_report.rb
166
154
  - lib/fido_metadata/store.rb
167
155
  - lib/fido_metadata/table_of_contents.rb
168
156
  - lib/fido_metadata/verification_method_descriptor.rb
169
157
  - lib/fido_metadata/version.rb
158
+ - lib/fido_metadata/x5c_key_finder.rb
170
159
  homepage: https://github.com/bdewater/fido_metadata
171
160
  licenses:
172
161
  - MIT