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 +4 -4
- data/CHANGELOG.md +24 -0
- data/Gemfile.lock +1 -3
- data/README.md +7 -13
- data/bin/console +2 -2
- data/fido_metadata.gemspec +0 -1
- data/lib/fido_metadata/client.rb +20 -31
- data/lib/fido_metadata/refinement/fixed_length_secure_compare.rb +23 -0
- data/lib/fido_metadata/statement.rb +8 -0
- data/lib/fido_metadata/version.rb +1 -1
- data/lib/fido_metadata/x5c_key_finder.rb +50 -0
- metadata +5 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2881c4ebdb1827ac7d037d7e4460bd8708451c7a1c5e0501d5144a6119b64a1e
|
4
|
+
data.tar.gz: b01d388d2710097a8e84319963c528c4933ce0ceb4a2fee2ee68ee63d0d6b38c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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/)
|
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
|
-
###
|
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
|
-
|
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 <<~
|
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
|
-
|
18
|
+
TOKEN_HINT
|
19
19
|
end
|
20
20
|
puts "Reset the cache via: FidoMetadata.configuration.cache_backend.clear"
|
21
21
|
|
data/fido_metadata.gemspec
CHANGED
@@ -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"
|
data/lib/fido_metadata/client.rb
CHANGED
@@ -3,7 +3,8 @@
|
|
3
3
|
require "jwt"
|
4
4
|
require "net/http"
|
5
5
|
require "openssl"
|
6
|
-
require "
|
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
|
-
|
20
|
-
|
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,
|
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
|
-
|
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
|
-
|
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
|
@@ -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.
|
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-
|
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
|