* 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>for-closed-social
@ -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}) | |||||
} |
@ -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. | |||||
[![Build Status](https://travis-ci.org/tstranex/u2f.svg?branch=master)](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"` | |||||
} | |||||
// RegisterRequest as defined by the FIDO U2F Javascript API 1.1. | |||||
type RegisterRequest struct { | |||||
Version string `json:"version"` | |||||
Challenge string `json:"challenge"` | |||||
} | |||||
// WebRegisterRequest contains the parameters needed for the u2f.register() | |||||
// high-level Javascript API function as defined by the | |||||
// FIDO U2F Javascript API 1.1. | |||||
type WebRegisterRequest struct { | |||||
AppID string `json:"appId"` | |||||
RegisterRequests []RegisterRequest `json:"registerRequests"` | |||||
RegisteredKeys []RegisteredKey `json:"registeredKeys"` | |||||
} | |||||
// RegisterResponse as defined by the FIDO U2F Javascript API 1.1. | |||||
type RegisterResponse struct { | |||||
Version string `json:"version"` | |||||
RegistrationData string `json:"registrationData"` | |||||
ClientData string `json:"clientData"` | |||||
} | |||||
// RegisteredKey as defined by the FIDO U2F Javascript API 1.1. | |||||
type RegisteredKey struct { | |||||
Version string `json:"version"` | |||||
KeyHandle string `json:"keyHandle"` | |||||
AppID string `json:"appId"` | |||||
} | |||||
// WebSignRequest contains the parameters needed for the u2f.sign() | |||||
// high-level Javascript API function as defined by the | |||||
// FIDO U2F Javascript API 1.1. | |||||
type WebSignRequest struct { | |||||
AppID string `json:"appId"` | |||||
Challenge string `json:"challenge"` | |||||
RegisteredKeys []RegisteredKey `json:"registeredKeys"` | |||||
} | |||||
// SignResponse as defined by the FIDO U2F Javascript API 1.1. | |||||
type SignResponse struct { | |||||
KeyHandle string `json:"keyHandle"` | |||||
SignatureData string `json:"signatureData"` | |||||
ClientData string `json:"clientData"` | |||||
} | |||||
// TrustedFacets as defined by the FIDO AppID and Facet Specification. | |||||
type TrustedFacets struct { | |||||
Version struct { | |||||
Major int `json:"major"` | |||||
Minor int `json:"minor"` | |||||
} `json:"version"` | |||||
Ids []string `json:"ids"` | |||||
} | |||||
// TrustedFacetsEndpoint is a container of TrustedFacets. | |||||
// It is used as the response for an appId URL endpoint. | |||||
type TrustedFacetsEndpoint struct { | |||||
TrustedFacets []TrustedFacets `json:"trustedFacets"` | |||||
} |
@ -0,0 +1,230 @@ | |||||
// 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/elliptic" | |||||
"crypto/sha256" | |||||
"crypto/x509" | |||||
"encoding/asn1" | |||||
"encoding/hex" | |||||
"errors" | |||||
"time" | |||||
) | |||||
// Registration represents a single enrolment or pairing between an | |||||
// application and a token. This data will typically be stored in a database. | |||||
type Registration struct { | |||||
// Raw serialized registration data as received from the token. | |||||
Raw []byte | |||||
KeyHandle []byte | |||||
PubKey ecdsa.PublicKey | |||||
// AttestationCert can be nil for Authenticate requests. | |||||
AttestationCert *x509.Certificate | |||||
} | |||||
// Config contains configurable options for the package. | |||||
type Config struct { | |||||
// SkipAttestationVerify controls whether the token attestation | |||||
// certificate should be verified on registration. Ideally it should | |||||
// always be verified. However, there is currently no public list of | |||||
// trusted attestation root certificates so it may be necessary to skip. | |||||
SkipAttestationVerify bool | |||||
// RootAttestationCertPool overrides the default root certificates used | |||||
// to verify client attestations. If nil, this defaults to the roots that are | |||||
// bundled in this library. | |||||
RootAttestationCertPool *x509.CertPool | |||||
} | |||||
// Register validates a RegisterResponse message to enrol a new token. | |||||
// An error is returned if any part of the response fails to validate. | |||||
// The returned Registration should be stored by the caller. | |||||
func Register(resp RegisterResponse, c Challenge, config *Config) (*Registration, error) { | |||||
if config == nil { | |||||
config = &Config{} | |||||
} | |||||
if time.Now().Sub(c.Timestamp) > timeout { | |||||
return nil, errors.New("u2f: challenge has expired") | |||||
} | |||||
regData, err := decodeBase64(resp.RegistrationData) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
clientData, err := decodeBase64(resp.ClientData) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
reg, sig, err := parseRegistration(regData) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
if err := verifyClientData(clientData, c); err != nil { | |||||
return nil, err | |||||
} | |||||
if err := verifyAttestationCert(*reg, config); err != nil { | |||||
return nil, err | |||||
} | |||||
if err := verifyRegistrationSignature(*reg, sig, c.AppID, clientData); err != nil { | |||||
return nil, err | |||||
} | |||||
return reg, nil | |||||
} | |||||
func parseRegistration(buf []byte) (*Registration, []byte, error) { | |||||
if len(buf) < 1+65+1+1+1 { | |||||
return nil, nil, errors.New("u2f: data is too short") | |||||
} | |||||
var r Registration | |||||
r.Raw = buf | |||||
if buf[0] != 0x05 { | |||||
return nil, nil, errors.New("u2f: invalid reserved byte") | |||||
} | |||||
buf = buf[1:] | |||||
x, y := elliptic.Unmarshal(elliptic.P256(), buf[:65]) | |||||
if x == nil { | |||||
return nil, nil, errors.New("u2f: invalid public key") | |||||
} | |||||
r.PubKey.Curve = elliptic.P256() | |||||
r.PubKey.X = x | |||||
r.PubKey.Y = y | |||||
buf = buf[65:] | |||||
khLen := int(buf[0]) | |||||
buf = buf[1:] | |||||
if len(buf) < khLen { | |||||
return nil, nil, errors.New("u2f: invalid key handle") | |||||
} | |||||
r.KeyHandle = buf[:khLen] | |||||
buf = buf[khLen:] | |||||
// The length of the x509 cert isn't specified so it has to be inferred | |||||
// by parsing. We can't use x509.ParseCertificate yet because it returns | |||||
// an error if there are any trailing bytes. So parse raw asn1 as a | |||||
// workaround to get the length. | |||||
sig, err := asn1.Unmarshal(buf, &asn1.RawValue{}) | |||||
if err != nil { | |||||
return nil, nil, err | |||||
} | |||||
buf = buf[:len(buf)-len(sig)] | |||||
fixCertIfNeed(buf) | |||||
cert, err := x509.ParseCertificate(buf) | |||||
if err != nil { | |||||
return nil, nil, err | |||||
} | |||||
r.AttestationCert = cert | |||||
return &r, sig, nil | |||||
} | |||||
// UnmarshalBinary implements encoding.BinaryMarshaler. | |||||
func (r *Registration) UnmarshalBinary(data []byte) error { | |||||
reg, _, err := parseRegistration(data) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*r = *reg | |||||
return nil | |||||
} | |||||
// MarshalBinary implements encoding.BinaryUnmarshaler. | |||||
func (r *Registration) MarshalBinary() ([]byte, error) { | |||||
return r.Raw, nil | |||||
} | |||||
func verifyAttestationCert(r Registration, config *Config) error { | |||||
if config.SkipAttestationVerify { | |||||
return nil | |||||
} | |||||
rootCertPool := roots | |||||
if config.RootAttestationCertPool != nil { | |||||
rootCertPool = config.RootAttestationCertPool | |||||
} | |||||
opts := x509.VerifyOptions{Roots: rootCertPool} | |||||
_, err := r.AttestationCert.Verify(opts) | |||||
return err | |||||
} | |||||
func verifyRegistrationSignature( | |||||
r Registration, signature []byte, appid string, clientData []byte) error { | |||||
appParam := sha256.Sum256([]byte(appid)) | |||||
challenge := sha256.Sum256(clientData) | |||||
buf := []byte{0} | |||||
buf = append(buf, appParam[:]...) | |||||
buf = append(buf, challenge[:]...) | |||||
buf = append(buf, r.KeyHandle...) | |||||
pk := elliptic.Marshal(r.PubKey.Curve, r.PubKey.X, r.PubKey.Y) | |||||
buf = append(buf, pk...) | |||||
return r.AttestationCert.CheckSignature( | |||||
x509.ECDSAWithSHA256, buf, signature) | |||||
} | |||||
func getRegisteredKey(appID string, r Registration) RegisteredKey { | |||||
return RegisteredKey{ | |||||
Version: u2fVersion, | |||||
KeyHandle: encodeBase64(r.KeyHandle), | |||||
AppID: appID, | |||||
} | |||||
} | |||||
// fixCertIfNeed fixes broken certificates described in | |||||
// https://github.com/Yubico/php-u2flib-server/blob/master/src/u2flib_server/U2F.php#L84 | |||||
func fixCertIfNeed(cert []byte) { | |||||
h := sha256.Sum256(cert) | |||||
switch hex.EncodeToString(h[:]) { | |||||
case | |||||
"349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8", | |||||
"dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f", | |||||
"1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae", | |||||
"d0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb", | |||||
"6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897", | |||||
"ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511": | |||||
// clear the offending byte. | |||||
cert[len(cert)-257] = 0 | |||||
} | |||||
} | |||||
// NewWebRegisterRequest creates a request to enrol a new token. | |||||
// regs is the list of the user's existing registration. The browser will | |||||
// refuse to re-register a device if it has an existing registration. | |||||
func NewWebRegisterRequest(c *Challenge, regs []Registration) *WebRegisterRequest { | |||||
req := RegisterRequest{ | |||||
Version: u2fVersion, | |||||
Challenge: encodeBase64(c.Challenge), | |||||
} | |||||
rr := WebRegisterRequest{ | |||||
AppID: c.AppID, | |||||
RegisterRequests: []RegisterRequest{req}, | |||||
} | |||||
for _, r := range regs { | |||||
rk := getRegisteredKey(c.AppID, r) | |||||
rr.RegisteredKeys = append(rr.RegisteredKeys, rk) | |||||
} | |||||
return &rr | |||||
} |
@ -0,0 +1,125 @@ | |||||
// 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 implements the server-side parts of the | |||||
FIDO Universal 2nd Factor (U2F) specification. | |||||
Applications will usually persist Challenge and Registration objects in a | |||||
database. | |||||
To enrol a new token: | |||||
app_id := "http://localhost" | |||||
c, _ := NewChallenge(app_id, []string{app_id}) | |||||
req, _ := u2f.NewWebRegisterRequest(c, existingTokens) | |||||
// Send the request to the browser. | |||||
var resp RegisterResponse | |||||
// Read resp from the browser. | |||||
reg, err := Register(resp, c) | |||||
if err != nil { | |||||
// Registration failed. | |||||
} | |||||
// Store reg in the database. | |||||
To perform an authentication: | |||||
var regs []Registration | |||||
// Fetch regs from the database. | |||||
c, _ := NewChallenge(app_id, []string{app_id}) | |||||
req, _ := c.SignRequest(regs) | |||||
// Send the request to the browser. | |||||
var resp SignResponse | |||||
// Read resp from the browser. | |||||
new_counter, err := reg.Authenticate(resp, c) | |||||
if err != nil { | |||||
// Authentication failed. | |||||
} | |||||
reg.Counter = new_counter | |||||
// Store updated Registration in the database. | |||||
The FIDO U2F specification can be found here: | |||||
https://fidoalliance.org/specifications/download | |||||
*/ | |||||
package u2f | |||||
import ( | |||||
"crypto/rand" | |||||
"crypto/subtle" | |||||
"encoding/base64" | |||||
"encoding/json" | |||||
"errors" | |||||
"strings" | |||||
"time" | |||||
) | |||||
const u2fVersion = "U2F_V2" | |||||
const timeout = 5 * time.Minute | |||||
func decodeBase64(s string) ([]byte, error) { | |||||
for i := 0; i < len(s)%4; i++ { | |||||
s += "=" | |||||
} | |||||
return base64.URLEncoding.DecodeString(s) | |||||
} | |||||
func encodeBase64(buf []byte) string { | |||||
s := base64.URLEncoding.EncodeToString(buf) | |||||
return strings.TrimRight(s, "=") | |||||
} | |||||
// Challenge represents a single transaction between the server and | |||||
// authenticator. This data will typically be stored in a database. | |||||
type Challenge struct { | |||||
Challenge []byte | |||||
Timestamp time.Time | |||||
AppID string | |||||
TrustedFacets []string | |||||
} | |||||
// NewChallenge generates a challenge for the given application. | |||||
func NewChallenge(appID string, trustedFacets []string) (*Challenge, error) { | |||||
challenge := make([]byte, 32) | |||||
n, err := rand.Read(challenge) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
if n != 32 { | |||||
return nil, errors.New("u2f: unable to generate random bytes") | |||||
} | |||||
var c Challenge | |||||
c.Challenge = challenge | |||||
c.Timestamp = time.Now() | |||||
c.AppID = appID | |||||
c.TrustedFacets = trustedFacets | |||||
return &c, nil | |||||
} | |||||
func verifyClientData(clientData []byte, challenge Challenge) error { | |||||
var cd ClientData | |||||
if err := json.Unmarshal(clientData, &cd); err != nil { | |||||
return err | |||||
} | |||||
foundFacetID := false | |||||
for _, facetID := range challenge.TrustedFacets { | |||||
if facetID == cd.Origin { | |||||
foundFacetID = true | |||||
break | |||||
} | |||||
} | |||||
if !foundFacetID { | |||||
return errors.New("u2f: untrusted facet id") | |||||
} | |||||
c := encodeBase64(challenge.Challenge) | |||||
if len(c) != len(cd.Challenge) || | |||||
subtle.ConstantTimeCompare([]byte(c), []byte(cd.Challenge)) != 1 { | |||||
return errors.New("u2f: challenge does not match") | |||||
} | |||||
return nil | |||||
} |