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 +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
|