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