web_authn 0.2.2 → 0.3.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/README.md +37 -0
- data/VERSION +1 -1
- data/lib/web_authn.rb +3 -0
- data/lib/web_authn/attestation_object.rb +16 -5
- data/lib/web_authn/attestation_statement.rb +6 -0
- data/lib/web_authn/attestation_statement/android_safetynet.rb +81 -0
- data/lib/web_authn/client_data_json.rb +13 -13
- data/lib/web_authn/context/registration.rb +7 -1
- data/spec/context/registration_spec.rb +38 -0
- data/web_authn.gemspec +1 -0
- metadata +19 -3
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: ba07fe60ce73457f0f61ddc7c53dbf2b07886d93
         | 
| 4 | 
            +
              data.tar.gz: a9d76ab96805dd91c619551c09e0701a2a7b0795
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 4550a5b91a35c2c5e302b58072b2a063b203370a8128d03cc851d7866e8f6bee92eab6d86dda0a41dcc366b47f9c86bfc9a0eef1e6f4a7df24bba18bbf61e32f
         | 
| 7 | 
            +
              data.tar.gz: 5aa59a73d18cfd24b6bd86431676c4c570ee6e5ff0e7af5c83fc70767a134ec87a577d1a7ebe0aab187dd92d9e467987b0e0adb085fe244aa57be6d4ac6f9849
         | 
    
        data/README.md
    CHANGED
    
    | @@ -26,6 +26,43 @@ $ gem install web_authn | |
| 26 26 |  | 
| 27 27 | 
             
            ## Usage
         | 
| 28 28 |  | 
| 29 | 
            +
            ```ruby
         | 
| 30 | 
            +
            context = WebAuthn.context_for(
         | 
| 31 | 
            +
              client_data_json, # NOTE: URL-safe Base64 encoded
         | 
| 32 | 
            +
              origin: request.base_url,
         | 
| 33 | 
            +
              challenge: session[:challenge],
         | 
| 34 | 
            +
            )
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            if context.registration?
         | 
| 37 | 
            +
              context.verify!(
         | 
| 38 | 
            +
                attestation_object # URL-safe Base64 encoded
         | 
| 39 | 
            +
              )
         | 
| 40 | 
            +
              context.credential_id
         | 
| 41 | 
            +
              context.public_key # => `OpenSSL::PKey::RSA` or `OpenSSL::PKey::EC`
         | 
| 42 | 
            +
              context.public_cose_key # => `COSE::Key::RSA` or `COSE::Key::EC2` ref.) https://github.com/nov/cose-key
         | 
| 43 | 
            +
              context.sign_count # => `Integer`
         | 
| 44 | 
            +
            elsif context.authentication?
         | 
| 45 | 
            +
              context.verify!(
         | 
| 46 | 
            +
                authenticator_data, # URL-safe Base64 encoded
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                # NOTE:
         | 
| 49 | 
            +
                #  either 'public_key' or 'public_cose_key' is required.
         | 
| 50 | 
            +
                #  if `public_key` is given, you can also specify `digest` (default: `OpenSSL::Digest::SHA256.new`).
         | 
| 51 | 
            +
                #  if `public_cose_key` is given, it includes digest size information, so no `digest` is required.
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                # public_key: public_key, # `OpenSSL::PKey::RSA` or `OpenSSL::PKey::EC`
         | 
| 54 | 
            +
                # digest: OpenSSL::Digest::SHA256.new, # `OpenSSL::Digest::SHA(1|256|384|512)`` (default: `OpenSSL::Digest::SHA256`)
         | 
| 55 | 
            +
                public_cose_key: public_cose_key, # `COSE::Key::RSA` or `COSE::Key::EC` ref.) https://github.com/nov/cose-key
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                sign_count: previously_stored_sign_count,
         | 
| 58 | 
            +
                signature: signature # URL-safe Base64 encoded
         | 
| 59 | 
            +
              )
         | 
| 60 | 
            +
              context.sign_count # => Integer
         | 
| 61 | 
            +
            else
         | 
| 62 | 
            +
              # should never happen.
         | 
| 63 | 
            +
            end
         | 
| 64 | 
            +
            ```
         | 
| 65 | 
            +
             | 
| 29 66 | 
             
            See sample code in this repository, or [working sample site](https://web-authn.herokuapp.com/).
         | 
| 30 67 |  | 
| 31 68 | 
             
            Currently, there are several restrictions.
         | 
    
        data/VERSION
    CHANGED
    
    | @@ -1 +1 @@ | |
| 1 | 
            -
            0. | 
| 1 | 
            +
            0.3.0
         | 
    
        data/lib/web_authn.rb
    CHANGED
    
    | @@ -3,10 +3,12 @@ require 'active_support' | |
| 3 3 | 
             
            require 'active_support/core_ext'
         | 
| 4 4 | 
             
            require 'cbor'
         | 
| 5 5 | 
             
            require 'cose/key'
         | 
| 6 | 
            +
            require 'json/jwt'
         | 
| 6 7 |  | 
| 7 8 | 
             
            module WebAuthn
         | 
| 8 9 | 
             
              class Exception < StandardError; end
         | 
| 9 10 | 
             
              class InvalidContext < Exception; end
         | 
| 11 | 
            +
              class InvalidAttestation < Exception; end
         | 
| 10 12 | 
             
              class InvalidAssertion < Exception; end
         | 
| 11 13 | 
             
              class NotImplementedError < NotImplementedError; end
         | 
| 12 14 |  | 
| @@ -22,6 +24,7 @@ module WebAuthn | |
| 22 24 | 
             
            end
         | 
| 23 25 |  | 
| 24 26 | 
             
            require 'web_authn/attestation_object'
         | 
| 27 | 
            +
            require 'web_authn/attestation_statement'
         | 
| 25 28 | 
             
            require 'web_authn/attested_credential_data'
         | 
| 26 29 | 
             
            require 'web_authn/authenticator_data'
         | 
| 27 30 | 
             
            require 'web_authn/client_data_json'
         | 
| @@ -9,24 +9,35 @@ module WebAuthn | |
| 9 9 | 
             
                  delegate method, to: :authenticator_data
         | 
| 10 10 | 
             
                end
         | 
| 11 11 |  | 
| 12 | 
            -
                def initialize( | 
| 13 | 
            -
                  self.format =  | 
| 12 | 
            +
                def initialize(fmt:, att_stmt:, auth_data:)
         | 
| 13 | 
            +
                  self.format = fmt
         | 
| 14 14 | 
             
                  self.attestation_statement = case format
         | 
| 15 15 | 
             
                  when 'none'
         | 
| 16 16 | 
             
                    nil
         | 
| 17 | 
            -
                  when ' | 
| 17 | 
            +
                  when 'android-safetynet'
         | 
| 18 | 
            +
                    AttestationStatement::AndroidSafetynet.decode att_stmt
         | 
| 19 | 
            +
                  when 'packed', 'tpm', 'android-key', 'fido-u2f'
         | 
| 18 20 | 
             
                    raise NotImplementedError, "Unsupported Attestation Format: #{format}"
         | 
| 19 21 | 
             
                  else
         | 
| 20 22 | 
             
                    raise InvalidContext, 'Unknown Attestation Format'
         | 
| 21 23 | 
             
                  end
         | 
| 22 | 
            -
                  self.authenticator_data = AuthenticatorData.decode  | 
| 24 | 
            +
                  self.authenticator_data = AuthenticatorData.decode auth_data
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def verify_signature!(client_data_json)
         | 
| 28 | 
            +
                  attestation_statement.try(:verify!, authenticator_data, client_data_json)
         | 
| 23 29 | 
             
                end
         | 
| 24 30 |  | 
| 25 31 | 
             
                class << self
         | 
| 26 32 | 
             
                  def decode(encoded_attestation_object)
         | 
| 27 | 
            -
                     | 
| 33 | 
            +
                    cbor = CBOR.decode(
         | 
| 28 34 | 
             
                      Base64.urlsafe_decode64 encoded_attestation_object
         | 
| 29 35 | 
             
                    ).with_indifferent_access
         | 
| 36 | 
            +
                    new(
         | 
| 37 | 
            +
                      fmt: cbor[:fmt],
         | 
| 38 | 
            +
                      att_stmt: cbor[:attStmt],
         | 
| 39 | 
            +
                      auth_data: cbor[:authData]
         | 
| 40 | 
            +
                    )
         | 
| 30 41 | 
             
                  end
         | 
| 31 42 | 
             
                end
         | 
| 32 43 | 
             
              end
         | 
| @@ -0,0 +1,81 @@ | |
| 1 | 
            +
            module WebAuthn
         | 
| 2 | 
            +
              class AttestationStatement
         | 
| 3 | 
            +
                class AndroidSafetynet < AttestationStatement
         | 
| 4 | 
            +
                  attr_accessor :ver, :response, :certs
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  def initialize(ver:, response:)
         | 
| 7 | 
            +
                    self.ver = ver
         | 
| 8 | 
            +
                    self.response = response
         | 
| 9 | 
            +
                    self.certs = response.x5c.collect do |x5c|
         | 
| 10 | 
            +
                      OpenSSL::X509::Certificate.new(
         | 
| 11 | 
            +
                        Base64.decode64 x5c
         | 
| 12 | 
            +
                      )
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def verify!(authenticator_data, client_data_json)
         | 
| 17 | 
            +
                    verify_nonce! authenticator_data, client_data_json
         | 
| 18 | 
            +
                    verify_signature!
         | 
| 19 | 
            +
                    verify_certificate!
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    # TODO: put more ref.) https://www.w3.org/TR/webauthn/#android-safetynet-attestation
         | 
| 22 | 
            +
                    unless response[:ctsProfileMatch]
         | 
| 23 | 
            +
                      raise InvalidAttestation, 'Invalid Android Safetynet Response: ctsProfileMatch'
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  private
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def verify_nonce!(authenticator_data, client_data_json)
         | 
| 30 | 
            +
                    nonce = Base64.encode64(
         | 
| 31 | 
            +
                      OpenSSL::Digest::SHA256.digest [
         | 
| 32 | 
            +
                        authenticator_data.raw,
         | 
| 33 | 
            +
                        OpenSSL::Digest::SHA256.digest(client_data_json.raw)
         | 
| 34 | 
            +
                      ].join
         | 
| 35 | 
            +
                    ).strip
         | 
| 36 | 
            +
                    unless response[:nonce] == nonce
         | 
| 37 | 
            +
                      raise InvalidAttestation, 'Invalid Android Safetynet Response: nonce'
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  def verify_signature!
         | 
| 42 | 
            +
                    response.verify! certs.first.public_key
         | 
| 43 | 
            +
                  rescue JSON::JWS::VerificationFailed => e
         | 
| 44 | 
            +
                    raise InvalidAttestation, 'Invalid Android Safetynet Response: signature'
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  def verify_certificate!
         | 
| 48 | 
            +
                    signing_cert = certs.first
         | 
| 49 | 
            +
                    remaining_chain = certs[1..-1]
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    store = OpenSSL::X509::Store.new
         | 
| 52 | 
            +
                    store.set_default_paths
         | 
| 53 | 
            +
                    valid_chain = store.verify(signing_cert, remaining_chain)
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                    valid_subject = signing_cert.subject.to_a.detect do |key, value, type|
         | 
| 56 | 
            +
                      key == 'CN'
         | 
| 57 | 
            +
                    end.second == 'attest.android.com'
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    valid_timestamp = (
         | 
| 60 | 
            +
                      signing_cert.not_after > Time.now &&
         | 
| 61 | 
            +
                      signing_cert.not_before < Time.now
         | 
| 62 | 
            +
                    )
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    # TODO: do we need CRL check?
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                    unless valid_chain && valid_subject && valid_timestamp
         | 
| 67 | 
            +
                       raise InvalidAttestation, 'Invalid Android Safetynet Response: certificate'
         | 
| 68 | 
            +
                    end
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  class << self
         | 
| 72 | 
            +
                    def decode(att_stmt)
         | 
| 73 | 
            +
                      new(
         | 
| 74 | 
            +
                        ver: att_stmt[:ver],
         | 
| 75 | 
            +
                        response: JSON::JWT.decode(att_stmt[:response], :skip_verification)
         | 
| 76 | 
            +
                      )
         | 
| 77 | 
            +
                    end
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
              end
         | 
| 81 | 
            +
            end
         | 
| @@ -2,23 +2,23 @@ module WebAuthn | |
| 2 2 | 
             
              class ClientDataJSON
         | 
| 3 3 | 
             
                attr_accessor :type, :origin, :challenge, :raw
         | 
| 4 4 |  | 
| 5 | 
            -
                def initialize( | 
| 6 | 
            -
                  self.type =  | 
| 7 | 
            -
                  self.origin =  | 
| 8 | 
            -
                  self.challenge =  | 
| 9 | 
            -
                  self.raw =  | 
| 5 | 
            +
                def initialize(type:, origin:, challenge:, raw: nil)
         | 
| 6 | 
            +
                  self.type = type
         | 
| 7 | 
            +
                  self.origin = origin
         | 
| 8 | 
            +
                  self.challenge = challenge
         | 
| 9 | 
            +
                  self.raw = raw
         | 
| 10 10 | 
             
                end
         | 
| 11 11 |  | 
| 12 12 | 
             
                class << self
         | 
| 13 13 | 
             
                  def decode(encoded_client_data_json)
         | 
| 14 | 
            -
                     | 
| 15 | 
            -
                     | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 18 | 
            -
                       | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
                     | 
| 14 | 
            +
                    raw = Base64.urlsafe_decode64 encoded_client_data_json
         | 
| 15 | 
            +
                    json = JSON.parse(raw).with_indifferent_access
         | 
| 16 | 
            +
                    new(
         | 
| 17 | 
            +
                      type: json[:type],
         | 
| 18 | 
            +
                      origin: json[:origin],
         | 
| 19 | 
            +
                      challenge: Base64.urlsafe_decode64(json[:challenge]),
         | 
| 20 | 
            +
                      raw: raw
         | 
| 21 | 
            +
                    )
         | 
| 22 22 | 
             
                  end
         | 
| 23 23 | 
             
                end
         | 
| 24 24 | 
             
              end
         | 
| @@ -4,7 +4,8 @@ module WebAuthn | |
| 4 4 | 
             
                  attr_accessor :attestation_object
         | 
| 5 5 |  | 
| 6 6 | 
             
                  # TODO: will need more methods, or let developers access deep methods by themselves.
         | 
| 7 | 
            -
                  %i(credential_id rp_id_hash flags public_key public_cose_key sign_count | 
| 7 | 
            +
                  %i(credential_id rp_id_hash flags public_key public_cose_key sign_count
         | 
| 8 | 
            +
                     attestation_statement).each do |method|
         | 
| 8 9 | 
             
                    delegate method, to: :attestation_object
         | 
| 9 10 | 
             
                  end
         | 
| 10 11 |  | 
| @@ -17,6 +18,7 @@ module WebAuthn | |
| 17 18 | 
             
                      encoded_attestation_object
         | 
| 18 19 | 
             
                    )
         | 
| 19 20 | 
             
                    verify_flags!
         | 
| 21 | 
            +
                    verify_signature!
         | 
| 20 22 | 
             
                    self
         | 
| 21 23 | 
             
                  end
         | 
| 22 24 |  | 
| @@ -26,6 +28,10 @@ module WebAuthn | |
| 26 28 | 
             
                    super
         | 
| 27 29 | 
             
                    raise InvalidAssertion, 'Missing Flag: "at"' unless flags.at?
         | 
| 28 30 | 
             
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def verify_signature!
         | 
| 33 | 
            +
                    attestation_object.verify_signature! client_data_json
         | 
| 34 | 
            +
                  end
         | 
| 29 35 | 
             
                end
         | 
| 30 36 | 
             
              end
         | 
| 31 37 | 
             
            end
         | 
| @@ -43,5 +43,43 @@ RSpec.describe WebAuthn::Context::Registration do | |
| 43 43 | 
             
                  subject.public_key.to_pem.should == public_key_pem
         | 
| 44 44 | 
             
                end
         | 
| 45 45 | 
             
                its(:sign_count) { should == sign_count }
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                context 'when android-safetynet attestation given' do
         | 
| 48 | 
            +
                  let(:context) do
         | 
| 49 | 
            +
                    {
         | 
| 50 | 
            +
                      origin: 'https://web-authn.self-issued.app',
         | 
| 51 | 
            +
                      challenge: 'random-string-generated-by-rp-server'
         | 
| 52 | 
            +
                    }
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                  let(:client_data_json) do
         | 
| 55 | 
            +
                    'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiY21GdVpHOXRMWE4wY21sdVp5MW5aVzVsY21GMFpXUXRZbmt0Y25BdGMyVnlkbVZ5Iiwib3JpZ2luIjoiaHR0cHM6XC9cL3dlYi1hdXRobi5zZWxmLWlzc3VlZC5hcHAiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uY2hyb21lLmNhbmFyeSJ9'
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                  let(:attestation_object) do
         | 
| 58 | 
            +
                    'o2NmbXRxYW5kcm9pZC1zYWZldHluZXRnYXR0U3RtdKJjdmVyaDEyODc0MDQxaHJlc3BvbnNlWRMHZXlKaGJHY2lPaUpTVXpJMU5pSXNJbmcxWXlJNld5Sk5TVWxGYVdwRFEwRXpTMmRCZDBsQ1FXZEpTVmxyV1c4MVJqQm5PRFpyZDBSUldVcExiMXBKYUhaalRrRlJSVXhDVVVGM1ZrUkZURTFCYTBkQk1WVkZRbWhOUTFaV1RYaElha0ZqUW1kT1ZrSkJiMVJHVldSMllqSmtjMXBUUWxWamJsWjZaRU5DVkZwWVNqSmhWMDVzWTNwRmJFMURUVWRCTVZWRlFYaE5ZMUl5T1haYU1uaHNTVVZzZFdSSFZubGliVll3U1VWR01XUkhhSFpqYld3d1pWTkNTRTE2UVdWR2R6QjRUbnBGZVUxRVVYaE5la1UwVGtST1lVWjNNSGhQUkVWNVRVUk5kMDFFUVhkTlJFSmhUVWQzZUVONlFVcENaMDVXUWtGWlZFRnNWbFJOVWsxM1JWRlpSRlpSVVVsRVFYQkVXVmQ0Y0ZwdE9YbGliV3hvVFZKWmQwWkJXVVJXVVZGSVJFRXhUbUl6Vm5Wa1IwWndZbWxDVjJGWFZqTk5VazEzUlZGWlJGWlJVVXRFUVhCSVlqSTVibUpIVldkVFZ6VnFUVkp6ZDBkUldVUldVVkZFUkVKS2FHUklVbXhqTTFGMVdWYzFhMk50T1hCYVF6VnFZakl3ZDJkblJXbE5RVEJIUTFOeFIxTkpZak5FVVVWQ1FWRlZRVUUwU1VKRWQwRjNaMmRGUzBGdlNVSkJVVU5WYWpoM1dXOVFhWGhMWW1KV09ITm5XV2QyVFZSbVdDdGtTWE5HVkU5clowdFBiR2hVTUdrd1ltTkVSbHBMTW5KUGVFcGFNblZUVEZOV2FGbDJhWEJhVGtVelNFcFJXWFYxV1hkR2FtbDVLM2xyWm1GMFFVZFRhbEo2UmpGaU16RjFORE12TjI5SE5XcE5hRE5UTXpkaGJIZHFWV0k0UTFkcFZIaHZhWEJXVDFsM1MwdDZkVlY1YTNGRlEzUnFiR2hLTkVGclYyRkVVeXRhZUV0RmNVOWhaVGwwYmtOblpVaHNiRnBGTDA5U1oyVk5ZWGd5V0U1RGIwZzJjM0pVUlZKamEzTnFlbHBhY2tGWGVFdHpaR1oyVm5KWVRucERVamxFZUZaQlUzVkpOa3g2ZDJnNFJGTnNNa1ZQYjJ0aWMyRnVXaXNyTDBweFRXVkJRa1ptVUhkcWVYZHlZakJ3Y2tWVmVUQndZV1ZXYzNWa0t6QndaV1Y0U3k4MUswVTJhM0JaUjBzMFdrc3libXR2Vmt4MVowVTFkR0ZJY2tGcU9ETlJLMUJQWW1KMlQzcFhZMFpyY0c1V1MzbHFielpMVVVGdFdEWlhTa0ZuVFVKQlFVZHFaMmRHUjAxSlNVSlJha0ZVUW1kT1ZraFRWVVZFUkVGTFFtZG5ja0puUlVaQ1VXTkVRVlJCWkVKblRsWklVa1ZGUm1wQlZXZG9TbWhrU0ZKc1l6TlJkVmxYTld0amJUbHdXa00xYW1JeU1IZGhRVmxKUzNkWlFrSlJWVWhCVVVWRldFUkNZVTFETUVkRFEzTkhRVkZWUmtKNlFVTm9hVVp2WkVoU2QwOXBPSFpqUjNSd1RHMWtkbUl5WTNaYU0wNTVUV2s1U0ZaR1RraFRWVVpJVFhrMWFtTnVVWGRMVVZsSlMzZFpRa0pSVlVoTlFVZEhTRmRvTUdSSVFUWk1lVGwyV1ROT2QweHVRbkpoVXpWdVlqSTVia3d3WkZWVk1HUktVVlZqZWsxQ01FZEJNVlZrUkdkUlYwSkNVVWM0U1hKUmRFWlNOa05WVTJ0cGEySXpZV2x0YzIweU5tTkNWRUZOUW1kT1ZraFNUVUpCWmpoRlFXcEJRVTFDT0VkQk1WVmtTWGRSV1UxQ1lVRkdTR1pEZFVaRFlWb3pXakp6VXpORGFIUkRSRzlJTm0xbWNuQk1UVU5GUjBFeFZXUkpRVkZoVFVKbmQwUkJXVXRMZDFsQ1FrRklWMlZSU1VaQmVrRkpRbWRhYm1kUmQwSkJaMGwzVFZGWlJGWlNNR1pDUTI5M1MwUkJiVzlEVTJkSmIxbG5ZVWhTTUdORWIzWk1NazU1WWtNMWQyRXlhM1ZhTWpsMlduazVTRlpHVGtoVFZVWklUWGsxYW1OdGQzZEVVVmxLUzI5YVNXaDJZMDVCVVVWTVFsRkJSR2RuUlVKQlJpOVNlazV1UXpWRWVrSlZRblJ1YURKdWRFcE1WMFZSYURsNlJXVkdXbVpRVERsUmIydHliRUZ2V0dkcVYyZE9PSEJUVWxVeGJGWkhTWEIwZWsxNFIyaDVNeTlQVWxKYVZHRTJSREpFZVRob2RrTkVja1pKTXl0c1Exa3dNVTFNTlZFMldFNUZOVkp6TW1ReFVtbGFjRTF6ZWtRMFMxRmFUa2N6YUZvd1FrWk9VUzlqYW5KRGJVeENUMGRMYTBWVk1XUnRRVmh6UmtwWVNtbFBjakpEVGxSQ1QxUjFPVVZpVEZkb1VXWmtRMFl4WW5kNmVYVXJWelppVVZOMk9GRkVialZQWkUxVEwxQnhSVEZrUldkbGRDODJSVWxTUWpjMk1VdG1XbEVyTDBSRk5reHdNMVJ5V2xSd1QwWkVSR2RZYUN0TVowZFBjM2RvUld4cU9XTXpkbHBJUjBwdWFHcHdkRGh5YTJKcGNpOHlkVXhIWm5oc1ZsbzBTekY0TlVSU1RqQlFWVXhrT1hsUVUyMXFaeXRoYWpFcmRFaDNTVEZ0VVcxYVZsazNjWFpQTlVSbmFFOTRhRXBOUjJ4Nk5teE1hVnB0ZW05blBTSXNJazFKU1VWWVJFTkRRVEJUWjBGM1NVSkJaMGxPUVdWUGNFMUNlamhqWjFrMFVEVndWRWhVUVU1Q1oydHhhR3RwUnpsM01FSkJVWE5HUVVSQ1RVMVRRWGRJWjFsRVZsRlJURVY0WkVoaVJ6bHBXVmQ0VkdGWFpIVkpSa3AyWWpOUloxRXdSV2RNVTBKVFRXcEZWRTFDUlVkQk1WVkZRMmhOUzFJeWVIWlpiVVp6VlRKc2JtSnFSVlJOUWtWSFFURlZSVUY0VFV0U01uaDJXVzFHYzFVeWJHNWlha0ZsUm5jd2VFNTZRVEpOVkZWM1RVUkJkMDVFU21GR2R6QjVUVlJGZVUxVVZYZE5SRUYzVGtSS1lVMUdVWGhEZWtGS1FtZE9Wa0pCV1ZSQmJGWlVUVkkwZDBoQldVUldVVkZMUlhoV1NHSXlPVzVpUjFWblZraEtNV016VVdkVk1sWjVaRzFzYWxwWVRYaEtWRUZxUW1kT1ZrSkJUVlJJUldSMllqSmtjMXBUUWtwaWJsSnNZMjAxYkdSRFFrSmtXRkp2WWpOS2NHUklhMmRTZWsxM1oyZEZhVTFCTUVkRFUzRkhVMGxpTTBSUlJVSkJVVlZCUVRSSlFrUjNRWGRuWjBWTFFXOUpRa0ZSUkV0VmEzWnhTSFl2VDBwSGRXOHlia2xaWVU1V1YxaFJOVWxYYVRBeFExaGFZWG8yVkVsSVRFZHdMMnhQU2lzMk1EQXZOR2hpYmpkMmJqWkJRVUl6UkZaNlpGRlBkSE0zUnpWd1NEQnlTbTV1VDBaVlFVczNNVWMwYm5wTFRXWklRMGRWYTNOWEwyMXZibUVyV1RKbGJVcFJNazRyWVdsamQwcExaWFJRUzFKVFNXZEJkVkJQUWpaQllXaG9PRWhpTWxoUE0yZzVVbFZyTWxRd1NFNXZkVUl5Vm5wNGIwMVliR3Q1VnpkWVZWSTFiWGMyU210TVNHNUJOVEpZUkZadlVsUlhhMDUwZVRWdlEwbE9USFpIYlc1U2Mwb3hlbTkxUVhGWlIxWlJUV012TjNONUt5OUZXV2hCVEhKV1NrVkJPRXRpZEhsWUszSTRjMjUzVlRWRE1XaFZjbmRoVnpaTlYwOUJVbUU0Y1VKd1RsRmpWMVJyWVVsbGIxbDJlUzl6UjBsS1JXMXFVakIyUmtWM1NHUndNV05UWVZkSmNqWXZOR2MzTW00M1QzRllkMlpwYm5VM1dsbFhPVGRGWm05UFUxRktaVUY2UVdkTlFrRkJSMnBuWjBWNlRVbEpRa3g2UVU5Q1owNVdTRkU0UWtGbU9FVkNRVTFEUVZsWmQwaFJXVVJXVWpCc1FrSlpkMFpCV1VsTGQxbENRbEZWU0VGM1JVZERRM05IUVZGVlJrSjNUVU5OUWtsSFFURlZaRVYzUlVJdmQxRkpUVUZaUWtGbU9FTkJVVUYzU0ZGWlJGWlNNRTlDUWxsRlJraG1RM1ZHUTJGYU0xb3ljMU16UTJoMFEwUnZTRFp0Wm5Kd1RFMUNPRWRCTVZWa1NYZFJXVTFDWVVGR1NuWnBRakZrYmtoQ04wRmhaMkpsVjJKVFlVeGtMMk5IV1ZsMVRVUlZSME5EYzBkQlVWVkdRbmRGUWtKRGEzZEtla0ZzUW1kbmNrSm5SVVpDVVdOM1FWbFpXbUZJVWpCalJHOTJUREk1YW1NelFYVmpSM1J3VEcxa2RtSXlZM1phTTA1NVRXcEJlVUpuVGxaSVVqaEZTM3BCY0UxRFpXZEtZVUZxYUdsR2IyUklVbmRQYVRoMldUTktjMHh1UW5KaFV6VnVZakk1Ymt3eVpIcGpha2wyV2pOT2VVMXBOV3BqYlhkM1VIZFpSRlpTTUdkQ1JHZDNUbXBCTUVKbldtNW5VWGRDUVdkSmQwdHFRVzlDWjJkeVFtZEZSa0pSWTBOQlVsbGpZVWhTTUdOSVRUWk1lVGwzWVRKcmRWb3lPWFphZVRsNVdsaENkbU15YkRCaU0wbzFUSHBCVGtKbmEzRm9hMmxIT1hjd1FrRlJjMFpCUVU5RFFWRkZRVWhNWlVwc2RWSlVOMkoyY3pJMlozbEJXamh6YnpneGRISlZTVk5rTjA4ME5YTnJSRlZ0UVdkbE1XTnVlR2hITVZBeVkwNXRVM2hpVjNOdmFVTjBNbVYxZURsTVUwUXJVRUZxTWt4SldWSkdTRmN6TVM4MmVHOXBZekZyTkhSaVYxaHJSRU5xYVhJek4zaFVWRTV4VWtGTlVGVjVSbEpYVTJSMmRDdHViRkJ4ZDI1aU9FOWhNa2t2YldGVFNuVnJZM2hFYWs1VFpuQkVhQzlDWkRGc1drNW5aR1F2T0dOTVpITkZNeXQzZVhCMVprbzVkVmhQTVdsUmNHNW9PWHBpZFVaSmQzTkpUMDVIYkRGd00wRTRRMmQ0YTNGSkwxVkJhV2d6U21GSFQzRmpjR05rWVVOSmVtdENZVkk1ZFZsUk1WZzBhekpXWnpWQlVGSk1iM1Y2Vm5rM1lUaEpWbXMyZDNWNU5uQnRLMVEzU0ZRMFRGazRhV0pUTlVaRldteG1RVVpNVTFjNFRuZHpWbm81VTBKTE1sWnhiakZPTUZCSlRXNDFlRUUyVGxwV1l6ZHZPRE0xUkV4QlJuTm9SVmRtUXpkVVNXVXpaejA5SWwxOS5leUp1YjI1alpTSTZJaTlYVG1SVFZXOHpiRUl4ZWt0Mk5UVlpNV1EyY2psR1pXdFJkbE5rZERjNWNHaDRjMmhuTjNsMVIxazlJaXdpZEdsdFpYTjBZVzF3VFhNaU9qRTFNelUyT1Rjd056TTFOVEFzSW1Gd2ExQmhZMnRoWjJWT1lXMWxJam9pWTI5dExtZHZiMmRzWlM1aGJtUnliMmxrTG1kdGN5SXNJbUZ3YTBScFoyVnpkRk5vWVRJMU5pSTZJako1TXpBNFJ6Y3hMM2RaUmtZMk9XRnVSVzlKT1VGYWNrTmFPREZFV0dSMk1YaEhNMjg0UVZkUlNITTlJaXdpWTNSelVISnZabWxzWlUxaGRHTm9JanAwY25WbExDSmhjR3REWlhKMGFXWnBZMkYwWlVScFoyVnpkRk5vWVRJMU5pSTZXeUk0VURGelZ6QkZVRXBqYzJ4M04xVjZVbk5wV0V3Mk5IY3JUelV3UldRclVrSkpRM1JoZVRGbk1qUk5QU0pkTENKaVlYTnBZMGx1ZEdWbmNtbDBlU0k2ZEhKMVpYMC5QVW9fT0h0dW96SWUxZGZFNlctSUVlYkZtN0R0Qll6M2NmZ0UzRlB5X3dVQlFCMjUwUE1WWjNVbk1NVjYwb0Q2U3d0ajZtQ0lQYnNYZnBULXFuY184eHlTUURndXZQUmp1b191b1JtUWF4aV9FSEZVaTZrZFV0akhaNkY1bWpYcVF4LWRiaFlONU00dG01WlM2bUxaMkdlbGlVcE1UVG9nU2FQUVZiVnl1Y3g0aGpfT1VDLWVPM1lCRmRldmw0d0pmSy15QVJxaGtmOHN6NEgwa1E5TU9IT2U0N0x2a0h3RnI2MElQSjNaQ3QwSklKYnJWVDZnMHJJal9iSlo3aXFrTDFOWUhrbURvNUhnSkgyTXA3QnYtZlJfMHcydzJyWkIzZk5zVGhxRm9UbUI4RU5XUmJjQ3lWQXBncVdIbFU5bUh5SlJGWVVsbnVDcGRGSEwwUjFaX1FoYXV0aERhdGFYxTLLgNysw8NSRiywHzv-MC3m83EvMP0g7NGcO6W4WJSVRAAAAAAAAAAAAAAAAAAAAAAAAAAAAEEBKg96NvDCk5gmyyLqM0zXE0ZZnSkTarHzUfYU2PHWwdQhjjWLXayf0-jYLazWjpSr-N8DM1Zhls4jfmQCqa50X6UBAgMmIAEhWCAGD72C5VXE3mwMjzc_X0_7wUgIOA6kt2KoDIn1-1PHdSJYIAoZmBXyEJkjvlu251BDb8VoOtIketAD1VvYc3WUJJrZ'
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  it do
         | 
| 62 | 
            +
                    expect do
         | 
| 63 | 
            +
                      subject
         | 
| 64 | 
            +
                    end.not_to raise_error
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  context 'when client_data_json is invalid' do
         | 
| 68 | 
            +
                    let(:client_data_json) do
         | 
| 69 | 
            +
                      Base64.urlsafe_encode64({
         | 
| 70 | 
            +
                        type: "webauthn.create",
         | 
| 71 | 
            +
                        challenge: "cmFuZG9tLXN0cmluZy1nZW5lcmF0ZWQtYnktcnAtc2VydmVy",
         | 
| 72 | 
            +
                        origin: "https://web-authn.self-issued.app",
         | 
| 73 | 
            +
                        androidPackageName: "com.chrome.canary.malformed"
         | 
| 74 | 
            +
                      }.to_json, padding: false)
         | 
| 75 | 
            +
                    end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                    it do
         | 
| 78 | 
            +
                      expect do
         | 
| 79 | 
            +
                        subject
         | 
| 80 | 
            +
                      end.to raise_error WebAuthn::InvalidAttestation, 'Invalid Android Safetynet Response: nonce'
         | 
| 81 | 
            +
                    end
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
                end
         | 
| 46 84 | 
             
              end
         | 
| 47 85 | 
             
            end
         | 
    
        data/web_authn.gemspec
    CHANGED
    
    | @@ -15,6 +15,7 @@ Gem::Specification.new do |gem| | |
| 15 15 | 
             
              gem.add_runtime_dependency 'activesupport'
         | 
| 16 16 | 
             
              gem.add_runtime_dependency 'cbor'
         | 
| 17 17 | 
             
              gem.add_runtime_dependency 'cose-key'
         | 
| 18 | 
            +
              gem.add_runtime_dependency 'json-jwt'
         | 
| 18 19 | 
             
              gem.add_development_dependency 'rake', '~> 10.0'
         | 
| 19 20 | 
             
              gem.add_development_dependency 'simplecov'
         | 
| 20 21 | 
             
              gem.add_development_dependency 'rspec'
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: web_authn
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.3.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - nov matake
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2018-09- | 
| 11 | 
            +
            date: 2018-09-12 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: activesupport
         | 
| @@ -52,6 +52,20 @@ dependencies: | |
| 52 52 | 
             
                - - ">="
         | 
| 53 53 | 
             
                  - !ruby/object:Gem::Version
         | 
| 54 54 | 
             
                    version: '0'
         | 
| 55 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 56 | 
            +
              name: json-jwt
         | 
| 57 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 58 | 
            +
                requirements:
         | 
| 59 | 
            +
                - - ">="
         | 
| 60 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            +
                    version: '0'
         | 
| 62 | 
            +
              type: :runtime
         | 
| 63 | 
            +
              prerelease: false
         | 
| 64 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 65 | 
            +
                requirements:
         | 
| 66 | 
            +
                - - ">="
         | 
| 67 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 68 | 
            +
                    version: '0'
         | 
| 55 69 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 56 70 | 
             
              name: rake
         | 
| 57 71 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -128,6 +142,8 @@ files: | |
| 128 142 | 
             
            - bin/setup
         | 
| 129 143 | 
             
            - lib/web_authn.rb
         | 
| 130 144 | 
             
            - lib/web_authn/attestation_object.rb
         | 
| 145 | 
            +
            - lib/web_authn/attestation_statement.rb
         | 
| 146 | 
            +
            - lib/web_authn/attestation_statement/android_safetynet.rb
         | 
| 131 147 | 
             
            - lib/web_authn/attested_credential_data.rb
         | 
| 132 148 | 
             
            - lib/web_authn/authenticator_data.rb
         | 
| 133 149 | 
             
            - lib/web_authn/authenticator_data/flags.rb
         | 
| @@ -165,7 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 165 181 | 
             
                  version: '0'
         | 
| 166 182 | 
             
            requirements: []
         | 
| 167 183 | 
             
            rubyforge_project: 
         | 
| 168 | 
            -
            rubygems_version: 2. | 
| 184 | 
            +
            rubygems_version: 2.5.2
         | 
| 169 185 | 
             
            signing_key: 
         | 
| 170 186 | 
             
            specification_version: 4
         | 
| 171 187 | 
             
            summary: WebAuthn RP library
         |