-
Notifications
You must be signed in to change notification settings - Fork 62
/
client.go
129 lines (108 loc) · 4.91 KB
/
client.go
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
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.
// https://www.w3.org/TR/webauthn/#sec-client-data
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):https://(host)[:(port)]
func FullyQualifiedOrigin(rawOrigin string) (fqOrigin string, err error) {
var origin *url.URL
if origin, err = url.Parse(rawOrigin); err != nil {
return "", err
}
return fmt.Sprintf("%s:https://%s", origin.Scheme, origin.Host), nil
}
// Handles steps 3 through 6 of verfying 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
}