You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

assertion.go 5.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. package protocol
  2. import (
  3. "crypto/sha256"
  4. "encoding/base64"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "github.com/duo-labs/webauthn/protocol/webauthncose"
  10. )
  11. // The raw response returned to us from an authenticator when we request a
  12. // credential for login/assertion.
  13. type CredentialAssertionResponse struct {
  14. PublicKeyCredential
  15. AssertionResponse AuthenticatorAssertionResponse `json:"response"`
  16. }
  17. // The parsed CredentialAssertionResponse that has been marshalled into a format
  18. // that allows us to verify the client and authenticator data inside the response
  19. type ParsedCredentialAssertionData struct {
  20. ParsedPublicKeyCredential
  21. Response ParsedAssertionResponse
  22. Raw CredentialAssertionResponse
  23. }
  24. // The AuthenticatorAssertionResponse contains the raw authenticator assertion data and is parsed into
  25. // ParsedAssertionResponse
  26. type AuthenticatorAssertionResponse struct {
  27. AuthenticatorResponse
  28. AuthenticatorData URLEncodedBase64 `json:"authenticatorData"`
  29. Signature URLEncodedBase64 `json:"signature"`
  30. UserHandle URLEncodedBase64 `json:"userHandle,omitempty"`
  31. }
  32. // Parsed form of AuthenticatorAssertionResponse
  33. type ParsedAssertionResponse struct {
  34. CollectedClientData CollectedClientData
  35. AuthenticatorData AuthenticatorData
  36. Signature []byte
  37. UserHandle []byte
  38. }
  39. // Parse the credential request response into a format that is either required by the specification
  40. // or makes the assertion verification steps easier to complete. This takes an http.Request that contains
  41. // the assertion response data in a raw, mostly base64 encoded format, and parses the data into
  42. // manageable structures
  43. func ParseCredentialRequestResponse(response *http.Request) (*ParsedCredentialAssertionData, error) {
  44. if response == nil || response.Body == nil {
  45. return nil, ErrBadRequest.WithDetails("No response given")
  46. }
  47. return ParseCredentialRequestResponseBody(response.Body)
  48. }
  49. // Parse the credential request response into a format that is either required by the specification
  50. // or makes the assertion verification steps easier to complete. This takes an io.Reader that contains
  51. // the assertion response data in a raw, mostly base64 encoded format, and parses the data into
  52. // manageable structures
  53. func ParseCredentialRequestResponseBody(body io.Reader) (*ParsedCredentialAssertionData, error) {
  54. var car CredentialAssertionResponse
  55. err := json.NewDecoder(body).Decode(&car)
  56. if err != nil {
  57. return nil, ErrBadRequest.WithDetails("Parse error for Assertion")
  58. }
  59. if car.ID == "" {
  60. return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID missing")
  61. }
  62. _, err = base64.RawURLEncoding.DecodeString(car.ID)
  63. if err != nil {
  64. return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID not base64url encoded")
  65. }
  66. if car.Type != "public-key" {
  67. return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with bad type")
  68. }
  69. var par ParsedCredentialAssertionData
  70. par.ID, par.RawID, par.Type, par.ClientExtensionResults = car.ID, car.RawID, car.Type, car.ClientExtensionResults
  71. par.Raw = car
  72. par.Response.Signature = car.AssertionResponse.Signature
  73. par.Response.UserHandle = car.AssertionResponse.UserHandle
  74. // Step 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
  75. // We don't call it cData but this is Step 5 in the spec.
  76. err = json.Unmarshal(car.AssertionResponse.ClientDataJSON, &par.Response.CollectedClientData)
  77. if err != nil {
  78. return nil, err
  79. }
  80. err = par.Response.AuthenticatorData.Unmarshal(car.AssertionResponse.AuthenticatorData)
  81. if err != nil {
  82. return nil, ErrParsingData.WithDetails("Error unmarshalling auth data")
  83. }
  84. return &par, nil
  85. }
  86. // Follow the remaining steps outlined in §7.2 Verifying an authentication assertion
  87. // (https://www.w3.org/TR/webauthn/#verifying-assertion) and return an error if there
  88. // is a failure during each step.
  89. func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPartyID, relyingPartyOrigin, appID string, verifyUser bool, credentialBytes []byte) error {
  90. // Steps 4 through 6 in verifying the assertion data (https://www.w3.org/TR/webauthn/#verifying-assertion) are
  91. // "assertive" steps, i.e "Let JSONtext be the result of running UTF-8 decode on the value of cData."
  92. // We handle these steps in part as we verify but also beforehand
  93. // Handle steps 7 through 10 of assertion by verifying stored data against the Collected Client Data
  94. // returned by the authenticator
  95. validError := p.Response.CollectedClientData.Verify(storedChallenge, AssertCeremony, relyingPartyOrigin)
  96. if validError != nil {
  97. return validError
  98. }
  99. // Begin Step 11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the RP.
  100. rpIDHash := sha256.Sum256([]byte(relyingPartyID))
  101. var appIDHash [32]byte
  102. if appID != "" {
  103. appIDHash = sha256.Sum256([]byte(appID))
  104. }
  105. // Handle steps 11 through 14, verifying the authenticator data.
  106. validError = p.Response.AuthenticatorData.Verify(rpIDHash[:], appIDHash[:], verifyUser)
  107. if validError != nil {
  108. return ErrAuthData.WithInfo(validError.Error())
  109. }
  110. // allowedUserCredentialIDs := session.AllowedCredentialIDs
  111. // Step 15. Let hash be the result of computing a hash over the cData using SHA-256.
  112. clientDataHash := sha256.Sum256(p.Raw.AssertionResponse.ClientDataJSON)
  113. // Step 16. Using the credential public key looked up in step 3, verify that sig is
  114. // a valid signature over the binary concatenation of authData and hash.
  115. sigData := append(p.Raw.AssertionResponse.AuthenticatorData, clientDataHash[:]...)
  116. var (
  117. key interface{}
  118. err error
  119. )
  120. if appID == "" {
  121. key, err = webauthncose.ParsePublicKey(credentialBytes)
  122. } else {
  123. key, err = webauthncose.ParseFIDOPublicKey(credentialBytes)
  124. }
  125. valid, err := webauthncose.VerifySignature(key, sigData, p.Response.Signature)
  126. if !valid {
  127. return ErrAssertionSignature.WithDetails(fmt.Sprintf("Error validating the assertion signature: %+v\n", err))
  128. }
  129. return nil
  130. }