gitea

Development moved to Codeberg

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
  84. 84
  85. 85
  86. 86
  87. 87
  88. 88
  89. 89
  90. 90
  91. 91
  92. 92
  93. 93
  94. 94
  95. 95
  96. 96
  97. 97
  98. 98
  99. 99
  100. 100
  101. 101
  102. 102
  103. 103
  104. 104
  105. 105
  106. 106
  107. 107
  108. 108
  109. 109
  110. 110
  111. 111
  112. 112
  113. 113
  114. 114
  115. 115
  116. 116
  117. 117
  118. 118
  119. 119
  120. 120
  121. 121
  122. 122
  123. 123
  124. 124
  125. 125
  126. 126
  127. 127
  128. 128
  129. 129
  130. 130
  131. 131
  132. 132
  133. 133
  134. 134
  135. 135
  136. 136
  137. 137
  138. 138
  139. 139
  140. 140
  141. 141
  142. 142
  143. 143
  144. 144
  145. 145
  146. 146
  147. 147
  148. 148
  149. 149
  150. 150
  151. 151
  152. 152
  153. 153
  154. 154
  155. 155
  156. 156
  157. 157
  158. 158
  159. 159
  160. 160
  161. 161
  162. 162
  163. 163
  164. 164
  165. 165
  166. 166
  167. 167
  168. 168
  169. 169
  170. 170
  171. 171
  172. 172
  173. 173
  174. 174
  175. 175
  176. 176
  177. 177
  178. 178
  179. 179
  180. 180
  181. 181
  182. 182
  183. 183
  184. 184
  185. 185
  186. 186
  187. 187
  188. 188
  189. 189
  190. 190
  191. 191
  192. 192
  193. 193
  194. 194
  195. 195
  196. 196
  197. 197
  198. 198
  199. 199
  200. 200
  201. 201
  202. 202
  203. 203
  204. 204
  205. 205
  206. 206
  207. 207
  208. 208
  209. 209
  210. 210
  211. 211
  212. 212
  213. 213
  214. 214
  215. 215
  216. 216
package protocol

import (
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"io"
	"net/http"
)

// The basic credential type that is inherited by WebAuthn's
// PublicKeyCredential type
// https://w3c.github.io/webappsec-credential-management/#credential
type Credential struct {
	// ID is The credential’s identifier. The requirements for the
	// identifier are distinct for each type of credential. It might
	// represent a username for username/password tuples, for example.
	ID string `json:"id"`
	// Type is the value of the object’s interface object's [[type]] slot,
	// which specifies the credential type represented by this object.
	// This should be type "public-key" for Webauthn credentials.
	Type string `json:"type"`
}

// The PublicKeyCredential interface inherits from Credential, and contains
//  the attributes that are returned to the caller when a new credential
// is created, or a new assertion is requested.
type ParsedCredential struct {
	ID   string `cbor:"id"`
	Type string `cbor:"type"`
}

type PublicKeyCredential struct {
	Credential
	RawID                  URLEncodedBase64                      `json:"rawId"`
	ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
}

type ParsedPublicKeyCredential struct {
	ParsedCredential
	RawID                  []byte                                `json:"rawId"`
	ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
}

type CredentialCreationResponse struct {
	PublicKeyCredential
	AttestationResponse AuthenticatorAttestationResponse `json:"response"`
}

type ParsedCredentialCreationData struct {
	ParsedPublicKeyCredential
	Response ParsedAttestationResponse
	Raw      CredentialCreationResponse
}

func ParseCredentialCreationResponse(response *http.Request) (*ParsedCredentialCreationData, error) {
	if response == nil || response.Body == nil {
		return nil, ErrBadRequest.WithDetails("No response given")
	}
	return ParseCredentialCreationResponseBody(response.Body)
}

func ParseCredentialCreationResponseBody(body io.Reader) (*ParsedCredentialCreationData, error) {
	var ccr CredentialCreationResponse
	err := json.NewDecoder(body).Decode(&ccr)
	if err != nil {
		return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo(err.Error())
	}

	if ccr.ID == "" {
		return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Missing ID")
	}

	testB64, err := base64.RawURLEncoding.DecodeString(ccr.ID)
	if err != nil || !(len(testB64) > 0) {
		return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("ID not base64.RawURLEncoded")
	}

	if ccr.PublicKeyCredential.Credential.Type == "" {
		return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Missing type")
	}

	if ccr.PublicKeyCredential.Credential.Type != "public-key" {
		return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Type not public-key")
	}

	var pcc ParsedCredentialCreationData
	pcc.ID, pcc.RawID, pcc.Type = ccr.ID, ccr.RawID, ccr.Type
	pcc.Raw = ccr

	parsedAttestationResponse, err := ccr.AttestationResponse.Parse()
	if err != nil {
		return nil, ErrParsingData.WithDetails("Error parsing attestation response")
	}

	pcc.Response = *parsedAttestationResponse

	return &pcc, nil
}

// Verifies the Client and Attestation data as laid out by §7.1. Registering a new credential
// https://www.w3.org/TR/webauthn/#registering-a-new-credential
func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUser bool, relyingPartyID, relyingPartyOrigin string) error {

	// Handles steps 3 through 6 - Verifying the Client Data against the Relying Party's stored data
	verifyError := pcc.Response.CollectedClientData.Verify(storedChallenge, CreateCeremony, relyingPartyOrigin)
	if verifyError != nil {
		return verifyError
	}

	// Step 7. Compute the hash of response.clientDataJSON using SHA-256.
	clientDataHash := sha256.Sum256(pcc.Raw.AttestationResponse.ClientDataJSON)

	// Step 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse
	// structure to obtain the attestation statement format fmt, the authenticator data authData, and the
	// attestation statement attStmt. is handled while

	// We do the above step while parsing and decoding the CredentialCreationResponse
	// Handle steps 9 through 14 - This verifies the attestaion object and
	verifyError = pcc.Response.AttestationObject.Verify(relyingPartyID, clientDataHash[:], verifyUser)
	if verifyError != nil {
		return verifyError
	}

	// Step 15. If validation is successful, obtain a list of acceptable trust anchors (attestation root
	// certificates or ECDAA-Issuer public keys) for that attestation type and attestation statement
	// format fmt, from a trusted source or from policy. For example, the FIDO Metadata Service provides
	// one way to obtain such information, using the aaguid in the attestedCredentialData in authData.
	// [https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-metadata-service-v2.0-id-20180227.html]

	// TODO: There are no valid AAGUIDs yet or trust sources supported. We could implement policy for the RP in
	// the future, however.

	// Step 16. Assess the attestation trustworthiness using outputs of the verification procedure in step 14, as follows:
	// - If self attestation was used, check if self attestation is acceptable under Relying Party policy.
	// - If ECDAA was used, verify that the identifier of the ECDAA-Issuer public key used is included in
	//   the set of acceptable trust anchors obtained in step 15.
	// - Otherwise, use the X.509 certificates returned by the verification procedure to verify that the
	//   attestation public key correctly chains up to an acceptable root certificate.

	// TODO: We're not supporting trust anchors, self-attestation policy, or acceptable root certs yet

	// Step 17. Check that the credentialId is not yet registered to any other user. If registration is
	// requested for a credential that is already registered to a different user, the Relying Party SHOULD
	// fail this registration ceremony, or it MAY decide to accept the registration, e.g. while deleting
	// the older registration.

	// TODO: We can't support this in the code's current form, the Relying Party would need to check for this
	// against their database

	// Step 18 If the attestation statement attStmt verified successfully and is found to be trustworthy, then
	// register the new credential with the account that was denoted in the options.user passed to create(), by
	// associating it with the credentialId and credentialPublicKey in the attestedCredentialData in authData, as
	// appropriate for the Relying Party's system.

	// Step 19. If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above,
	// the Relying Party SHOULD fail the registration ceremony.

	// TODO: Not implemented for the reasons mentioned under Step 16

	return nil
}

// GetAppID takes a AuthenticationExtensions object or nil. It then performs the following checks in order:
//
// 1. Check that the Session Data's AuthenticationExtensions has been provided and return a blank appid if it hasn't been.
// 2. Check that the AuthenticationExtensionsClientOutputs contains the extensions output and return a blank appid if it doesn't.
// 3. Check that the Credential AttestationType is `fido-u2f` and return a blank appid if it isn't.
// 4. Check that the AuthenticationExtensionsClientOutputs contains the appid key and return a blank appid if it doesn't.
// 5. Check that the AuthenticationExtensionsClientOutputs appid is a bool and return an error if it isn't.
// 6. Check that the appid output is true and return a blank appid if it isn't.
// 7. Check that the Session Data has an appid extension defined and return an error if it doesn't.
// 8. Check that the appid extension in Session Data is a string and return an error if it isn't.
// 9. Return the appid extension value from the Session Data.
func (ppkc ParsedPublicKeyCredential) GetAppID(authExt AuthenticationExtensions, credentialAttestationType string) (appID string, err error) {
	var (
		value, clientValue interface{}
		enableAppID, ok    bool
	)

	if authExt == nil {
		return "", nil
	}

	if ppkc.ClientExtensionResults == nil {
		return "", nil
	}

	// If the credential does not have the correct attestation type it is assumed to NOT be a fido-u2f credential.
	// https://w3c.github.io/webauthn/#sctn-fido-u2f-attestation
	if credentialAttestationType != "fido-u2f" {
		return "", nil
	}

	if clientValue, ok = ppkc.ClientExtensionResults["appid"]; !ok {
		return "", nil
	}

	if enableAppID, ok = clientValue.(bool); !ok {
		return "", ErrBadRequest.WithDetails("Client Output appid did not have the expected type")
	}

	if !enableAppID {
		return "", nil
	}

	if value, ok = authExt["appid"]; !ok {
		return "", ErrBadRequest.WithDetails("Session Data does not have an appid but Client Output indicates it should be set")
	}

	if appID, ok = value.(string); !ok {
		return "", ErrBadRequest.WithDetails("Session Data appid did not have the expected type")
	}

	return appID, nil
}