/
assertion.go
142 lines (118 loc) · 5.58 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
package protocol
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/lkalneus/webauthn/protocol/webauthncose"
)
// The raw response returned to us from an authenticator when we request a
// credential for login/assertion.
type CredentialAssertionResponse struct {
PublicKeyCredential
AssertionResponse AuthenticatorAssertionResponse `json:"response"`
}
// 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"`
}
// Parsed form of AuthenticatorAssertionResponse
type ParsedAssertionResponse struct {
CollectedClientData CollectedClientData
AuthenticatorData AuthenticatorData
Signature []byte
UserHandle []byte
}
// Parse 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 http.Request that contains
// the attestation 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")
}
return ParseCredentialRequestResponseBody(response.Body)
}
// Parse 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 attestation response data in a raw, mostly base64 encoded format, and parses the data into
// manageable structures
func ParseCredentialRequestResponseBody(body io.Reader) (*ParsedCredentialAssertionData, error) {
var car CredentialAssertionResponse
err := json.NewDecoder(body).Decode(&car)
if err != nil {
return nil, ErrBadRequest.WithDetails("Parse error for Assertion")
}
if car.ID == "" {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID missing")
}
_, err = base64.RawURLEncoding.DecodeString(car.ID)
if 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 par ParsedCredentialAssertionData
par.ID, par.RawID, par.Type = car.ID, car.RawID, car.Type
par.Raw = car
par.Response.Signature = car.AssertionResponse.Signature
par.Response.UserHandle = car.AssertionResponse.UserHandle
// 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.
err = json.Unmarshal(car.AssertionResponse.ClientDataJSON, &par.Response.CollectedClientData)
if err != nil {
return nil, err
}
err = par.Response.AuthenticatorData.Unmarshal(car.AssertionResponse.AuthenticatorData)
if err != nil {
return nil, ErrParsingData.WithDetails("Error unmarshalling auth data")
}
return &par, nil
}
// Follow the remaining steps outlined in §7.2 Verifying an authentication assertion
// (https://www.w3.org/TR/webauthn/#verifying-assertion) and return an error if there
// is a failure during each step.
func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPartyID, relyingPartyOrigin 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, relyingPartyOrigin)
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))
// Handle steps 11 through 14, verifying the authenticator data.
validError = p.Response.AuthenticatorData.Verify(rpIDHash[:], verifyUser)
if validError != nil {
return ErrAuthData.WithInfo(validError.Error())
}
// 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[:]...)
key, err := webauthncose.ParsePublicKey(credentialBytes)
valid, err := webauthncose.VerifySignature(key, sigData, p.Response.Signature)
if !valid {
return ErrAssertionSignature.WithDetails(fmt.Sprintf("Error validating the assertion signature: %+v\n", err))
}
return nil
}