fido_metadata 0.1.0 → 0.2.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.
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