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 [![GoDoc](https://godoc.org/github.com/koesie10/webauthn?status.svg)](https://godoc.org/github.com/koesie10/webauthn) [![Build Status](https://travis-ci.org/koesie10/webauthn.svg?branch=master)](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)); } }