Browse Source
Add support for FIDO U2F (#3971)
Add support for FIDO U2F (#3971)
* Add support for U2F Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add vendor library Add missing translations Signed-off-by: Jonas Franz <info@jonasfranz.software> * Minor improvements Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add U2F support for Firefox, Chrome (Android) by introducing a custom JS library Add U2F error handling Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add U2F login page to OAuth Signed-off-by: Jonas Franz <info@jonasfranz.software> * Move U2F user settings to a separate file Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add unit tests for u2f model Renamed u2f table name Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix problems caused by refactoring Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add U2F documentation Signed-off-by: Jonas Franz <info@jonasfranz.software> * Remove not needed console.log-s Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add default values to app.ini.sample Add FIDO U2F to comparison Signed-off-by: Jonas Franz <info@jonasfranz.software>release/v1.5
committed by
Lauris BH
34 changed files with 1599 additions and 9 deletions
-
10custom/conf/app.ini.sample
-
4docs/content/doc/advanced/config-cheat-sheet.en-us.md
-
9docs/content/doc/features/comparison.en-us.md
-
22models/error.go
-
7models/fixtures/u2f_registration.yml
-
2models/migrations/migrations.go
-
19models/migrations/v65.go
-
1models/models.go
-
120models/u2f.go
-
61models/u2f_test.go
-
20modules/auth/user_form.go
-
8modules/setting/setting.go
-
22options/locale/locale_en-US.ini
-
128public/js/index.js
-
5public/vendor/librejs.html
-
1public/vendor/plugins/u2f/index.js
-
16routers/routes/routes.go
-
139routers/user/auth.go
-
8routers/user/setting/security.go
-
99routers/user/setting/security_u2f.go
-
3templates/base/footer.tmpl
-
22templates/user/auth/u2f.tmpl
-
32templates/user/auth/u2f_error.tmpl
-
1templates/user/settings/security.tmpl
-
2templates/user/settings/security_openid.tmpl
-
56templates/user/settings/security_u2f.tmpl
-
21vendor/github.com/tstranex/u2f/LICENSE
-
97vendor/github.com/tstranex/u2f/README.md
-
136vendor/github.com/tstranex/u2f/auth.go
-
89vendor/github.com/tstranex/u2f/certs.go
-
87vendor/github.com/tstranex/u2f/messages.go
-
230vendor/github.com/tstranex/u2f/register.go
-
125vendor/github.com/tstranex/u2f/util.go
-
6vendor/vendor.json
@ -0,0 +1,7 @@ |
|||
- |
|||
id: 1 |
|||
name: "U2F Key" |
|||
user_id: 1 |
|||
counter: 0 |
|||
created_unix: 946684800 |
|||
updated_unix: 946684800 |
@ -0,0 +1,19 @@ |
|||
package migrations |
|||
|
|||
import ( |
|||
"code.gitea.io/gitea/modules/util" |
|||
"github.com/go-xorm/xorm" |
|||
) |
|||
|
|||
func addU2FReg(x *xorm.Engine) error { |
|||
type U2FRegistration struct { |
|||
ID int64 `xorm:"pk autoincr"` |
|||
Name string |
|||
UserID int64 `xorm:"INDEX"` |
|||
Raw []byte |
|||
Counter uint32 |
|||
CreatedUnix util.TimeStamp `xorm:"INDEX created"` |
|||
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` |
|||
} |
|||
return x.Sync2(&U2FRegistration{}) |
|||
} |
@ -0,0 +1,120 @@ |
|||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
|||
// Use of this source code is governed by a MIT-style
|
|||
// license that can be found in the LICENSE file.
|
|||
|
|||
package models |
|||
|
|||
import ( |
|||
"code.gitea.io/gitea/modules/log" |
|||
"code.gitea.io/gitea/modules/util" |
|||
|
|||
"github.com/tstranex/u2f" |
|||
) |
|||
|
|||
// U2FRegistration represents the registration data and counter of a security key
|
|||
type U2FRegistration struct { |
|||
ID int64 `xorm:"pk autoincr"` |
|||
Name string |
|||
UserID int64 `xorm:"INDEX"` |
|||
Raw []byte |
|||
Counter uint32 |
|||
CreatedUnix util.TimeStamp `xorm:"INDEX created"` |
|||
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` |
|||
} |
|||
|
|||
// TableName returns a better table name for U2FRegistration
|
|||
func (reg U2FRegistration) TableName() string { |
|||
return "u2f_registration" |
|||
} |
|||
|
|||
// Parse will convert the db entry U2FRegistration to an u2f.Registration struct
|
|||
func (reg *U2FRegistration) Parse() (*u2f.Registration, error) { |
|||
r := new(u2f.Registration) |
|||
return r, r.UnmarshalBinary(reg.Raw) |
|||
} |
|||
|
|||
func (reg *U2FRegistration) updateCounter(e Engine) error { |
|||
_, err := e.ID(reg.ID).Cols("counter").Update(reg) |
|||
return err |
|||
} |
|||
|
|||
// UpdateCounter will update the database value of counter
|
|||
func (reg *U2FRegistration) UpdateCounter() error { |
|||
return reg.updateCounter(x) |
|||
} |
|||
|
|||
// U2FRegistrationList is a list of *U2FRegistration
|
|||
type U2FRegistrationList []*U2FRegistration |
|||
|
|||
// ToRegistrations will convert all U2FRegistrations to u2f.Registrations
|
|||
func (list U2FRegistrationList) ToRegistrations() []u2f.Registration { |
|||
regs := make([]u2f.Registration, len(list)) |
|||
for _, reg := range list { |
|||
r, err := reg.Parse() |
|||
if err != nil { |
|||
log.Fatal(4, "parsing u2f registration: %v", err) |
|||
continue |
|||
} |
|||
regs = append(regs, *r) |
|||
} |
|||
|
|||
return regs |
|||
} |
|||
|
|||
func getU2FRegistrationsByUID(e Engine, uid int64) (U2FRegistrationList, error) { |
|||
regs := make(U2FRegistrationList, 0) |
|||
return regs, e.Where("user_id = ?", uid).Find(®s) |
|||
} |
|||
|
|||
// GetU2FRegistrationByID returns U2F registration by id
|
|||
func GetU2FRegistrationByID(id int64) (*U2FRegistration, error) { |
|||
return getU2FRegistrationByID(x, id) |
|||
} |
|||
|
|||
func getU2FRegistrationByID(e Engine, id int64) (*U2FRegistration, error) { |
|||
reg := new(U2FRegistration) |
|||
if found, err := e.ID(id).Get(reg); err != nil { |
|||
return nil, err |
|||
} else if !found { |
|||
return nil, ErrU2FRegistrationNotExist{ID: id} |
|||
} |
|||
return reg, nil |
|||
} |
|||
|
|||
// GetU2FRegistrationsByUID returns all U2F registrations of the given user
|
|||
func GetU2FRegistrationsByUID(uid int64) (U2FRegistrationList, error) { |
|||
return getU2FRegistrationsByUID(x, uid) |
|||
} |
|||
|
|||
func createRegistration(e Engine, user *User, name string, reg *u2f.Registration) (*U2FRegistration, error) { |
|||
raw, err := reg.MarshalBinary() |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
r := &U2FRegistration{ |
|||
UserID: user.ID, |
|||
Name: name, |
|||
Counter: 0, |
|||
Raw: raw, |
|||
} |
|||
_, err = e.InsertOne(r) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
return r, nil |
|||
} |
|||
|
|||
// CreateRegistration will create a new U2FRegistration from the given Registration
|
|||
func CreateRegistration(user *User, name string, reg *u2f.Registration) (*U2FRegistration, error) { |
|||
return createRegistration(x, user, name, reg) |
|||
} |
|||
|
|||
// DeleteRegistration will delete U2FRegistration
|
|||
func DeleteRegistration(reg *U2FRegistration) error { |
|||
return deleteRegistration(x, reg) |
|||
} |
|||
|
|||
func deleteRegistration(e Engine, reg *U2FRegistration) error { |
|||
_, err := e.Delete(reg) |
|||
return err |
|||
} |
@ -0,0 +1,61 @@ |
|||
package models |
|||
|
|||
import ( |
|||
"testing" |
|||
|
|||
"github.com/stretchr/testify/assert" |
|||
"github.com/tstranex/u2f" |
|||
) |
|||
|
|||
func TestGetU2FRegistrationByID(t *testing.T) { |
|||
assert.NoError(t, PrepareTestDatabase()) |
|||
|
|||
res, err := GetU2FRegistrationByID(1) |
|||
assert.NoError(t, err) |
|||
assert.Equal(t, "U2F Key", res.Name) |
|||
|
|||
_, err = GetU2FRegistrationByID(342432) |
|||
assert.Error(t, err) |
|||
assert.True(t, IsErrU2FRegistrationNotExist(err)) |
|||
} |
|||
|
|||
func TestGetU2FRegistrationsByUID(t *testing.T) { |
|||
assert.NoError(t, PrepareTestDatabase()) |
|||
|
|||
res, err := GetU2FRegistrationsByUID(1) |
|||
assert.NoError(t, err) |
|||
assert.Len(t, res, 1) |
|||
assert.Equal(t, "U2F Key", res[0].Name) |
|||
} |
|||
|
|||
func TestU2FRegistration_TableName(t *testing.T) { |
|||
assert.Equal(t, "u2f_registration", U2FRegistration{}.TableName()) |
|||
} |
|||
|
|||
func TestU2FRegistration_UpdateCounter(t *testing.T) { |
|||
assert.NoError(t, PrepareTestDatabase()) |
|||
reg := AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration) |
|||
reg.Counter = 1 |
|||
assert.NoError(t, reg.UpdateCounter()) |
|||
AssertExistsIf(t, true, &U2FRegistration{ID: 1, Counter: 1}) |
|||
} |
|||
|
|||
func TestCreateRegistration(t *testing.T) { |
|||
assert.NoError(t, PrepareTestDatabase()) |
|||
user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User) |
|||
|
|||
res, err := CreateRegistration(user, "U2F Created Key", &u2f.Registration{Raw: []byte("Test")}) |
|||
assert.NoError(t, err) |
|||
assert.Equal(t, "U2F Created Key", res.Name) |
|||
assert.Equal(t, []byte("Test"), res.Raw) |
|||
|
|||
AssertExistsIf(t, true, &U2FRegistration{Name: "U2F Created Key", UserID: user.ID}) |
|||
} |
|||
|
|||
func TestDeleteRegistration(t *testing.T) { |
|||
assert.NoError(t, PrepareTestDatabase()) |
|||
reg := AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration) |
|||
|
|||
assert.NoError(t, DeleteRegistration(reg)) |
|||
AssertNotExistsBean(t, &U2FRegistration{ID: 1}) |
|||
} |
1
public/vendor/plugins/u2f/index.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,99 @@ |
|||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
|||
// Use of this source code is governed by a MIT-style
|
|||
// license that can be found in the LICENSE file.
|
|||
|
|||
package setting |
|||
|
|||
import ( |
|||
"errors" |
|||
|
|||
"code.gitea.io/gitea/models" |
|||
"code.gitea.io/gitea/modules/auth" |
|||
"code.gitea.io/gitea/modules/context" |
|||
"code.gitea.io/gitea/modules/setting" |
|||
|
|||
"github.com/tstranex/u2f" |
|||
) |
|||
|
|||
// U2FRegister initializes the u2f registration procedure
|
|||
func U2FRegister(ctx *context.Context, form auth.U2FRegistrationForm) { |
|||
if form.Name == "" { |
|||
ctx.Error(409) |
|||
return |
|||
} |
|||
challenge, err := u2f.NewChallenge(setting.U2F.AppID, setting.U2F.TrustedFacets) |
|||
if err != nil { |
|||
ctx.ServerError("NewChallenge", err) |
|||
return |
|||
} |
|||
err = ctx.Session.Set("u2fChallenge", challenge) |
|||
if err != nil { |
|||
ctx.ServerError("Session.Set", err) |
|||
return |
|||
} |
|||
regs, err := models.GetU2FRegistrationsByUID(ctx.User.ID) |
|||
if err != nil { |
|||
ctx.ServerError("GetU2FRegistrationsByUID", err) |
|||
return |
|||
} |
|||
for _, reg := range regs { |
|||
if reg.Name == form.Name { |
|||
ctx.Error(409, "Name already taken") |
|||
return |
|||
} |
|||
} |
|||
ctx.Session.Set("u2fName", form.Name) |
|||
ctx.JSON(200, u2f.NewWebRegisterRequest(challenge, regs.ToRegistrations())) |
|||
} |
|||
|
|||
// U2FRegisterPost receives the response of the security key
|
|||
func U2FRegisterPost(ctx *context.Context, response u2f.RegisterResponse) { |
|||
challSess := ctx.Session.Get("u2fChallenge") |
|||
u2fName := ctx.Session.Get("u2fName") |
|||
if challSess == nil || u2fName == nil { |
|||
ctx.ServerError("U2FRegisterPost", errors.New("not in U2F session")) |
|||
return |
|||
} |
|||
challenge := challSess.(*u2f.Challenge) |
|||
name := u2fName.(string) |
|||
config := &u2f.Config{ |
|||
// Chrome 66+ doesn't return the device's attestation
|
|||
// certificate by default.
|
|||
SkipAttestationVerify: true, |
|||
} |
|||
reg, err := u2f.Register(response, *challenge, config) |
|||
if err != nil { |
|||
ctx.ServerError("u2f.Register", err) |
|||
return |
|||
} |
|||
if _, err = models.CreateRegistration(ctx.User, name, reg); err != nil { |
|||
ctx.ServerError("u2f.Register", err) |
|||
return |
|||
} |
|||
ctx.Status(200) |
|||
} |
|||
|
|||
// U2FDelete deletes an security key by id
|
|||
func U2FDelete(ctx *context.Context, form auth.U2FDeleteForm) { |
|||
reg, err := models.GetU2FRegistrationByID(form.ID) |
|||
if err != nil { |
|||
if models.IsErrU2FRegistrationNotExist(err) { |
|||
ctx.Status(200) |
|||
return |
|||
} |
|||
ctx.ServerError("GetU2FRegistrationByID", err) |
|||
return |
|||
} |
|||
if reg.UserID != ctx.User.ID { |
|||
ctx.Status(401) |
|||
return |
|||
} |
|||
if err := models.DeleteRegistration(reg); err != nil { |
|||
ctx.ServerError("DeleteRegistration", err) |
|||
return |
|||
} |
|||
ctx.JSON(200, map[string]interface{}{ |
|||
"redirect": setting.AppSubURL + "/user/settings/security", |
|||
}) |
|||
return |
|||
} |
@ -0,0 +1,22 @@ |
|||
{{template "base/head" .}} |
|||
<div class="user signin"> |
|||
<div class="ui middle centered very relaxed page grid"> |
|||
<div class="column"> |
|||
<h3 class="ui top attached header"> |
|||
{{.i18n.Tr "twofa"}} |
|||
</h3> |
|||
<div class="ui attached segment"> |
|||
<i class="huge key icon"></i> |
|||
<h3>{{.i18n.Tr "u2f_insert_key"}}</h3> |
|||
{{template "base/alert" .}} |
|||
<p>{{.i18n.Tr "u2f_sign_in"}}</p> |
|||
</div> |
|||
<div id="wait-for-key" class="ui attached segment"><div class="ui active indeterminate inline loader"></div> {{.i18n.Tr "u2f_press_button"}} </div> |
|||
<div class="ui attached segment"> |
|||
<a href="/user/two_factor">{{.i18n.Tr "u2f_use_twofa"}}</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
{{template "user/auth/u2f_error" .}} |
|||
{{template "base/footer" .}} |
@ -0,0 +1,32 @@ |
|||
<div class="ui small modal" id="u2f-error"> |
|||
<div class="header">{{.i18n.Tr "u2f_error"}}</div> |
|||
<div class="content"> |
|||
<div class="ui negative message"> |
|||
<div class="header"> |
|||
{{.i18n.Tr "u2f_error"}} |
|||
</div> |
|||
<div class="hide" id="unsupported-browser"> |
|||
{{.i18n.Tr "u2f_unsupported_browser"}} |
|||
</div> |
|||
<div class="hide" id="u2f-error-1"> |
|||
{{.i18n.Tr "u2f_error_1"}} |
|||
</div> |
|||
<div class="hide" id="u2f-error-2"> |
|||
{{.i18n.Tr "u2f_error_2"}} |
|||
</div> |
|||
<div class="hide" id="u2f-error-3"> |
|||
{{.i18n.Tr "u2f_error_3"}} |
|||
</div> |
|||
<div class="hide" id="u2f-error-4"> |
|||
{{.i18n.Tr "u2f_error_4"}} |
|||
</div> |
|||
<div class="hide u2f-error-5"> |
|||
{{.i18n.Tr "u2f_error_5"}} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="actions"> |
|||
<button onclick="window.location.reload()" class="success ui button hide u2f_error_5">{{.i18n.Tr "u2f_reload"}}</button> |
|||
<div class="ui cancel button">{{.i18n.Tr "cancel"}}</div> |
|||
</div> |
|||
</div> |
@ -0,0 +1,56 @@ |
|||
<h4 class="ui top attached header"> |
|||
{{.i18n.Tr "settings.u2f"}} |
|||
</h4> |
|||
<div class="ui attached segment"> |
|||
<p>{{.i18n.Tr "settings.u2f_desc" | Str2html}}</p> |
|||
{{if .TwofaEnrolled}} |
|||
<div class="ui key list"> |
|||
{{range .U2FRegistrations}} |
|||
<div class="item"> |
|||
<div class="right floated content"> |
|||
<button class="ui red tiny button delete-button" id="delete-registration" data-url="{{$.Link}}/u2f/delete" data-id="{{.ID}}"> |
|||
{{$.i18n.Tr "settings.delete_key"}} |
|||
</button> |
|||
</div> |
|||
<div class="content"> |
|||
<strong>{{.Name}}</strong> |
|||
</div> |
|||
</div> |
|||
{{end}} |
|||
</div> |
|||
<div class="ui form"> |
|||
{{.CsrfTokenHtml}} |
|||
<div class="required field"> |
|||
<label for="nickname">{{.i18n.Tr "settings.u2f_nickname"}}</label> |
|||
<input id="nickname" name="nickname" type="text" required> |
|||
</div> |
|||
<button id="register-security-key" class="positive ui labeled icon button"><i class="usb icon"></i>{{.i18n.Tr "settings.u2f_register_key"}}</button> |
|||
</div> |
|||
{{else}} |
|||
<b>{{.i18n.Tr "settings.u2f_require_twofa"}}</b> |
|||
{{end}} |
|||
</div> |
|||
|
|||
<div class="ui small modal" id="register-device"> |
|||
<div class="header">{{.i18n.Tr "settings.u2f_register_key"}}</div> |
|||
<div class="content"> |
|||
<i class="notched spinner loading icon"></i> {{.i18n.Tr "settings.u2f_press_button"}} |
|||
</div> |
|||
<div class="actions"> |
|||
<div class="ui cancel button">{{.i18n.Tr "cancel"}}</div> |
|||
</div> |
|||
</div> |
|||
|
|||
{{template "user/auth/u2f_error" .}} |
|||
|
|||
<div class="ui small basic delete modal" id="delete-registration"> |
|||
<div class="ui icon header"> |
|||
<i class="trash icon"></i> |
|||
{{.i18n.Tr "settings.u2f_delete_key"}} |
|||
</div> |
|||
<div class="content"> |
|||
<p>{{.i18n.Tr "settings.u2f_delete_key_desc"}}</p> |
|||
</div> |
|||
{{template "base/delete_modal_actions" .}} |
|||
</div> |
|||
|
@ -0,0 +1,21 @@ |
|||
The MIT License (MIT) |
|||
|
|||
Copyright (c) 2015 The Go FIDO U2F Library Authors |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in |
|||
all copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|||
THE SOFTWARE. |
@ -0,0 +1,97 @@ |
|||
# Go FIDO U2F Library |
|||
|
|||
This Go package implements the parts of the FIDO U2F specification required on |
|||
the server side of an application. |
|||
|
|||
[](https://travis-ci.org/tstranex/u2f) |
|||
|
|||
## Features |
|||
|
|||
- Native Go implementation |
|||
- No dependancies other than the Go standard library |
|||
- Token attestation certificate verification |
|||
|
|||
## Usage |
|||
|
|||
Please visit http://godoc.org/github.com/tstranex/u2f for the full |
|||
documentation. |
|||
|
|||
### How to enrol a new token |
|||
|
|||
```go |
|||
app_id := "http://localhost" |
|||
|
|||
// Send registration request to the browser. |
|||
c, _ := NewChallenge(app_id, []string{app_id}) |
|||
req, _ := c.RegisterRequest() |
|||
|
|||
// Read response from the browser. |
|||
var resp RegisterResponse |
|||
reg, err := Register(resp, c, nil) |
|||
if err != nil { |
|||
// Registration failed. |
|||
} |
|||
|
|||
// Store registration in the database. |
|||
``` |
|||
|
|||
### How to perform an authentication |
|||
|
|||
```go |
|||
// Fetch registration and counter from the database. |
|||
var reg Registration |
|||
var counter uint32 |
|||
|
|||
// Send authentication request to the browser. |
|||
c, _ := NewChallenge(app_id, []string{app_id}) |
|||
req, _ := c.SignRequest(reg) |
|||
|
|||
// Read response from the browser. |
|||
var resp SignResponse |
|||
newCounter, err := reg.Authenticate(resp, c, counter) |
|||
if err != nil { |
|||
// Authentication failed. |
|||
} |
|||
|
|||
// Store updated counter in the database. |
|||
``` |
|||
|
|||
## Installation |
|||
|
|||
``` |
|||
$ go get github.com/tstranex/u2f |
|||
``` |
|||
|
|||
## Example |
|||
|
|||
See u2fdemo/main.go for an full example server. To run it: |
|||
|
|||
``` |
|||
$ go install github.com/tstranex/u2f/u2fdemo |
|||
$ ./bin/u2fdemo |
|||
``` |
|||
|
|||
Open https://localhost:3483 in Chrome. |
|||
Ignore the SSL warning (due to the self-signed certificate for localhost). |
|||
You can then test registering and authenticating using your token. |
|||
|
|||
## Changelog |
|||
|
|||
- 2016-12-18: The package has been updated to work with the new |
|||
U2F Javascript 1.1 API specification. This causes some breaking changes. |
|||
|
|||
`SignRequest` has been replaced by `WebSignRequest` which now includes |
|||
multiple registrations. This is useful when the user has multiple devices |
|||
registered since you can now authenticate against any of them with a single |
|||
request. |
|||
|
|||
`WebRegisterRequest` has been introduced, which should generally be used |
|||
instead of using `RegisterRequest` directly. It includes the list of existing |
|||
registrations with the new registration request. If the user's device already |
|||
matches one of the existing registrations, it will refuse to re-register. |
|||
|
|||
`Challenge.RegisterRequest` has been replaced by `NewWebRegisterRequest`. |
|||
|
|||
## License |
|||
|
|||
The Go FIDO U2F Library is licensed under the MIT License. |
@ -0,0 +1,136 @@ |
|||
// Go FIDO U2F Library
|
|||
// Copyright 2015 The Go FIDO U2F Library Authors. All rights reserved.
|
|||
// Use of this source code is governed by the MIT
|
|||
// license that can be found in the LICENSE file.
|
|||
|
|||
package u2f |
|||
|
|||
import ( |
|||
"crypto/ecdsa" |
|||
"crypto/sha256" |
|||
"encoding/asn1" |
|||
"errors" |
|||
"math/big" |
|||
"time" |
|||
) |
|||
|
|||
// SignRequest creates a request to initiate an authentication.
|
|||
func (c *Challenge) SignRequest(regs []Registration) *WebSignRequest { |
|||
var sr WebSignRequest |
|||
sr.AppID = c.AppID |
|||
sr.Challenge = encodeBase64(c.Challenge) |
|||
for _, r := range regs { |
|||
rk := getRegisteredKey(c.AppID, r) |
|||
sr.RegisteredKeys = append(sr.RegisteredKeys, rk) |
|||
} |
|||
return &sr |
|||
} |
|||
|
|||
// ErrCounterTooLow is raised when the counter value received from the device is
|
|||
// lower than last stored counter value. This may indicate that the device has
|
|||
// been cloned (or is malfunctioning). The application may choose to disable
|
|||
// the particular device as precaution.
|
|||
var ErrCounterTooLow = errors.New("u2f: counter too low") |
|||
|
|||
// Authenticate validates a SignResponse authentication response.
|
|||
// An error is returned if any part of the response fails to validate.
|
|||
// The counter should be the counter associated with appropriate device
|
|||
// (i.e. resp.KeyHandle).
|
|||
// The latest counter value is returned, which the caller should store.
|
|||
func (reg *Registration) Authenticate(resp SignResponse, c Challenge, counter uint32) (newCounter uint32, err error) { |
|||
if time.Now().Sub(c.Timestamp) > timeout { |
|||
return 0, errors.New("u2f: challenge has expired") |
|||
} |
|||
if resp.KeyHandle != encodeBase64(reg.KeyHandle) { |
|||
return 0, errors.New("u2f: wrong key handle") |
|||
} |
|||
|
|||
sigData, err := decodeBase64(resp.SignatureData) |
|||
if err != nil { |
|||
return 0, err |
|||
} |
|||
|
|||
clientData, err := decodeBase64(resp.ClientData) |
|||
if err != nil { |
|||
return 0, err |
|||
} |
|||
|
|||
ar, err := parseSignResponse(sigData) |
|||
if err != nil { |
|||
return 0, err |
|||
} |
|||
|
|||
if ar.Counter < counter { |
|||
return 0, ErrCounterTooLow |
|||
} |
|||
|
|||
if err := verifyClientData(clientData, c); err != nil { |
|||
return 0, err |
|||
} |
|||
|
|||
if err := verifyAuthSignature(*ar, ®.PubKey, c.AppID, clientData); err != nil { |
|||
return 0, err |
|||
} |
|||
|
|||
if !ar.UserPresenceVerified { |
|||
return 0, errors.New("u2f: user was not present") |
|||
} |
|||
|
|||
return ar.Counter, nil |
|||
} |
|||
|
|||
type ecdsaSig struct { |
|||
R, S *big.Int |
|||
} |
|||
|
|||
type authResp struct { |
|||
UserPresenceVerified bool |
|||
Counter uint32 |
|||
sig ecdsaSig |
|||
raw []byte |
|||
} |
|||
|
|||
func parseSignResponse(sd []byte) (*authResp, error) { |
|||
if len(sd) < 5 { |
|||
return nil, errors.New("u2f: data is too short") |
|||
} |
|||
|
|||
var ar authResp |
|||
|
|||
userPresence := sd[0] |
|||
if userPresence|1 != 1 { |
|||
return nil, errors.New("u2f: invalid user presence byte") |
|||
} |
|||
ar.UserPresenceVerified = userPresence == 1 |
|||
|
|||
ar.Counter = uint32(sd[1])<<24 | uint32(sd[2])<<16 | uint32(sd[3])<<8 | uint32(sd[4]) |
|||
|
|||
ar.raw = sd[:5] |
|||
|
|||
rest, err := asn1.Unmarshal(sd[5:], &ar.sig) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
if len(rest) != 0 { |
|||
return nil, errors.New("u2f: trailing data") |
|||
} |
|||
|
|||
return &ar, nil |
|||
} |
|||
|
|||
func verifyAuthSignature(ar authResp, pubKey *ecdsa.PublicKey, appID string, clientData []byte) error { |
|||
appParam := sha256.Sum256([]byte(appID)) |
|||
challenge := sha256.Sum256(clientData) |
|||
|
|||
var buf []byte |
|||
buf = append(buf, appParam[:]...) |
|||
buf = append(buf, ar.raw...) |
|||
buf = append(buf, challenge[:]...) |
|||
hash := sha256.Sum256(buf) |
|||
|
|||
if !ecdsa.Verify(pubKey, hash[:], ar.sig.R, ar.sig.S) { |
|||
return errors.New("u2f: invalid signature") |
|||
} |
|||
|
|||
return nil |
|||
} |
@ -0,0 +1,89 @@ |
|||
// Go FIDO U2F Library
|
|||
// Copyright 2015 The Go FIDO U2F Library Authors. All rights reserved.
|
|||
// Use of this source code is governed by the MIT
|
|||
// license that can be found in the LICENSE file.
|
|||
|
|||
package u2f |
|||
|
|||
import ( |
|||
"crypto/x509" |
|||
"log" |
|||
) |
|||
|
|||
const plugUpCert = `-----BEGIN CERTIFICATE----- |
|||
MIIBrjCCAVSgAwIBAgIJAMGSvUZlGSGVMAoGCCqGSM49BAMCMDIxMDAuBgNVBAMM |
|||
J1BsdWctdXAgRklETyBJbnRlcm5hbCBBdHRlc3RhdGlvbiBDQSAjMTAeFw0xNDA5 |
|||
MjMxNjM3NTFaFw0zNDA5MjMxNjM3NTFaMDIxMDAuBgNVBAMMJ1BsdWctdXAgRklE |
|||
TyBJbnRlcm5hbCBBdHRlc3RhdGlvbiBDQSAjMTBZMBMGByqGSM49AgEGCCqGSM49 |
|||
AwEHA0IABH9mscDgEHo4AUh7J8JHqRxsSVxbvsbe6Pxy5cUFKfQlWNjxRrZcbhOb |
|||
UY3WsAwmKuUdOcghbpTILhdp8LG9z5GjUzBRMA8GA1UdEwEB/wQFMAMBAf8wHQYD |
|||
VR0OBBYEFM+nRPKhYlDwOemShePaUOd9sDqoMB8GA1UdIwQYMBaAFM+nRPKhYlDw |
|||
OemShePaUOd9sDqoMAoGCCqGSM49BAMCA0gAMEUCIQDVzqnX1rgvyJaZ7WZUm1ED |
|||
hJKSsDxRXEnH+/voqpq/zgIgH4RUR6vr9YNrkzuCq5R07gF7P4qhtg/4jy+dhl7o |
|||
NAU= |
|||
-----END CERTIFICATE----- |
|||
` |
|||
|
|||
const neowaveCert = `-----BEGIN CERTIFICATE----- |
|||
MIICJDCCAcugAwIBAgIJAIo+0R9DGvSBMAoGCCqGSM49BAMCMG8xCzAJBgNVBAYT |
|||
AkZSMQ8wDQYDVQQIDAZGcmFuY2UxETAPBgNVBAcMCEdhcmRhbm5lMRAwDgYDVQQK |
|||
DAdOZW93YXZlMSowKAYDVQQDDCFOZW93YXZlIEtFWURPIEZJRE8gVTJGIENBIEJh |
|||
dGNoIDEwHhcNMTUwMTI4MTA1ODM1WhcNMjUwMTI1MTA1ODM1WjBvMQswCQYDVQQG |
|||
EwJGUjEPMA0GA1UECAwGRnJhbmNlMREwDwYDVQQHDAhHYXJkYW5uZTEQMA4GA1UE |
|||
CgwHTmVvd2F2ZTEqMCgGA1UEAwwhTmVvd2F2ZSBLRVlETyBGSURPIFUyRiBDQSBC |
|||
YXRjaCAxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBlUmE1BRE/M/CE/ZCN+x |
|||
eutfnVsThMwIDN+4DL9gqXoKCeRMiDQ1zwm/yQS80BYSEz7Du9RU+2mlnyhwhu+f |
|||
BqNQME4wHQYDVR0OBBYEFF42te8/iq5HGom4sIhgkJWLq5jkMB8GA1UdIwQYMBaA |
|||
FF42te8/iq5HGom4sIhgkJWLq5jkMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwID |
|||
RwAwRAIgVTxBFb2Hclq5Yi5gQp6WoZAcHETfKASvTQVOE88REGQCIA5DcwGVLsZB |
|||
QTb94Xgtb/WUieCvmwukFl/gEO15f3uA |
|||
-----END CERTIFICATE----- |
|||
` |
|||
|
|||
const yubicoRootCert = `-----BEGIN CERTIFICATE----- |
|||
MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ |
|||
dWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw |
|||
MDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290 |
|||
IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK |
|||
AoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk |
|||
5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep |
|||
8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw |
|||
nebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT
|
|||
9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw |
|||
LvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ |
|||
hjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN |
|||
BgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4 |
|||
MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt |
|||
hX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k |
|||
LVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U |
|||
sG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc |
|||
U9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw== |
|||
-----END CERTIFICATE----- |
|||
` |
|||
|
|||
const entersektCert = `-----BEGIN CERTIFICATE----- |
|||
MIICHjCCAcOgAwIBAgIBADAKBggqhkjOPQQDAjBvMQswCQYDVQQGEwJaQTEVMBMG |
|||
A1UECAwMV2VzdGVybiBDYXBlMRUwEwYDVQQHDAxTdGVsbGVuYm9zY2gxEjAQBgNV |
|||
BAoMCUVudGVyc2VrdDELMAkGA1UECwwCSVQxETAPBgNVBAMMCFRyYW5zYWt0MB4X |
|||
DTE0MTEwMTExMjczNFoXDTE1MTEwMTExMjczNFowbzELMAkGA1UEBhMCWkExFTAT |
|||
BgNVBAgMDFdlc3Rlcm4gQ2FwZTEVMBMGA1UEBwwMU3RlbGxlbmJvc2NoMRIwEAYD |
|||
VQQKDAlFbnRlcnNla3QxCzAJBgNVBAsMAklUMREwDwYDVQQDDAhUcmFuc2FrdDBZ |
|||
MBMGByqGSM49AgEGCCqGSM49AwEHA0IABBh10blFheMZy3k2iqW9TzLhS1DbJ/Xf |
|||
DxqQJJkpqTLq7vI+K3O4C20YtN0jsVrj7UylWoSRlPL5F7IkbeQ6aZ6jUDBOMB0G |
|||
A1UdDgQWBBQWRFF7mVAipWTdfBWk2B8Dv4Ab4jAfBgNVHSMEGDAWgBQWRFF7mVAi |
|||
pWTdfBWk2B8Dv4Ab4jAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0kAMEYCIQCo |
|||
bMURXOxv6pqz6ECBh0zgL2vVhEfTOZJOW0PACGalWgIhAME0LHGi6ZS7z9yzHNqi |
|||
cnRb+okM+PIy/hBcBuqTWCbw |
|||
-----END CERTIFICATE----- |
|||
` |
|||
|
|||
func mustLoadPool(pemCerts []byte) *x509.CertPool { |
|||
p := x509.NewCertPool() |
|||
if !p.AppendCertsFromPEM(pemCerts) { |
|||
log.Fatal("u2f: Error loading root cert pool.") |
|||
return nil |
|||
} |
|||
return p |
|||
} |
|||
|
|||
var roots = mustLoadPool([]byte(yubicoRootCert + entersektCert + neowaveCert + plugUpCert)) |
@ -0,0 +1,87 @@ |
|||
// Go FIDO U2F Library
|
|||
// Copyright 2015 The Go FIDO U2F Library Authors. All rights reserved.
|
|||
// Use of this source code is governed by the MIT
|
|||
// license that can be found in the LICENSE file.
|
|||
|
|||
package u2f |
|||
|
|||
import ( |
|||
"encoding/json" |
|||
) |
|||
|
|||
// JwkKey represents a public key used by a browser for the Channel ID TLS
|
|||
// extension.
|
|||
type JwkKey struct { |
|||
KTy string `json:"kty"` |
|||
Crv string `json:"crv"` |
|||
X string `json:"x"` |
|||
Y string `json:"y"` |
|||
} |
|||
|
|||
// ClientData as defined by the FIDO U2F Raw Message Formats specification.
|
|||
type ClientData struct { |
|||
Typ string `json:"typ"` |
|||
Challenge string `json:"challenge"` |
|||
Origin string `json:"origin"` |
|||
CIDPubKey json.RawMessage `json:"cid_pubkey"` |
|||