-
Notifications
You must be signed in to change notification settings - Fork 62
/
assertion.go
189 lines (151 loc) · 7.22 KB
/
assertion.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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
package protocol
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
// The CredentialAssertionResponse is the raw response returned to the Relying Party from an authenticator when we request a
// credential for login/assertion.
type CredentialAssertionResponse struct {
PublicKeyCredential
AssertionResponse AuthenticatorAssertionResponse `json:"response"`
}
// The ParsedCredentialAssertionData is the parsed CredentialAssertionResponse that has been marshalled into a format
// that allows us to verify the client and authenticator data inside the response.
type ParsedCredentialAssertionData struct {
ParsedPublicKeyCredential
Response ParsedAssertionResponse
Raw CredentialAssertionResponse
}
// The AuthenticatorAssertionResponse contains the raw authenticator assertion data and is parsed into
// ParsedAssertionResponse.
type AuthenticatorAssertionResponse struct {
AuthenticatorResponse
AuthenticatorData URLEncodedBase64 `json:"authenticatorData"`
Signature URLEncodedBase64 `json:"signature"`
UserHandle URLEncodedBase64 `json:"userHandle,omitempty"`
}
// ParsedAssertionResponse is the parsed form of AuthenticatorAssertionResponse.
type ParsedAssertionResponse struct {
CollectedClientData CollectedClientData
AuthenticatorData AuthenticatorData
Signature []byte
UserHandle []byte
}
// ParseCredentialRequestResponse parses the credential request response into a format that is either required by the
// specification or makes the assertion verification steps easier to complete. This takes a http.Request that contains
// the assertion response data in a raw, mostly base64 encoded format, and parses the data into manageable structures.
func ParseCredentialRequestResponse(response *http.Request) (*ParsedCredentialAssertionData, error) {
if response == nil || response.Body == nil {
return nil, ErrBadRequest.WithDetails("No response given")
}
defer response.Body.Close()
defer io.Copy(io.Discard, response.Body)
return ParseCredentialRequestResponseBody(response.Body)
}
// ParseCredentialRequestResponseBody parses the credential request response into a format that is either required by
// the specification or makes the assertion verification steps easier to complete. This takes an io.Reader that contains
// the assertion response data in a raw, mostly base64 encoded format, and parses the data into manageable structures.
func ParseCredentialRequestResponseBody(body io.Reader) (par *ParsedCredentialAssertionData, err error) {
var car CredentialAssertionResponse
if err = decodeBody(body, &car); err != nil {
return nil, ErrBadRequest.WithDetails("Parse error for Assertion").WithInfo(err.Error())
}
return car.Parse()
}
// Parse validates and parses the CredentialAssertionResponse into a ParseCredentialCreationResponseBody. This receiver
// is unlikely to be expressly guaranteed under the versioning policy. Users looking for this guarantee should see
// ParseCredentialRequestResponseBody instead, and this receiver should only be used if that function is inadequate
// for their use case.
func (car CredentialAssertionResponse) Parse() (par *ParsedCredentialAssertionData, err error) {
if car.ID == "" {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID missing")
}
if _, err = base64.RawURLEncoding.DecodeString(car.ID); err != nil {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID not base64url encoded")
}
if car.Type != "public-key" {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with bad type")
}
var attachment AuthenticatorAttachment
switch car.AuthenticatorAttachment {
case "platform":
attachment = Platform
case "cross-platform":
attachment = CrossPlatform
}
par = &ParsedCredentialAssertionData{
ParsedPublicKeyCredential{
ParsedCredential{car.ID, car.Type}, car.RawID, car.ClientExtensionResults, attachment,
},
ParsedAssertionResponse{
Signature: car.AssertionResponse.Signature,
UserHandle: car.AssertionResponse.UserHandle,
},
car,
}
// Step 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
// We don't call it cData but this is Step 5 in the spec.
if err = json.Unmarshal(car.AssertionResponse.ClientDataJSON, &par.Response.CollectedClientData); err != nil {
return nil, err
}
if err = par.Response.AuthenticatorData.Unmarshal(car.AssertionResponse.AuthenticatorData); err != nil {
return nil, ErrParsingData.WithDetails("Error unmarshalling auth data")
}
return par, nil
}
// Verify the remaining elements of the assertion data by following the steps outlined in the referenced specification
// documentation.
//
// Specification: §7.2 Verifying an Authentication Assertion (https://www.w3.org/TR/webauthn/#sctn-verifying-assertion)
func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPartyID string, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode, appID string, verifyUser bool, credentialBytes []byte) error {
// Steps 4 through 6 in verifying the assertion data (https://www.w3.org/TR/webauthn/#verifying-assertion) are
// "assertive" steps, i.e "Let JSONtext be the result of running UTF-8 decode on the value of cData."
// We handle these steps in part as we verify but also beforehand
// Handle steps 7 through 10 of assertion by verifying stored data against the Collected Client Data
// returned by the authenticator
validError := p.Response.CollectedClientData.Verify(storedChallenge, AssertCeremony, rpOrigins, rpTopOrigins, rpTopOriginsVerify)
if validError != nil {
return validError
}
// Begin Step 11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the RP.
rpIDHash := sha256.Sum256([]byte(relyingPartyID))
var appIDHash [32]byte
if appID != "" {
appIDHash = sha256.Sum256([]byte(appID))
}
// Handle steps 11 through 14, verifying the authenticator data.
validError = p.Response.AuthenticatorData.Verify(rpIDHash[:], appIDHash[:], verifyUser)
if validError != nil {
return validError
}
// allowedUserCredentialIDs := session.AllowedCredentialIDs
// Step 15. Let hash be the result of computing a hash over the cData using SHA-256.
clientDataHash := sha256.Sum256(p.Raw.AssertionResponse.ClientDataJSON)
// Step 16. Using the credential public key looked up in step 3, verify that sig is
// a valid signature over the binary concatenation of authData and hash.
sigData := append(p.Raw.AssertionResponse.AuthenticatorData, clientDataHash[:]...)
var (
key any
err error
)
// If the Session Data does not contain the appID extension or it wasn't reported as used by the Client/RP then we
// use the standard CTAP2 public key parser.
if appID == "" {
key, err = webauthncose.ParsePublicKey(credentialBytes)
} else {
key, err = webauthncose.ParseFIDOPublicKey(credentialBytes)
}
if err != nil {
return ErrAssertionSignature.WithDetails(fmt.Sprintf("Error parsing the assertion public key: %+v", err))
}
valid, err := webauthncose.VerifySignature(key, sigData, p.Response.Signature)
if !valid || err != nil {
return ErrAssertionSignature.WithDetails(fmt.Sprintf("Error validating the assertion signature: %+v", err))
}
return nil
}