Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encapsulate WebAuthn::PublicKey behaviour #284

Merged
merged 3 commits into from
Dec 17, 2019
Merged

Encapsulate WebAuthn::PublicKey behaviour #284

merged 3 commits into from
Dec 17, 2019

Conversation

ssuttner
Copy link
Member

WebAuthn has some custom logic to account for backward compatibility on the deserialization of stored public keys in older formats. See this comment.

Up until now, this logic is was not properly encapsulated so that it could be reused from other parts of the gem, or even to be used as a public API.

Motivated by #222 and in collaboration with @padulafacundo a new method is now exposed to be able to check that a stored public key abides by the expected formatting.
If WebAuthn::PublicKey.deserialize(stored_public_key) succeeds, then a user can be sure the key is valid.

@@ -42,7 +42,7 @@ def initialize(authenticator_data:, signature:, user_handle: nil, **options)
def verify(expected_challenge, expected_origin = nil, public_key:, sign_count:, user_verification: nil,
rp_id: nil)
super(expected_challenge, expected_origin, user_verification: user_verification, rp_id: rp_id)
verify_item(:signature, credential_cose_key(public_key))
verify_item(:signature, WebAuthn::PublicKey.deserialize(public_key))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any other part of the gem where we should be calling WebAuthn::PublicKey.deserialize that I'm missing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this

def valid_credential_public_key?
cose_key = COSE::Key.deserialize(public_key)
!!cose_key.alg
end

be

WebAuthn::PublicKey.deserialize(public_key).valid?

instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My question was more directed to places where we actually needed to account for the two formats.

Answering the question you are now asking: I think it could!
But, I think I'd like to have a deeper discussion on whether all the usages of COSE::Key.deserialize should be replaced with WebAuthn::PublickKey.deserialize or not.

It's a pretty interesting discussion to have.
Do we want to make it part of this PR, or discuss separately?
I'm fine either way.

Copy link
Contributor

@grzuy grzuy Dec 16, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My question was more directed to places where we actually needed to account for the two formats.

Answering the question you are now asking: I think it could!
But, I think I'd like to have a deeper discussion on whether all the usages of COSE::Key.deserialize should be replaced with WebAuthn::PublickKey.deserialize or not.

I think in most cases "Yes", given that the WebAuthn spec builds extra domain logic on top of COSE Keys (in https://www.w3.org/TR/webauthn-2/#sctn-attested-credential-data). So ideally I imagine no references to COSE key other than from WebAuthn::PublicKey, so that every "webauthn specific key logic" is contained just there.

For example, by moving the validation of the "alg" to WebAuthn::PublicKey#valid?.

It's a pretty interesting discussion to have.
Do we want to make it part of this PR, or discuss separately?

Separate PR is fine.
This provides value already in it's current form I think.

I'm fine either way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect! Let's keep working on this on separate PRs.

end
let(:cose_public_key) do
Base64.urlsafe_decode64(
"pQECAyYgASFYIPJKd_-Rl0QtQwbLggjGC_EbUFIMriCkdc2yuaukkBuNIlggaBsBjCwnMzFL7OUGJNm4b-HVpFNUa_NbsHGARuYKHfU"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better place to define this cose_public_key? Maybe the seeds?

Copy link
Contributor

@grzuy grzuy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

Thanks @ssuttner and @padulafacundo 🎉

WebAuthn::SignatureVerifier
.new(credential_cose_key.alg, credential_cose_key.to_pkey)
.new(webauthn_public_key.cose_key.alg, webauthn_public_key.pkey)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webauthn_public_key.alg?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let's expose that method as well 👍
Good call out!

@@ -2,11 +2,11 @@

require "cose/algorithm"
require "cose/key"
require "webauthn/attestation_statement/fido_u2f/public_key"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also remove cose requires above?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right!


require "webauthn/attestation_statement/fido_u2f/public_key"
require "cose/key"
require "cose/key/ec2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"cose/key" requires all keys already.

)
else
COSE::Key.deserialize(public_key)
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think rubocop has a cop for this, which I would enable if supported, but I think I prefer not having indentation depend of the length of a variable name. Smells a bit weird to me that I would need to update ~20 lines if I decide to change the name of cose_key variable to key e.g.

What do you think about?

diff --git a/lib/webauthn/public_key.rb b/lib/webauthn/public_key.rb
index 65521bd..5fc2e99 100644
--- a/lib/webauthn/public_key.rb
+++ b/lib/webauthn/public_key.rb
@@ -8,26 +8,27 @@ require "cose/algorithm"
 module WebAuthn
   class PublicKey
     def self.deserialize(public_key)
-      cose_key = if WebAuthn::AttestationStatement::FidoU2f::PublicKey.uncompressed_point?(public_key)
-                   # Gem version v1.11.0 and lower, used to behave so that Credential#public_key
-                   # returned an EC P-256 uncompressed point.
-                   #
-                   # Because of https://github.com/cedarcode/webauthn-ruby/issues/137 this was changed
-                   # and Credential#public_key started returning the unchanged COSE_Key formatted
-                   # credentialPublicKey (as in https://www.w3.org/TR/webauthn/#credentialpublickey).
-                   #
-                   # Given that the credential public key is expected to be stored long-term by the gem
-                   # user and later be passed as the public_key argument in the
-                   # AuthenticatorAssertionResponse.verify call, we then need to support the two formats.
-                   COSE::Key::EC2.new(
-                     alg: COSE::Algorithm.by_name("ES256").id,
-                     crv: 1,
-                     x: public_key[1..32],
-                     y: public_key[33..-1]
-                   )
-                 else
-                   COSE::Key.deserialize(public_key)
-                 end
+      cose_key =
+        if WebAuthn::AttestationStatement::FidoU2f::PublicKey.uncompressed_point?(public_key)
+          # Gem version v1.11.0 and lower, used to behave so that Credential#public_key
+          # returned an EC P-256 uncompressed point.
+          #
+          # Because of https://github.com/cedarcode/webauthn-ruby/issues/137 this was changed
+          # and Credential#public_key started returning the unchanged COSE_Key formatted
+          # credentialPublicKey (as in https://www.w3.org/TR/webauthn/#credentialpublickey).
+          #
+          # Given that the credential public key is expected to be stored long-term by the gem
+          # user and later be passed as the public_key argument in the
+          # AuthenticatorAssertionResponse.verify call, we then need to support the two formats.
+          COSE::Key::EC2.new(
+            alg: COSE::Algorithm.by_name("ES256").id,
+            crv: 1,
+            x: public_key[1..32],
+            y: public_key[33..-1]
+          )
+        else
+          COSE::Key.deserialize(public_key)
+        end
 
       new(cose_key: cose_key)
     end

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is not a rubocop rule agains what you just proposed.
But initially, I had:

      cose_key = if WebAuthn::AttestationStatement::FidoU2f::PublicKey.uncompressed_point?(public_key)
        # Gem version v1.11.0 and lower, used to behave so that Credential#public_key
        # returned an EC P-256 uncompressed point.
        #
        # Because of https://github.com/cedarcode/webauthn-ruby/issues/137 this was changed
        # and Credential#public_key started returning the unchanged COSE_Key formatted
        # credentialPublicKey (as in https://www.w3.org/TR/webauthn/#credentialpublickey).
        #
        # Given that the credential public key is expected to be stored long-term by the gem
        # user and later be passed as the public_key argument in the
        # AuthenticatorAssertionResponse.verify call, we then need to support the two formats.
        COSE::Key::EC2.new(
          alg: COSE::Algorithm.by_name("ES256").id,
          crv: 1,
          x: public_key[1..32],
          y: public_key[33..-1]
        )
      else
        COSE::Key.deserialize(public_key)
      end

And rubocop complained with the following errors:

lib/webauthn/public_key.rb:21:9: C: Layout/IndentationWidth: Use 2 (not -9) spaces for indentation.
        COSE::Key::EC2.new(
        ^^^^^^^^^
lib/webauthn/public_key.rb:27:7: C: Layout/ElseAlignment: Align else with if.
      else
      ^^^^
lib/webauthn/public_key.rb:29:7: W: Layout/EndAlignment: end at 29, 6 is not aligned with if at 10, 17.
      end
      ^^^

When I asked rubocop to fix it for me by using rubocop -a, I got the output that triggered this discussion.
I was just trying to abide by whatever rubocop suggested.

No worries, I agree with you on the benefits of refactoring it to not depend on the variable name.
Thanks for bringing this up! 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see.

Didn't know that rubocop auto-corrected to that... :-/

Thanks for the update

end
end
end
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@@ -42,7 +42,7 @@ def initialize(authenticator_data:, signature:, user_handle: nil, **options)
def verify(expected_challenge, expected_origin = nil, public_key:, sign_count:, user_verification: nil,
rp_id: nil)
super(expected_challenge, expected_origin, user_verification: user_verification, rp_id: rp_id)
verify_item(:signature, credential_cose_key(public_key))
verify_item(:signature, WebAuthn::PublicKey.deserialize(public_key))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this

def valid_credential_public_key?
cose_key = COSE::Key.deserialize(public_key)
!!cose_key.alg
end

be

WebAuthn::PublicKey.deserialize(public_key).valid?

instead?

@grzuy grzuy changed the title Encapsulate Webauthn::PublicKey behaviour Encapsulate WebAuthn::PublicKey behaviour Nov 29, 2019
@grzuy grzuy self-requested a review December 16, 2019 14:15
Copy link
Contributor

@grzuy grzuy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again!

@grzuy
Copy link
Contributor

grzuy commented Dec 17, 2019

Will merge after fixup squshing...

  * this will allow to keep the code isolated and more maintainable
  * at the same time, the WebAuthn::PublicKey.deserialize interface
  will be available for public use
  * clean up public interface to avoid extra method navigation when
  trying to fetch the 'alg' value
@grzuy grzuy merged commit 114a96d into master Dec 17, 2019
@grzuy grzuy deleted the webauthn_keys branch December 17, 2019 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants