-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwebauthn.go
More file actions
154 lines (134 loc) · 5.27 KB
/
webauthn.go
File metadata and controls
154 lines (134 loc) · 5.27 KB
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
package gocalibur
import (
"errors"
"fmt"
"math/big"
"strings"
"github.com/lmittmann/w3"
)
// WebAuthn ABI encoder/decoder using w3.
// We use a dummy function to encode/decode the WebAuthnAuth struct.
var webAuthnAuthFunc = w3.MustNewFunc(
"encodeWebAuthnAuth((bytes authenticatorData,string clientDataJSON,uint256 challengeIndex,uint256 typeIndex,uint256 r,uint256 s))",
"",
)
// WebAuthn constants for clientDataJSON field positions.
const (
DefaultTypeIndex = 1 // Index of "type" in clientDataJSON
DefaultChallengeIndex = 23 // Index of "challenge" in clientDataJSON
)
// WebAuthnAuth represents a WebAuthn authentication response for Calibur.
// This struct matches the WebAuthn.WebAuthnAuth struct in Calibur's contracts.
type WebAuthnAuth struct {
AuthenticatorData []byte // From credential.response.authenticatorData
ClientDataJSON string // From credential.response.clientDataJSON
TypeIndex uint64 // Index of "type" field in clientDataJSON
ChallengeIndex uint64 // Index of "challenge" field in clientDataJSON
R *big.Int // ECDSA signature r value
S *big.Int // ECDSA signature s value
}
// ParseWebAuthnAuthFromBrowser parses a WebAuthn assertion response from a browser.
// The r and s values should be extracted from the DER-encoded signature and normalized.
//
// Example usage:
//
// r, s := parseDERSignature(browserSignature)
// s = gocalibur.NormalizeS(s, elliptic.P256().Params().N)
// auth, err := gocalibur.ParseWebAuthnAuthFromBrowser(authData, clientDataJSON, r, s)
func ParseWebAuthnAuthFromBrowser(
authenticatorData []byte,
clientDataJSON string,
signatureR, signatureS *big.Int,
) (*WebAuthnAuth, error) {
if len(authenticatorData) < 37 {
return nil, errors.New("authenticatorData too short (minimum 37 bytes)")
}
if clientDataJSON == "" {
return nil, errors.New("clientDataJSON is empty")
}
if signatureR == nil || signatureS == nil {
return nil, errors.New("signature r and s values are required")
}
typeIndex, challengeIndex := findClientDataIndices(clientDataJSON)
return &WebAuthnAuth{
AuthenticatorData: authenticatorData,
ClientDataJSON: clientDataJSON,
TypeIndex: typeIndex,
ChallengeIndex: challengeIndex,
R: signatureR,
S: signatureS,
}, nil
}
// webAuthnAuthStruct is the struct type used for ABI encoding/decoding WebAuthnAuth.
// Field order must match the Solidity struct.
type webAuthnAuthStruct struct {
AuthenticatorData []byte `abi:"authenticatorData"`
ClientDataJSON string `abi:"clientDataJSON"`
ChallengeIndex *big.Int `abi:"challengeIndex"`
TypeIndex *big.Int `abi:"typeIndex"`
R *big.Int `abi:"r"`
S *big.Int `abi:"s"`
}
// EncodeWebAuthnAuth ABI-encodes a WebAuthnAuth struct for use with Calibur.
// This produces the signature bytes expected by KeyLib.verify() for WebAuthnP256 keys.
func EncodeWebAuthnAuth(auth *WebAuthnAuth) ([]byte, error) {
if auth == nil {
return nil, errors.New("auth is nil")
}
// Create the struct value matching Solidity's WebAuthn.WebAuthnAuth
authStruct := webAuthnAuthStruct{
AuthenticatorData: auth.AuthenticatorData,
ClientDataJSON: auth.ClientDataJSON,
ChallengeIndex: big.NewInt(int64(auth.ChallengeIndex)),
TypeIndex: big.NewInt(int64(auth.TypeIndex)),
R: auth.R,
S: auth.S,
}
// Use w3.Func to encode the struct as a function argument, then strip the 4-byte selector
encoded, err := webAuthnAuthFunc.EncodeArgs(authStruct)
if err != nil {
return nil, fmt.Errorf("encoding WebAuthnAuth: %w", err)
}
// Strip the 4-byte function selector to get just the ABI-encoded struct
return encoded[4:], nil
}
// DecodeWebAuthnAuth decodes ABI-encoded WebAuthnAuth bytes.
func DecodeWebAuthnAuth(data []byte) (*WebAuthnAuth, error) {
if len(data) < 32 {
return nil, errors.New("data too short for WebAuthnAuth")
}
// Use w3.Func to decode - we need to prepend a dummy 4-byte selector
// because DecodeArgs expects calldata format
calldata := make([]byte, 4+len(data))
copy(calldata[:4], webAuthnAuthFunc.Selector[:])
copy(calldata[4:], data)
var authStruct webAuthnAuthStruct
if err := webAuthnAuthFunc.DecodeArgs(calldata, &authStruct); err != nil {
return nil, fmt.Errorf("decoding WebAuthnAuth: %w", err)
}
return &WebAuthnAuth{
AuthenticatorData: authStruct.AuthenticatorData,
ClientDataJSON: authStruct.ClientDataJSON,
TypeIndex: authStruct.TypeIndex.Uint64(),
ChallengeIndex: authStruct.ChallengeIndex.Uint64(),
R: authStruct.R,
S: authStruct.S,
}, nil
}
// findClientDataIndices finds the positions of "type" and "challenge" in clientDataJSON.
// The contract expects indices pointing to the opening quote of each field.
func findClientDataIndices(clientData string) (typeIndex, challengeIndex uint64) {
typeIdx := strings.Index(clientData, `"type"`)
if typeIdx >= 0 {
typeIndex = uint64(typeIdx) // Position of opening quote in "type"
} else {
typeIndex = DefaultTypeIndex
}
challengeIdx := strings.Index(clientData, `"challenge"`)
if challengeIdx >= 0 {
challengeIndex = uint64(challengeIdx) // Position of opening quote in "challenge"
} else {
challengeIndex = DefaultChallengeIndex
}
return typeIndex, challengeIndex
}