1 2 3 4 5 6 7 8 9 10 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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
|
package protocol
import (
"crypto/subtle"
"fmt"
"net/url"
"strings"
)
// CollectedClientData represents the contextual bindings of both the WebAuthn Relying Party
// and the client. It is a key-value mapping whose keys are strings. Values can be any type
// that has a valid encoding in JSON. Its structure is defined by the following Web IDL.
//
// Specification: ยง5.8.1. Client Data Used in WebAuthn Signatures (https://www.w3.org/TR/webauthn/#dictdef-collectedclientdata)
type CollectedClientData struct {
// Type the string "webauthn.create" when creating new credentials,
// and "webauthn.get" when getting an assertion from an existing credential. The
// purpose of this member is to prevent certain types of signature confusion attacks
//(where an attacker substitutes one legitimate signature for another).
Type CeremonyType `json:"type"`
Challenge string `json:"challenge"`
Origin string `json:"origin"`
TokenBinding *TokenBinding `json:"tokenBinding,omitempty"`
// Chromium (Chrome) returns a hint sometimes about how to handle clientDataJSON in a safe manner.
Hint string `json:"new_keys_may_be_added_here,omitempty"`
}
type CeremonyType string
const (
CreateCeremony CeremonyType = "webauthn.create"
AssertCeremony CeremonyType = "webauthn.get"
)
type TokenBinding struct {
Status TokenBindingStatus `json:"status"`
ID string `json:"id,omitempty"`
}
type TokenBindingStatus string
const (
// Indicates token binding was used when communicating with the
// Relying Party. In this case, the id member MUST be present.
Present TokenBindingStatus = "present"
// Indicates token binding was used when communicating with the
// negotiated when communicating with the Relying Party.
Supported TokenBindingStatus = "supported"
// Indicates token binding not supported
// when communicating with the Relying Party.
NotSupported TokenBindingStatus = "not-supported"
)
// FullyQualifiedOrigin returns the origin per the HTML spec: (scheme)://(host)[:(port)].
func FullyQualifiedOrigin(rawOrigin string) (fqOrigin string, err error) {
if strings.HasPrefix(rawOrigin, "android:apk-key-hash:") {
return rawOrigin, nil
}
var origin *url.URL
if origin, err = url.ParseRequestURI(rawOrigin); err != nil {
return "", err
}
if origin.Host == "" {
return "", fmt.Errorf("url '%s' does not have a host", rawOrigin)
}
origin.Path, origin.RawPath, origin.RawQuery, origin.User = "", "", "", nil
return origin.String(), nil
}
// Verify handles steps 3 through 6 of verifying the registering client data of a
// new credential and steps 7 through 10 of verifying an authentication assertion
// See https://www.w3.org/TR/webauthn/#registering-a-new-credential
// and https://www.w3.org/TR/webauthn/#verifying-assertion
func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyType, rpOrigins []string) error {
// Registration Step 3. Verify that the value of C.type is webauthn.create.
// Assertion Step 7. Verify that the value of C.type is the string webauthn.get.
if c.Type != ceremony {
return ErrVerification.WithDetails("Error validating ceremony type").WithInfo(fmt.Sprintf("Expected Value: %s, Received: %s", ceremony, c.Type))
}
// Registration Step 4. Verify that the value of C.challenge matches the challenge
// that was sent to the authenticator in the create() call.
// Assertion Step 8. Verify that the value of C.challenge matches the challenge
// that was sent to the authenticator in the PublicKeyCredentialRequestOptions
// passed to the get() call.
challenge := c.Challenge
if subtle.ConstantTimeCompare([]byte(storedChallenge), []byte(challenge)) != 1 {
return ErrVerification.
WithDetails("Error validating challenge").
WithInfo(fmt.Sprintf("Expected b Value: %#v\nReceived b: %#v\n", storedChallenge, challenge))
}
// Registration Step 5 & Assertion Step 9. Verify that the value of C.origin matches
// the Relying Party's origin.
fqOrigin, err := FullyQualifiedOrigin(c.Origin)
if err != nil {
return ErrParsingData.WithDetails("Error decoding clientData origin as URL")
}
found := false
for _, origin := range rpOrigins {
if strings.EqualFold(fqOrigin, origin) {
found = true
break
}
}
if !found {
return ErrVerification.
WithDetails("Error validating origin").
WithInfo(fmt.Sprintf("Expected Values: %s, Received: %s", rpOrigins, fqOrigin))
}
// Registration Step 6 and Assertion Step 10. 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.
if c.TokenBinding != nil {
if c.TokenBinding.Status == "" {
return ErrParsingData.WithDetails("Error decoding clientData, token binding present without status")
}
if c.TokenBinding.Status != Present && c.TokenBinding.Status != Supported && c.TokenBinding.Status != NotSupported {
return ErrParsingData.
WithDetails("Error decoding clientData, token binding present with invalid status").
WithInfo(fmt.Sprintf("Got: %s", c.TokenBinding.Status))
}
}
// Not yet fully implemented by the spec, browsers, and me.
return nil
}
|