Class: DiscourseWebauthn::SecurityKeyRegistrationService

Inherits:
SecurityKeyBaseValidationService show all
Defined in:
lib/webauthn/security_key_registration_service.rb

Instance Method Summary collapse

Methods inherited from SecurityKeyBaseValidationService

#initialize, #validate_challenge, #validate_origin, #validate_rp_id_hash, #validate_user_verification, #validate_webauthn_type

Constructor Details

This class inherits a constructor from DiscourseWebauthn::SecurityKeyBaseValidationService

Instance Method Details

#register_second_factor_security_keyObject

See w3c.github.io/webauthn/#sctn-registering-a-new-credential for the registration steps followed here. Memoized methods are called in their place in the step flow to make the process clearer.



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/webauthn/security_key_registration_service.rb', line 11

def register_second_factor_security_key
  # 4. Verify that the value of C.type is webauthn.create.
  validate_webauthn_type(::DiscourseWebauthn::ACCEPTABLE_REGISTRATION_TYPE)

  # 5. Verify that the value of C.challenge equals the base64url encoding of options.challenge.
  validate_challenge

  # 6. Verify that the value of C.origin matches the Relying Party's origin.
  validate_origin

  # 7. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS
  #    connection over which the assertion was obtained. If Token Binding was used on that TLS connection,
  #    also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.
  #    Not using this right now.

  # 8. Let hash be the result of computing a hash over response.clientDataJSON using SHA-256.
  client_data_hash

  # 9. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse
  #    structure to obtain the attestation statement format fmt, the authenticator data authData,
  #    and the attestation statement attStmt.
  attestation

  # 10. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.
  # check the SHA256 hash of the rpId is the same as the authData bytes 0..31
  validate_rp_id_hash

  # 11. Verify that the User Present bit of the flags in authData is set.
  # https://blog.bigbinary.com/2011/07/20/ruby-pack-unpack.html
  #
  # bit 0 is the least significant bit - LSB first
  #
  # 12. If user verification is required for this registration, verify that
  #     the User Verified bit of the flags in authData is set.
  validate_user_verification

  # 13. Verify that the "alg" parameter in the credential public key in authData matches the alg
  #     attribute of one of the items in options.pubKeyCredParams.
  #     https://w3c.github.io/webauthn/#table-attestedCredentialData
  #     See https://www.iana.org/assignments/cose/cose.xhtml#algorithms for supported algorithm
  #     codes.
  credential_public_key, credential_public_key_bytes, credential_id =
    extract_public_key_and_credential_from_attestation(auth_data)
  if ::DiscourseWebauthn::SUPPORTED_ALGORITHMS.exclude?(credential_public_key.alg)
    raise(
      UnsupportedPublicKeyAlgorithmError,
      I18n.t("webauthn.validation.unsupported_public_key_algorithm_error"),
    )
  end

  # 14. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator
  #     extension outputs in the extensions in authData are as expected, considering the client extension input
  #     values that were given in options.extensions. In particular, any extension identifier values in the
  #     clientExtensionResults and the extensions in authData MUST also be present as extension identifier values
  #     in options.extensions, i.e., no extensions are present that were not requested. In the general case, the
  #     meaning of "are as expected" is specific to the Relying Party and which extensions are in use.
  #     Not using this right now.

  # 15. Determine the attestation statement format by performing a USASCII case-sensitive match on fmt against the
  #     set of supported WebAuthn Attestation Statement Format Identifier values. An up-to-date list of registered
  #     WebAuthn Attestation Statement Format Identifier values is maintained in the IANA registry of the same
  #     name [WebAuthn-Registries].
  # 16. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature,
  #     by using the attestation statement format fmt’s verification procedure given attStmt, authData and hash.
  if ::DiscourseWebauthn::VALID_ATTESTATION_FORMATS.exclude?(attestation["fmt"]) ||
       attestation["fmt"] != "none"
    raise(
      UnsupportedAttestationFormatError,
      I18n.t("webauthn.validation.unsupported_attestation_format_error"),
    )
  end

  #==================================================
  # ONLY APPLIES IF fmt !== none, this is all to do with
  # verifying attestation. May want to come back to this at
  # some point for additional security.
  #==================================================
  #
  # 17. If validation is successful, obtain a list of acceptable trust anchors (attestation root certificates or
  #     ECDAA-Issuer public keys) for that attestation type and attestation statement format fmt, from a trusted
  #     source or from policy. For example, the FIDO Metadata Service [FIDOMetadataService] provides one way
  #     to obtain such information, using the aaguid in the attestedCredentialData in authData.
  #
  # 18. Assess the attestation trustworthiness using the outputs of the verification procedure in step 16, as follows:
  #     If no attestation was provided, verify that None attestation is acceptable under Relying Party policy.
  #==================================================

  # 19. Check that the credentialId is not yet registered to any other user. If registration
  #     is requested for a credential that is already registered to a different user,
  #     the Relying Party SHOULD fail this registration ceremony, or it MAY decide to accept
  #     the registration, e.g. while deleting the older registration.
  encoded_credential_id = Base64.strict_encode64(credential_id)
  endcoded_public_key = Base64.strict_encode64(credential_public_key_bytes)
  if UserSecurityKey.exists?(credential_id: encoded_credential_id)
    raise(CredentialIdInUseError, I18n.t("webauthn.validation.credential_id_in_use_error"))
  end

  # 20. If the attestation statement attStmt verified successfully and is found to be trustworthy,
  #     then register the new credential with the account that was denoted in options.user, by
  #     associating it with the credentialId and credentialPublicKey in the attestedCredentialData
  #     in authData, as appropriate for the Relying Party's system.
  UserSecurityKey.create!(
    user: @current_user,
    credential_id: encoded_credential_id,
    public_key: endcoded_public_key,
    name: @params[:name],
    factor_type: UserSecurityKey.factor_types[:second_factor],
  )
rescue CBOR::UnpackError, CBOR::TypeError, CBOR::MalformedFormatError, CBOR::StackError
  raise MalformedAttestationError, I18n.t("webauthn.validation.malformed_attestation_error")
end