Repository: koesie10/webauthn
Branch: master
Commit: 8626f9623b4b
Files: 34
Total size: 131.6 KB
Directory structure:
gitextract_a82pg2ux/
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── attestation/
│ ├── androidsafetynet/
│ │ ├── androidsafetynet.go
│ │ └── androidsafetynet_test.go
│ ├── attestion.go
│ ├── fido/
│ │ ├── fido.go
│ │ └── fido_test.go
│ └── packed/
│ ├── packed.go
│ └── packed_test.go
├── cose/
│ ├── cose.go
│ ├── cose_test.go
│ ├── doc.go
│ └── ecdsa.go
├── go.mod
├── go.sum
├── protocol/
│ ├── api.go
│ ├── assertion.go
│ ├── attestation.go
│ ├── attestation_registry.go
│ ├── challenge.go
│ ├── common.go
│ ├── doc.go
│ ├── errors.go
│ └── webauthn_test.go
├── webauthn/
│ ├── config.go
│ ├── doc.go
│ ├── login.go
│ ├── registration.go
│ ├── session.go
│ ├── user.go
│ └── webauthn.go
└── webauthn.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Created by https://www.gitignore.io/api/go,intellij+all,visualstudiocode
### Go ###
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
### Go Patch ###
/vendor/
/Godeps/
### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
### Intellij+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# End of https://www.gitignore.io/api/go,intellij+all,visualstudiocode
================================================
FILE: .travis.yml
================================================
language: go
go:
- "1.11.x"
install: true
script:
- env GO111MODULE=on go build ./...
- env GO111MODULE=on go test ./...
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Koen Vlaswinkel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# webauthn : Web Authentication API in Go
## Overview [](https://godoc.org/github.com/koesie10/webauthn) [](https://travis-ci.org/koesie10/webauthn)
This project provides a low-level and a high-level API to use the [Web Authentication API](https://www.w3.org/TR/webauthn/) (WebAuthn).
[Demo](https://github.com/koesie10/webauthn-demo)
## Install
```
go get github.com/koesie10/webauthn
```
## Attestation
By default, this library does not support any attestation statement formats. To use the default attestation formats,
you will need to import `github.com/koesie10/webauthn/attestation` or any of its subpackages if you would just like
to support some attestation statement formats.
Please note that the Android SafetyNet attestation statement format depends on
[`gopkg.in/square/go-jose.v2`](https://github.com/square/go-jose), which means that this package will be imported
when you import either `github.com/koesie10/webauthn/attestation` or
`github.com/koesie10/webauthn/attestation/androidsafetynet`.
## High-level API
The high-level API can be used with the `net/http` package and simplifies the low-level API. It is located in the `webauthn` subpackage. It is intended
for use with e.g. `fetch` or `XMLHttpRequest` JavaScript clients.
First, make sure your user entity implements [`User`](https://godoc.org/github.com/koesie10/webauthn/webauthn#User). Then, create a new entity
implements [`Authenticator`](https://godoc.org/github.com/koesie10/webauthn/webauthn#Authenticator) that stores each authenticator the user
registers.
Then, either make your existing repository implement [`AuthenticatorStore`](https://godoc.org/github.com/koesie10/webauthn/webauthn#AuthenticatorStore)
or create a new repository.
Finally, you can create the main [`WebAuthn`](https://godoc.org/github.com/koesie10/webauthn/webauthn#WebAuthn) struct supplying the
[`Config`](https://godoc.org/github.com/koesie10/webauthn/webauthn#Config) options:
```golang
w, err := webauthn.New(&webauthn.Config{
// A human-readable identifier for the relying party (i.e. your app), intended only for display.
RelyingPartyName: "webauthn-demo",
// Storage for the authenticator.
AuthenticatorStore: storage,
})
```
Then, you can use the methods defined, such as [`StartRegistration`](https://godoc.org/github.com/koesie10/webauthn/webauthn#WebAuthn.StartRegistration)
to handle registration and login. Every handler requires a [`Session`](https://godoc.org/github.com/koesie10/webauthn/webauthn#Session), which stores
intermediate registration/login data. If you use [`gorilla/sessions`](https://github.com/gorilla/sessions), use
[`webauthn.WrapMap`](https://godoc.org/github.com/koesie10/webauthn/webauthn#WrapMap)`(session.Values)`. Read the documentation for complete information
on what parameters need to be passed and what values are returned.
For example, a handler for finishing the registration might look like this:
```golang
func (r *http.Request, rw http.ResponseWriter) {
ctx := r.Context()
// Get the user in some way, in this case from the context
user, ok := UserFromContext(ctx)
if !ok {
rw.WriteHeader(http.StatusForbidden)
return
}
// Get or create a session in some way, in this case from the context
sess := SessionFromContext(ctx)
// Then call FinishRegistration to register the authenticator to the user
h.webauthn.FinishRegistration(r, rw, user, webauthn.WrapMap(sess))
}
```
A complete demo application using the high-level API which implements all of these interfaces and stores data in memory is available
[here](https://github.com/koesie10/webauthn-demo).
## JavaScript examples
[This class](webauthn.js) is an example that can be used to handle the registration and login phases. It can be used as follows:
```javascript
const w = new WebAuthn();
// Registration
w.register().then(() => {
alert('This authenticator has been registered.');
}).catch(err => {
console.error(err);
alert('Failed to register: ' + err);
});
// Login
w.login().then(() => {
alert('You have been logged in.');
}).catch(err => {
console.error(err);
alert('Failed to login: ' + err);
});
```
Or, with latest `async/await` paradigm:
```javascript
const w = new WebAuthn();
// Registration
try {
await w.register();
alert('This authenticator has been registered.');
} catch (err) {
console.error(err)
alert('Failed to register: ' + err);
}
// Login
try {
await w.login();
alert('You have been logged in.');
} catch(err) {
console.error(err);
alert('Failed to login: ' + err);
}
```
## Low-level API
The low-level closely resembles the specification and the high-level API should be preferred. However, if you would like to use the low-level
API, the main entry points are:
* [`ParseAttestationResponse`](https://godoc.org/github.com/koesie10/webauthn/protocol#ParseAttestationResponse)
* [`IsValidAttestation`](https://godoc.org/github.com/koesie10/webauthn/protocol#IsValidAttestation)
* [`ParseAssertionResponse`](https://godoc.org/github.com/koesie10/webauthn/protocol#ParseAssertionResponse)
* [`IsValidAssertion`](https://godoc.org/github.com/koesie10/webauthn/protocol#IsValidAssertion)
## License
MIT.
================================================
FILE: attestation/androidsafetynet/androidsafetynet.go
================================================
// androidsafetynet implements the Android SafetyNet (WebAuthn spec section 8.5) attestation statement format
package androidsafetynet
import (
"bytes"
"crypto/sha256"
"crypto/x509"
"encoding/json"
"time"
"gopkg.in/square/go-jose.v2"
"github.com/koesie10/webauthn/protocol"
)
// Now is used to overwrite the time at which the certificate is verified and is just used for tests.
var now = time.Now
func init() {
protocol.RegisterFormat("android-safetynet", verifyAndroidSafetynet)
}
type AndroidSafetyNetAttestionResponse struct {
Nonce []byte `json:"nonce"`
TimestampMs int64 `json:"timestampMs"`
ApkPackageName string `json:"apkPackageName"`
ApkDigestSha256 []byte `json:"apkDigestSha256"`
CtsProfileMatch bool `json:"ctsProfileMatch"`
ApkCertificateDigestSha256 [][]byte `json:"apkCertificateDigestSha256"`
BasicIntegrity bool `json:"basicIntegrity"`
}
func verifyAndroidSafetynet(a protocol.Attestation, clientDataHash []byte) error {
// Verify that response is a valid SafetyNet response of version ver.
rawVer, ok := a.AttStmt["ver"]
if !ok {
return protocol.ErrInvalidAttestation.WithDebug("missing ver for android-safetynet")
}
ver, ok := rawVer.(string)
if !ok {
return protocol.ErrInvalidAttestation.WithDebugf("invalid ver for android-safetynet, is of invalid type %T", rawVer)
}
if ver == "" {
return protocol.ErrInvalidAttestation.WithDebug("invalid ver for android-safetynet")
}
rawResponse, ok := a.AttStmt["response"]
if !ok {
return protocol.ErrInvalidAttestation.WithDebug("missing response for android-safetynet")
}
responseBytes, ok := rawResponse.([]byte)
if !ok {
return protocol.ErrInvalidAttestation.WithDebugf("invalid response for android-safetynet, is of invalid type %T", responseBytes)
}
response, err := jose.ParseSigned(string(responseBytes))
if err != nil {
return protocol.ErrInvalidAttestation.WithDebugf("invalid response for android-safetynet: %v", err)
}
if len(response.Signatures) != 1 {
return protocol.ErrInvalidAttestation.WithDebugf("invalid response for android-safetynet: more or less than 1 signature")
}
// Verify that the attestation certificate is issued to the hostname "attest.android.com"
cert, err := response.Signatures[0].Protected.Certificates(x509.VerifyOptions{
DNSName: "attest.android.com",
CurrentTime: now(),
})
if err != nil {
return protocol.ErrInvalidAttestation.WithDebugf("invalid response for android-safetynet: %v", err).WithCause(err)
}
leaf := cert[0][0]
payload, err := response.Verify(leaf.PublicKey)
if err != nil {
return protocol.ErrInvalidAttestation.WithDebugf("invalid response for android-safetynet: %v", err).WithCause(err)
}
attestationResponse := AndroidSafetyNetAttestionResponse{}
if err := json.Unmarshal(payload, &attestationResponse); err != nil {
return protocol.ErrInvalidAttestation.WithDebugf("invalid response for android-safetynet: %v", err)
}
// Verify that the nonce in the response is identical to the SHA-256 hash of the concatenation of authenticatorData and clientDataHash.
nonceBytes := append(a.AuthData.Raw, clientDataHash...)
expectedNonce := sha256.Sum256(nonceBytes)
if !bytes.Equal(expectedNonce[:], attestationResponse.Nonce) {
return protocol.ErrInvalidAttestation.WithDebugf("invalid response for android-safetynet: invalid nonce")
}
// Verify that the ctsProfileMatch attribute in the payload of response is true.
if !attestationResponse.CtsProfileMatch {
return protocol.ErrInvalidAttestation.WithDebugf("invalid response for android-safetynet: does not match CTS profile")
}
// If successful, return attestation type Basic with the attestation trust path set to the above attestation certificate.
return nil
}
================================================
FILE: attestation/androidsafetynet/androidsafetynet_test.go
================================================
package androidsafetynet
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/koesie10/webauthn/protocol"
)
func TestIsValidAttestation(t *testing.T) {
now = func() time.Time {
return time.Date(2018, 10, 24, 18, 39, 21, 0, time.UTC)
}
for i := range attestationRequests {
t.Run(fmt.Sprintf("Run %d", i), func(t *testing.T) {
r := protocol.CredentialCreationOptions{}
if err := json.Unmarshal([]byte(attestationRequests[i]), &r); err != nil {
t.Fatal(err)
}
b := protocol.AttestationResponse{}
if err := json.Unmarshal([]byte(attestationResponses[i]), &b); err != nil {
t.Fatal(err)
}
p, err := protocol.ParseAttestationResponse(b)
if err != nil {
t.Fatal(err)
}
d, err := protocol.IsValidAttestation(p, r.PublicKey.Challenge, "", "")
if err != nil {
e := protocol.ToWebAuthnError(err)
t.Fatal(fmt.Sprintf("%s, %s: %s", e.Name, e.Description, e.Debug))
}
if !d {
t.Fatal("is not valid")
}
})
}
}
var attestationRequests = []string{
`{"publicKey":{"rp":{"name":"webauthn-demo"},"user":{"name":"Bewus","id":"QmV3dXM=","displayName":"koen"},"challenge":"d3cY1I6n1ar6gLpDEhTi5nBgP1xwIGsb6HM/NR8PK1o=","pubKeyCredParams":[{"type":"public-key","alg":-7}],"timeout":30000,"authenticatorSelection":{"requireResidentKey":false},"attestation":"direct"}}`,
}
var attestationResponses = []string{
`{"id":"ARKBRFD84uLN6qG_rHsV0K2Bh9Lj3_HaJsXdC_DpPslKO6ZWmD38-hz90Lf_MzELErMa9AqR21Sr9brNzE2un1U","rawId":"ARKBRFD84uLN6qG/rHsV0K2Bh9Lj3/HaJsXdC/DpPslKO6ZWmD38+hz90Lf/MzELErMa9AqR21Sr9brNzE2un1U=","response":{"attestationObject":"o2NmbXRxYW5kcm9pZC1zYWZldHluZXRnYXR0U3RtdKJjdmVyaDE0MzY2MDE5aHJlc3BvbnNlWRS9ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbmcxWXlJNld5Sk5TVWxHYTJwRFEwSkljV2RCZDBsQ1FXZEpVVkpZY205T01GcFBaRkpyUWtGQlFVRkJRVkIxYm5wQlRrSm5hM0ZvYTJsSE9YY3dRa0ZSYzBaQlJFSkRUVkZ6ZDBOUldVUldVVkZIUlhkS1ZsVjZSV1ZOUW5kSFFURlZSVU5vVFZaU01qbDJXako0YkVsR1VubGtXRTR3U1VaT2JHTnVXbkJaTWxaNlRWSk5kMFZSV1VSV1VWRkVSWGR3U0ZaR1RXZFJNRVZuVFZVNGVFMUNORmhFVkVVMFRWUkJlRTFFUVROTlZHc3dUbFp2V0VSVVJUVk5WRUYzVDFSQk0wMVVhekJPVm05M1lrUkZURTFCYTBkQk1WVkZRbWhOUTFaV1RYaEZla0ZTUW1kT1ZrSkJaMVJEYTA1b1lrZHNiV0l6U25WaFYwVjRSbXBCVlVKblRsWkNRV05VUkZVeGRtUlhOVEJaVjJ4MVNVWmFjRnBZWTNoRmVrRlNRbWRPVmtKQmIxUkRhMlIyWWpKa2MxcFRRazFVUlUxNFIzcEJXa0puVGxaQ1FVMVVSVzFHTUdSSFZucGtRelZvWW0xU2VXSXliR3RNYlU1MllsUkRRMEZUU1hkRVVWbEtTMjlhU1doMlkwNUJVVVZDUWxGQlJHZG5SVkJCUkVORFFWRnZRMmRuUlVKQlRtcFlhM293WlVzeFUwVTBiU3N2UnpWM1QyOHJXRWRUUlVOeWNXUnVPRGh6UTNCU04yWnpNVFJtU3pCU2FETmFRMWxhVEVaSWNVSnJOa0Z0V2xaM01rczVSa2N3VHpseVVsQmxVVVJKVmxKNVJUTXdVWFZ1VXpsMVowaEROR1ZuT1c5MmRrOXRLMUZrV2pKd09UTllhSHAxYmxGRmFGVlhXRU40UVVSSlJVZEtTek5UTW1GQlpucGxPVGxRVEZNeU9XaE1ZMUYxV1ZoSVJHRkROMDlhY1U1dWIzTnBUMGRwWm5NNGRqRnFhVFpJTDNob2JIUkRXbVV5YkVvck4wZDFkSHBsZUV0d2VIWndSUzkwV2xObVlsazVNRFZ4VTJ4Q2FEbG1jR293TVRWamFtNVJSbXRWYzBGVmQyMUxWa0ZWZFdWVmVqUjBTMk5HU3pSd1pYWk9UR0Y0UlVGc0swOXJhV3hOZEVsWlJHRmpSRFZ1Wld3MGVFcHBlWE0wTVROb1lXZHhWekJYYUdnMVJsQXpPV2hIYXpsRkwwSjNVVlJxWVhwVGVFZGtkbGd3YlRaNFJsbG9hQzh5VmsxNVdtcFVORXQ2VUVwRlEwRjNSVUZCWVU5RFFXeG5kMmRuU2xWTlFUUkhRVEZWWkVSM1JVSXZkMUZGUVhkSlJtOUVRVlJDWjA1V1NGTlZSVVJFUVV0Q1oyZHlRbWRGUmtKUlkwUkJWRUZOUW1kT1ZraFNUVUpCWmpoRlFXcEJRVTFDTUVkQk1WVmtSR2RSVjBKQ1VYRkNVWGRIVjI5S1FtRXhiMVJMY1hWd2J6UlhObmhVTm1veVJFRm1RbWRPVmtoVFRVVkhSRUZYWjBKVFdUQm1hSFZGVDNaUWJTdDRaMjU0YVZGSE5rUnlabEZ1T1V0NlFtdENaMmR5UW1kRlJrSlJZMEpCVVZKWlRVWlpkMHAzV1VsTGQxbENRbEZWU0UxQlIwZEhNbWd3WkVoQk5reDVPWFpaTTA1M1RHNUNjbUZUTlc1aU1qbHVUREprTUdONlJuWk5WRUZ5UW1kbmNrSm5SVVpDVVdOM1FXOVpabUZJVWpCalJHOTJURE5DY21GVE5XNWlNamx1VERKa2VtTnFTWFpTTVZKVVRWVTRlRXh0VG5sa1JFRmtRbWRPVmtoU1JVVkdha0ZWWjJoS2FHUklVbXhqTTFGMVdWYzFhMk50T1hCYVF6VnFZakl3ZDBsUldVUldVakJuUWtKdmQwZEVRVWxDWjFwdVoxRjNRa0ZuU1hkRVFWbExTM2RaUWtKQlNGZGxVVWxHUVhwQmRrSm5UbFpJVWpoRlMwUkJiVTFEVTJkSmNVRm5hR2cxYjJSSVVuZFBhVGgyV1ROS2MweHVRbkpoVXpWdVlqSTVia3d3WkZWVmVrWlFUVk0xYW1OdGQzZG5aMFZGUW1kdmNrSm5SVVZCWkZvMVFXZFJRMEpKU0RGQ1NVaDVRVkJCUVdSM1EydDFVVzFSZEVKb1dVWkpaVGRGTmt4TldqTkJTMUJFVjFsQ1VHdGlNemRxYW1RNE1FOTVRVE5qUlVGQlFVRlhXbVJFTTFCTVFVRkJSVUYzUWtsTlJWbERTVkZEVTFwRFYyVk1Tblp6YVZaWE5rTm5LMmRxTHpsM1dWUktVbnAxTkVocGNXVTBaVmswWXk5dGVYcHFaMGxvUVV4VFlta3ZWR2g2WTNweGRHbHFNMlJyTTNaaVRHTkpWek5NYkRKQ01HODNOVWRSWkdoTmFXZGlRbWRCU0ZWQlZtaFJSMjFwTDFoM2RYcFVPV1ZIT1ZKTVNTdDRNRm95ZFdKNVdrVldla0UzTlZOWlZtUmhTakJPTUVGQlFVWnRXRkU1ZWpWQlFVRkNRVTFCVW1wQ1JVRnBRbU5EZDBFNWFqZE9WRWRZVURJM09IbzBhSEl2ZFVOSWFVRkdUSGx2UTNFeVN6QXJlVXhTZDBwVlltZEpaMlk0WjBocWRuQjNNbTFDTVVWVGFuRXlUMll6UVRCQlJVRjNRMnR1UTJGRlMwWlZlVm8zWmk5UmRFbDNSRkZaU2t0dldrbG9kbU5PUVZGRlRFSlJRVVJuWjBWQ1FVazVibFJtVWt0SlYyZDBiRmRzTTNkQ1REVTFSVlJXTm10aGVuTndhRmN4ZVVGak5VUjFiVFpZVHpReGExcDZkMG8yTVhkS2JXUlNVbFF2VlhORFNYa3hTMFYwTW1Nd1JXcG5iRzVLUTBZeVpXRjNZMFZYYkV4UldUSllVRXg1Um1wclYxRk9ZbE5vUWpGcE5GY3lUbEpIZWxCb2RETnRNV0kwT1doaWMzUjFXRTAyZEZnMVEzbEZTRzVVYURoQ2IyMDBMMWRzUm1sb2VtaG5iamd4Ukd4a2IyZDZMMHN5VlhkTk5sTTJRMEl2VTBWNGEybFdabllyZW1KS01ISnFkbWM1TkVGc1pHcFZabFYzYTBrNVZrNU5ha1ZRTldVNGVXUkNNMjlNYkRabmJIQkRaVVkxWkdkbVUxZzBWVGw0TXpWdmFpOUpTV1F6VlVVdlpGQndZaTl4WjBkMmMydG1aR1Y2ZEcxVmRHVXZTMU50Y21sM1kyZFZWMWRsV0daVVlra3plbk5wYTNkYVltdHdiVkpaUzIxcVVHMW9kalJ5YkdsNlIwTkhkRGhRYmpod2NUaE5Na3RFWmk5UU0ydFdiM1F6WlRFNFVUMGlMQ0pOU1VsRlUycERRMEY2UzJkQmQwbENRV2RKVGtGbFR6QnRjVWRPYVhGdFFrcFhiRkYxUkVGT1FtZHJjV2hyYVVjNWR6QkNRVkZ6UmtGRVFrMU5VMEYzU0dkWlJGWlJVVXhGZUdSSVlrYzVhVmxYZUZSaFYyUjFTVVpLZG1JelVXZFJNRVZuVEZOQ1UwMXFSVlJOUWtWSFFURlZSVU5vVFV0U01uaDJXVzFHYzFVeWJHNWlha1ZVVFVKRlIwRXhWVVZCZUUxTFVqSjRkbGx0Um5OVk1teHVZbXBCWlVaM01IaE9la0V5VFZSVmQwMUVRWGRPUkVwaFJuY3dlVTFVUlhsTlZGVjNUVVJCZDA1RVNtRk5SVWw0UTNwQlNrSm5UbFpDUVZsVVFXeFdWRTFTTkhkSVFWbEVWbEZSUzBWNFZraGlNamx1WWtkVloxWklTakZqTTFGblZUSldlV1J0YkdwYVdFMTRSWHBCVWtKblRsWkNRVTFVUTJ0a1ZWVjVRa1JSVTBGNFZIcEZkMmRuUldsTlFUQkhRMU54UjFOSllqTkVVVVZDUVZGVlFVRTBTVUpFZDBGM1oyZEZTMEZ2U1VKQlVVUlJSMDA1UmpGSmRrNHdOWHByVVU4NUszUk9NWEJKVW5aS2VucDVUMVJJVnpWRWVrVmFhRVF5WlZCRGJuWlZRVEJSYXpJNFJtZEpRMlpMY1VNNVJXdHpRelJVTW1aWFFsbHJMMnBEWmtNelVqTldXazFrVXk5a1RqUmFTME5GVUZwU2NrRjZSSE5wUzFWRWVsSnliVUpDU2pWM2RXUm5lbTVrU1UxWlkweGxMMUpIUjBac05YbFBSRWxMWjJwRmRpOVRTa2d2VlV3clpFVmhiSFJPTVRGQ2JYTkxLMlZSYlUxR0t5dEJZM2hIVG1oeU5UbHhUUzg1YVd3M01Va3laRTQ0UmtkbVkyUmtkM1ZoWldvMFlsaG9jREJNWTFGQ1ltcDRUV05KTjBwUU1HRk5NMVEwU1N0RWMyRjRiVXRHYzJKcWVtRlVUa001ZFhwd1JteG5UMGxuTjNKU01qVjRiM2x1VlhoMk9IWk9iV3R4TjNwa1VFZElXR3Q0VjFrM2IwYzVhaXRLYTFKNVFrRkNhemRZY2twbWIzVmpRbHBGY1VaS1NsTlFhemRZUVRCTVMxY3dXVE42Tlc5Nk1rUXdZekYwU2t0M1NFRm5UVUpCUVVkcVoyZEZlazFKU1VKTWVrRlBRbWRPVmtoUk9FSkJaamhGUWtGTlEwRlpXWGRJVVZsRVZsSXdiRUpDV1hkR1FWbEpTM2RaUWtKUlZVaEJkMFZIUTBOelIwRlJWVVpDZDAxRFRVSkpSMEV4VldSRmQwVkNMM2RSU1UxQldVSkJaamhEUVZGQmQwaFJXVVJXVWpCUFFrSlpSVVpLYWxJclJ6UlJOamdyWWpkSFEyWkhTa0ZpYjA5ME9VTm1NSEpOUWpoSFFURlZaRWwzVVZsTlFtRkJSa3AyYVVJeFpHNUlRamRCWVdkaVpWZGlVMkZNWkM5alIxbFpkVTFFVlVkRFEzTkhRVkZWUmtKM1JVSkNRMnQzU25wQmJFSm5aM0pDWjBWR1FsRmpkMEZaV1ZwaFNGSXdZMFJ2ZGt3eU9XcGpNMEYxWTBkMGNFeHRaSFppTW1OMldqTk9lVTFxUVhsQ1owNVdTRkk0UlV0NlFYQk5RMlZuU21GQmFtaHBSbTlrU0ZKM1QyazRkbGt6U25OTWJrSnlZVk0xYm1JeU9XNU1NbVI2WTJwSmRsb3pUbmxOYVRWcVkyMTNkMUIzV1VSV1VqQm5Ra1JuZDA1cVFUQkNaMXB1WjFGM1FrRm5TWGRMYWtGdlFtZG5ja0puUlVaQ1VXTkRRVkpaWTJGSVVqQmpTRTAyVEhrNWQyRXlhM1ZhTWpsMlduazVlVnBZUW5aak1td3dZak5LTlV4NlFVNUNaMnR4YUd0cFJ6bDNNRUpCVVhOR1FVRlBRMEZSUlVGSGIwRXJUbTV1TnpoNU5uQlNhbVE1V0d4UlYwNWhOMGhVWjJsYUwzSXpVazVIYTIxVmJWbElVRkZ4TmxOamRHazVVRVZoYW5aM1VsUXlhVmRVU0ZGeU1ESm1aWE54VDNGQ1dUSkZWRlYzWjFwUksyeHNkRzlPUm5ab2MwODVkSFpDUTA5SllYcHdjM2RYUXpsaFNqbDRhblUwZEZkRVVVZzRUbFpWTmxsYVdpOVlkR1ZFVTBkVk9WbDZTbkZRYWxrNGNUTk5SSGh5ZW0xeFpYQkNRMlkxYnpodGR5OTNTalJoTWtjMmVIcFZjalpHWWpaVU9FMWpSRTh5TWxCTVVrdzJkVE5OTkZSNmN6TkJNazB4YWpaaWVXdEtXV2s0ZDFkSlVtUkJka3RNVjFwMUwyRjRRbFppZWxsdGNXMTNhMjAxZWt4VFJGYzFia2xCU21KRlRFTlJRMXAzVFVnMU5uUXlSSFp4YjJaNGN6WkNRbU5EUmtsYVZWTndlSFUyZURaMFpEQldOMU4yU2tORGIzTnBjbE50U1dGMGFpODVaRk5UVmtSUmFXSmxkRGh4THpkVlN6UjJORnBWVGpnd1lYUnVXbm94ZVdjOVBTSmRmUS5leUp1YjI1alpTSTZJbEJQT1U0MWVrY3pZbTlOUms1Nk5UbHFWRU01ZG1vdlp6QkxlalExTjJoRVMyOTRTREZzZURSbVlWazlJaXdpZEdsdFpYTjBZVzF3VFhNaU9qRTFOREEwTURZeU16RTBOellzSW1Gd2ExQmhZMnRoWjJWT1lXMWxJam9pWTI5dExtZHZiMmRzWlM1aGJtUnliMmxrTG1kdGN5SXNJbUZ3YTBScFoyVnpkRk5vWVRJMU5pSTZJbVZSWXl0MmVsVmpaSGd3UmxaT1RIWllTSFZIY0VRd0sxSTRNRGR6VlVWMmNDdEtaV3hsV1ZwemFVRTlJaXdpWTNSelVISnZabWxzWlUxaGRHTm9JanAwY25WbExDSmhjR3REWlhKMGFXWnBZMkYwWlVScFoyVnpkRk5vWVRJMU5pSTZXeUk0VURGelZ6QkZVRXBqYzJ4M04xVjZVbk5wV0V3Mk5IY3JUelV3UldRclVrSkpRM1JoZVRGbk1qUk5QU0pkTENKaVlYTnBZMGx1ZEdWbmNtbDBlU0k2ZEhKMVpYMC5qaFE5a3RMLVVCdEJpX2NQNXd4SGRFejJZWXNGMXBLWXpqOEZUZXdXbVVvWE5CTWJSOWRBaEdDSnJCZ2w4RGZNSzFrMUJFQXdQUzRTMWJVczBXQ3haYmN4cWJtcS12UF9OWHI1QjlDQXkxUUpxdC1tRlRMUm5EZkZ1a2hfQjdZMUxEZUJaaGYtc1E4WnBfQUlncHRnYlBWa2Z6TE1PQVpkeE5xVk91dmU0YmJTSjQ5bWQwVklBbDkwc3h1YXVUT0x5bFpxN2ZhYXRZMGFqQ1VKZkNpRUJiLVZxSUhOZmQySExhaUZwcjVxRUFPeU8tQmx1V204TmVfSWZiMnFkTVZBa2p1a1YyVmZheElLbG93a05HZ2ZycjFjVVpJNE5oVTNfeDhLTjRGclpQd29tT2ZFdmlHWk9tZFRvNnNPSXpROTJVYkx2MXlGYUw1TDZIZ1I2Z1NnX0FoYXV0aERhdGFYxSpD77HzPafHbmULkXmwl2mw9P/lQRkLyNgWFO1qQIjVRQAAAAAAAAAAAAAAAAAAAAAAAAAAAEEBEoFEUPzi4s3qob+sexXQrYGH0uPf8domxd0L8Ok+yUo7plaYPfz6HP3Qt/8zMQsSsxr0CpHbVKv1us3MTa6fVaUBAgMmIAEhWCDavINo/+JM4T1eJeKjaZ+vGa2Do7YVh2EyD0vtmoZrrCJYIOtAindNbNXogAQxBAJii2Vd1Wl5rZb9KPak8J6iTKle","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiZDNjWTFJNm4xYXI2Z0xwREVoVGk1bkJnUDF4d0lHc2I2SE1fTlI4UEsxbyIsIm9yaWdpbiI6Imh0dHBzOlwvXC9iMzk5ZmEwMC5uZ3Jvay5pbyIsImFuZHJvaWRQYWNrYWdlTmFtZSI6ImNvbS5hbmRyb2lkLmNocm9tZSJ9"},"type":"public-key"}`,
}
================================================
FILE: attestation/attestion.go
================================================
// attestation can be imported to import all supported attestation formats
package attestation
import (
_ "github.com/koesie10/webauthn/attestation/androidsafetynet"
_ "github.com/koesie10/webauthn/attestation/fido"
_ "github.com/koesie10/webauthn/attestation/packed"
)
================================================
FILE: attestation/fido/fido.go
================================================
// fido implements the FIDO U2F (WebAuthn spec section 8.6) attestation statement format
package fido
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/x509"
"github.com/koesie10/webauthn/protocol"
)
func init() {
protocol.RegisterFormat("fido-u2f", verifyFIDO)
}
func verifyFIDO(a protocol.Attestation, clientDataHash []byte) error {
rawSig, ok := a.AttStmt["sig"]
if !ok {
return protocol.ErrInvalidAttestation.WithDebug("missing sig for fido-u2f")
}
sig, ok := rawSig.([]byte)
if !ok {
return protocol.ErrInvalidAttestation.WithDebug("invalid sig for fido-u2f")
}
rawX5c, ok := a.AttStmt["x5c"]
if !ok {
return protocol.ErrInvalidAttestation.WithDebug("missing x5c for fido-u2f")
}
x5c, ok := rawX5c.([]interface{})
if !ok {
return protocol.ErrInvalidAttestation.WithDebug("invalid x5c for fido-u2f")
}
// Check that x5c has exactly one element
if len(x5c) != 1 {
return protocol.ErrInvalidAttestation.WithDebug("invalid x5c for fido-u2f")
}
// let attCert be that element
attCert, ok := x5c[0].([]byte)
if !ok {
return protocol.ErrInvalidAttestation.WithDebug("invalid x5c for fido-u2f")
}
// Let certificate public key be the public key conveyed by attCert
cert, err := x509.ParseCertificate(attCert)
if err != nil {
return protocol.ErrInvalidAttestation.WithDebugf("invalid x5c for fido-u2f: %v", err)
}
// If certificate public key is not an Elliptic Curve (EC) public key over the P-256 curve, terminate
// this algorithm and return an appropriate error
if cert.PublicKeyAlgorithm != x509.ECDSA {
return protocol.ErrInvalidAttestation.WithDebug("x5c public key algorithm is invalid")
}
if cert.PublicKey.(*ecdsa.PublicKey).Curve != elliptic.P256() {
return protocol.ErrInvalidAttestation.WithDebug("x5c signature algorithm is invalid")
}
publicKey, ok := a.AuthData.AttestedCredentialData.COSEKey.(*ecdsa.PublicKey)
if !ok {
return protocol.ErrInvalidAttestation.WithDebug("COSE public key algorithm is invalid")
}
x := publicKey.X.Bytes()
y := publicKey.Y.Bytes()
if len(x) != 32 {
return protocol.ErrInvalidAttestation.WithDebug("COSE public key x is invalid")
}
if len(y) != 32 {
return protocol.ErrInvalidAttestation.WithDebug("COSE public key y is invalid")
}
// Let publicKeyU2F be the concatenation 0x04 || x || y
publicKeyU2F := []byte{0x04}
publicKeyU2F = append(publicKeyU2F, x...)
publicKeyU2F = append(publicKeyU2F, y...)
// Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F)
verificationData := []byte{0x00}
verificationData = append(verificationData, a.AuthData.RPIDHash...)
verificationData = append(verificationData, clientDataHash...)
verificationData = append(verificationData, a.AuthData.AttestedCredentialData.CredentialID...)
verificationData = append(verificationData, publicKeyU2F...)
// Verify the sig using verificationData and certificate public key per [SEC1].
if err := cert.CheckSignature(x509.ECDSAWithSHA256, verificationData, sig); err != nil {
return protocol.ErrInvalidSignature.WithDebug(err.Error())
}
return nil
}
================================================
FILE: attestation/fido/fido_test.go
================================================
package fido_test
import (
"encoding/json"
"fmt"
"testing"
"github.com/koesie10/webauthn/protocol"
)
func TestIsValidAttestation(t *testing.T) {
for i := range attestationRequests {
t.Run(fmt.Sprintf("Run %d", i), func(t *testing.T) {
r := protocol.CredentialCreationOptions{}
if err := json.Unmarshal([]byte(attestationRequests[i]), &r); err != nil {
t.Fatal(err)
}
b := protocol.AttestationResponse{}
if err := json.Unmarshal([]byte(attestationResponses[i]), &b); err != nil {
t.Fatal(err)
}
p, err := protocol.ParseAttestationResponse(b)
if err != nil {
t.Fatal(err)
}
d, err := protocol.IsValidAttestation(p, r.PublicKey.Challenge, "", "")
if err != nil {
e := protocol.ToWebAuthnError(err)
t.Fatal(fmt.Sprintf("%s, %s: %s", e.Name, e.Description, e.Debug))
}
if !d {
t.Fatal("is not valid")
}
})
}
}
var attestationRequests = []string{
`{"publicKey":{"rp":{"name":"accountsvc"},"user":{"id":"MTAwNjg1ODU4NDE3ODI5NDc4NA==","name":"Koen Vlaswinkel","displayName":"Koen Vlaswinkel"},"pubKeyCredParams":[{"type":"public-key","alg":-7}],"timeout":10000,"attestation":"direct","challenge":"+1jQysnwaIjNU+GrwRp4PWNBMlX0i9/caRkcKd7LPj8="}}`,
`{"publicKey":{"rp":{"name":"webauthn-demo"},"user":{"name":"koen","id":"a29lbg==","displayName":"koen"},"challenge":"2HzAlPIGskbn53hBJZeH3kZ6XfcHWMnzbATVG/FSgkI=","pubKeyCredParams":[{"type":"public-key","alg":-7}],"timeout":30000,"authenticatorSelection":{"requireResidentKey":false},"attestation":"direct"}}`,
}
var attestationResponses = []string{
`{"id":"LOXI3xfiLvIP04MD_S2ZmJYwn3cvMX1FUXxiQO7xlfUvrfcj99UVO2aMrMAwsGvsujY7NHWiM6G3B6ryKJDBBdab-cl4tVZeOwOMhgvHLXk","rawId":"LOXI3xfiLvIP04MD/S2ZmJYwn3cvMX1FUXxiQO7xlfUvrfcj99UVO2aMrMAwsGvsujY7NHWiM6G3B6ryKJDBBdab+cl4tVZeOwOMhgvHLXk=","response":{"attestationObject":"o2dhdHRTdG10omNzaWdYRjBEAiAJ8Q7i8DQzKlb00g4Wby4PoEjlI+s3bS+kVKI3PKoyXQIgDzcP2c5vpplZdmftN+zUDNfXtG1TniWbJv2+6kGZ8bljeDVjgVkBKzCCAScwgc6gAwIBAgIBADAKBggqhkjOPQQDAjAWMRQwEgYDVQQDDAtLcnlwdG9uIEtleTAeFw0xODA5MTcxODQ3NDJaFw0yODA5MTcxODQ3NDJaMBYxFDASBgNVBAMMC0tyeXB0b24gS2V5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwzIpvM5A6mZQXYxRIhfp0sb/21yTcr/sp5Y5DU0IWODQf5ldS2rlDCl62yEaQDM9Akxbsay/vA/S5ut4VSsvoKMNMAswCQYDVR0TBAIwADAKBggqhkjOPQQDAgNIADBFAiA4Yx+5MtKVnjme6V3qXKQ2qcgaHfO6DMgXM9kwOCZcNAIhAJdNk5PPSA04ITfrX9HQy5azo8sH9yhkW7c6gLdb/Kz+aGF1dGhEYXRhWNRJlg3liA6MaHQ0Fw9kdmBbj+SuuaKGMseZXPO6gx2XY0EAAAAALOXI3xfiLvIP04MD/S2ZmABQLOXI3xfiLvIP04MD/S2ZmJYwn3cvMX1FUXxiQO7xlfUvrfcj99UVO2aMrMAwsGvsujY7NHWiM6G3B6ryKJDBBdab+cl4tVZeOwOMhgvHLXmlAQIDJiABIVggwzIpvM5A6mZQXYxRIhfp0sb/21yTcr/sp5Y5DU0IWOAiWCDQf5ldS2rlDCl62yEaQDM9Akxbsay/vA/S5ut4VSsvoGNmbXRoZmlkby11MmY=","clientDataJSON":"eyJjaGFsbGVuZ2UiOiItMWpReXNud2FJak5VLUdyd1JwNFBXTkJNbFgwaTlfY2FSa2NLZDdMUGo4IiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo1Mzg3OSIsInRva2VuQmluZGluZyI6eyJzdGF0dXMiOiJub3Qtc3VwcG9ydGVkIn0sInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ=="},"type":"public-key"}`,
`{"id":"EBT1LOefp-8ID0n2jchlyaPrKcWZ6jdHH8nb0Z-hi9JHsOpTpCNUbJ7ijJOKdetLOy2cqdxNq8zkWYmCgpapKg","rawId":"EBT1LOefp+8ID0n2jchlyaPrKcWZ6jdHH8nb0Z+hi9JHsOpTpCNUbJ7ijJOKdetLOy2cqdxNq8zkWYmCgpapKg==","response":{"attestationObject":"o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEgwRgIhAJkpVpWsMm/Z1OnF/+B/juq/IAlKqhakms5HkNf6ZKLWAiEAm2qNX/bHUkkdaJ0seanz5xxVDCn+bKGEPyQP3ZpPczNjeDVjgVkCUzCCAk8wggE3oAMCAQICBA0ACxYwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMDExLzAtBgNVBAMMJll1YmljbyBVMkYgRUUgU2VyaWFsIDIzOTI1NzM0MDE1NzY1MjcwMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETKz6btEEuhlL1uBm1+E/zGpgDxDSSFx+o9vUTNDVDbJROHujvR665t7mJQoFWMbpvmEYpEOOWkNfHtLrDOi7haM7MDkwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjUwEwYLKwYBBAGC5RwCAQEEBAMCBSAwDQYJKoZIhvcNAQELBQADggEBAI7CTaiBlYLnMIQZnJ8UCvrqgFuin80CTT4UAiGWsBwh0eY+CRSwL4LEFZITkLlFYyOsfMDlI7oddSN/Jmn8HzrPWvzKVP/+mCuRMSdz735wFNYX5xle+NLkoctZjyHOCqdd4B8lgX0nzwNiPZuf+sdY5fhzhLRmtbpfBDToTP57tLR5WlIY6kJ6QKecpZ5sVNxCzSVxRncAptZV7YSsX2we05Kt5mHkBHqhi5CTPQQmOObHov7cB+4q5CpufDzEBFTKPL3tWxV6HvQr0J6Mp6bZFICq5nTP7VPatnnJelRA9VmPSpQuLjpRqpJFKRobj8eQ9yuveXG/7uutBOzBHW9oYXV0aERhdGFYxEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjQQAAAAAAAAAAAAAAAAAAAAAAAAAAAEAQFPUs55+n7wgPSfaNyGXJo+spxZnqN0cfydvRn6GL0kew6lOkI1RsnuKMk4p160s7LZyp3E2rzORZiYKClqkqpQECAyYgASFYIF6oiA6H+mU150XH7WJ2vnzNmdzgr5YloPao7ePjNjlOIlggg0f3u4CtxsBkkKjo7v4luyJui9tJ1rGTBF3YkYlcADo=","clientDataJSON":"eyJjaGFsbGVuZ2UiOiIySHpBbFBJR3NrYm41M2hCSlplSDNrWjZYZmNIV01uemJBVFZHX0ZTZ2tJIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo5MDAwIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9"},"type":"public-key"}`,
}
================================================
FILE: attestation/packed/packed.go
================================================
// packed implements the Packed (WebAuthn spec section 8.2) attestation statement format
package packed
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"math/big"
"github.com/koesie10/webauthn/protocol"
)
func init() {
protocol.RegisterFormat("packed", verifyPacked)
}
var extensionIDFIDOGenCAAAGUID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 45724, 1, 1, 4}
func verifyPacked(a protocol.Attestation, clientDataHash []byte) error {
rawAlg, ok := a.AttStmt["alg"]
if !ok {
return protocol.ErrInvalidAttestation.WithDebug("missing alg for packed")
}
algInt, ok := rawAlg.(int64)
if !ok {
return protocol.ErrInvalidAttestation.WithDebugf("invalid alg for packed, is of invalid type %T", rawAlg)
}
alg := protocol.COSEAlgorithmIdentifier(algInt)
rawSig, ok := a.AttStmt["sig"]
if !ok {
return protocol.ErrInvalidAttestation.WithDebug("missing sig for packed")
}
sig, ok := rawSig.([]byte)
if !ok {
return protocol.ErrInvalidAttestation.WithDebug("invalid sig for packed")
}
// 2. If x5c is present, this indicates that the attestation type is not ECDAA. In this case:
if _, ok := a.AttStmt["x5c"]; ok {
return verifyBasic(a, clientDataHash, alg, sig)
}
// 3. If ecdaaKeyId is present, then the attestation type is ECDAA. In this case:
if _, ok := a.AttStmt["ecdaaKeyId"]; ok {
return verifyECDAA(a, clientDataHash, alg, sig)
}
// 4. If neither x5c nor ecdaaKeyId is present, self attestation is in use.
return verifySelf(a, clientDataHash, alg, sig)
}
func verifyBasic(a protocol.Attestation, clientDataHash []byte, alg protocol.COSEAlgorithmIdentifier, sig []byte) error {
x5c, ok := a.AttStmt["x5c"].([]interface{})
if !ok {
return protocol.ErrInvalidAttestation.WithDebug("invalid x5c for packed")
}
// let attCert be that element
attestnCert, ok := x5c[0].([]byte)
if !ok {
return protocol.ErrInvalidAttestation.WithDebug("invalid x5c for packed")
}
// Let certificate public key be the public key conveyed by attCert
cert, err := x509.ParseCertificate(attestnCert)
if err != nil {
return protocol.ErrInvalidAttestation.WithDebugf("invalid x5c for packed: %v", err)
}
// 2.1 Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash using
// the attestation public key in attestnCert with the algorithm specified in alg.
signedBytes := append(a.AuthData.Raw, clientDataHash...)
if err := cert.CheckSignature(cert.SignatureAlgorithm, signedBytes, sig); err != nil {
// Fallback to ECDSAWithSA256 if signature algorithm is incorret, as is the case with Yubico's keys
err = cert.CheckSignature(x509.ECDSAWithSHA256, signedBytes, sig)
if err != nil {
return protocol.ErrInvalidAttestation.WithDebugf("invalid signature for packed: %v", err)
}
}
// 2.2 Verify that attestnCert meets the requirements in §8.2.1 Packed attestation statement certificate requirements.
// Version MUST be set to 3 (which is indicated by an ASN.1 INTEGER with value 2).
if cert.Version != 3 {
return protocol.ErrInvalidAttestation.WithDebug("invalid version for certificate")
}
// The Basic Constraints extension MUST have the CA component set to false.
if cert.IsCA {
return protocol.ErrInvalidAttestation.WithDebug("CA is set for certificate")
}
var aaguidValue []byte
for _, ext := range cert.Extensions {
// If the related attestation root certificate is used for multiple authenticator models, the Extension
// OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) MUST be present, containing the AAGUID as a 16-byte
// OCTET STRING.
if ext.Id.Equal(extensionIDFIDOGenCAAAGUID) {
// The extension MUST NOT be marked as critical.
if ext.Critical {
return protocol.ErrInvalidAttestation.WithDebugf("extension id-fido-gen-ce-aaguid is present, but is marked as critical")
}
aaguidValue = ext.Value
}
}
// 2.3 If attestnCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) verify that
// the value of this extension matches the aaguid in authenticatorData.
if len(aaguidValue) > 0 {
// Note that an X.509 Extension encodes the DER-encoding of the value in an OCTET STRING. Thus, the AAGUID MUST
// be wrapped in two OCTET STRINGS to be valid
var aaguid []byte
if _, err := asn1.Unmarshal(aaguidValue, &aaguid); err != nil {
return protocol.ErrInvalidAttestation.WithDebugf("invalid AAGUID: %v", err)
}
if !bytes.Equal(a.AuthData.AttestedCredentialData.AAGUID, aaguid) {
return protocol.ErrInvalidAttestation.WithDebugf("invalid AAGUID")
}
}
// If successful, return attestation type Basic and attestation trust path x5c.
return nil
}
func verifyECDAA(a protocol.Attestation, clientDataHash []byte, alg protocol.COSEAlgorithmIdentifier, sig []byte) error {
return protocol.ErrInvalidAttestation.WithDebugf("unsupported packed format ECDAA")
}
func verifySelf(a protocol.Attestation, clientDataHash []byte, alg protocol.COSEAlgorithmIdentifier, sig []byte) error {
// 4.1 Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData.
// 4.2 Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash using
// the credential public key with alg.
signedBytes := append(a.AuthData.Raw, clientDataHash...)
switch v := a.AuthData.AttestedCredentialData.COSEKey.(type) {
case *ecdsa.PublicKey:
// Right now, only EC256 is supported
if alg != protocol.ES256 || v.Curve != elliptic.P256() {
return protocol.ErrInvalidAttestation.WithDebugf("unsupported packed self attestation ECDSA key curve %s", v.Curve.Params().Name)
}
// 6.4.5.1 Signature Formats for Packed Attestation ES256
signature := make([]*big.Int, 2)
if rest, err := asn1.Unmarshal(sig, signature); err != nil {
return protocol.ErrInvalidAttestation.WithDebugf("invalid ECDSA signature: %v", err).WithCause(err)
} else if rest != nil {
return protocol.ErrInvalidAttestation.WithDebugf("invalid ECDSA signature: too much data")
}
hash := sha256.Sum256(signedBytes)
if !ecdsa.Verify(v, hash[:], signature[0], signature[1]) {
return protocol.ErrInvalidAttestation.WithDebugf("invalid signature for packed")
}
default:
return protocol.ErrInvalidAttestation.WithDebugf("unsupported packed self attestation public key type %T", a.AuthData.AttestedCredentialData.COSEKey)
}
// If successful, return implementation-specific values representing attestation type Self and an empty attestation trust path.
return nil
}
================================================
FILE: attestation/packed/packed_test.go
================================================
package packed_test
import (
"encoding/json"
"fmt"
"testing"
"github.com/koesie10/webauthn/protocol"
)
func TestIsValidAttestation(t *testing.T) {
for i := range attestationRequests {
t.Run(fmt.Sprintf("Run %d", i), func(t *testing.T) {
r := protocol.CredentialCreationOptions{}
if err := json.Unmarshal([]byte(attestationRequests[i]), &r); err != nil {
t.Fatal(err)
}
b := protocol.AttestationResponse{}
if err := json.Unmarshal([]byte(attestationResponses[i]), &b); err != nil {
t.Fatal(err)
}
p, err := protocol.ParseAttestationResponse(b)
if err != nil {
t.Fatal(err)
}
d, err := protocol.IsValidAttestation(p, r.PublicKey.Challenge, "", "")
if err != nil {
e := protocol.ToWebAuthnError(err)
t.Fatal(fmt.Sprintf("%s, %s: %s", e.Name, e.Description, e.Debug))
}
if !d {
t.Fatal("is not valid")
}
})
}
}
var attestationRequests = []string{
`{"publicKey":{"rp":{"name":"webauthn-demo"},"user":{"name":"koen","id":"a29lbg==","displayName":"koen"},"challenge":"JUtlYcgpkSiFNzsThDYuOrtSVY1VeLofM+mWTRCCXqU=","pubKeyCredParams":[{"type":"public-key","alg":-7}],"timeout":30000,"authenticatorSelection":{"requireResidentKey":false},"attestation":"direct"}}`,
}
var attestationResponses = []string{
`{"id":"SNBSJTt1DHEuG9XBd6lfc4XXqxkppWfFbt4P5sRVQEPIPANIHHCmPo1AwY5pkUGcpVL3W-uHyWEn4vbgzp34Qw","rawId":"SNBSJTt1DHEuG9XBd6lfc4XXqxkppWfFbt4P5sRVQEPIPANIHHCmPo1AwY5pkUGcpVL3W+uHyWEn4vbgzp34Qw==","response":{"attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIgFls/elhmdZmqEBEKafdcyvQPDrTdBRMW92v6RKJj1bACIQCZ+46sXn65dMEpPuGxvMUruV5i7XN25ctFV/iAi3wSomN4NWOBWQLCMIICvjCCAaagAwIBAgIEdIb9wjANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbzELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTk1NTAwMzg0MjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJVd8633JH0xde/9nMTzGk6HjrrhgQlWYVD7OIsuX2Unv1dAmqWBpQ0KxS8YRFwKE1SKE1PIpOWacE5SO8BN6+2jbDBqMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4xMBMGCysGAQQBguUcAgEBBAQDAgUgMCEGCysGAQQBguUcAQEEBBIEEPigEfOMCk0VgAYXER+e3H0wDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAMVxIgOaaUn44Zom9af0KqG9J655OhUVBVW+q0As6AIod3AH5bHb2aDYakeIyyBCnnGMHTJtuekbrHbXYXERIn4aKdkPSKlyGLsA/A+WEi+OAfXrNVfjhrh7iE6xzq0sg4/vVJoywe4eAJx0fS+Dl3axzTTpYl71Nc7p/NX6iCMmdik0pAuYJegBcTckE3AoYEg4K99AM/JaaKIblsbFh8+3LxnemeNf7UwOczaGGvjS6UzGVI0Odf9lKcPIwYhuTxM5CaNMXTZQ7xq4/yTfC3kPWtE4hFT34UJJflZBiLrxG4OsYxkHw/n5vKgmpspB3GfYuYTWhkDKiE8CYtyg87mhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NBAAAAA/igEfOMCk0VgAYXER+e3H0AQEjQUiU7dQxxLhvVwXepX3OF16sZKaVnxW7eD+bEVUBDyDwDSBxwpj6NQMGOaZFBnKVS91vrh8lhJ+L24M6d+EOlAQIDJiABIVggLxxTguKmjCV4N5OMqd2Sl9AIxSltaPevmQxSqnyNlAciWCDEHOaQDaZ6pC2gC+Z0KS4Ln/XQiJp0X1BmTd+K+FdqSg==","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJKVXRsWWNncGtTaUZOenNUaERZdU9ydFNWWTFWZUxvZk0tbVdUUkNDWHFVIiwibmV3X2tleXNfbWF5X2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0="},"type":"public-key"}`,
}
================================================
FILE: cose/cose.go
================================================
package cose
import (
"bytes"
"fmt"
"github.com/ugorji/go/codec"
)
// Errors
var (
ErrMissingKeyType = fmt.Errorf("cose: missing key type")
ErrMissingAlgorithm = fmt.Errorf("cose: missing algorithm")
ErrUnsupportedKeyType = fmt.Errorf("cose: unsupported key type")
ErrUnsupportedAlgorithm = fmt.Errorf("cose: unsupported algorithm")
ErrInvalidFormat = fmt.Errorf("cose: invalid format")
)
// ParseCOSE parses a raw COSE key into a public key, either *ecdsa.PublicKey or *rsa.PublicKey.
func ParseCOSE(buf []byte) (interface{}, error) {
m := make(map[int]interface{})
cbor := codec.CborHandle{}
if err := codec.NewDecoder(bytes.NewReader(buf), &cbor).Decode(&m); err != nil {
return nil, err
}
return ParseCOSEMap(m)
}
// ParseCOSEMap parses a COSE key that has been decoded from it's CBOR format to a dictionary.
func ParseCOSEMap(m map[int]interface{}) (interface{}, error) {
rawKty, ok := m[1]
if !ok {
return nil, ErrMissingKeyType
}
kty, ok := rawKty.(uint64)
if !ok {
return nil, ErrMissingKeyType
}
rawAlg, ok := m[3]
if !ok {
return nil, ErrMissingAlgorithm
}
alg, ok := rawAlg.(int64)
if !ok {
return nil, ErrMissingAlgorithm
}
// https://tools.ietf.org/html/rfc8152#section-13
switch kty {
case 2: // EC2
return parseECDSA(alg, m)
default:
return nil, ErrUnsupportedKeyType
}
}
================================================
FILE: cose/cose_test.go
================================================
package cose_test
import (
"crypto/ecdsa"
"testing"
"github.com/koesie10/webauthn/cose"
)
func TestParseCOSE(t *testing.T) {
key, err := cose.ParseCOSE(coseKey)
if err != nil {
t.Fatal(err)
}
_ = key.(*ecdsa.PublicKey)
}
var coseKey = []byte{165, 1, 2, 3, 38, 32, 1, 33, 88, 32, 216, 135, 166, 35, 155, 95, 158, 137, 152, 93, 252, 213, 238, 69, 20, 97, 196, 158, 87, 181, 241, 175, 77, 207, 20, 244, 241, 201, 179, 138, 100, 239, 34, 88, 32, 163, 48, 62, 105, 84, 41, 231, 50, 219, 25, 77, 105, 244, 230, 187, 108, 215, 105, 155, 163, 198, 146, 133, 33, 252, 5, 101, 90, 174, 75, 99, 141}
================================================
FILE: cose/doc.go
================================================
// cose contains utility functions related to COSE keys, Section 7 of [RFC8152].
//
package cose // import "github.com/koesie10/webauthn/cose"
================================================
FILE: cose/ecdsa.go
================================================
package cose
import (
"crypto/ecdsa"
"crypto/elliptic"
"math/big"
)
func parseECDSA(alg int64, m map[int]interface{}) (interface{}, error) {
var curve elliptic.Curve
switch alg {
case -7:
curve = elliptic.P256()
case -35:
curve = elliptic.P384()
case -36:
curve = elliptic.P521()
default:
return nil, ErrUnsupportedAlgorithm
}
rawD, ok := m[-4]
if !ok { // public key if there is no d
return parseECDSAPublicKey(curve, m)
}
// otherwise, we have a private key
dBytes, ok := rawD.([]byte)
if !ok {
return nil, ErrInvalidFormat
}
return &ecdsa.PrivateKey{
D: big.NewInt(0).SetBytes(dBytes),
}, nil
}
func parseECDSAPublicKey(curve elliptic.Curve, m map[int]interface{}) (*ecdsa.PublicKey, error) {
rawX, ok := m[-2]
if !ok {
return nil, ErrInvalidFormat
}
xBytes, ok := rawX.([]byte)
if !ok {
return nil, ErrInvalidFormat
}
rawY, ok := m[-3]
if !ok {
return nil, ErrInvalidFormat
}
yBytes, ok := rawY.([]byte)
if !ok {
return nil, ErrInvalidFormat
}
x := big.NewInt(0).SetBytes(xBytes)
y := big.NewInt(0).SetBytes(yBytes)
return &ecdsa.PublicKey{
Curve: curve,
X: x,
Y: y,
}, nil
}
================================================
FILE: go.mod
================================================
module github.com/koesie10/webauthn
go 1.13
require (
github.com/pkg/errors v0.9.1
github.com/ugorji/go/codec v1.1.7
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d // indirect
gopkg.in/square/go-jose.v2 v2.4.1
)
================================================
FILE: go.sum
================================================
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20180918125716-ed9a3b5f078b h1:pvvReAGi9NbL0Z7RgD5a7GFc3WAW0oE9q57MfVKlstw=
github.com/ugorji/go/codec v0.0.0-20180918125716-ed9a3b5f078b/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/crypto v0.0.0-20181024171144-74cb1d3d52f4 h1:4v3KN0hcTEAkyusBypx0RrpPAhKsTP3YXj10LonM8J8=
golang.org/x/crypto v0.0.0-20181024171144-74cb1d3d52f4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/square/go-jose.v2 v2.1.9 h1:YCFbL5T2gbmC2sMG12s1x2PAlTK5TZNte3hjZEIcCAg=
gopkg.in/square/go-jose.v2 v2.1.9/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y=
gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
================================================
FILE: protocol/api.go
================================================
package protocol
// CredentialCreationOptions contains the options that should be passed to navigator.credentials.create().
// https://www.w3.org/TR/webauthn/#credentialcreationoptions-extension
type CredentialCreationOptions struct {
PublicKey PublicKeyCredentialCreationOptions `json:"publicKey"`
}
// CredentialRequestOptions contains the options that should be passed to navigator.credentials.get().
// https://www.w3.org/TR/webauthn/#credentialrequestoptions-extension
type CredentialRequestOptions struct {
PublicKey PublicKeyCredentialRequestOptions `json:"publicKey"`
}
// The PublicKeyCredentialCreationOptions dictionary supplies create() with the data it needs to generate an attestation.
// https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialcreationoptions
type PublicKeyCredentialCreationOptions struct {
// This member contains data about the Relying Party responsible for the request.
// Its value’s name member is REQUIRED. See §5.4.1 Public Key Entity Description (dictionary
// PublicKeyCredentialEntity) for further details.
// Its value’s id member specifies the RP ID with which the credential should be associated. If omitted, its value
// will be the CredentialsContainer object’s relevant settings object's origin's effective domain. See §5.4.2
// Relying Party Parameters for Credential Generation (dictionary PublicKeyCredentialRpEntity) for further details.
RP PublicKeyCredentialRpEntity `json:"rp"`
// This member contains data about the user account for which the Relying Party is requesting attestation.
// Its value’s name, displayName and id members are REQUIRED. See §5.4.1 Public Key Entity Description
// (dictionary PublicKeyCredentialEntity) and §5.4.3 User Account Parameters for Credential Generation
// (dictionary PublicKeyCredentialUserEntity) for further details.
User PublicKeyCredentialUserEntity `json:"user"`
// This member contains a challenge intended to be used for generating the newly created credential’s attestation
// object. See the §13.1 Cryptographic Challenges security consideration.
Challenge Challenge `json:"challenge"`
// This member contains information about the desired properties of the credential to be created. The sequence is
// ordered from most preferred to least preferred. The client makes a best-effort to create the most preferred
// credential that it can.
PubKeyCredParams []PublicKeyCredentialParameters `json:"pubKeyCredParams,omitempty"`
// This member specifies a time, in milliseconds, that the caller is willing to wait for the call to complete.
// This is treated as a hint, and MAY be overridden by the client.
Timeout uint `json:"timeout,omitempty"`
// This member is intended for use by Relying Parties that wish to limit the creation of multiple credentials for
// the same account on a single authenticator. The client is requested to return an error if the new credential
// would be created on an authenticator that also contains one of the credentials enumerated in this parameter.
ExcludeCredentials []PublicKeyCredentialDescriptor `json:"excludeCredentials,omitempty"`
// This member is intended for use by Relying Parties that wish to select the appropriate authenticators to
// participate in the create() operation.
AuthenticatorSelection AuthenticatorSelectionCriteria `json:"authenticatorSelection,omitempty"`
// This member is intended for use by Relying Parties that wish to express their preference for attestation
// conveyance. The default is none.
Attestation AttestationConveyancePreference `json:"attestation,omitempty"`
// This member contains additional parameters requesting additional processing by the client and authenticator. For
// example, the caller may request that only authenticators with certain capabilities be used to create the
// credential, or that particular information be returned in the attestation object. Some extensions are defined in
// §9 WebAuthn Extensions; consult the IANA "WebAuthn Extension Identifier" registry established by
// [WebAuthn-Registries] for an up-to-date list of registered WebAuthn Extensions.
Extensions AuthenticationExtensionsClientInputs `json:"extensions,omitempty"`
}
// The PublicKeyCredentialRequestOptions dictionary supplies get() with the data it needs to generate an assertion. Its
// challenge member MUST be present, while its other members are OPTIONAL.
// https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrequestoptions
type PublicKeyCredentialRequestOptions struct {
// This member represents a challenge that the selected authenticator signs, along with other data, when producing
// an authentication assertion. See the §13.1 Cryptographic Challenges security consideration.
Challenge Challenge `json:"challenge"`
// This OPTIONAL member specifies a time, in milliseconds, that the caller is willing to wait for the call to
// complete. The value is treated as a hint, and MAY be overridden by the client.
Timeout uint `json:"timeout,omitempty"`
// This OPTIONAL member specifies the relying party identifier claimed by the caller. If omitted, its value will be
// the CredentialsContainer object’s relevant settings object's origin's effective domain.
RPID string `json:"rpId,omitempty"`
// This OPTIONAL member contains a list of PublicKeyCredentialDescriptor objects representing public key credentials
// acceptable to the caller, in descending order of the caller’s preference (the first item in the list is the most
// preferred credential, and so on down the list).
AllowCredentials []PublicKeyCredentialDescriptor `json:"allowCredentials,omitempty"`
// This member describes the Relying Party's requirements regarding user verification for the get() operation.
// Eligible authenticators are filtered to only those capable of satisfying this requirement.
UserVerification UserVerificationRequirement `json:"userVerification,omitempty"`
// This OPTIONAL member contains additional parameters requesting additional processing by the client and
// authenticator. For example, if transaction confirmation is sought from the user, then the prompt string might
// be included as an extension.
Extensions AuthenticationExtensionsClientInputs `json:"extensions,omitempty"`
}
// The PublicKeyCredentialRpEntity dictionary is used to supply additional Relying Party attributes when creating a
// new credential.
// https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity
type PublicKeyCredentialRpEntity struct {
PublicKeyCredentialEntity
// A unique identifier for the Relying Party entity, which sets the RP ID.
ID string `json:"id,omitempty"`
}
// The PublicKeyCredentialEntity dictionary describes a user account, or a WebAuthn Relying Party, with which a
// public key credential is associated.
// https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialentity
type PublicKeyCredentialEntity struct {
// A human-palatable name for the entity. Its function depends on what the PublicKeyCredentialEntity represents.
Name string `json:"name"`
}
// The PublicKeyCredentialUserEntity dictionary is used to supply additional user account attributes when creating a
// new credential.
// https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialuserentity
type PublicKeyCredentialUserEntity struct {
PublicKeyCredentialEntity
// The user handle of the user account entity. To ensure secure operation, authentication and authorization
// decisions MUST be made on the basis of this id member, not the displayName nor name members. See
// Section 6.1 of [RFC8266].
ID []byte `json:"id"`
// A human-palatable name for the user account, intended only for display. For example, "Alex P. Müller" or
// "田中 倫". The Relying Party SHOULD let the user choose this, and SHOULD NOT restrict the choice more than
// necessary.
DisplayName string `json:"displayName"`
}
// PublicKeyCredentialType defines the valid credential types. It is an extension point; values can be added to it in the
// future, as more credential types are defined. The values of this enumeration are used for versioning the
// Authentication Assertion and attestation structures according to the type of the authenticator.
// Currently one credential type is defined, namely "public-key".
// https://www.w3.org/TR/webauthn/#enumdef-publickeycredentialtype
type PublicKeyCredentialType string
const (
// PublicKeyCredentialTypePublicKey is the only credential type defined, namely "public-key".
PublicKeyCredentialTypePublicKey PublicKeyCredentialType = "public-key"
)
// A COSEAlgorithmIdentifier's value is a number identifying a cryptographic algorithm. The algorithm identifiers
// SHOULD be values registered in the IANA COSE Algorithms registry [IANA-COSE-ALGS-REG], for instance, -7 for
// "ES256" and -257 for "RS256".
// https://www.w3.org/TR/webauthn/#alg-identifier
type COSEAlgorithmIdentifier int
const (
// ES256 is the COSE Algorithm Identifier of ECDSA 256
ES256 COSEAlgorithmIdentifier = -7
// RS256 is the COSE Algorithm Identifier of RSA 256
RS256 COSEAlgorithmIdentifier = -257
)
// AuthenticatorTransport represents the transport used by an authenticator. Authenticators may implement various
// transports for communicating with clients. This enumeration defines hints as to
// how clients might communicate with a particular authenticator in order to obtain an assertion for a specific
// credential. Note that these hints represent the WebAuthn Relying Party's best belief as to how an authenticator may
// be reached. A Relying Party may obtain a list of transports hints from some attestation statement formats or via
// some out-of-band mechanism; it is outside the scope of this specification to define that mechanism.
// https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport
type AuthenticatorTransport string
const (
// AuthenticatorTransportUSB indicates the respective authenticator can be contacted over removable USB.
AuthenticatorTransportUSB AuthenticatorTransport = "usb"
// AuthenticatorTransportNFC indicates the respective authenticator can be contacted over Near Field Communication (NFC).
AuthenticatorTransportNFC = "nfc"
// AuthenticatorTransportBLE indicates the respective authenticator can be contacted over Bluetooth Smart (Bluetooth Low Energy / BLE).
AuthenticatorTransportBLE = "ble"
// AuthenticatorTransportInternal indicates the respective authenticator is contacted using a client device-specific transport. These
// authenticators are not removable from the client device.
AuthenticatorTransportInternal = "internal"
)
// PublicKeyCredentialParameters is used to supply additional parameters when creating a new credential.
// https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialparameters
type PublicKeyCredentialParameters struct {
// This member specifies the type of credential to be created.
Type PublicKeyCredentialType `json:"type"`
// This member specifies the cryptographic signature algorithm with which the newly generated credential will be
// used, and thus also the type of asymmetric key pair to be generated, e.g., RSA or Elliptic Curve.
Algorithm COSEAlgorithmIdentifier `json:"alg"`
}
// PublicKeyCredentialDescriptor contains the attributes that are specified by a caller when referring to a public key credential as
// an input parameter to the create() or get() methods. It mirrors the fields of the PublicKeyCredential object
// returned by the latter methods.
// https://www.w3.org/TR/webauthn/#credential-dictionary
type PublicKeyCredentialDescriptor struct {
// This member contains the type of the public key credential the caller is referring to.
Type PublicKeyCredentialType `json:"type"`
// This member contains the credential ID of the public key credential the caller is referring to.
ID []byte `json:"id"`
// This OPTIONAL member contains a hint as to how the client might communicate with the managing authenticator of
// the public key credential the caller is referring to.
Transport []AuthenticatorTransport `json:"transports,omitempty"`
}
// The AuthenticatorSelectionCriteria may be used by WebAuthn Relying Parties to specify their requirements
// regarding authenticator attributes.
// https://www.w3.org/TR/webauthn/#dictdef-authenticatorselectioncriteria
type AuthenticatorSelectionCriteria struct {
// If this member is present, eligible authenticators are filtered to only authenticators attached with the
// specified §5.4.5 Authenticator Attachment enumeration (enum AuthenticatorAttachment).
AuthenticatorAttachment AuthenticatorAttachment `json:"authenticatorAttachment,omitempty"`
// This member describes the Relying Parties' requirements regarding resident credentials. If the parameter is set
// to true, the authenticator MUST create a client-side-resident public key credential source when creating a
// public key credential.
RequireResidentKey bool `json:"requireResidentKey"`
// This member describes the Relying Party's requirements regarding user verification for the create() operation.
// Eligible authenticators are filtered to only those capable of satisfying this requirement.
UserVerification UserVerificationRequirement `json:"userVerification,omitempty"`
}
// AuthenticatorAttachment's values describe authenticators' attachment modalities. Relying Parties use this for two purposes:
// to express a preferred authenticator attachment modality when calling navigator.credentials.create() to create a
// credential, and
// to inform the client of the Relying Party's best belief about how to locate the managing authenticators of the
// credentials listed in allowCredentials when calling navigator.credentials.get().
// https://www.w3.org/TR/webauthn/#enumdef-authenticatorattachment
type AuthenticatorAttachment string
const (
// AuthenticatorAttachmentPlatform indicates platform attachment.
AuthenticatorAttachmentPlatform AuthenticatorAttachment = "platform"
// AuthenticatorAttachmentCrossPlatform indicates cross-platform attachment.
AuthenticatorAttachmentCrossPlatform = "cross-platform"
)
// UserVerificationRequirement may be used by a WebAuthn Relying Party to require user verification for some of its
// operations but not for others.
// https://www.w3.org/TR/webauthn/#enumdef-userverificationrequirement
type UserVerificationRequirement string
const (
// UserVerificationRequired indicates that the Relying Party requires user verification for the operation and will fail the
// operation if the response does not have the UV flag set.
UserVerificationRequired UserVerificationRequirement = "required"
// UserVerificationPreferred indicates that the Relying Party prefers user verification for the operation if possible, but
// will not fail the operation if the response does not have the UV flag set.
UserVerificationPreferred = "preferred"
// UserVerificationDiscouraged indicates that the Relying Party does not want user verification employed during the operation
// (e.g., in the interest of minimizing disruption to the user interaction flow).
UserVerificationDiscouraged = "discouraged"
)
// AttestationConveyancePreference may be used by WebAuthn Relying Parties to specify their preference regarding attestation
// conveyance during credential generation.
// https://www.w3.org/TR/webauthn/#enumdef-attestationconveyancepreference
type AttestationConveyancePreference string
const (
// AttestationConveyancePreferenceNone indicates that the Relying Party is not interested in authenticator attestation. For example, in
// order to potentially avoid having to obtain user consent to relay identifying information to the Relying Party,
// or to save a roundtrip to an Attestation CA. This is the default value.
AttestationConveyancePreferenceNone = "none"
// AttestationConveyancePreferenceIndirect indicates that the Relying Party prefers an attestation conveyance yielding verifiable attestation
// statements, but allows the client to decide how to obtain such attestation statements. The client MAY replace
// the authenticator-generated attestation statements with attestation statements generated by an Anonymization CA,
// in order to protect the user’s privacy, or to assist Relying Parties with attestation verification in a
// heterogeneous ecosystem.
AttestationConveyancePreferenceIndirect = "indirect"
// AttestationConveyancePreferenceDirect indicates that the Relying Party wants to receive the attestation statement as generated by the
// authenticator.
AttestationConveyancePreferenceDirect = "direct"
)
// AuthenticationExtensionsClientInputs contains the client extension input values for zero or more WebAuthn extensions, as defined
// in §9 WebAuthn Extensions.
// https://www.w3.org/TR/webauthn/#dictdef-authenticationextensionsclientinputs
type AuthenticationExtensionsClientInputs map[string]interface{}
================================================
FILE: protocol/assertion.go
================================================
package protocol
import (
"crypto/sha256"
"crypto/x509"
"encoding/json"
)
// AssertionResponse contains the attributes that are returned to the caller when a new assertion is requested.
// https://www.w3.org/TR/webauthn/#publickeycredential
type AssertionResponse struct {
PublicKeyCredential
// This attribute contains the authenticator's response to the client’s request to generate an authentication assertion.
Response AuthenticatorAssertionResponse `json:"response"`
}
// ParsedAssertionResponse is a parsed version of AssertionResponse.
// https://www.w3.org/TR/webauthn/#publickeycredential
type ParsedAssertionResponse struct {
ParsedPublicKeyCredential
// This attribute contains the authenticator's response to the client’s request to generate an authentication assertion.
Response ParsedAuthenticatorAssertionResponse
// RawResponse contains the unparsed AssertionResponse.
RawResponse AssertionResponse
}
// The AuthenticatorAssertionResponse interface represents an authenticator's response to a client’s request for
// generation of a new authentication assertion given the WebAuthn Relying Party's challenge and OPTIONAL list of
// credentials it is aware of. This response contains a cryptographic signature proving possession of the credential
// private key, and optionally evidence of user consent to a specific transaction.
// https://www.w3.org/TR/webauthn/#authenticatorassertionresponse
type AuthenticatorAssertionResponse struct {
AuthenticatorResponse
// This attribute contains the authenticator data returned by the authenticator. See §6.1 Authenticator data.
AuthenticatorData []byte `json:"authenticatorData"`
// This attribute contains the raw signature returned from the authenticator. See §6.3.3 The
// authenticatorGetAssertion operation.
Signature []byte `json:"signature"`
// This attribute contains the user handle returned from the authenticator, or null if the authenticator did not
// return a user handle. See §6.3.3 The authenticatorGetAssertion operation.
UserHandle []byte `json:"userHandle,omitempty"`
}
// ParsedAuthenticatorAssertionResponse is a parsed version of AuthenticatorAssertionResponse.
// https://www.w3.org/TR/webauthn/#authenticatorassertionresponse
type ParsedAuthenticatorAssertionResponse struct {
ParsedAuthenticatorResponse
// This attribute contains the authenticator data returned by the authenticator. See §6.1 Authenticator data.
AuthData AuthenticatorData
// This attribute contains the raw signature returned from the authenticator. See §6.3.3 The
// authenticatorGetAssertion operation.
Signature []byte
// This attribute contains the user handle returned from the authenticator, or null if the authenticator did not
// return a user handle. See §6.3.3 The authenticatorGetAssertion operation.
UserHandle []byte
}
// ParseAssertionResponse will parse a raw AssertionResponse as supplied by a client to a ParsedAssertionResponse
// that may be used by clients to examine data. If the data is invalid, an error is returned, usually of the type
// Error.
func ParseAssertionResponse(p AssertionResponse) (ParsedAssertionResponse, error) {
r := ParsedAssertionResponse{}
r.ID, r.RawID, r.Type = p.ID, p.RawID, p.Type
r.Response.Signature = p.Response.Signature
r.RawResponse = p
// 6. Let C, the client data claimed as used for the signature, be the result of running an implementation-specific
// JSON parser on JSONtext.
if err := json.Unmarshal(p.Response.ClientDataJSON, &r.Response.ClientData); err != nil {
return ParsedAssertionResponse{}, ErrInvalidRequest.WithDebug(err.Error()).WithHint("Unable to parse client data")
}
if err := r.Response.AuthData.UnmarshalBinary(p.Response.AuthenticatorData); err != nil {
return ParsedAssertionResponse{}, ErrInvalidRequest.WithDebug(err.Error()).WithHint("Unable to parse auth data")
}
return r, nil
}
// IsValidAssertion may be used to check whether an assertion is valid. If originalChallenge is nil, the challenge value
// will not be checked (INSECURE). If relyingPartyID is empty, the relying party hash will not be checked (INSECURE). If
// relyingPartyOrigin is empty, the relying party origin will not be checked (INSEUCRE).
// If cert is nil, the hash will not be checked (INSECURE). Before calling this method, clients should execute the
// following steps: If the allowCredentials option was given when this authentication ceremony was initiated, verify that
// credential.id identifies one of the public key credentials that were listed in allowCredentials; If
// credential.response.userHandle is present, verify that the user identified by this value is the owner of the public
// key credential identified by credential.id. If the data is invalid, an error is returned, usually of the type
// Error.
func IsValidAssertion(p ParsedAssertionResponse, originalChallenge []byte, relyingPartyID, relyingPartyOrigin string, cert *x509.Certificate) (bool, error) {
// Check the client data, i.e. steps 7-10
if err := p.Response.ClientData.IsValid("webauthn.get", originalChallenge, relyingPartyOrigin); err != nil {
return false, err
}
// Check the auth data, i.e. steps 10-13
if err := p.Response.AuthData.IsValid(relyingPartyID); err != nil {
return false, err
}
if cert != nil {
// 15. Let hash be the result of computing a hash over the cData using SHA-256.
clientDataHash := sha256.Sum256(p.RawResponse.Response.ClientDataJSON)
// 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.
verificationData := append(p.RawResponse.Response.AuthenticatorData, clientDataHash[:]...)
if err := cert.CheckSignature(x509.ECDSAWithSHA256, verificationData, p.Response.Signature); err != nil {
return false, ErrInvalidSignature.WithDebug(err.Error())
}
}
// TODO: 17. If the signature counter value authData.signCount is nonzero or the value stored in conjunction with
// credential’s id attribute is nonzero, then run the following sub-step: ...
return true, nil
}
================================================
FILE: protocol/attestation.go
================================================
package protocol
import (
"bytes"
"crypto/sha256"
"encoding/json"
"github.com/ugorji/go/codec"
)
// AttestationResponse contains the attributes that are returned to the caller when a new credential is created.
// https://www.w3.org/TR/webauthn/#publickeycredential
type AttestationResponse struct {
PublicKeyCredential
// This attribute contains the authenticator's response to the client’s request to create a public key credential.
Response AuthenticatorAttestationResponse `json:"response"`
}
// ParsedAttestationResponse is a parsed version of AttestationResponse
// https://www.w3.org/TR/webauthn/#publickeycredential
type ParsedAttestationResponse struct {
ParsedPublicKeyCredential
// This attribute contains the authenticator's response to the client’s request to create a public key credential.
Response ParsedAuthenticatorAttestationResponse
// RawResponse contains the unparsed AttestationResponse.
RawResponse AttestationResponse
}
// The AuthenticatorAttestationResponse interface represents the authenticator's response to a client’s request for the
// creation of a new public key credential. It contains information about the new credential that can be used to
// identify it for later use, and metadata that can be used by the WebAuthn Relying Party to assess the characteristics
// of the credential during registration.
// https://www.w3.org/TR/webauthn/#authenticatorattestationresponse
type AuthenticatorAttestationResponse struct {
AuthenticatorResponse
// This attribute contains an attestation object, which is opaque to, and cryptographically protected against
// tampering by, the client. The attestation object contains both authenticator data and an attestation statement.
// The former contains the AAGUID, a unique credential ID, and the credential public key. The contents of the
// attestation statement are determined by the attestation statement format used by the authenticator. It also
// contains any additional information that the Relying Party's server requires to validate the attestation
// statement, as well as to decode and validate the authenticator data along with the JSON-serialized client data.
// For more details, see §6.4 Attestation, §6.4.4 Generating an Attestation Object, and Figure 5.
AttestationObject []byte `json:"attestationObject"`
}
// ParsedAuthenticatorAttestationResponse is a parsed version of AuthenticatorAttestationResponse
// https://www.w3.org/TR/webauthn/#authenticatorattestationresponse
type ParsedAuthenticatorAttestationResponse struct {
ParsedAuthenticatorResponse
// This attribute contains an attestation object, which is opaque to, and cryptographically protected against
// tampering by, the client. The attestation object contains both authenticator data and an attestation statement.
// The former contains the AAGUID, a unique credential ID, and the credential public key. The contents of the
// attestation statement are determined by the attestation statement format used by the authenticator. It also
// contains any additional information that the Relying Party's server requires to validate the attestation
// statement, as well as to decode and validate the authenticator data along with the JSON-serialized client data.
// For more details, see §6.4 Attestation, §6.4.4 Generating an Attestation Object, and Figure 5.
Attestation Attestation
}
// Attestation represents the attestionObject. An important component of the attestation object is the attestation
// statement. This is a specific type of signed data object, containing statements about a public key credential itself
// and the authenticator that created it. It contains an attestation signature created using the key of the attesting
// authority (except for the case of self attestation, when it is created using the credential private key). In order to
// correctly interpret an attestation statement, a Relying Party needs to understand these two aspects of attestation:
// https://www.w3.org/TR/webauthn/#attestation-object
type Attestation struct {
Fmt string `json:"fmt"`
AuthData AuthenticatorData `json:"authData"`
AttStmt map[string]interface{} `json:"attStmt"`
}
// ParseAttestationResponse will parse a raw AttestationResponse as supplied by a client to a ParsedAttestationResponse
// that may be used by clients to examine data. If the data is invalid, an error is returned, usually of the type
// Error.
func ParseAttestationResponse(p AttestationResponse) (ParsedAttestationResponse, error) {
r := ParsedAttestationResponse{}
r.ID, r.RawID, r.Type = p.ID, p.RawID, p.Type
r.RawResponse = p
// 2. Let C, the client data claimed as collected during the credential creation, be the result of running an
// implementation-specific JSON parser on JSONtext.
if err := json.Unmarshal(p.Response.ClientDataJSON, &r.Response.ClientData); err != nil {
return ParsedAttestationResponse{}, ErrInvalidRequest.WithDebug(err.Error()).WithHint("Unable to parse client data")
}
cbor := codec.CborHandle{}
// 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.
if err := codec.NewDecoder(bytes.NewReader(p.Response.AttestationObject), &cbor).Decode(&r.Response.Attestation); err != nil {
return ParsedAttestationResponse{}, ErrInvalidRequest.WithDebug(err.Error()).WithHint("Unable to parse attestation")
}
return r, nil
}
// IsValidAttestation may be used to check whether an attestation is valid. If originalChallenge is nil, the challenge value
// will not be checked (INSECURE). If relyingPartyID is empty, the relying party ID hash will not be checked (INSECURE). If
// relyingPartyOrigin is empty, the relying party origin will not be checked (INSEUCRE).
// If the data is invalid, an error is returned, usually of the type Error.
func IsValidAttestation(p ParsedAttestationResponse, originalChallenge []byte, relyingPartyID, relyingPartyOrigin string) (bool, error) {
// Check the client data, i.e. steps 3-6
if err := p.Response.ClientData.IsValid("webauthn.create", originalChallenge, relyingPartyOrigin); err != nil {
return false, err
}
// 7. Compute the hash of response.clientDataJSON using SHA-256
clientDataHash := sha256.Sum256(p.RawResponse.Response.ClientDataJSON)
// Check the attestation, i.e. steps 9-14
if err := p.Response.Attestation.IsValid(relyingPartyID, clientDataHash[:]); err != nil {
return false, err
}
return true, nil
}
// IsValid checks whether the Attestation is valid. If relyingPartyID is empty, the relying party ID hash will not be
// checked (INSEUCRE). To register a new attestation type, use RegisterFormat. If the data is invalid, an error is
// returned, usually of the type Error.
func (a Attestation) IsValid(relyingPartyID string, clientDataHash []byte) error {
// Check the auth data, i.e. steps 9-11
if err := a.AuthData.IsValid(relyingPartyID); err != nil {
return err
}
// 13. Determine the attestation statement format by performing a USASCII case-sensitive match on fmt against the set
// of supported WebAuthn Attestation Statement Format Identifier values.
format, ok := attestationFormats[a.Fmt]
if !ok {
return ErrUnsupportedAttestationFormat.WithDebugf("The attestation format %q is unknown", a.Fmt)
}
// 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, by using the
// attestation statement format fmt’s verification procedure given attStmt, authData and the hash of the serialized
// client data computed in step 7.
if err := format(a, clientDataHash); err != nil {
return err
}
// NOTE: However, if permitted by policy, the Relying Party MAY register the credential ID and credential public
// key but treat the credential as one with self attestation (see §6.4.3 Attestation Types). If doing so, the
// Relying Party is asserting there is no cryptographic proof that the public key credential has been generated
// by a particular authenticator model. See [FIDOSecRef] and [UAFProtocol] for a more detailed discussion.
return nil
}
================================================
FILE: protocol/attestation_registry.go
================================================
package protocol
// AttestationFormatFunction will be called when checking whether an Attestation is valid.
type AttestationFormatFunction func(Attestation, []byte) error
var attestationFormats = make(map[string]AttestationFormatFunction)
// RegisterFormat will register an attestation format. If the name already exists, it will be overwritten without
// warning.
func RegisterFormat(name string, f AttestationFormatFunction) {
attestationFormats[name] = f
}
================================================
FILE: protocol/challenge.go
================================================
package protocol
import "crypto/rand"
// ChallengeSize represents the size of a challenge created by NewChallenge.
const ChallengeSize = 32
// Challenge represents a challenge. It is defined as a separate type to make it clear that NewChallenge should
// be used to create it.
type Challenge []byte
// NewChallenge creates a new cryptographically secure random challenge of ChallengeSize bytes.
func NewChallenge() (Challenge, error) {
b := make([]byte, ChallengeSize)
_, err := rand.Read(b)
if err != nil {
return nil, err
}
return b, nil
}
================================================
FILE: protocol/common.go
================================================
package protocol
import (
"bytes"
"crypto/sha256"
"encoding"
"encoding/base64"
"encoding/binary"
"fmt"
"github.com/koesie10/webauthn/cose"
)
// The PublicKeyCredential interface inherits from Credential [CREDENTIAL-MANAGEMENT-1], and contains the attributes
// that are returned to the caller when a new credential is created, or a new assertion is requested.
// See AttestationResponse and AssertionResponse
// https://www.w3.org/TR/webauthn/#publickeycredential
type PublicKeyCredential struct {
// This attribute is inherited from Credential, though PublicKeyCredential overrides Credential's getter, instead
// returning the base64url encoding of the data contained in the object’s [[identifier]] internal slot.
ID string `json:"id"`
// This attribute returns the ArrayBuffer contained in the [[identifier]] internal slot.
RawID []byte `json:"rawId"`
// The PublicKeyCredential interface object's [[type]] internal slot's value is the string "public-key".
Type string `json:"type"`
}
// ParsedPublicKeyCredential is a parsed version of PublicKeyCredential
// https://www.w3.org/TR/webauthn/#publickeycredential
type ParsedPublicKeyCredential struct {
// This attribute is inherited from Credential, though PublicKeyCredential overrides Credential's getter, instead
// returning the base64url encoding of the data contained in the object’s [[identifier]] internal slot.
ID string
// This attribute returns the ArrayBuffer contained in the [[identifier]] internal slot.
RawID []byte
// The PublicKeyCredential interface object's [[type]] internal slot's value is the string "public-key".
Type string
}
// AuthenticatorResponse is used by authenticators to respond to Relying Party requests.
// https://www.w3.org/TR/webauthn/#authenticatorresponse
type AuthenticatorResponse struct {
// This attribute contains a JSON serialization of the client data passed to the authenticator by the client in
// its call to either create() or get().
ClientDataJSON []byte `json:"clientDataJSON"`
}
// ParsedAuthenticatorResponse is a parsed version of AuthenticatorResponse.
// https://www.w3.org/TR/webauthn/#authenticatorresponse
type ParsedAuthenticatorResponse struct {
// This attribute contains the parsed client data passed to the authenticator by the client in its call to either
// create() or get().
ClientData CollectedClientData
}
// 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/#client-data
type CollectedClientData struct {
// This member contains 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 string `json:"type"`
// This member contains the base64url encoding of the challenge provided by the RP. See the §13.1 Cryptographic
// Challenges security consideration.
Challenge string `json:"challenge"`
// This member contains the fully qualified origin of the requester, as provided to the authenticator by the client,
// in the syntax defined by [RFC6454].
Origin string `json:"origin"`
// This OPTIONAL member contains information about the state of the Token Binding protocol used when communicating
// with the Relying Party. Its absence indicates that the client doesn’t support token binding.
TokenBinding *TokenBinding `json:"tokenBinding,omitempty"`
}
// TokenBinding represents the token binding.
// https://www.w3.org/TR/webauthn/#dictdef-tokenbinding
type TokenBinding struct {
// This member is one of the following:
Status TokenBindingStatus `json:"status,omitempty"`
// This member MUST be present if status is present, and MUST a base64url encoding of the Token Binding ID that was
// used when communicating with the Relying Party.
ID string `json:"id,omitempty"`
}
// TokenBindingStatus represents the status of a TokenBinding.
// https://www.w3.org/TR/webauthn/#enumdef-tokenbindingstatus
type TokenBindingStatus string
const (
// TokenBindingStatusPresent indicates the client supports token binding, but it was not negotiated when
// communicating with the Relying Party.
TokenBindingStatusPresent TokenBindingStatus = "present"
// TokenBindingStatusSupported indicates token binding was used when communicating with the Relying Party. In this
// case, the id member MUST be present.
TokenBindingStatusSupported = "supported"
)
// IsValid checks whether the CollectedClientData is valid. If originalChallenge is nil, the challenge value
// will not be checked (INSECURE). If relyingPartyOrigin is empty, the relying party will not be checked (INSEUCRE).
// If the data is invalid, an error is returned, usually of the type Error.
func (c CollectedClientData) IsValid(requiredType string, originalChallenge []byte, relyingPartyOrigin string) error {
// Verify that the value of C.type is requiredType
if c.Type != requiredType {
return ErrInvalidType.WithDebugf("%q did not match required %q", c.Type, requiredType)
}
if originalChallenge != nil {
// Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the
// create()/get() call
challenge, err := base64.RawURLEncoding.DecodeString(c.Challenge) // This is raw URL encoding, so the JSON parser does not handle it
if err != nil {
return ErrInvalidChallenge.WithDebug(err.Error())
}
if !bytes.Equal(challenge, originalChallenge) {
return ErrInvalidChallenge
}
}
// Verify that the value of C.origin matches the Relying Party's origin.
if relyingPartyOrigin != "" && c.Origin != relyingPartyOrigin {
return ErrInvalidOrigin.WithDebugf("%q did not match required %q", relyingPartyOrigin, c.Origin)
}
// TODO: 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.
return nil
}
// AuthenticatorData encodes contextual bindings made by the authenticator. These bindings are controlled
// by the authenticator itself, and derive their trust from the WebAuthn Relying Party's assessment of the security
// properties of the authenticator. In one extreme case, the authenticator may be embedded in the client, and its
// bindings may be no more trustworthy than the client data. At the other extreme, the authenticator may be a discrete
// entity with high-security hardware and software, connected to the client over a secure channel. In both cases, the
// Relying Party receives the authenticator data in the same format, and uses its knowledge of the authenticator to
// make trust decisions.
type AuthenticatorData struct {
// SHA-256 hash of the RP ID associated with the credential.
RPIDHash []byte
// Flags
Flags AuthenticatorDataFlags
// Signature counter, 32-bit unsigned big-endian integer.
SignCount uint32
// attested credential data (if present). See §6.4.1 Attested credential data for details. Its length depends on the
// length of the credential ID and credential public key being attested.
AttestedCredentialData AttestedCredentialData
// Raw contains the raw bytes of this AuthenticatorData.
Raw []byte
}
// IsValid checks whether the AuthenticatorData is valid. If relyingPartyID is empty, the relying party will not be
// checked (INSEUCRE). If the data is invalid, an error is returned, usually of the type Error.
func (a AuthenticatorData) IsValid(relyingPartyID string) error {
// Verify that the RP ID hash in authData is indeed the SHA-256 hash of the RP ID expected by the RP
rpHash := sha256.Sum256([]byte(relyingPartyID))
if relyingPartyID != "" && !bytes.Equal(rpHash[:], a.RPIDHash) {
return ErrInvalidOrigin.WithDebugf("hash %X did not match required %X", a.RPIDHash, rpHash[:])
}
// Verify that the User Present bit of the flags in authData is set
if !a.Flags.UserPresent() {
return ErrNoUserPresent
}
return nil
}
var _ encoding.BinaryUnmarshaler = (*AuthenticatorData)(nil)
var _ encoding.BinaryMarshaler = (*AuthenticatorData)(nil)
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
func (a *AuthenticatorData) UnmarshalBinary(authData []byte) error {
if len(authData) < 37 {
return ErrInvalidRequest.WithDebug("invalid authenticator data")
}
a.RPIDHash = authData[0:32]
a.Flags = AuthenticatorDataFlags(authData[32])
a.SignCount = binary.BigEndian.Uint32(authData[33:37])
if a.Flags.HasAttestedCredentialData() && len(authData) > 37 {
a.AttestedCredentialData.AAGUID = authData[37:53]
credentialIDLength := binary.BigEndian.Uint16(authData[53:55])
a.AttestedCredentialData.CredentialID = authData[55 : 55+credentialIDLength]
var err error
a.AttestedCredentialData.COSEKey, err = cose.ParseCOSE(authData[55+credentialIDLength:])
if err != nil {
return ErrInvalidRequest.WithDebugf("unable to parse COSE key: %v", err.Error())
}
}
a.Raw = authData
return nil
}
// MarshalBinary implements the encoding.BinaryMarshaler interface.
func (a *AuthenticatorData) MarshalBinary() ([]byte, error) {
return nil, fmt.Errorf("unsupported operation")
}
// AuthenticatorDataFlags are the flags that are present in the authenticator data.
type AuthenticatorDataFlags byte
const (
// AuthenticatorDataFlagUserPresent indicates the UP flag.
AuthenticatorDataFlagUserPresent = 0x001 // 0000 0001
// AuthenticatorDataFlagUserVerified indicates the UV flag.
AuthenticatorDataFlagUserVerified = 0x004 // 0000 0100
// AuthenticatorDataFlagHasCredentialData indicates the AT flag.
AuthenticatorDataFlagHasCredentialData = 0x040 // 0100 0000
// AuthenticatorDataFlagHasExtension indicates the ED flag.
AuthenticatorDataFlagHasExtension = 0x080 // 1000 0000
)
// UserPresent returns whether the UP flag is set.
func (f AuthenticatorDataFlags) UserPresent() bool {
return (f & AuthenticatorDataFlagUserPresent) == AuthenticatorDataFlagUserPresent
}
// UserVerified returns whether the UV flag is set.
func (f AuthenticatorDataFlags) UserVerified() bool {
return (f & AuthenticatorDataFlagUserVerified) == AuthenticatorDataFlagUserVerified
}
// HasAttestedCredentialData returns whether the AT flag is set.
func (f AuthenticatorDataFlags) HasAttestedCredentialData() bool {
return (f & AuthenticatorDataFlagHasCredentialData) == AuthenticatorDataFlagHasCredentialData
}
// HasExtensions returns whether the ED flag is set.
func (f AuthenticatorDataFlags) HasExtensions() bool {
return (f & AuthenticatorDataFlagHasExtension) == AuthenticatorDataFlagHasExtension
}
// AttestedCredentialData represents the AttestedCredentialData type in the WebAuthn specification.
// https://www.w3.org/TR/webauthn/#attested-credential-data
type AttestedCredentialData struct {
// The AAGUID of the authenticator.
AAGUID []byte
// A probabilistically-unique byte sequence identifying a public key credential source and its authentication
// assertions.
CredentialID []byte
// The decoded credential public key.
COSEKey interface{}
}
================================================
FILE: protocol/doc.go
================================================
// protocol is a low-level package that closely resembles the WebAuthn specification. You should prefer to use the
// webauthn package. The main methods in this package are ParseAttestationResponse, ParseAssertionResponse,
// IsValidAssertion and IsValidAttestation.
//
// The version of the specification that is implemented is https://www.w3.org/TR/2018/CR-webauthn-20180807/.
package protocol // import "github.com/koesie10/webauthn/protocol"
================================================
FILE: protocol/errors.go
================================================
package protocol
import (
"fmt"
"net/http"
"github.com/pkg/errors"
)
// Default errors
var (
ErrInvalidSignature = &Error{
Name: "invalid_signature",
Description: "The signature is invalid",
Hint: "Check that the provided token is in the correct format",
Code: http.StatusUnauthorized,
}
ErrInvalidRequest = &Error{
Name: "invalid_request",
Description: "The request is malformed",
Hint: "Make sure that the parameters provided are correct",
Code: http.StatusBadRequest,
}
ErrUnsupportedAttestationFormat = &Error{
Name: "unsupported_attestation_format",
Description: "The attestation format is unsupported",
Code: http.StatusBadRequest,
}
ErrInvalidAttestation = &Error{
Name: "invalid_attestation",
Description: "The attestation is malformed",
Hint: "Check that you provided a token in the right format.",
Code: http.StatusBadRequest,
}
ErrInvalidType = &Error{
Name: "invalid_type",
Description: "The attestion/assertion type is invalid",
Hint: "Check that the client data was submitted for the right call",
Code: http.StatusBadRequest,
}
ErrInvalidChallenge = &Error{
Name: "invalid_challenge",
Description: "The challenge is invalid",
Hint: "Check that the challenge was supplied for the right request",
Code: http.StatusBadRequest,
}
ErrInvalidOrigin = &Error{
Name: "invalid_origin",
Description: "The origin is invalid",
Code: http.StatusBadRequest,
}
ErrNoUserPresent = &Error{
Name: "no_user_present",
Description: "No user was presented during authentication",
Code: http.StatusBadRequest,
}
)
// Error is a representation of errors returned from this package.
type Error struct {
// Name is the name of this error.
Name string `json:"error"`
// Description is the description of this error.
Description string `json:"description"`
// Hint contains further information about the error.
Hint string `json:"hint,omitempty"`
// Code contains the status code that should be returned when this error is returned.
Code int `json:"status_code,omitempty"`
// Debug contains debug information about this error that should not be shown to the user.
Debug string `json:"debug,omitempty"`
// Cause contains the error that caused this error, if available
Cause error `json:"-"`
}
// ToWebAuthnError converts any error into the *Error type. If that is not possible, it will return an *Error
// which wraps the error.
func ToWebAuthnError(err error) *Error {
if e, ok := err.(*Error); ok {
return e
} else if e, ok := errors.Cause(err).(*Error); ok {
return e
}
return &Error{
Name: "error",
Description: "This error was not recognized",
Debug: err.Error(),
Code: http.StatusInternalServerError,
}
}
// Error implements the error interface.
func (e *Error) Error() string {
return e.Name
}
// WithHintf will add/replace the hint of the error.
func (e *Error) WithHintf(hint string, args ...interface{}) *Error {
return e.WithHint(fmt.Sprintf(hint, args...))
}
// WithHint will add/replace the hint of the error.
func (e *Error) WithHint(hint string) *Error {
err := *e
err.Hint = hint
return &err
}
// WithDebugf will add/replace the debug information of the error.
func (e *Error) WithDebugf(debug string, args ...interface{}) *Error {
return e.WithDebug(fmt.Sprintf(debug, args...))
}
// WithDebug will add/replace the debug information of the error.
func (e *Error) WithDebug(debug string) *Error {
err := *e
err.Debug = debug
return &err
}
func (e *Error) WithCause(cause error) *Error {
err := *e
err.Cause = cause
return &err
}
================================================
FILE: protocol/webauthn_test.go
================================================
package protocol_test
import (
"crypto/x509"
"encoding/json"
"fmt"
"testing"
"github.com/koesie10/webauthn/protocol"
)
func TestIsValidAssertion(t *testing.T) {
for i := range assertionRequests {
t.Run(fmt.Sprintf("Run %d", i), func(t *testing.T) {
rawAttestation := protocol.AttestationResponse{}
if err := json.Unmarshal([]byte(attestationResponses[i]), &rawAttestation); err != nil {
t.Fatal(err)
}
attestation, err := protocol.ParseAttestationResponse(rawAttestation)
if err != nil {
t.Fatal(err)
}
publicKey := attestation.Response.Attestation.AuthData.AttestedCredentialData.COSEKey
cert := &x509.Certificate{
PublicKey: publicKey,
}
r := protocol.CredentialCreationOptions{}
if err := json.Unmarshal([]byte(assertionRequests[i]), &r); err != nil {
t.Fatal(err)
}
b := protocol.AssertionResponse{}
if err := json.Unmarshal([]byte(assertionResponses[i]), &b); err != nil {
t.Fatal(err)
}
p, err := protocol.ParseAssertionResponse(b)
if err != nil {
t.Fatal(err)
}
d, err := protocol.IsValidAssertion(p, r.PublicKey.Challenge, "", "", cert)
if err != nil {
t.Fatal(err)
}
if !d {
t.Fatal("is not valid")
}
})
}
}
var attestationResponses = []string{
`{"id":"LOXI3xfiLvIP04MD_S2ZmJYwn3cvMX1FUXxiQO7xlfUvrfcj99UVO2aMrMAwsGvsujY7NHWiM6G3B6ryKJDBBdab-cl4tVZeOwOMhgvHLXk","rawId":"LOXI3xfiLvIP04MD/S2ZmJYwn3cvMX1FUXxiQO7xlfUvrfcj99UVO2aMrMAwsGvsujY7NHWiM6G3B6ryKJDBBdab+cl4tVZeOwOMhgvHLXk=","response":{"attestationObject":"o2dhdHRTdG10omNzaWdYRjBEAiAJ8Q7i8DQzKlb00g4Wby4PoEjlI+s3bS+kVKI3PKoyXQIgDzcP2c5vpplZdmftN+zUDNfXtG1TniWbJv2+6kGZ8bljeDVjgVkBKzCCAScwgc6gAwIBAgIBADAKBggqhkjOPQQDAjAWMRQwEgYDVQQDDAtLcnlwdG9uIEtleTAeFw0xODA5MTcxODQ3NDJaFw0yODA5MTcxODQ3NDJaMBYxFDASBgNVBAMMC0tyeXB0b24gS2V5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwzIpvM5A6mZQXYxRIhfp0sb/21yTcr/sp5Y5DU0IWODQf5ldS2rlDCl62yEaQDM9Akxbsay/vA/S5ut4VSsvoKMNMAswCQYDVR0TBAIwADAKBggqhkjOPQQDAgNIADBFAiA4Yx+5MtKVnjme6V3qXKQ2qcgaHfO6DMgXM9kwOCZcNAIhAJdNk5PPSA04ITfrX9HQy5azo8sH9yhkW7c6gLdb/Kz+aGF1dGhEYXRhWNRJlg3liA6MaHQ0Fw9kdmBbj+SuuaKGMseZXPO6gx2XY0EAAAAALOXI3xfiLvIP04MD/S2ZmABQLOXI3xfiLvIP04MD/S2ZmJYwn3cvMX1FUXxiQO7xlfUvrfcj99UVO2aMrMAwsGvsujY7NHWiM6G3B6ryKJDBBdab+cl4tVZeOwOMhgvHLXmlAQIDJiABIVggwzIpvM5A6mZQXYxRIhfp0sb/21yTcr/sp5Y5DU0IWOAiWCDQf5ldS2rlDCl62yEaQDM9Akxbsay/vA/S5ut4VSsvoGNmbXRoZmlkby11MmY=","clientDataJSON":"eyJjaGFsbGVuZ2UiOiItMWpReXNud2FJak5VLUdyd1JwNFBXTkJNbFgwaTlfY2FSa2NLZDdMUGo4IiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo1Mzg3OSIsInRva2VuQmluZGluZyI6eyJzdGF0dXMiOiJub3Qtc3VwcG9ydGVkIn0sInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ=="},"type":"public-key"}`,
`{"id":"SNBSJTt1DHEuG9XBd6lfc4XXqxkppWfFbt4P5sRVQEPIPANIHHCmPo1AwY5pkUGcpVL3W-uHyWEn4vbgzp34Qw","rawId":"SNBSJTt1DHEuG9XBd6lfc4XXqxkppWfFbt4P5sRVQEPIPANIHHCmPo1AwY5pkUGcpVL3W+uHyWEn4vbgzp34Qw==","response":{"attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIgFls/elhmdZmqEBEKafdcyvQPDrTdBRMW92v6RKJj1bACIQCZ+46sXn65dMEpPuGxvMUruV5i7XN25ctFV/iAi3wSomN4NWOBWQLCMIICvjCCAaagAwIBAgIEdIb9wjANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbzELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTk1NTAwMzg0MjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJVd8633JH0xde/9nMTzGk6HjrrhgQlWYVD7OIsuX2Unv1dAmqWBpQ0KxS8YRFwKE1SKE1PIpOWacE5SO8BN6+2jbDBqMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4xMBMGCysGAQQBguUcAgEBBAQDAgUgMCEGCysGAQQBguUcAQEEBBIEEPigEfOMCk0VgAYXER+e3H0wDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAMVxIgOaaUn44Zom9af0KqG9J655OhUVBVW+q0As6AIod3AH5bHb2aDYakeIyyBCnnGMHTJtuekbrHbXYXERIn4aKdkPSKlyGLsA/A+WEi+OAfXrNVfjhrh7iE6xzq0sg4/vVJoywe4eAJx0fS+Dl3axzTTpYl71Nc7p/NX6iCMmdik0pAuYJegBcTckE3AoYEg4K99AM/JaaKIblsbFh8+3LxnemeNf7UwOczaGGvjS6UzGVI0Odf9lKcPIwYhuTxM5CaNMXTZQ7xq4/yTfC3kPWtE4hFT34UJJflZBiLrxG4OsYxkHw/n5vKgmpspB3GfYuYTWhkDKiE8CYtyg87mhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NBAAAAA/igEfOMCk0VgAYXER+e3H0AQEjQUiU7dQxxLhvVwXepX3OF16sZKaVnxW7eD+bEVUBDyDwDSBxwpj6NQMGOaZFBnKVS91vrh8lhJ+L24M6d+EOlAQIDJiABIVggLxxTguKmjCV4N5OMqd2Sl9AIxSltaPevmQxSqnyNlAciWCDEHOaQDaZ6pC2gC+Z0KS4Ln/XQiJp0X1BmTd+K+FdqSg==","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJKVXRsWWNncGtTaUZOenNUaERZdU9ydFNWWTFWZUxvZk0tbVdUUkNDWHFVIiwibmV3X2tleXNfbWF5X2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0="},"type":"public-key"}`,
`{"id":"EBT1LOefp-8ID0n2jchlyaPrKcWZ6jdHH8nb0Z-hi9JHsOpTpCNUbJ7ijJOKdetLOy2cqdxNq8zkWYmCgpapKg","rawId":"EBT1LOefp+8ID0n2jchlyaPrKcWZ6jdHH8nb0Z+hi9JHsOpTpCNUbJ7ijJOKdetLOy2cqdxNq8zkWYmCgpapKg==","response":{"attestationObject":"o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEgwRgIhAJkpVpWsMm/Z1OnF/+B/juq/IAlKqhakms5HkNf6ZKLWAiEAm2qNX/bHUkkdaJ0seanz5xxVDCn+bKGEPyQP3ZpPczNjeDVjgVkCUzCCAk8wggE3oAMCAQICBA0ACxYwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMDExLzAtBgNVBAMMJll1YmljbyBVMkYgRUUgU2VyaWFsIDIzOTI1NzM0MDE1NzY1MjcwMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETKz6btEEuhlL1uBm1+E/zGpgDxDSSFx+o9vUTNDVDbJROHujvR665t7mJQoFWMbpvmEYpEOOWkNfHtLrDOi7haM7MDkwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjUwEwYLKwYBBAGC5RwCAQEEBAMCBSAwDQYJKoZIhvcNAQELBQADggEBAI7CTaiBlYLnMIQZnJ8UCvrqgFuin80CTT4UAiGWsBwh0eY+CRSwL4LEFZITkLlFYyOsfMDlI7oddSN/Jmn8HzrPWvzKVP/+mCuRMSdz735wFNYX5xle+NLkoctZjyHOCqdd4B8lgX0nzwNiPZuf+sdY5fhzhLRmtbpfBDToTP57tLR5WlIY6kJ6QKecpZ5sVNxCzSVxRncAptZV7YSsX2we05Kt5mHkBHqhi5CTPQQmOObHov7cB+4q5CpufDzEBFTKPL3tWxV6HvQr0J6Mp6bZFICq5nTP7VPatnnJelRA9VmPSpQuLjpRqpJFKRobj8eQ9yuveXG/7uutBOzBHW9oYXV0aERhdGFYxEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjQQAAAAAAAAAAAAAAAAAAAAAAAAAAAEAQFPUs55+n7wgPSfaNyGXJo+spxZnqN0cfydvRn6GL0kew6lOkI1RsnuKMk4p160s7LZyp3E2rzORZiYKClqkqpQECAyYgASFYIF6oiA6H+mU150XH7WJ2vnzNmdzgr5YloPao7ePjNjlOIlggg0f3u4CtxsBkkKjo7v4luyJui9tJ1rGTBF3YkYlcADo=","clientDataJSON":"eyJjaGFsbGVuZ2UiOiIySHpBbFBJR3NrYm41M2hCSlplSDNrWjZYZmNIV01uemJBVFZHX0ZTZ2tJIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo5MDAwIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9"},"type":"public-key"}`,
// Android SafetyNet
`{"id":"ARKBRFD84uLN6qG_rHsV0K2Bh9Lj3_HaJsXdC_DpPslKO6ZWmD38-hz90Lf_MzELErMa9AqR21Sr9brNzE2un1U","rawId":"ARKBRFD84uLN6qG/rHsV0K2Bh9Lj3/HaJsXdC/DpPslKO6ZWmD38+hz90Lf/MzELErMa9AqR21Sr9brNzE2un1U=","response":{"attestationObject":"o2NmbXRxYW5kcm9pZC1zYWZldHluZXRnYXR0U3RtdKJjdmVyaDE0MzY2MDE5aHJlc3BvbnNlWRS9ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbmcxWXlJNld5Sk5TVWxHYTJwRFEwSkljV2RCZDBsQ1FXZEpVVkpZY205T01GcFBaRkpyUWtGQlFVRkJRVkIxYm5wQlRrSm5hM0ZvYTJsSE9YY3dRa0ZSYzBaQlJFSkRUVkZ6ZDBOUldVUldVVkZIUlhkS1ZsVjZSV1ZOUW5kSFFURlZSVU5vVFZaU01qbDJXako0YkVsR1VubGtXRTR3U1VaT2JHTnVXbkJaTWxaNlRWSk5kMFZSV1VSV1VWRkVSWGR3U0ZaR1RXZFJNRVZuVFZVNGVFMUNORmhFVkVVMFRWUkJlRTFFUVROTlZHc3dUbFp2V0VSVVJUVk5WRUYzVDFSQk0wMVVhekJPVm05M1lrUkZURTFCYTBkQk1WVkZRbWhOUTFaV1RYaEZla0ZTUW1kT1ZrSkJaMVJEYTA1b1lrZHNiV0l6U25WaFYwVjRSbXBCVlVKblRsWkNRV05VUkZVeGRtUlhOVEJaVjJ4MVNVWmFjRnBZWTNoRmVrRlNRbWRPVmtKQmIxUkRhMlIyWWpKa2MxcFRRazFVUlUxNFIzcEJXa0puVGxaQ1FVMVVSVzFHTUdSSFZucGtRelZvWW0xU2VXSXliR3RNYlU1MllsUkRRMEZUU1hkRVVWbEtTMjlhU1doMlkwNUJVVVZDUWxGQlJHZG5SVkJCUkVORFFWRnZRMmRuUlVKQlRtcFlhM293WlVzeFUwVTBiU3N2UnpWM1QyOHJXRWRUUlVOeWNXUnVPRGh6UTNCU04yWnpNVFJtU3pCU2FETmFRMWxhVEVaSWNVSnJOa0Z0V2xaM01rczVSa2N3VHpseVVsQmxVVVJKVmxKNVJUTXdVWFZ1VXpsMVowaEROR1ZuT1c5MmRrOXRLMUZrV2pKd09UTllhSHAxYmxGRmFGVlhXRU40UVVSSlJVZEtTek5UTW1GQlpucGxPVGxRVEZNeU9XaE1ZMUYxV1ZoSVJHRkROMDlhY1U1dWIzTnBUMGRwWm5NNGRqRnFhVFpJTDNob2JIUkRXbVV5YkVvck4wZDFkSHBsZUV0d2VIWndSUzkwV2xObVlsazVNRFZ4VTJ4Q2FEbG1jR293TVRWamFtNVJSbXRWYzBGVmQyMUxWa0ZWZFdWVmVqUjBTMk5HU3pSd1pYWk9UR0Y0UlVGc0swOXJhV3hOZEVsWlJHRmpSRFZ1Wld3MGVFcHBlWE0wTVROb1lXZHhWekJYYUdnMVJsQXpPV2hIYXpsRkwwSjNVVlJxWVhwVGVFZGtkbGd3YlRaNFJsbG9hQzh5VmsxNVdtcFVORXQ2VUVwRlEwRjNSVUZCWVU5RFFXeG5kMmRuU2xWTlFUUkhRVEZWWkVSM1JVSXZkMUZGUVhkSlJtOUVRVlJDWjA1V1NGTlZSVVJFUVV0Q1oyZHlRbWRGUmtKUlkwUkJWRUZOUW1kT1ZraFNUVUpCWmpoRlFXcEJRVTFDTUVkQk1WVmtSR2RSVjBKQ1VYRkNVWGRIVjI5S1FtRXhiMVJMY1hWd2J6UlhObmhVTm1veVJFRm1RbWRPVmtoVFRVVkhSRUZYWjBKVFdUQm1hSFZGVDNaUWJTdDRaMjU0YVZGSE5rUnlabEZ1T1V0NlFtdENaMmR5UW1kRlJrSlJZMEpCVVZKWlRVWlpkMHAzV1VsTGQxbENRbEZWU0UxQlIwZEhNbWd3WkVoQk5reDVPWFpaTTA1M1RHNUNjbUZUTlc1aU1qbHVUREprTUdONlJuWk5WRUZ5UW1kbmNrSm5SVVpDVVdOM1FXOVpabUZJVWpCalJHOTJURE5DY21GVE5XNWlNamx1VERKa2VtTnFTWFpTTVZKVVRWVTRlRXh0VG5sa1JFRmtRbWRPVmtoU1JVVkdha0ZWWjJoS2FHUklVbXhqTTFGMVdWYzFhMk50T1hCYVF6VnFZakl3ZDBsUldVUldVakJuUWtKdmQwZEVRVWxDWjFwdVoxRjNRa0ZuU1hkRVFWbExTM2RaUWtKQlNGZGxVVWxHUVhwQmRrSm5UbFpJVWpoRlMwUkJiVTFEVTJkSmNVRm5hR2cxYjJSSVVuZFBhVGgyV1ROS2MweHVRbkpoVXpWdVlqSTVia3d3WkZWVmVrWlFUVk0xYW1OdGQzZG5aMFZGUW1kdmNrSm5SVVZCWkZvMVFXZFJRMEpKU0RGQ1NVaDVRVkJCUVdSM1EydDFVVzFSZEVKb1dVWkpaVGRGTmt4TldqTkJTMUJFVjFsQ1VHdGlNemRxYW1RNE1FOTVRVE5qUlVGQlFVRlhXbVJFTTFCTVFVRkJSVUYzUWtsTlJWbERTVkZEVTFwRFYyVk1Tblp6YVZaWE5rTm5LMmRxTHpsM1dWUktVbnAxTkVocGNXVTBaVmswWXk5dGVYcHFaMGxvUVV4VFlta3ZWR2g2WTNweGRHbHFNMlJyTTNaaVRHTkpWek5NYkRKQ01HODNOVWRSWkdoTmFXZGlRbWRCU0ZWQlZtaFJSMjFwTDFoM2RYcFVPV1ZIT1ZKTVNTdDRNRm95ZFdKNVdrVldla0UzTlZOWlZtUmhTakJPTUVGQlFVWnRXRkU1ZWpWQlFVRkNRVTFCVW1wQ1JVRnBRbU5EZDBFNWFqZE9WRWRZVURJM09IbzBhSEl2ZFVOSWFVRkdUSGx2UTNFeVN6QXJlVXhTZDBwVlltZEpaMlk0WjBocWRuQjNNbTFDTVVWVGFuRXlUMll6UVRCQlJVRjNRMnR1UTJGRlMwWlZlVm8zWmk5UmRFbDNSRkZaU2t0dldrbG9kbU5PUVZGRlRFSlJRVVJuWjBWQ1FVazVibFJtVWt0SlYyZDBiRmRzTTNkQ1REVTFSVlJXTm10aGVuTndhRmN4ZVVGak5VUjFiVFpZVHpReGExcDZkMG8yTVhkS2JXUlNVbFF2VlhORFNYa3hTMFYwTW1Nd1JXcG5iRzVLUTBZeVpXRjNZMFZYYkV4UldUSllVRXg1Um1wclYxRk9ZbE5vUWpGcE5GY3lUbEpIZWxCb2RETnRNV0kwT1doaWMzUjFXRTAyZEZnMVEzbEZTRzVVYURoQ2IyMDBMMWRzUm1sb2VtaG5iamd4Ukd4a2IyZDZMMHN5VlhkTk5sTTJRMEl2VTBWNGEybFdabllyZW1KS01ISnFkbWM1TkVGc1pHcFZabFYzYTBrNVZrNU5ha1ZRTldVNGVXUkNNMjlNYkRabmJIQkRaVVkxWkdkbVUxZzBWVGw0TXpWdmFpOUpTV1F6VlVVdlpGQndZaTl4WjBkMmMydG1aR1Y2ZEcxVmRHVXZTMU50Y21sM1kyZFZWMWRsV0daVVlra3plbk5wYTNkYVltdHdiVkpaUzIxcVVHMW9kalJ5YkdsNlIwTkhkRGhRYmpod2NUaE5Na3RFWmk5UU0ydFdiM1F6WlRFNFVUMGlMQ0pOU1VsRlUycERRMEY2UzJkQmQwbENRV2RKVGtGbFR6QnRjVWRPYVhGdFFrcFhiRkYxUkVGT1FtZHJjV2hyYVVjNWR6QkNRVkZ6UmtGRVFrMU5VMEYzU0dkWlJGWlJVVXhGZUdSSVlrYzVhVmxYZUZSaFYyUjFTVVpLZG1JelVXZFJNRVZuVEZOQ1UwMXFSVlJOUWtWSFFURlZSVU5vVFV0U01uaDJXVzFHYzFVeWJHNWlha1ZVVFVKRlIwRXhWVVZCZUUxTFVqSjRkbGx0Um5OVk1teHVZbXBCWlVaM01IaE9la0V5VFZSVmQwMUVRWGRPUkVwaFJuY3dlVTFVUlhsTlZGVjNUVVJCZDA1RVNtRk5SVWw0UTNwQlNrSm5UbFpDUVZsVVFXeFdWRTFTTkhkSVFWbEVWbEZSUzBWNFZraGlNamx1WWtkVloxWklTakZqTTFGblZUSldlV1J0YkdwYVdFMTRSWHBCVWtKblRsWkNRVTFVUTJ0a1ZWVjVRa1JSVTBGNFZIcEZkMmRuUldsTlFUQkhRMU54UjFOSllqTkVVVVZDUVZGVlFVRTBTVUpFZDBGM1oyZEZTMEZ2U1VKQlVVUlJSMDA1UmpGSmRrNHdOWHByVVU4NUszUk9NWEJKVW5aS2VucDVUMVJJVnpWRWVrVmFhRVF5WlZCRGJuWlZRVEJSYXpJNFJtZEpRMlpMY1VNNVJXdHpRelJVTW1aWFFsbHJMMnBEWmtNelVqTldXazFrVXk5a1RqUmFTME5GVUZwU2NrRjZSSE5wUzFWRWVsSnliVUpDU2pWM2RXUm5lbTVrU1UxWlkweGxMMUpIUjBac05YbFBSRWxMWjJwRmRpOVRTa2d2VlV3clpFVmhiSFJPTVRGQ2JYTkxLMlZSYlUxR0t5dEJZM2hIVG1oeU5UbHhUUzg1YVd3M01Va3laRTQ0UmtkbVkyUmtkM1ZoWldvMFlsaG9jREJNWTFGQ1ltcDRUV05KTjBwUU1HRk5NMVEwU1N0RWMyRjRiVXRHYzJKcWVtRlVUa001ZFhwd1JteG5UMGxuTjNKU01qVjRiM2x1VlhoMk9IWk9iV3R4TjNwa1VFZElXR3Q0VjFrM2IwYzVhaXRLYTFKNVFrRkNhemRZY2twbWIzVmpRbHBGY1VaS1NsTlFhemRZUVRCTVMxY3dXVE42Tlc5Nk1rUXdZekYwU2t0M1NFRm5UVUpCUVVkcVoyZEZlazFKU1VKTWVrRlBRbWRPVmtoUk9FSkJaamhGUWtGTlEwRlpXWGRJVVZsRVZsSXdiRUpDV1hkR1FWbEpTM2RaUWtKUlZVaEJkMFZIUTBOelIwRlJWVVpDZDAxRFRVSkpSMEV4VldSRmQwVkNMM2RSU1UxQldVSkJaamhEUVZGQmQwaFJXVVJXVWpCUFFrSlpSVVpLYWxJclJ6UlJOamdyWWpkSFEyWkhTa0ZpYjA5ME9VTm1NSEpOUWpoSFFURlZaRWwzVVZsTlFtRkJSa3AyYVVJeFpHNUlRamRCWVdkaVpWZGlVMkZNWkM5alIxbFpkVTFFVlVkRFEzTkhRVkZWUmtKM1JVSkNRMnQzU25wQmJFSm5aM0pDWjBWR1FsRmpkMEZaV1ZwaFNGSXdZMFJ2ZGt3eU9XcGpNMEYxWTBkMGNFeHRaSFppTW1OMldqTk9lVTFxUVhsQ1owNVdTRkk0UlV0NlFYQk5RMlZuU21GQmFtaHBSbTlrU0ZKM1QyazRkbGt6U25OTWJrSnlZVk0xYm1JeU9XNU1NbVI2WTJwSmRsb3pUbmxOYVRWcVkyMTNkMUIzV1VSV1VqQm5Ra1JuZDA1cVFUQkNaMXB1WjFGM1FrRm5TWGRMYWtGdlFtZG5ja0puUlVaQ1VXTkRRVkpaWTJGSVVqQmpTRTAyVEhrNWQyRXlhM1ZhTWpsMlduazVlVnBZUW5aak1td3dZak5LTlV4NlFVNUNaMnR4YUd0cFJ6bDNNRUpCVVhOR1FVRlBRMEZSUlVGSGIwRXJUbTV1TnpoNU5uQlNhbVE1V0d4UlYwNWhOMGhVWjJsYUwzSXpVazVIYTIxVmJWbElVRkZ4TmxOamRHazVVRVZoYW5aM1VsUXlhVmRVU0ZGeU1ESm1aWE54VDNGQ1dUSkZWRlYzWjFwUksyeHNkRzlPUm5ab2MwODVkSFpDUTA5SllYcHdjM2RYUXpsaFNqbDRhblUwZEZkRVVVZzRUbFpWTmxsYVdpOVlkR1ZFVTBkVk9WbDZTbkZRYWxrNGNUTk5SSGh5ZW0xeFpYQkNRMlkxYnpodGR5OTNTalJoTWtjMmVIcFZjalpHWWpaVU9FMWpSRTh5TWxCTVVrdzJkVE5OTkZSNmN6TkJNazB4YWpaaWVXdEtXV2s0ZDFkSlVtUkJka3RNVjFwMUwyRjRRbFppZWxsdGNXMTNhMjAxZWt4VFJGYzFia2xCU21KRlRFTlJRMXAzVFVnMU5uUXlSSFp4YjJaNGN6WkNRbU5EUmtsYVZWTndlSFUyZURaMFpEQldOMU4yU2tORGIzTnBjbE50U1dGMGFpODVaRk5UVmtSUmFXSmxkRGh4THpkVlN6UjJORnBWVGpnd1lYUnVXbm94ZVdjOVBTSmRmUS5leUp1YjI1alpTSTZJbEJQT1U0MWVrY3pZbTlOUms1Nk5UbHFWRU01ZG1vdlp6QkxlalExTjJoRVMyOTRTREZzZURSbVlWazlJaXdpZEdsdFpYTjBZVzF3VFhNaU9qRTFOREEwTURZeU16RTBOellzSW1Gd2ExQmhZMnRoWjJWT1lXMWxJam9pWTI5dExtZHZiMmRzWlM1aGJtUnliMmxrTG1kdGN5SXNJbUZ3YTBScFoyVnpkRk5vWVRJMU5pSTZJbVZSWXl0MmVsVmpaSGd3UmxaT1RIWllTSFZIY0VRd0sxSTRNRGR6VlVWMmNDdEtaV3hsV1ZwemFVRTlJaXdpWTNSelVISnZabWxzWlUxaGRHTm9JanAwY25WbExDSmhjR3REWlhKMGFXWnBZMkYwWlVScFoyVnpkRk5vWVRJMU5pSTZXeUk0VURGelZ6QkZVRXBqYzJ4M04xVjZVbk5wV0V3Mk5IY3JUelV3UldRclVrSkpRM1JoZVRGbk1qUk5QU0pkTENKaVlYTnBZMGx1ZEdWbmNtbDBlU0k2ZEhKMVpYMC5qaFE5a3RMLVVCdEJpX2NQNXd4SGRFejJZWXNGMXBLWXpqOEZUZXdXbVVvWE5CTWJSOWRBaEdDSnJCZ2w4RGZNSzFrMUJFQXdQUzRTMWJVczBXQ3haYmN4cWJtcS12UF9OWHI1QjlDQXkxUUpxdC1tRlRMUm5EZkZ1a2hfQjdZMUxEZUJaaGYtc1E4WnBfQUlncHRnYlBWa2Z6TE1PQVpkeE5xVk91dmU0YmJTSjQ5bWQwVklBbDkwc3h1YXVUT0x5bFpxN2ZhYXRZMGFqQ1VKZkNpRUJiLVZxSUhOZmQySExhaUZwcjVxRUFPeU8tQmx1V204TmVfSWZiMnFkTVZBa2p1a1YyVmZheElLbG93a05HZ2ZycjFjVVpJNE5oVTNfeDhLTjRGclpQd29tT2ZFdmlHWk9tZFRvNnNPSXpROTJVYkx2MXlGYUw1TDZIZ1I2Z1NnX0FoYXV0aERhdGFYxSpD77HzPafHbmULkXmwl2mw9P/lQRkLyNgWFO1qQIjVRQAAAAAAAAAAAAAAAAAAAAAAAAAAAEEBEoFEUPzi4s3qob+sexXQrYGH0uPf8domxd0L8Ok+yUo7plaYPfz6HP3Qt/8zMQsSsxr0CpHbVKv1us3MTa6fVaUBAgMmIAEhWCDavINo/+JM4T1eJeKjaZ+vGa2Do7YVh2EyD0vtmoZrrCJYIOtAindNbNXogAQxBAJii2Vd1Wl5rZb9KPak8J6iTKle","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiZDNjWTFJNm4xYXI2Z0xwREVoVGk1bkJnUDF4d0lHc2I2SE1fTlI4UEsxbyIsIm9yaWdpbiI6Imh0dHBzOlwvXC9iMzk5ZmEwMC5uZ3Jvay5pbyIsImFuZHJvaWRQYWNrYWdlTmFtZSI6ImNvbS5hbmRyb2lkLmNocm9tZSJ9"},"type":"public-key"}`,
}
var assertionRequests = []string{
`{"publicKey":{"allowCredentials":[{"id":"LOXI3xfiLvIP04MD/S2ZmJYwn3cvMX1FUXxiQO7xlfUvrfcj99UVO2aMrMAwsGvsujY7NHWiM6G3B6ryKJDBBdab+cl4tVZeOwOMhgvHLXk=","type":"public-key"}],"challenge":"+c0hMsULvTWp6ASl45YyOQRA/yVVK60XccCQ+Vui9j8=","timeout":10000}}`,
`{"publicKey":{"challenge":"mcPXIDRHSPBF2gJWU58GPrR3TodLDXR1kHJhgVanYnU=","timeout":30000,"allowCredentials":[{"type":"public-key","id":"SNBSJTt1DHEuG9XBd6lfc4XXqxkppWfFbt4P5sRVQEPIPANIHHCmPo1AwY5pkUGcpVL3W+uHyWEn4vbgzp34Qw=="}]}}`,
`{"publicKey":{"challenge":"/hXFS7WKYWTgqEx5AOG7SuGL3+6alkqi2TJkTu+MkBM=","timeout":30000,"allowCredentials":[{"type":"public-key","id":"EBT1LOefp+8ID0n2jchlyaPrKcWZ6jdHH8nb0Z+hi9JHsOpTpCNUbJ7ijJOKdetLOy2cqdxNq8zkWYmCgpapKg=="}]}}`,
// Android SafetyNet
`{"publicKey":{"challenge":"HC33hV7jFYx6m4hUkvNF0GLVn2WihTilaEtniha+Qvw=","timeout":30000,"allowCredentials":[{"type":"public-key","id":"ARKBRFD84uLN6qG/rHsV0K2Bh9Lj3/HaJsXdC/DpPslKO6ZWmD38+hz90Lf/MzELErMa9AqR21Sr9brNzE2un1U="}]}}`,
}
var assertionResponses = []string{
`{"id":"LOXI3xfiLvIP04MD_S2ZmJYwn3cvMX1FUXxiQO7xlfUvrfcj99UVO2aMrMAwsGvsujY7NHWiM6G3B6ryKJDBBdab-cl4tVZeOwOMhgvHLXk","rawId":"LOXI3xfiLvIP04MD/S2ZmJYwn3cvMX1FUXxiQO7xlfUvrfcj99UVO2aMrMAwsGvsujY7NHWiM6G3B6ryKJDBBdab+cl4tVZeOwOMhgvHLXk=","response":{"clientDataJSON":"eyJjaGFsbGVuZ2UiOiItYzBoTXNVTHZUV3A2QVNsNDVZeU9RUkFfeVZWSzYwWGNjQ1EtVnVpOWo4IiwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUzODc5IiwidHlwZSI6IndlYmF1dGhuLmdldCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MBAAAAAQ==","signature":"MEYCIQD7W6TPIviP+BztYxEMsan/esy/O0S4pJO+9QxDaA0ehAIhANo5D+5UxwbtJGFcvSryl0+RdJd3j4lIKVhEe7WpvZeV","userHandle":""},"type":"public-key"}`,
`{"id":"SNBSJTt1DHEuG9XBd6lfc4XXqxkppWfFbt4P5sRVQEPIPANIHHCmPo1AwY5pkUGcpVL3W-uHyWEn4vbgzp34Qw","rawId":"SNBSJTt1DHEuG9XBd6lfc4XXqxkppWfFbt4P5sRVQEPIPANIHHCmPo1AwY5pkUGcpVL3W+uHyWEn4vbgzp34Qw==","response":{"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJtY1BYSURSSFNQQkYyZ0pXVTU4R1ByUjNUb2RMRFhSMWtISmhnVmFuWW5VIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo5MDAwIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MBAAAABA==","signature":"MEUCIQCWGnyWIV4s13/9TRcLtDesxa0UJs+pwNaF3YDP/5RHDwIgIWlEiH74R7sPiyNffp8Tof3qo1s8jVvFDxCGejlICFI=","userHandle":""},"type":"public-key"}`,
`{"id":"EBT1LOefp-8ID0n2jchlyaPrKcWZ6jdHH8nb0Z-hi9JHsOpTpCNUbJ7ijJOKdetLOy2cqdxNq8zkWYmCgpapKg","rawId":"EBT1LOefp+8ID0n2jchlyaPrKcWZ6jdHH8nb0Z+hi9JHsOpTpCNUbJ7ijJOKdetLOy2cqdxNq8zkWYmCgpapKg==","response":{"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJfaFhGUzdXS1lXVGdxRXg1QU9HN1N1R0wzLTZhbGtxaTJUSmtUdS1Na0JNIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo5MDAwIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9","authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MBAAAAAA==","signature":"MEUCIFGAxD82g/HBEQc2qblhIQsOCvMIuFzmiT54uMSCwYg6AiEAuuIUy6PyaW43xEpAnqrPcCPmUiJwpJ7IV/h6OGjqN2E=","userHandle":""},"type":"public-key"}`,
// Android SafetyNet
`{"id":"ARKBRFD84uLN6qG_rHsV0K2Bh9Lj3_HaJsXdC_DpPslKO6ZWmD38-hz90Lf_MzELErMa9AqR21Sr9brNzE2un1U","rawId":"ARKBRFD84uLN6qG/rHsV0K2Bh9Lj3/HaJsXdC/DpPslKO6ZWmD38+hz90Lf/MzELErMa9AqR21Sr9brNzE2un1U=","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiSEMzM2hWN2pGWXg2bTRoVWt2TkYwR0xWbjJXaWhUaWxhRXRuaWhhLVF2dyIsIm9yaWdpbiI6Imh0dHBzOlwvXC9iMzk5ZmEwMC5uZ3Jvay5pbyIsImFuZHJvaWRQYWNrYWdlTmFtZSI6ImNvbS5hbmRyb2lkLmNocm9tZSJ9","authenticatorData":"KkPvsfM9p8duZQuRebCXabD0/+VBGQvI2BYU7WpAiNUFAAAAAQ==","signature":"MEQCIBapcKD8L5Kp92QBr4XpHNwiRPjo/MGTEIEwCsklxfvAAiABn02rbcatTqHFtHwbnHwdNOLa5apxCBRuFPPwABBm3w==","userHandle":""},"type":"public-key"}`,
}
================================================
FILE: webauthn/config.go
================================================
package webauthn
import "fmt"
var defaultSessionKeyPrefixChallenge = "webauthn.challenge"
var defaultSessionKeyPrefixUserID = "webauthn.user.id"
// Config holds all the configuration for WebAuthn
type Config struct {
// RelyingPartyName is a human-palatable identifier for the Relying Party, intended only for display. For example,
// "ACME Corporation", "Wonderful Widgets, Inc." or "ОАО Примертех".
RelyingPartyName string
// RelyingPartyID is a unique identifier for the Relying Party entity. It must be a valid domain string that
// identifies the Relying Party on whose behalf a registration or login is being performed. A public key credential
// can only be used for authentication with the same RP ID it was registered with.
// By default, it is set to the caller's origin effective domain. It may be overridden, as long as the RP ID is
// a registrable domain suffix or is equal to the caller's effective domain.
// For example, given a Relying Party whose origin is https://login.example.com:1337, then the following RP IDs
// are valid: login.example.com (default) and example.com, but not m.login.example.com and not com.
// In production, this value should be set. If it is not set, the implementation is INSECURE and the RP ID hash
// supplied by the authenticator will not be checked.
RelyingPartyID string
// RelyingPartyOrigin is the RP origin that an authenticator response will be compared with. If it is empty,
// the value will be ignored. However, this is INSECURE and should not be used in production.
// For example, given a Relying Party whose origin is https://login.example.com:1337, this value should be set
// to "https://login.example.com:1337".
RelyingPartyOrigin string
// AuthenticatorStore will be used to store authenticators of a user.
AuthenticatorStore AuthenticatorStore
// SessionKeyPrefixChallenge holds the prefix of the key of the challenge in the session. If it is not set, it will
// be set to "webauthn.challenge".
SessionKeyPrefixChallenge string
// SessionKeyPrefixUserID holds the prefix of the key of the user ID in the session. If it is not set, it will be
// set to "webauthn.user.id".
SessionKeyPrefixUserID string
// Timeout is the amount of time in milliseconds the user will be permitted to authenticate with their device on
// registration and login. The default is 30000, i.e. 30 seconds.
Timeout uint
// Debug sets a few settings related to ease of debugging, such as sharing more error information to clients.
Debug bool
}
// Validate validates that all required fields in Config are set.
func (c *Config) Validate() error {
if c.RelyingPartyName == "" {
return fmt.Errorf("missing RelyingPartyName")
}
if c.AuthenticatorStore == nil {
return fmt.Errorf("missing AuthenticatorStore")
}
if c.SessionKeyPrefixChallenge == "" {
c.SessionKeyPrefixChallenge = defaultSessionKeyPrefixChallenge
}
if c.SessionKeyPrefixUserID == "" {
c.SessionKeyPrefixUserID = defaultSessionKeyPrefixUserID
}
if c.Timeout == 0 {
c.Timeout = 30000
}
return nil
}
================================================
FILE: webauthn/doc.go
================================================
// webauthn is a high-level package that contains HTTP request handlers which can be used to implement webauthn
// in any application.
//
package webauthn // import "github.com/koesie10/webauthn/webauthn"
================================================
FILE: webauthn/login.go
================================================
package webauthn
import (
"bytes"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"net/http"
"github.com/koesie10/webauthn/protocol"
)
// GetLoginOptions will return the options that need to be passed to navigator.credentials.get(). This should
// be returned to the user via e.g. JSON over HTTP. For convenience, use StartLogin.
func (w *WebAuthn) GetLoginOptions(user User, session Session) (*protocol.CredentialRequestOptions, error) {
chal, err := protocol.NewChallenge()
if err != nil {
return nil, err
}
options := &protocol.CredentialRequestOptions{
PublicKey: protocol.PublicKeyCredentialRequestOptions{
Challenge: chal,
Timeout: w.Config.Timeout,
},
}
if user != nil {
authenticators, err := w.Config.AuthenticatorStore.GetAuthenticators(user)
if err != nil {
return nil, err
}
allowCredentials := make([]protocol.PublicKeyCredentialDescriptor, len(authenticators))
for i, authr := range authenticators {
allowCredentials[i] = protocol.PublicKeyCredentialDescriptor{
ID: authr.WebAuthCredentialID(),
Type: protocol.PublicKeyCredentialTypePublicKey,
}
}
options.PublicKey.AllowCredentials = allowCredentials
}
if err := session.Set(w.Config.SessionKeyPrefixChallenge+".login", []byte(chal)); err != nil {
return nil, err
}
return options, nil
}
// StartLogin is a HTTP request handler which writes the options to be passed to navigator.credentials.get()
// to the http.ResponseWriter. The user argument is optional and can be nil, in which case the allowCredentials
// option will not be set and AuthenticatorStore.GetAuthenticators will not be called.
func (w *WebAuthn) StartLogin(r *http.Request, rw http.ResponseWriter, user User, session Session) {
options, err := w.GetLoginOptions(user, session)
if err != nil {
w.writeError(r, rw, err)
return
}
w.write(r, rw, options)
}
// ParseAndFinishLogin should receive the response of navigator.credentials.get(). If
// user is non-nil, it will be checked that the authenticator is owned by that user. If the request is valid,
// the authenticator will be returned. For convenience, use FinishLogin.
func (w *WebAuthn) ParseAndFinishLogin(assertionResponse protocol.AssertionResponse, user User, session Session) (Authenticator, error) {
rawChal, err := session.Get(w.Config.SessionKeyPrefixChallenge + ".login")
if err != nil {
return nil, protocol.ErrInvalidRequest.WithDebug("missing challenge in session")
}
chal, ok := rawChal.([]byte)
if !ok {
return nil, protocol.ErrInvalidRequest.WithDebug("invalid challenge session value")
}
if err := session.Delete(w.Config.SessionKeyPrefixChallenge + ".login"); err != nil {
return nil, err
}
p, err := protocol.ParseAssertionResponse(assertionResponse)
if err != nil {
return nil, err
}
// 1. If the allowCredentials option was given when this authentication ceremony was initiated, verify that
// credential.id identifies one of the public key credentials that were listed in allowCredentials.
if user != nil {
authenticators, err := w.Config.AuthenticatorStore.GetAuthenticators(user)
if err != nil {
return nil, err
}
var authrFound bool
for _, authr := range authenticators {
if bytes.Equal(authr.WebAuthID(), p.RawID) {
authrFound = true
break
}
}
if !authrFound {
return nil, protocol.ErrInvalidRequest.WithDebug("authenticator is not owned by user")
}
}
// 2. If credential.response.userHandle is present, verify that the user identified by this value is the owner of
// the public key credential identified by credential.id.
if p.Response.UserHandle != nil && len(p.Response.UserHandle) > 0 {
if user != nil {
if !bytes.Equal(p.Response.UserHandle, user.WebAuthID()) {
return nil, protocol.ErrInvalidRequest.WithDebug("authenticator's user handle does not equal user ID")
}
} else {
authenticators, err := w.Config.AuthenticatorStore.GetAuthenticators(&defaultUser{id: p.Response.UserHandle})
if err != nil {
return nil, err
}
var authrFound bool
for _, authr := range authenticators {
if bytes.Equal(authr.WebAuthID(), p.RawID) {
authrFound = true
break
}
}
if !authrFound {
return nil, protocol.ErrInvalidRequest.WithDebug("authenticator is not owned by user")
}
}
}
// Using credential’s id attribute (or the corresponding rawId, if base64url encoding is inappropriate for your use
// case), look up the corresponding credential public key.
authr, err := w.Config.AuthenticatorStore.GetAuthenticator(p.RawID)
if err != nil {
return nil, err
}
block, _ := pem.Decode(authr.WebAuthPublicKey())
if block == nil {
return nil, fmt.Errorf("invalid stored public key, unable to decode")
}
cert, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
valid, err := protocol.IsValidAssertion(p, chal, w.Config.RelyingPartyID, w.Config.RelyingPartyOrigin, &x509.Certificate{
PublicKey: cert,
})
if err != nil {
return nil, err
}
if !valid {
return nil, protocol.ErrInvalidRequest.WithDebug("invalid login")
}
return authr, nil
}
// FinishLogin is a HTTP request handler which should receive the response of navigator.credentials.get(). If
// user is non-nil, it will be checked that the authenticator is owned by that user. If the request is valid,
// the authenticator will be returned and nothing will have been written to http.ResponseWriter. If authenticator is
// nil, an error has been written to http.ResponseWriter and should be returned as-is.
func (w *WebAuthn) FinishLogin(r *http.Request, rw http.ResponseWriter, user User, session Session) Authenticator {
var assertionResponse protocol.AssertionResponse
d := json.NewDecoder(r.Body)
d.DisallowUnknownFields()
if err := d.Decode(&assertionResponse); err != nil {
w.writeError(r, rw, protocol.ErrInvalidRequest.WithDebug(err.Error()))
return nil
}
authr, err := w.ParseAndFinishLogin(assertionResponse, user, session)
if err != nil {
w.writeError(r, rw, err)
return nil
}
return authr
}
================================================
FILE: webauthn/registration.go
================================================
package webauthn
import (
"bytes"
"crypto/x509"
"encoding/json"
"encoding/pem"
"net/http"
"github.com/koesie10/webauthn/protocol"
)
// GetRegistrationOptions will return the options that need to be passed to navigator.credentials.create(). This should
// be returned to the user via e.g. JSON over HTTP. For convenience, use StartRegistration.
func (w *WebAuthn) GetRegistrationOptions(user User, session Session) (*protocol.CredentialCreationOptions, error) {
chal, err := protocol.NewChallenge()
if err != nil {
return nil, err
}
u := protocol.PublicKeyCredentialUserEntity{
ID: user.WebAuthID(),
PublicKeyCredentialEntity: protocol.PublicKeyCredentialEntity{
Name: user.WebAuthName(),
},
DisplayName: user.WebAuthDisplayName(),
}
options := &protocol.CredentialCreationOptions{
PublicKey: protocol.PublicKeyCredentialCreationOptions{
Challenge: chal,
RP: protocol.PublicKeyCredentialRpEntity{
ID: w.Config.RelyingPartyID,
PublicKeyCredentialEntity: protocol.PublicKeyCredentialEntity{
Name: w.Config.RelyingPartyName,
},
},
PubKeyCredParams: []protocol.PublicKeyCredentialParameters{
{
Type: protocol.PublicKeyCredentialTypePublicKey,
Algorithm: protocol.ES256,
},
},
Timeout: w.Config.Timeout,
User: u,
Attestation: protocol.AttestationConveyancePreferenceDirect,
},
}
authenticators, err := w.Config.AuthenticatorStore.GetAuthenticators(user)
if err != nil {
return nil, err
}
excludeCredentials := make([]protocol.PublicKeyCredentialDescriptor, len(authenticators))
for i, authr := range authenticators {
excludeCredentials[i] = protocol.PublicKeyCredentialDescriptor{
ID: authr.WebAuthCredentialID(),
Type: protocol.PublicKeyCredentialTypePublicKey,
}
}
options.PublicKey.ExcludeCredentials = excludeCredentials
if err := session.Set(w.Config.SessionKeyPrefixChallenge+".register", []byte(chal)); err != nil {
return nil, err
}
if err := session.Set(w.Config.SessionKeyPrefixUserID+".register", u.ID); err != nil {
return nil, err
}
return options, nil
}
// StartRegistration is a HTTP request handler which writes the options to be passed to navigator.credentials.create()
// to the http.ResponseWriter.
func (w *WebAuthn) StartRegistration(r *http.Request, rw http.ResponseWriter, user User, session Session) {
options, err := w.GetRegistrationOptions(user, session)
if err != nil {
w.writeError(r, rw, err)
return
}
w.write(r, rw, options)
}
// ParseAndFinishRegistration should receive the response of navigator.credentials.create(). If
// the request is valid, AuthenticatorStore.AddAuthenticator will be called and the authenticator that was registered
// will be returned. For convenience, use FinishRegistration.
func (w *WebAuthn) ParseAndFinishRegistration(attestationResponse protocol.AttestationResponse, user User, session Session) (Authenticator, error) {
rawChal, err := session.Get(w.Config.SessionKeyPrefixChallenge + ".register")
if err != nil {
return nil, protocol.ErrInvalidRequest.WithDebug("missing challenge in session")
}
chal, ok := rawChal.([]byte)
if !ok {
return nil, protocol.ErrInvalidRequest.WithDebug("invalid challenge session value")
}
if err := session.Delete(w.Config.SessionKeyPrefixChallenge + ".register"); err != nil {
return nil, err
}
rawUserID, err := session.Get(w.Config.SessionKeyPrefixUserID + ".register")
if err != nil {
return nil, protocol.ErrInvalidRequest.WithDebug("missing user ID in session")
}
userID, ok := rawUserID.([]byte)
if !ok {
return nil, protocol.ErrInvalidRequest.WithDebug("invalid user ID session value")
}
if err := session.Delete(w.Config.SessionKeyPrefixUserID + ".register"); err != nil {
return nil, err
}
if !bytes.Equal(user.WebAuthID(), userID) {
return nil, protocol.ErrInvalidRequest.WithDebug("user has changed since start of registration")
}
p, err := protocol.ParseAttestationResponse(attestationResponse)
if err != nil {
return nil, err
}
valid, err := protocol.IsValidAttestation(p, chal, w.Config.RelyingPartyID, w.Config.RelyingPartyOrigin)
if err != nil {
return nil, err
}
if !valid {
return nil, protocol.ErrInvalidRequest.WithDebug("invalid registration")
}
data, err := x509.MarshalPKIXPublicKey(p.Response.Attestation.AuthData.AttestedCredentialData.COSEKey)
if err != nil {
return nil, err
}
authr := &defaultAuthenticator{
id: p.RawID,
credentialID: p.Response.Attestation.AuthData.AttestedCredentialData.CredentialID,
publicKey: pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: data,
}),
aaguid: p.Response.Attestation.AuthData.AttestedCredentialData.AAGUID,
signCount: p.Response.Attestation.AuthData.SignCount,
}
if err := w.Config.AuthenticatorStore.AddAuthenticator(user, authr); err != nil {
return nil, err
}
return authr, nil
}
// FinishRegistration is a HTTP request handler which should receive the response of navigator.credentials.create(). If
// the request is valid, AuthenticatorStore.AddAuthenticator will be called and an empty response with HTTP status code
// 201 (Created) will be written to the http.ResponseWriter. If authenticator is nil, an error has been written to
// http.ResponseWriter and should be returned as-is.
func (w *WebAuthn) FinishRegistration(r *http.Request, rw http.ResponseWriter, user User, session Session) Authenticator {
var attestationResponse protocol.AttestationResponse
d := json.NewDecoder(r.Body)
d.DisallowUnknownFields()
if err := d.Decode(&attestationResponse); err != nil {
w.writeError(r, rw, protocol.ErrInvalidRequest.WithDebug(err.Error()))
return nil
}
authr, err := w.ParseAndFinishRegistration(attestationResponse, user, session)
if err != nil {
w.writeError(r, rw, err)
return nil
}
rw.WriteHeader(http.StatusCreated)
return authr
}
================================================
FILE: webauthn/session.go
================================================
package webauthn
// Session will be used by the request handlers to save temporary data, such as the challenge and user ID.
type Session interface {
Set(name string, value interface{}) error
Get(name string) (interface{}, error)
Delete(name string) error
}
var _ Session = (*mapSession)(nil)
type mapSession struct {
Values map[interface{}]interface{}
}
func (s *mapSession) Get(name string) (interface{}, error) {
return s.Values[name], nil
}
func (s *mapSession) Set(name string, value interface{}) error {
s.Values[name] = value
return nil
}
func (s *mapSession) Delete(name string) error {
delete(s.Values, name)
return nil
}
// WrapMap can be used to create a Session for e.g. a gorilla/sessions type.
func WrapMap(values map[interface{}]interface{}) Session {
return &mapSession{values}
}
================================================
FILE: webauthn/user.go
================================================
package webauthn
// User should be implemented by users used in the request handlers.
type User interface {
// WebAuthID should return the ID of the user. This could for example be the binary encoding of an int.
WebAuthID() []byte
// WebAuthName should return the name of the user.
WebAuthName() string
// WebAuthDisplayName should return the display name of the user.
WebAuthDisplayName() string
}
// Authenticator represents an authenticator that can be used by a user.
type Authenticator interface {
WebAuthID() []byte
WebAuthCredentialID() []byte
WebAuthPublicKey() []byte
WebAuthAAGUID() []byte
WebAuthSignCount() uint32
}
// AuthenticatorStore should be implemented by the storage layer to store authenticators.
type AuthenticatorStore interface {
// AddAuthenticator should add the given authenticator to a user. The authenticator's type should not be depended
// on; it is constructed by this package. All information should be stored in a way such that it is retrievable
// in the future using GetAuthenticator and GetAuthenticators.
AddAuthenticator(user User, authenticator Authenticator) error
// GetAuthenticator gets a single Authenticator by the given id, as returned by Authenticator.WebAuthID.
GetAuthenticator(id []byte) (Authenticator, error)
// GetAuthenticators gets a list of all registered authenticators for this user. It might be the case that the user
// has been constructed by this package and the only non-empty value is the WebAuthID. In this case, the store
// should still return the authenticators as specified by the ID.
GetAuthenticators(user User) ([]Authenticator, error)
}
type defaultUser struct {
id []byte
}
var _ User = (*defaultUser)(nil)
func (u *defaultUser) WebAuthID() []byte {
return u.id
}
func (u *defaultUser) WebAuthName() string {
return "default"
}
func (u *defaultUser) WebAuthDisplayName() string {
return "default"
}
type defaultAuthenticator struct {
id []byte
credentialID []byte
publicKey []byte
aaguid []byte
signCount uint32
}
var _ Authenticator = (*defaultAuthenticator)(nil)
func (a *defaultAuthenticator) WebAuthID() []byte {
return a.id
}
func (a *defaultAuthenticator) WebAuthCredentialID() []byte {
return a.credentialID
}
func (a *defaultAuthenticator) WebAuthPublicKey() []byte {
return a.publicKey
}
func (a *defaultAuthenticator) WebAuthAAGUID() []byte {
return a.aaguid
}
func (a *defaultAuthenticator) WebAuthSignCount() uint32 {
return a.signCount
}
================================================
FILE: webauthn/webauthn.go
================================================
package webauthn
import (
"encoding/json"
"fmt"
"net/http"
"github.com/koesie10/webauthn/protocol"
"github.com/pkg/errors"
)
// WebAuthn is the primary interface of this package and contains the request handlers that should be called.
type WebAuthn struct {
Config *Config
}
// New creates a new WebAuthn based on the given Config. The Config will be validated and an error will be returned
// if it is invalid.
func New(c *Config) (*WebAuthn, error) {
if err := c.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate config: %v", err)
}
return &WebAuthn{
Config: c,
}, nil
}
func (w *WebAuthn) write(r *http.Request, rw http.ResponseWriter, res interface{}) {
w.writeCode(r, rw, http.StatusOK, res)
}
func (w *WebAuthn) writeCode(r *http.Request, rw http.ResponseWriter, code int, res interface{}) {
js, err := json.Marshal(res)
if err != nil {
w.writeError(r, rw, err)
return
}
if code == 0 {
code = http.StatusOK
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(code)
rw.Write(js)
}
func (w *WebAuthn) writeError(r *http.Request, rw http.ResponseWriter, err error) {
if v, ok := errors.Cause(err).(*protocol.Error); ok {
w.writeErrorCode(r, rw, v.Code, err)
return
}
w.writeErrorCode(r, rw, http.StatusInternalServerError, err)
}
func (w *WebAuthn) writeErrorCode(r *http.Request, rw http.ResponseWriter, code int, err error) {
e := protocol.ToWebAuthnError(err)
if code == 0 {
code = http.StatusInternalServerError
}
if !w.Config.Debug {
e.Debug = ""
}
js, err := json.Marshal(e)
if err != nil {
w.writeError(r, rw, err)
return
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(code)
rw.Write(js)
}
================================================
FILE: webauthn.js
================================================
class WebAuthn {
// Decode a base64 string into a Uint8Array.
static _decodeBuffer(value) {
return Uint8Array.from(atob(value), c => c.charCodeAt(0));
}
// Encode an ArrayBuffer into a base64 string.
static _encodeBuffer(value) {
return btoa(new Uint8Array(value).reduce((s, byte) => s + String.fromCharCode(byte), ''));
}
// Checks whether the status returned matches the status given.
static _checkStatus(status) {
return res => {
if (res.status === status) {
return res;
}
throw new Error(res.statusText);
};
}
register() {
return fetch('/webauthn/registration/start', {
method: 'POST'
})
.then(WebAuthn._checkStatus(200))
.then(res => res.json())
.then(res => {
res.publicKey.challenge = WebAuthn._decodeBuffer(res.publicKey.challenge);
res.publicKey.user.id = WebAuthn._decodeBuffer(res.publicKey.user.id);
if (res.publicKey.excludeCredentials) {
for (var i = 0; i < res.publicKey.excludeCredentials.length; i++) {
res.publicKey.excludeCredentials[i].id = WebAuthn._decodeBuffer(res.publicKey.excludeCredentials[i].id);
}
}
return res;
})
.then(res => navigator.credentials.create(res))
.then(credential => {
return fetch('/webauthn/registration/finish', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: credential.id,
rawId: WebAuthn._encodeBuffer(credential.rawId),
response: {
attestationObject: WebAuthn._encodeBuffer(credential.response.attestationObject),
clientDataJSON: WebAuthn._encodeBuffer(credential.response.clientDataJSON)
},
type: credential.type
}),
})
})
.then(WebAuthn._checkStatus(201));
}
login() {
return fetch('/webauthn/login/start', {
method: 'POST'
})
.then(WebAuthn._checkStatus(200))
.then(res => res.json())
.then(res => {
res.publicKey.challenge = WebAuthn._decodeBuffer(res.publicKey.challenge);
if (res.publicKey.allowCredentials) {
for (let i = 0; i < res.publicKey.allowCredentials.length; i++) {
res.publicKey.allowCredentials[i].id = WebAuthn._decodeBuffer(res.publicKey.allowCredentials[i].id);
}
}
return res;
})
.then(res => navigator.credentials.get(res))
.then(credential => {
return fetch('/webauthn/login/finish', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: credential.id,
rawId: WebAuthn._encodeBuffer(credential.rawId),
response: {
clientDataJSON: WebAuthn._encodeBuffer(credential.response.clientDataJSON),
authenticatorData: WebAuthn._encodeBuffer(credential.response.authenticatorData),
signature: WebAuthn._encodeBuffer(credential.response.signature),
userHandle: WebAuthn._encodeBuffer(credential.response.userHandle),
},
type: credential.type
}),
})
})
.then(WebAuthn._checkStatus(200));
}
}
gitextract_a82pg2ux/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── attestation/ │ ├── androidsafetynet/ │ │ ├── androidsafetynet.go │ │ └── androidsafetynet_test.go │ ├── attestion.go │ ├── fido/ │ │ ├── fido.go │ │ └── fido_test.go │ └── packed/ │ ├── packed.go │ └── packed_test.go ├── cose/ │ ├── cose.go │ ├── cose_test.go │ ├── doc.go │ └── ecdsa.go ├── go.mod ├── go.sum ├── protocol/ │ ├── api.go │ ├── assertion.go │ ├── attestation.go │ ├── attestation_registry.go │ ├── challenge.go │ ├── common.go │ ├── doc.go │ ├── errors.go │ └── webauthn_test.go ├── webauthn/ │ ├── config.go │ ├── doc.go │ ├── login.go │ ├── registration.go │ ├── session.go │ ├── user.go │ └── webauthn.go └── webauthn.js
SYMBOL INDEX (143 symbols across 24 files)
FILE: attestation/androidsafetynet/androidsafetynet.go
function init (line 19) | func init() {
type AndroidSafetyNetAttestionResponse (line 23) | type AndroidSafetyNetAttestionResponse struct
function verifyAndroidSafetynet (line 33) | func verifyAndroidSafetynet(a protocol.Attestation, clientDataHash []byt...
FILE: attestation/androidsafetynet/androidsafetynet_test.go
function TestIsValidAttestation (line 12) | func TestIsValidAttestation(t *testing.T) {
FILE: attestation/fido/fido.go
function init (line 12) | func init() {
function verifyFIDO (line 16) | func verifyFIDO(a protocol.Attestation, clientDataHash []byte) error {
FILE: attestation/fido/fido_test.go
function TestIsValidAttestation (line 11) | func TestIsValidAttestation(t *testing.T) {
FILE: attestation/packed/packed.go
function init (line 16) | func init() {
function verifyPacked (line 22) | func verifyPacked(a protocol.Attestation, clientDataHash []byte) error {
function verifyBasic (line 57) | func verifyBasic(a protocol.Attestation, clientDataHash []byte, alg prot...
function verifyECDAA (line 133) | func verifyECDAA(a protocol.Attestation, clientDataHash []byte, alg prot...
function verifySelf (line 137) | func verifySelf(a protocol.Attestation, clientDataHash []byte, alg proto...
FILE: attestation/packed/packed_test.go
function TestIsValidAttestation (line 11) | func TestIsValidAttestation(t *testing.T) {
FILE: cose/cose.go
function ParseCOSE (line 20) | func ParseCOSE(buf []byte) (interface{}, error) {
function ParseCOSEMap (line 33) | func ParseCOSEMap(m map[int]interface{}) (interface{}, error) {
FILE: cose/cose_test.go
function TestParseCOSE (line 10) | func TestParseCOSE(t *testing.T) {
FILE: cose/ecdsa.go
function parseECDSA (line 9) | func parseECDSA(alg int64, m map[int]interface{}) (interface{}, error) {
function parseECDSAPublicKey (line 39) | func parseECDSAPublicKey(curve elliptic.Curve, m map[int]interface{}) (*...
FILE: protocol/api.go
type CredentialCreationOptions (line 5) | type CredentialCreationOptions struct
type CredentialRequestOptions (line 11) | type CredentialRequestOptions struct
type PublicKeyCredentialCreationOptions (line 17) | type PublicKeyCredentialCreationOptions struct
type PublicKeyCredentialRequestOptions (line 63) | type PublicKeyCredentialRequestOptions struct
type PublicKeyCredentialRpEntity (line 89) | type PublicKeyCredentialRpEntity struct
type PublicKeyCredentialEntity (line 98) | type PublicKeyCredentialEntity struct
type PublicKeyCredentialUserEntity (line 106) | type PublicKeyCredentialUserEntity struct
type PublicKeyCredentialType (line 124) | type PublicKeyCredentialType
constant PublicKeyCredentialTypePublicKey (line 128) | PublicKeyCredentialTypePublicKey PublicKeyCredentialType = "public-key"
type COSEAlgorithmIdentifier (line 135) | type COSEAlgorithmIdentifier
constant ES256 (line 139) | ES256 COSEAlgorithmIdentifier = -7
constant RS256 (line 141) | RS256 COSEAlgorithmIdentifier = -257
type AuthenticatorTransport (line 151) | type AuthenticatorTransport
constant AuthenticatorTransportUSB (line 155) | AuthenticatorTransportUSB AuthenticatorTransport = "usb"
constant AuthenticatorTransportNFC (line 157) | AuthenticatorTransportNFC = "nfc"
constant AuthenticatorTransportBLE (line 159) | AuthenticatorTransportBLE = "ble"
constant AuthenticatorTransportInternal (line 162) | AuthenticatorTransportInternal = "internal"
type PublicKeyCredentialParameters (line 167) | type PublicKeyCredentialParameters struct
type PublicKeyCredentialDescriptor (line 179) | type PublicKeyCredentialDescriptor struct
type AuthenticatorSelectionCriteria (line 192) | type AuthenticatorSelectionCriteria struct
type AuthenticatorAttachment (line 211) | type AuthenticatorAttachment
constant AuthenticatorAttachmentPlatform (line 215) | AuthenticatorAttachmentPlatform AuthenticatorAttachment = "platform"
constant AuthenticatorAttachmentCrossPlatform (line 217) | AuthenticatorAttachmentCrossPlatform = "cross-platform"
type UserVerificationRequirement (line 223) | type UserVerificationRequirement
constant UserVerificationRequired (line 228) | UserVerificationRequired UserVerificationRequirement = "required"
constant UserVerificationPreferred (line 231) | UserVerificationPreferred = "preferred"
constant UserVerificationDiscouraged (line 234) | UserVerificationDiscouraged = "discouraged"
type AttestationConveyancePreference (line 240) | type AttestationConveyancePreference
constant AttestationConveyancePreferenceNone (line 246) | AttestationConveyancePreferenceNone = "none"
constant AttestationConveyancePreferenceIndirect (line 252) | AttestationConveyancePreferenceIndirect = "indirect"
constant AttestationConveyancePreferenceDirect (line 255) | AttestationConveyancePreferenceDirect = "direct"
type AuthenticationExtensionsClientInputs (line 261) | type AuthenticationExtensionsClientInputs
FILE: protocol/assertion.go
type AssertionResponse (line 11) | type AssertionResponse struct
type ParsedAssertionResponse (line 19) | type ParsedAssertionResponse struct
type AuthenticatorAssertionResponse (line 32) | type AuthenticatorAssertionResponse struct
type ParsedAuthenticatorAssertionResponse (line 46) | type ParsedAuthenticatorAssertionResponse struct
function ParseAssertionResponse (line 61) | func ParseAssertionResponse(p AssertionResponse) (ParsedAssertionRespons...
function IsValidAssertion (line 89) | func IsValidAssertion(p ParsedAssertionResponse, originalChallenge []byt...
FILE: protocol/attestation.go
type AttestationResponse (line 13) | type AttestationResponse struct
type ParsedAttestationResponse (line 21) | type ParsedAttestationResponse struct
type AuthenticatorAttestationResponse (line 34) | type AuthenticatorAttestationResponse struct
type ParsedAuthenticatorAttestationResponse (line 48) | type ParsedAuthenticatorAttestationResponse struct
type Attestation (line 66) | type Attestation struct
method IsValid (line 122) | func (a Attestation) IsValid(relyingPartyID string, clientDataHash []b...
function ParseAttestationResponse (line 75) | func ParseAttestationResponse(p AttestationResponse) (ParsedAttestationR...
function IsValidAttestation (line 102) | func IsValidAttestation(p ParsedAttestationResponse, originalChallenge [...
FILE: protocol/attestation_registry.go
type AttestationFormatFunction (line 4) | type AttestationFormatFunction
function RegisterFormat (line 10) | func RegisterFormat(name string, f AttestationFormatFunction) {
FILE: protocol/challenge.go
constant ChallengeSize (line 6) | ChallengeSize = 32
type Challenge (line 10) | type Challenge
function NewChallenge (line 13) | func NewChallenge() (Challenge, error) {
FILE: protocol/common.go
type PublicKeyCredential (line 18) | type PublicKeyCredential struct
type ParsedPublicKeyCredential (line 30) | type ParsedPublicKeyCredential struct
type AuthenticatorResponse (line 42) | type AuthenticatorResponse struct
type ParsedAuthenticatorResponse (line 50) | type ParsedAuthenticatorResponse struct
type CollectedClientData (line 60) | type CollectedClientData struct
method IsValid (line 102) | func (c CollectedClientData) IsValid(requiredType string, originalChal...
type TokenBinding (line 78) | type TokenBinding struct
type TokenBindingStatus (line 88) | type TokenBindingStatus
constant TokenBindingStatusPresent (line 93) | TokenBindingStatusPresent TokenBindingStatus = "present"
constant TokenBindingStatusSupported (line 96) | TokenBindingStatusSupported = "supported"
type AuthenticatorData (line 139) | type AuthenticatorData struct
method IsValid (line 155) | func (a AuthenticatorData) IsValid(relyingPartyID string) error {
method UnmarshalBinary (line 174) | func (a *AuthenticatorData) UnmarshalBinary(authData []byte) error {
method MarshalBinary (line 202) | func (a *AuthenticatorData) MarshalBinary() ([]byte, error) {
type AuthenticatorDataFlags (line 207) | type AuthenticatorDataFlags
method UserPresent (line 221) | func (f AuthenticatorDataFlags) UserPresent() bool {
method UserVerified (line 226) | func (f AuthenticatorDataFlags) UserVerified() bool {
method HasAttestedCredentialData (line 231) | func (f AuthenticatorDataFlags) HasAttestedCredentialData() bool {
method HasExtensions (line 236) | func (f AuthenticatorDataFlags) HasExtensions() bool {
constant AuthenticatorDataFlagUserPresent (line 211) | AuthenticatorDataFlagUserPresent = 0x001
constant AuthenticatorDataFlagUserVerified (line 213) | AuthenticatorDataFlagUserVerified = 0x004
constant AuthenticatorDataFlagHasCredentialData (line 215) | AuthenticatorDataFlagHasCredentialData = 0x040
constant AuthenticatorDataFlagHasExtension (line 217) | AuthenticatorDataFlagHasExtension = 0x080
type AttestedCredentialData (line 242) | type AttestedCredentialData struct
FILE: protocol/errors.go
type Error (line 60) | type Error struct
method Error (line 92) | func (e *Error) Error() string {
method WithHintf (line 97) | func (e *Error) WithHintf(hint string, args ...interface{}) *Error {
method WithHint (line 102) | func (e *Error) WithHint(hint string) *Error {
method WithDebugf (line 109) | func (e *Error) WithDebugf(debug string, args ...interface{}) *Error {
method WithDebug (line 114) | func (e *Error) WithDebug(debug string) *Error {
method WithCause (line 120) | func (e *Error) WithCause(cause error) *Error {
function ToWebAuthnError (line 77) | func ToWebAuthnError(err error) *Error {
FILE: protocol/webauthn_test.go
function TestIsValidAssertion (line 12) | func TestIsValidAssertion(t *testing.T) {
FILE: webauthn.js
class WebAuthn (line 1) | class WebAuthn {
method _decodeBuffer (line 3) | static _decodeBuffer(value) {
method _encodeBuffer (line 8) | static _encodeBuffer(value) {
method _checkStatus (line 13) | static _checkStatus(status) {
method register (line 22) | register() {
method login (line 60) | login() {
FILE: webauthn/config.go
type Config (line 9) | type Config struct
method Validate (line 48) | func (c *Config) Validate() error {
FILE: webauthn/login.go
method GetLoginOptions (line 16) | func (w *WebAuthn) GetLoginOptions(user User, session Session) (*protoco...
method StartLogin (line 57) | func (w *WebAuthn) StartLogin(r *http.Request, rw http.ResponseWriter, u...
method ParseAndFinishLogin (line 70) | func (w *WebAuthn) ParseAndFinishLogin(assertionResponse protocol.Assert...
method FinishLogin (line 172) | func (w *WebAuthn) FinishLogin(r *http.Request, rw http.ResponseWriter, ...
FILE: webauthn/registration.go
method GetRegistrationOptions (line 15) | func (w *WebAuthn) GetRegistrationOptions(user User, session Session) (*...
method StartRegistration (line 78) | func (w *WebAuthn) StartRegistration(r *http.Request, rw http.ResponseWr...
method ParseAndFinishRegistration (line 91) | func (w *WebAuthn) ParseAndFinishRegistration(attestationResponse protoc...
method FinishRegistration (line 161) | func (w *WebAuthn) FinishRegistration(r *http.Request, rw http.ResponseW...
FILE: webauthn/session.go
type Session (line 4) | type Session interface
type mapSession (line 12) | type mapSession struct
method Get (line 16) | func (s *mapSession) Get(name string) (interface{}, error) {
method Set (line 20) | func (s *mapSession) Set(name string, value interface{}) error {
method Delete (line 25) | func (s *mapSession) Delete(name string) error {
function WrapMap (line 31) | func WrapMap(values map[interface{}]interface{}) Session {
FILE: webauthn/user.go
type User (line 4) | type User interface
type Authenticator (line 14) | type Authenticator interface
type AuthenticatorStore (line 23) | type AuthenticatorStore interface
type defaultUser (line 36) | type defaultUser struct
method WebAuthID (line 42) | func (u *defaultUser) WebAuthID() []byte {
method WebAuthName (line 46) | func (u *defaultUser) WebAuthName() string {
method WebAuthDisplayName (line 50) | func (u *defaultUser) WebAuthDisplayName() string {
type defaultAuthenticator (line 54) | type defaultAuthenticator struct
method WebAuthID (line 64) | func (a *defaultAuthenticator) WebAuthID() []byte {
method WebAuthCredentialID (line 68) | func (a *defaultAuthenticator) WebAuthCredentialID() []byte {
method WebAuthPublicKey (line 72) | func (a *defaultAuthenticator) WebAuthPublicKey() []byte {
method WebAuthAAGUID (line 76) | func (a *defaultAuthenticator) WebAuthAAGUID() []byte {
method WebAuthSignCount (line 80) | func (a *defaultAuthenticator) WebAuthSignCount() uint32 {
FILE: webauthn/webauthn.go
type WebAuthn (line 13) | type WebAuthn struct
method write (line 28) | func (w *WebAuthn) write(r *http.Request, rw http.ResponseWriter, res ...
method writeCode (line 32) | func (w *WebAuthn) writeCode(r *http.Request, rw http.ResponseWriter, ...
method writeError (line 48) | func (w *WebAuthn) writeError(r *http.Request, rw http.ResponseWriter,...
method writeErrorCode (line 57) | func (w *WebAuthn) writeErrorCode(r *http.Request, rw http.ResponseWri...
function New (line 19) | func New(c *Config) (*WebAuthn, error) {
Condensed preview — 34 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (143K chars).
[
{
"path": ".gitignore",
"chars": 2311,
"preview": "\r\n# Created by https://www.gitignore.io/api/go,intellij+all,visualstudiocode\r\n\r\n### Go ###\r\n# Binaries for programs and "
},
{
"path": ".travis.yml",
"chars": 136,
"preview": "language: go\r\ngo:\r\n - \"1.11.x\"\r\n\r\ninstall: true\r\n\r\nscript:\r\n - env GO111MODULE=on go build ./...\r\n - env GO111MODULE="
},
{
"path": "LICENSE",
"chars": 1091,
"preview": "MIT License\r\n\r\nCopyright (c) 2020 Koen Vlaswinkel\r\n\r\nPermission is hereby granted, free of charge, to any person obtaini"
},
{
"path": "README.md",
"chars": 5526,
"preview": "# webauthn : Web Authentication API in Go\r\n\r\n## Overview [ attestation statement format\npackage an"
},
{
"path": "attestation/androidsafetynet/androidsafetynet_test.go",
"chars": 9313,
"preview": "package androidsafetynet\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/koesie10/webauthn/protocol\"\n"
},
{
"path": "attestation/attestion.go",
"chars": 274,
"preview": "// attestation can be imported to import all supported attestation formats\npackage attestation\n\nimport (\n\t_ \"github.com/"
},
{
"path": "attestation/fido/fido.go",
"chars": 3122,
"preview": "// fido implements the FIDO U2F (WebAuthn spec section 8.6) attestation statement format\npackage fido\n\nimport (\n\t\"crypto"
},
{
"path": "attestation/fido/fido_test.go",
"chars": 4663,
"preview": "package fido_test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/koesie10/webauthn/protocol\"\n)\n\nfunc TestIsV"
},
{
"path": "attestation/packed/packed.go",
"chars": 6539,
"preview": "// packed implements the Packed (WebAuthn spec section 8.2) attestation statement format\npackage packed\n\nimport (\n\t\"byte"
},
{
"path": "attestation/packed/packed_test.go",
"chars": 3226,
"preview": "package packed_test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/koesie10/webauthn/protocol\"\n)\n\nfunc TestI"
},
{
"path": "cose/cose.go",
"chars": 1360,
"preview": "package cose\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\n\t\"github.com/ugorji/go/codec\"\n)\n\n// Errors\nvar (\n\tErrMissingKeyType = fmt."
},
{
"path": "cose/cose_test.go",
"chars": 603,
"preview": "package cose_test\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"testing\"\n\n\t\"github.com/koesie10/webauthn/cose\"\n)\n\nfunc TestParseCOSE(t *te"
},
{
"path": "cose/doc.go",
"chars": 143,
"preview": "// cose contains utility functions related to COSE keys, Section 7 of [RFC8152].\n//\npackage cose // import \"github.com/k"
},
{
"path": "cose/ecdsa.go",
"chars": 1161,
"preview": "package cose\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"math/big\"\n)\n\nfunc parseECDSA(alg int64, m map[int]interface{"
},
{
"path": "go.mod",
"chars": 226,
"preview": "module github.com/koesie10/webauthn\n\ngo 1.13\n\nrequire (\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/ugorji/go/codec v1.1.7"
},
{
"path": "go.sum",
"chars": 2158,
"preview": "github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwaw"
},
{
"path": "protocol/api.go",
"chars": 16825,
"preview": "package protocol\n\n// CredentialCreationOptions contains the options that should be passed to navigator.credentials.creat"
},
{
"path": "protocol/assertion.go",
"chars": 6069,
"preview": "package protocol\n\nimport (\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n)\n\n// AssertionResponse contains the attribu"
},
{
"path": "protocol/attestation.go",
"chars": 8217,
"preview": "package protocol\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\n\t\"github.com/ugorji/go/codec\"\n)\n\n// AttestationRe"
},
{
"path": "protocol/attestation_registry.go",
"chars": 464,
"preview": "package protocol\n\n// AttestationFormatFunction will be called when checking whether an Attestation is valid.\ntype Attest"
},
{
"path": "protocol/challenge.go",
"chars": 553,
"preview": "package protocol\n\nimport \"crypto/rand\"\n\n// ChallengeSize represents the size of a challenge created by NewChallenge.\ncon"
},
{
"path": "protocol/common.go",
"chars": 11416,
"preview": "package protocol\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\n\t\"github."
},
{
"path": "protocol/doc.go",
"chars": 446,
"preview": "// protocol is a low-level package that closely resembles the WebAuthn specification. You should prefer to use the\n// we"
},
{
"path": "protocol/errors.go",
"chars": 3726,
"preview": "package protocol\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// Default errors\nvar (\n\tErrInvalidSignature "
},
{
"path": "protocol/webauthn_test.go",
"chars": 17897,
"preview": "package protocol_test\n\nimport (\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/koesie10/webauthn/protoc"
},
{
"path": "webauthn/config.go",
"chars": 3061,
"preview": "package webauthn\n\nimport \"fmt\"\n\nvar defaultSessionKeyPrefixChallenge = \"webauthn.challenge\"\nvar defaultSessionKeyPrefixU"
},
{
"path": "webauthn/doc.go",
"chars": 205,
"preview": "// webauthn is a high-level package that contains HTTP request handlers which can be used to implement webauthn\n// in an"
},
{
"path": "webauthn/login.go",
"chars": 6061,
"preview": "package webauthn\n\nimport (\n\t\"bytes\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/koe"
},
{
"path": "webauthn/registration.go",
"chars": 5888,
"preview": "package webauthn\n\nimport (\n\t\"bytes\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"net/http\"\n\n\t\"github.com/koesie10/w"
},
{
"path": "webauthn/session.go",
"chars": 812,
"preview": "package webauthn\n\n// Session will be used by the request handlers to save temporary data, such as the challenge and user"
},
{
"path": "webauthn/user.go",
"chars": 2498,
"preview": "package webauthn\n\n// User should be implemented by users used in the request handlers.\ntype User interface {\n\t// WebAuth"
},
{
"path": "webauthn/webauthn.go",
"chars": 1727,
"preview": "package webauthn\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/koesie10/webauthn/protocol\"\n\t\"github.com/pk"
},
{
"path": "webauthn.js",
"chars": 3162,
"preview": "class WebAuthn {\r\n\t// Decode a base64 string into a Uint8Array.\r\n\tstatic _decodeBuffer(value) {\r\n\t\treturn Uint8Array.fro"
}
]
About this extraction
This page contains the full source code of the koesie10/webauthn GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 34 files (131.6 KB), approximately 46.2k tokens, and a symbol index with 143 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.