Support webauthn (#17957)
Migrate from U2F to Webauthn Co-authored-by: Andrew Thornton <art27@cantab.net> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>tokarchuk/v1.17
parent
8808293247
commit
35c3553870
@ -1,154 +0,0 @@ |
|||||||
// 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 auth |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db" |
|
||||||
"code.gitea.io/gitea/modules/log" |
|
||||||
"code.gitea.io/gitea/modules/timeutil" |
|
||||||
|
|
||||||
"github.com/tstranex/u2f" |
|
||||||
) |
|
||||||
|
|
||||||
// ____ ________________________________ .__ __ __ .__
|
|
||||||
// | | \_____ \_ _____/\______ \ ____ ____ |__| _______/ |_____________ _/ |_|__| ____ ____
|
|
||||||
// | | // ____/| __) | _// __ \ / ___\| |/ ___/\ __\_ __ \__ \\ __\ |/ _ \ / \
|
|
||||||
// | | // \| \ | | \ ___// /_/ > |\___ \ | | | | \// __ \| | | ( <_> ) | \
|
|
||||||
// |______/ \_______ \___ / |____|_ /\___ >___ /|__/____ > |__| |__| (____ /__| |__|\____/|___| /
|
|
||||||
// \/ \/ \/ \/_____/ \/ \/ \/
|
|
||||||
|
|
||||||
// ErrU2FRegistrationNotExist represents a "ErrU2FRegistrationNotExist" kind of error.
|
|
||||||
type ErrU2FRegistrationNotExist struct { |
|
||||||
ID int64 |
|
||||||
} |
|
||||||
|
|
||||||
func (err ErrU2FRegistrationNotExist) Error() string { |
|
||||||
return fmt.Sprintf("U2F registration does not exist [id: %d]", err.ID) |
|
||||||
} |
|
||||||
|
|
||||||
// IsErrU2FRegistrationNotExist checks if an error is a ErrU2FRegistrationNotExist.
|
|
||||||
func IsErrU2FRegistrationNotExist(err error) bool { |
|
||||||
_, ok := err.(ErrU2FRegistrationNotExist) |
|
||||||
return ok |
|
||||||
} |
|
||||||
|
|
||||||
// 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 `xorm:"BIGINT"` |
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` |
|
||||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` |
|
||||||
} |
|
||||||
|
|
||||||
func init() { |
|
||||||
db.RegisterModel(new(U2FRegistration)) |
|
||||||
} |
|
||||||
|
|
||||||
// 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 db.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(db.GetEngine(db.DefaultContext)) |
|
||||||
} |
|
||||||
|
|
||||||
// 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, 0, len(list)) |
|
||||||
for _, reg := range list { |
|
||||||
r, err := reg.Parse() |
|
||||||
if err != nil { |
|
||||||
log.Error("parsing u2f registration: %v", err) |
|
||||||
continue |
|
||||||
} |
|
||||||
regs = append(regs, *r) |
|
||||||
} |
|
||||||
|
|
||||||
return regs |
|
||||||
} |
|
||||||
|
|
||||||
func getU2FRegistrationsByUID(e db.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(db.GetEngine(db.DefaultContext), id) |
|
||||||
} |
|
||||||
|
|
||||||
func getU2FRegistrationByID(e db.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(db.GetEngine(db.DefaultContext), uid) |
|
||||||
} |
|
||||||
|
|
||||||
// HasU2FRegistrationsByUID returns whether a given user has U2F registrations
|
|
||||||
func HasU2FRegistrationsByUID(uid int64) (bool, error) { |
|
||||||
return db.GetEngine(db.DefaultContext).Where("user_id = ?", uid).Exist(&U2FRegistration{}) |
|
||||||
} |
|
||||||
|
|
||||||
func createRegistration(e db.Engine, userID int64, name string, reg *u2f.Registration) (*U2FRegistration, error) { |
|
||||||
raw, err := reg.MarshalBinary() |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
r := &U2FRegistration{ |
|
||||||
UserID: userID, |
|
||||||
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(userID int64, name string, reg *u2f.Registration) (*U2FRegistration, error) { |
|
||||||
return createRegistration(db.GetEngine(db.DefaultContext), userID, name, reg) |
|
||||||
} |
|
||||||
|
|
||||||
// DeleteRegistration will delete U2FRegistration
|
|
||||||
func DeleteRegistration(reg *U2FRegistration) error { |
|
||||||
return deleteRegistration(db.GetEngine(db.DefaultContext), reg) |
|
||||||
} |
|
||||||
|
|
||||||
func deleteRegistration(e db.Engine, reg *U2FRegistration) error { |
|
||||||
_, err := e.Delete(reg) |
|
||||||
return err |
|
||||||
} |
|
@ -1,100 +0,0 @@ |
|||||||
// Copyright 2020 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 auth |
|
||||||
|
|
||||||
import ( |
|
||||||
"encoding/hex" |
|
||||||
"testing" |
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/unittest" |
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert" |
|
||||||
"github.com/tstranex/u2f" |
|
||||||
) |
|
||||||
|
|
||||||
func TestGetU2FRegistrationByID(t *testing.T) { |
|
||||||
assert.NoError(t, unittest.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, unittest.PrepareTestDatabase()) |
|
||||||
|
|
||||||
res, err := GetU2FRegistrationsByUID(32) |
|
||||||
|
|
||||||
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, unittest.PrepareTestDatabase()) |
|
||||||
reg := unittest.AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration) |
|
||||||
reg.Counter = 1 |
|
||||||
assert.NoError(t, reg.UpdateCounter()) |
|
||||||
unittest.AssertExistsIf(t, true, &U2FRegistration{ID: 1, Counter: 1}) |
|
||||||
} |
|
||||||
|
|
||||||
func TestU2FRegistration_UpdateLargeCounter(t *testing.T) { |
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
||||||
reg := unittest.AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration) |
|
||||||
reg.Counter = 0xffffffff |
|
||||||
assert.NoError(t, reg.UpdateCounter()) |
|
||||||
unittest.AssertExistsIf(t, true, &U2FRegistration{ID: 1, Counter: 0xffffffff}) |
|
||||||
} |
|
||||||
|
|
||||||
func TestCreateRegistration(t *testing.T) { |
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
||||||
|
|
||||||
res, err := CreateRegistration(1, "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) |
|
||||||
|
|
||||||
unittest.AssertExistsIf(t, true, &U2FRegistration{Name: "U2F Created Key", UserID: 1}) |
|
||||||
} |
|
||||||
|
|
||||||
func TestDeleteRegistration(t *testing.T) { |
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
|
||||||
reg := unittest.AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration) |
|
||||||
|
|
||||||
assert.NoError(t, DeleteRegistration(reg)) |
|
||||||
unittest.AssertNotExistsBean(t, &U2FRegistration{ID: 1}) |
|
||||||
} |
|
||||||
|
|
||||||
const validU2FRegistrationResponseHex = "0504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9402a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c253082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871" |
|
||||||
|
|
||||||
func TestToRegistrations_SkipInvalidItemsWithoutCrashing(t *testing.T) { |
|
||||||
regKeyRaw, _ := hex.DecodeString(validU2FRegistrationResponseHex) |
|
||||||
regs := U2FRegistrationList{ |
|
||||||
&U2FRegistration{ID: 1}, |
|
||||||
&U2FRegistration{ID: 2, Name: "U2F Key", UserID: 2, Counter: 0, Raw: regKeyRaw, CreatedUnix: 946684800, UpdatedUnix: 946684800}, |
|
||||||
} |
|
||||||
|
|
||||||
actual := regs.ToRegistrations() |
|
||||||
assert.Len(t, actual, 1) |
|
||||||
} |
|
||||||
|
|
||||||
func TestToRegistrations(t *testing.T) { |
|
||||||
regKeyRaw, _ := hex.DecodeString(validU2FRegistrationResponseHex) |
|
||||||
regs := U2FRegistrationList{ |
|
||||||
&U2FRegistration{ID: 1, Name: "U2F Key", UserID: 1, Counter: 0, Raw: regKeyRaw, CreatedUnix: 946684800, UpdatedUnix: 946684800}, |
|
||||||
&U2FRegistration{ID: 2, Name: "U2F Key", UserID: 2, Counter: 0, Raw: regKeyRaw, CreatedUnix: 946684800, UpdatedUnix: 946684800}, |
|
||||||
} |
|
||||||
|
|
||||||
actual := regs.ToRegistrations() |
|
||||||
assert.Len(t, actual, 2) |
|
||||||
} |
|
@ -0,0 +1,222 @@ |
|||||||
|
// Copyright 2020 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 auth |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"encoding/base64" |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db" |
||||||
|
"code.gitea.io/gitea/modules/timeutil" |
||||||
|
"xorm.io/xorm" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/webauthn" |
||||||
|
) |
||||||
|
|
||||||
|
// ErrWebAuthnCredentialNotExist represents a "ErrWebAuthnCRedentialNotExist" kind of error.
|
||||||
|
type ErrWebAuthnCredentialNotExist struct { |
||||||
|
ID int64 |
||||||
|
CredentialID string |
||||||
|
} |
||||||
|
|
||||||
|
func (err ErrWebAuthnCredentialNotExist) Error() string { |
||||||
|
if err.CredentialID == "" { |
||||||
|
return fmt.Sprintf("WebAuthn credential does not exist [id: %d]", err.ID) |
||||||
|
} |
||||||
|
return fmt.Sprintf("WebAuthn credential does not exist [credential_id: %s]", err.CredentialID) |
||||||
|
} |
||||||
|
|
||||||
|
//IsErrWebAuthnCredentialNotExist checks if an error is a ErrWebAuthnCredentialNotExist.
|
||||||
|
func IsErrWebAuthnCredentialNotExist(err error) bool { |
||||||
|
_, ok := err.(ErrWebAuthnCredentialNotExist) |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
//WebAuthnCredential represents the WebAuthn credential data for a public-key
|
||||||
|
//credential conformant to WebAuthn Level 1
|
||||||
|
type WebAuthnCredential struct { |
||||||
|
ID int64 `xorm:"pk autoincr"` |
||||||
|
Name string |
||||||
|
LowerName string `xorm:"unique(s)"` |
||||||
|
UserID int64 `xorm:"INDEX unique(s)"` |
||||||
|
CredentialID string `xorm:"INDEX"` |
||||||
|
PublicKey []byte |
||||||
|
AttestationType string |
||||||
|
AAGUID []byte |
||||||
|
SignCount uint32 `xorm:"BIGINT"` |
||||||
|
CloneWarning bool |
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` |
||||||
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` |
||||||
|
} |
||||||
|
|
||||||
|
func init() { |
||||||
|
db.RegisterModel(new(WebAuthnCredential)) |
||||||
|
} |
||||||
|
|
||||||
|
// TableName returns a better table name for WebAuthnCredential
|
||||||
|
func (cred WebAuthnCredential) TableName() string { |
||||||
|
return "webauthn_credential" |
||||||
|
} |
||||||
|
|
||||||
|
// UpdateSignCount will update the database value of SignCount
|
||||||
|
func (cred *WebAuthnCredential) UpdateSignCount() error { |
||||||
|
return cred.updateSignCount(db.DefaultContext) |
||||||
|
} |
||||||
|
|
||||||
|
func (cred *WebAuthnCredential) updateSignCount(ctx context.Context) error { |
||||||
|
_, err := db.GetEngine(ctx).ID(cred.ID).Cols("sign_count").Update(cred) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// BeforeInsert will be invoked by XORM before updating a record
|
||||||
|
func (cred *WebAuthnCredential) BeforeInsert() { |
||||||
|
cred.LowerName = strings.ToLower(cred.Name) |
||||||
|
} |
||||||
|
|
||||||
|
// BeforeUpdate will be invoked by XORM before updating a record
|
||||||
|
func (cred *WebAuthnCredential) BeforeUpdate() { |
||||||
|
cred.LowerName = strings.ToLower(cred.Name) |
||||||
|
} |
||||||
|
|
||||||
|
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
|
||||||
|
func (cred *WebAuthnCredential) AfterLoad(session *xorm.Session) { |
||||||
|
cred.LowerName = strings.ToLower(cred.Name) |
||||||
|
} |
||||||
|
|
||||||
|
// WebAuthnCredentialList is a list of *WebAuthnCredential
|
||||||
|
type WebAuthnCredentialList []*WebAuthnCredential |
||||||
|
|
||||||
|
// ToCredentials will convert all WebAuthnCredentials to webauthn.Credentials
|
||||||
|
func (list WebAuthnCredentialList) ToCredentials() []webauthn.Credential { |
||||||
|
creds := make([]webauthn.Credential, 0, len(list)) |
||||||
|
for _, cred := range list { |
||||||
|
credID, _ := base64.RawStdEncoding.DecodeString(cred.CredentialID) |
||||||
|
creds = append(creds, webauthn.Credential{ |
||||||
|
ID: credID, |
||||||
|
PublicKey: cred.PublicKey, |
||||||
|
AttestationType: cred.AttestationType, |
||||||
|
Authenticator: webauthn.Authenticator{ |
||||||
|
AAGUID: cred.AAGUID, |
||||||
|
SignCount: cred.SignCount, |
||||||
|
CloneWarning: cred.CloneWarning, |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
return creds |
||||||
|
} |
||||||
|
|
||||||
|
//GetWebAuthnCredentialsByUID returns all WebAuthn credentials of the given user
|
||||||
|
func GetWebAuthnCredentialsByUID(uid int64) (WebAuthnCredentialList, error) { |
||||||
|
return getWebAuthnCredentialsByUID(db.DefaultContext, uid) |
||||||
|
} |
||||||
|
|
||||||
|
func getWebAuthnCredentialsByUID(ctx context.Context, uid int64) (WebAuthnCredentialList, error) { |
||||||
|
creds := make(WebAuthnCredentialList, 0) |
||||||
|
return creds, db.GetEngine(ctx).Where("user_id = ?", uid).Find(&creds) |
||||||
|
} |
||||||
|
|
||||||
|
//ExistsWebAuthnCredentialsForUID returns if the given user has credentials
|
||||||
|
func ExistsWebAuthnCredentialsForUID(uid int64) (bool, error) { |
||||||
|
return existsWebAuthnCredentialsByUID(db.DefaultContext, uid) |
||||||
|
} |
||||||
|
|
||||||
|
func existsWebAuthnCredentialsByUID(ctx context.Context, uid int64) (bool, error) { |
||||||
|
return db.GetEngine(ctx).Where("user_id = ?", uid).Exist(&WebAuthnCredential{}) |
||||||
|
} |
||||||
|
|
||||||
|
// GetWebAuthnCredentialByName returns WebAuthn credential by id
|
||||||
|
func GetWebAuthnCredentialByName(uid int64, name string) (*WebAuthnCredential, error) { |
||||||
|
return getWebAuthnCredentialByName(db.DefaultContext, uid, name) |
||||||
|
} |
||||||
|
|
||||||
|
func getWebAuthnCredentialByName(ctx context.Context, uid int64, name string) (*WebAuthnCredential, error) { |
||||||
|
cred := new(WebAuthnCredential) |
||||||
|
if found, err := db.GetEngine(ctx).Where("user_id = ? AND lower_name = ?", uid, strings.ToLower(name)).Get(cred); err != nil { |
||||||
|
return nil, err |
||||||
|
} else if !found { |
||||||
|
return nil, ErrWebAuthnCredentialNotExist{} |
||||||
|
} |
||||||
|
return cred, nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetWebAuthnCredentialByID returns WebAuthn credential by id
|
||||||
|
func GetWebAuthnCredentialByID(id int64) (*WebAuthnCredential, error) { |
||||||
|
return getWebAuthnCredentialByID(db.DefaultContext, id) |
||||||
|
} |
||||||
|
|
||||||
|
func getWebAuthnCredentialByID(ctx context.Context, id int64) (*WebAuthnCredential, error) { |
||||||
|
cred := new(WebAuthnCredential) |
||||||
|
if found, err := db.GetEngine(ctx).ID(id).Get(cred); err != nil { |
||||||
|
return nil, err |
||||||
|
} else if !found { |
||||||
|
return nil, ErrWebAuthnCredentialNotExist{ID: id} |
||||||
|
} |
||||||
|
return cred, nil |
||||||
|
} |
||||||
|
|
||||||
|
// HasWebAuthnRegistrationsByUID returns whether a given user has WebAuthn registrations
|
||||||
|
func HasWebAuthnRegistrationsByUID(uid int64) (bool, error) { |
||||||
|
return db.GetEngine(db.DefaultContext).Where("user_id = ?", uid).Exist(&WebAuthnCredential{}) |
||||||
|
} |
||||||
|
|
||||||
|
// GetWebAuthnCredentialByCredID returns WebAuthn credential by credential ID
|
||||||
|
func GetWebAuthnCredentialByCredID(credID string) (*WebAuthnCredential, error) { |
||||||
|
return getWebAuthnCredentialByCredID(db.DefaultContext, credID) |
||||||
|
} |
||||||
|
|
||||||
|
func getWebAuthnCredentialByCredID(ctx context.Context, credID string) (*WebAuthnCredential, error) { |
||||||
|
cred := new(WebAuthnCredential) |
||||||
|
if found, err := db.GetEngine(ctx).Where("credential_id = ?", credID).Get(cred); err != nil { |
||||||
|
return nil, err |
||||||
|
} else if !found { |
||||||
|
return nil, ErrWebAuthnCredentialNotExist{CredentialID: credID} |
||||||
|
} |
||||||
|
return cred, nil |
||||||
|
} |
||||||
|
|
||||||
|
// CreateCredential will create a new WebAuthnCredential from the given Credential
|
||||||
|
func CreateCredential(userID int64, name string, cred *webauthn.Credential) (*WebAuthnCredential, error) { |
||||||
|
return createCredential(db.DefaultContext, userID, name, cred) |
||||||
|
} |
||||||
|
|
||||||
|
func createCredential(ctx context.Context, userID int64, name string, cred *webauthn.Credential) (*WebAuthnCredential, error) { |
||||||
|
c := &WebAuthnCredential{ |
||||||
|
UserID: userID, |
||||||
|
Name: name, |
||||||
|
CredentialID: base64.RawStdEncoding.EncodeToString(cred.ID), |
||||||
|
PublicKey: cred.PublicKey, |
||||||
|
AttestationType: cred.AttestationType, |
||||||
|
AAGUID: cred.Authenticator.AAGUID, |
||||||
|
SignCount: cred.Authenticator.SignCount, |
||||||
|
CloneWarning: false, |
||||||
|
} |
||||||
|
|
||||||
|
if err := db.Insert(ctx, c); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return c, nil |
||||||
|
} |
||||||
|
|
||||||
|
// DeleteCredential will delete WebAuthnCredential
|
||||||
|
func DeleteCredential(id, userID int64) (bool, error) { |
||||||
|
return deleteCredential(db.DefaultContext, id, userID) |
||||||
|
} |
||||||
|
|
||||||
|
func deleteCredential(ctx context.Context, id, userID int64) (bool, error) { |
||||||
|
had, err := db.GetEngine(ctx).ID(id).Where("user_id = ?", userID).Delete(&WebAuthnCredential{}) |
||||||
|
return had > 0, err |
||||||
|
} |
||||||
|
|
||||||
|
//WebAuthnCredentials implementns the webauthn.User interface
|
||||||
|
func WebAuthnCredentials(userID int64) ([]webauthn.Credential, error) { |
||||||
|
dbCreds, err := GetWebAuthnCredentialsByUID(userID) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return dbCreds.ToCredentials(), nil |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
// Copyright 2020 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 auth |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/base64" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/unittest" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/webauthn" |
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestGetWebAuthnCredentialByID(t *testing.T) { |
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
|
||||||
|
res, err := GetWebAuthnCredentialByID(1) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, "WebAuthn credential", res.Name) |
||||||
|
|
||||||
|
_, err = GetWebAuthnCredentialByID(342432) |
||||||
|
assert.Error(t, err) |
||||||
|
assert.True(t, IsErrWebAuthnCredentialNotExist(err)) |
||||||
|
} |
||||||
|
|
||||||
|
func TestGetWebAuthnCredentialsByUID(t *testing.T) { |
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
|
||||||
|
res, err := GetWebAuthnCredentialsByUID(32) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Len(t, res, 1) |
||||||
|
assert.Equal(t, "WebAuthn credential", res[0].Name) |
||||||
|
} |
||||||
|
|
||||||
|
func TestWebAuthnCredential_TableName(t *testing.T) { |
||||||
|
assert.Equal(t, "webauthn_credential", WebAuthnCredential{}.TableName()) |
||||||
|
} |
||||||
|
|
||||||
|
func TestWebAuthnCredential_UpdateSignCount(t *testing.T) { |
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
cred := unittest.AssertExistsAndLoadBean(t, &WebAuthnCredential{ID: 1}).(*WebAuthnCredential) |
||||||
|
cred.SignCount = 1 |
||||||
|
assert.NoError(t, cred.UpdateSignCount()) |
||||||
|
unittest.AssertExistsIf(t, true, &WebAuthnCredential{ID: 1, SignCount: 1}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) { |
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
cred := unittest.AssertExistsAndLoadBean(t, &WebAuthnCredential{ID: 1}).(*WebAuthnCredential) |
||||||
|
cred.SignCount = 0xffffffff |
||||||
|
assert.NoError(t, cred.UpdateSignCount()) |
||||||
|
unittest.AssertExistsIf(t, true, &WebAuthnCredential{ID: 1, SignCount: 0xffffffff}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestCreateCredential(t *testing.T) { |
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||||
|
|
||||||
|
res, err := CreateCredential(1, "WebAuthn Created Credential", &webauthn.Credential{ID: []byte("Test")}) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, "WebAuthn Created Credential", res.Name) |
||||||
|
bs, err := base64.RawStdEncoding.DecodeString(res.CredentialID) |
||||||
|
assert.NoError(t, err) |
||||||
|
assert.Equal(t, []byte("Test"), bs) |
||||||
|
|
||||||
|
unittest.AssertExistsIf(t, true, &WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1}) |
||||||
|
} |
@ -1,7 +0,0 @@ |
|||||||
- |
|
||||||
id: 1 |
|
||||||
name: "U2F Key" |
|
||||||
user_id: 32 |
|
||||||
counter: 0 |
|
||||||
created_unix: 946684800 |
|
||||||
updated_unix: 946684800 |
|
@ -0,0 +1,8 @@ |
|||||||
|
- id: 1 |
||||||
|
name: "WebAuthn credential" |
||||||
|
user_id: 32 |
||||||
|
attestation_type: none |
||||||
|
sign_count: 0 |
||||||
|
clone_warning: false |
||||||
|
created_unix: 946684800 |
||||||
|
updated_unix: 946684800 |
@ -0,0 +1,91 @@ |
|||||||
|
// Copyright 2021 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 migrations |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/elliptic" |
||||||
|
"encoding/base64" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/timeutil" |
||||||
|
|
||||||
|
"github.com/tstranex/u2f" |
||||||
|
"xorm.io/xorm" |
||||||
|
) |
||||||
|
|
||||||
|
func addWebAuthnCred(x *xorm.Engine) error { |
||||||
|
|
||||||
|
// Create webauthnCredential table
|
||||||
|
type webauthnCredential struct { |
||||||
|
ID int64 `xorm:"pk autoincr"` |
||||||
|
Name string |
||||||
|
LowerName string `xorm:"unique(s)"` |
||||||
|
UserID int64 `xorm:"INDEX unique(s)"` |
||||||
|
CredentialID string `xorm:"INDEX"` |
||||||
|
PublicKey []byte |
||||||
|
AttestationType string |
||||||
|
AAGUID []byte |
||||||
|
SignCount uint32 `xorm:"BIGINT"` |
||||||
|
CloneWarning bool |
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` |
||||||
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` |
||||||
|
} |
||||||
|
if err := x.Sync2(&webauthnCredential{}); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Now migrate the old u2f registrations to the new format
|
||||||
|
type u2fRegistration struct { |
||||||
|
ID int64 `xorm:"pk autoincr"` |
||||||
|
Name string |
||||||
|
UserID int64 `xorm:"INDEX"` |
||||||
|
Raw []byte |
||||||
|
Counter uint32 `xorm:"BIGINT"` |
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` |
||||||
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` |
||||||
|
} |
||||||
|
|
||||||
|
var start int |
||||||
|
regs := make([]*u2fRegistration, 0, 50) |
||||||
|
for { |
||||||
|
err := x.OrderBy("id").Limit(50, start).Find(®s) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
for _, reg := range regs { |
||||||
|
parsed := new(u2f.Registration) |
||||||
|
err = parsed.UnmarshalBinary(reg.Raw) |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
c := &webauthnCredential{ |
||||||
|
ID: reg.ID, |
||||||
|
Name: reg.Name, |
||||||
|
LowerName: strings.ToLower(reg.Name), |
||||||
|
UserID: reg.UserID, |
||||||
|
CredentialID: base64.RawStdEncoding.EncodeToString(parsed.KeyHandle), |
||||||
|
PublicKey: elliptic.Marshal(elliptic.P256(), parsed.PubKey.X, parsed.PubKey.Y), |
||||||
|
AttestationType: "fido-u2f", |
||||||
|
AAGUID: []byte{}, |
||||||
|
SignCount: reg.Counter, |
||||||
|
} |
||||||
|
|
||||||
|
_, err := x.Insert(c) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(regs) < 50 { |
||||||
|
break |
||||||
|
} |
||||||
|
start += 50 |
||||||
|
regs = regs[:0] |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,78 @@ |
|||||||
|
// Copyright 2021 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 webauthn |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/binary" |
||||||
|
"encoding/gob" |
||||||
|
"net/url" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/auth" |
||||||
|
user_model "code.gitea.io/gitea/models/user" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/protocol" |
||||||
|
"github.com/duo-labs/webauthn/webauthn" |
||||||
|
) |
||||||
|
|
||||||
|
//WebAuthn represents the global WebAuthn instance
|
||||||
|
var WebAuthn *webauthn.WebAuthn |
||||||
|
|
||||||
|
//Init initializes the WebAuthn instance from the config.
|
||||||
|
func Init() { |
||||||
|
gob.Register(&webauthn.SessionData{}) |
||||||
|
|
||||||
|
appURL, _ := url.Parse(setting.AppURL) |
||||||
|
|
||||||
|
WebAuthn = &webauthn.WebAuthn{ |
||||||
|
Config: &webauthn.Config{ |
||||||
|
RPDisplayName: setting.AppName, |
||||||
|
RPID: setting.Domain, |
||||||
|
RPOrigin: protocol.FullyQualifiedOrigin(appURL), |
||||||
|
AuthenticatorSelection: protocol.AuthenticatorSelection{ |
||||||
|
UserVerification: "discouraged", |
||||||
|
}, |
||||||
|
AttestationPreference: protocol.PreferDirectAttestation, |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// User represents an implementation of webauthn.User based on User model
|
||||||
|
type User user_model.User |
||||||
|
|
||||||
|
//WebAuthnID implements the webauthn.User interface
|
||||||
|
func (u *User) WebAuthnID() []byte { |
||||||
|
id := make([]byte, 8) |
||||||
|
binary.PutVarint(id, u.ID) |
||||||
|
return id |
||||||
|
} |
||||||
|
|
||||||
|
//WebAuthnName implements the webauthn.User interface
|
||||||
|
func (u *User) WebAuthnName() string { |
||||||
|
if u.LoginName == "" { |
||||||
|
return u.Name |
||||||
|
} |
||||||
|
return u.LoginName |
||||||
|
} |
||||||
|
|
||||||
|
//WebAuthnDisplayName implements the webauthn.User interface
|
||||||
|
func (u *User) WebAuthnDisplayName() string { |
||||||
|
return (*user_model.User)(u).DisplayName() |
||||||
|
} |
||||||
|
|
||||||
|
//WebAuthnIcon implements the webauthn.User interface
|
||||||
|
func (u *User) WebAuthnIcon() string { |
||||||
|
return (*user_model.User)(u).AvatarLink() |
||||||
|
} |
||||||
|
|
||||||
|
//WebAuthnCredentials implementns the webauthn.User interface
|
||||||
|
func (u *User) WebAuthnCredentials() []webauthn.Credential { |
||||||
|
dbCreds, err := auth.GetWebAuthnCredentialsByUID(u.ID) |
||||||
|
if err != nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
return dbCreds.ToCredentials() |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
// Copyright 2021 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 webauthn |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestInit(t *testing.T) { |
||||||
|
setting.Domain = "domain" |
||||||
|
setting.AppName = "AppName" |
||||||
|
setting.AppURL = "https://domain/" |
||||||
|
rpOrigin := "https://domain" |
||||||
|
|
||||||
|
Init() |
||||||
|
|
||||||
|
assert.Equal(t, setting.Domain, WebAuthn.Config.RPID) |
||||||
|
assert.Equal(t, setting.AppName, WebAuthn.Config.RPDisplayName) |
||||||
|
assert.Equal(t, rpOrigin, WebAuthn.Config.RPOrigin) |
||||||
|
} |
File diff suppressed because one or more lines are too long
@ -1,136 +0,0 @@ |
|||||||
// Copyright 2017 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 auth |
|
||||||
|
|
||||||
import ( |
|
||||||
"errors" |
|
||||||
"net/http" |
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/auth" |
|
||||||
user_model "code.gitea.io/gitea/models/user" |
|
||||||
"code.gitea.io/gitea/modules/base" |
|
||||||
"code.gitea.io/gitea/modules/context" |
|
||||||
"code.gitea.io/gitea/modules/log" |
|
||||||
"code.gitea.io/gitea/modules/setting" |
|
||||||
"code.gitea.io/gitea/modules/web" |
|
||||||
"code.gitea.io/gitea/services/externalaccount" |
|
||||||
|
|
||||||
"github.com/tstranex/u2f" |
|
||||||
) |
|
||||||
|
|
||||||
var tplU2F base.TplName = "user/auth/u2f" |
|
||||||
|
|
||||||
// U2F shows the U2F login page
|
|
||||||
func U2F(ctx *context.Context) { |
|
||||||
ctx.Data["Title"] = ctx.Tr("twofa") |
|
||||||
ctx.Data["RequireU2F"] = true |
|
||||||
// Check auto-login.
|
|
||||||
if checkAutoLogin(ctx) { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// Ensure user is in a 2FA session.
|
|
||||||
if ctx.Session.Get("twofaUid") == nil { |
|
||||||
ctx.ServerError("UserSignIn", errors.New("not in U2F session")) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// See whether TOTP is also available.
|
|
||||||
if ctx.Session.Get("totpEnrolled") != nil { |
|
||||||
ctx.Data["TOTPEnrolled"] = true |
|
||||||
} |
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplU2F) |
|
||||||
} |
|
||||||
|
|
||||||
// U2FChallenge submits a sign challenge to the browser
|
|
||||||
func U2FChallenge(ctx *context.Context) { |
|
||||||
// Ensure user is in a U2F session.
|
|
||||||
idSess := ctx.Session.Get("twofaUid") |
|
||||||
if idSess == nil { |
|
||||||
ctx.ServerError("UserSignIn", errors.New("not in U2F session")) |
|
||||||
return |
|
||||||
} |
|
||||||
id := idSess.(int64) |
|
||||||
regs, err := auth.GetU2FRegistrationsByUID(id) |
|
||||||
if err != nil { |
|
||||||
ctx.ServerError("UserSignIn", err) |
|
||||||
return |
|
||||||
} |
|
||||||
if len(regs) == 0 { |
|
||||||
ctx.ServerError("UserSignIn", errors.New("no device registered")) |
|
||||||
return |
|
||||||
} |
|
||||||
challenge, err := u2f.NewChallenge(setting.U2F.AppID, setting.U2F.TrustedFacets) |
|
||||||
if err != nil { |
|
||||||
ctx.ServerError("u2f.NewChallenge", err) |
|
||||||
return |
|
||||||
} |
|
||||||
if err := ctx.Session.Set("u2fChallenge", challenge); err != nil { |
|
||||||
ctx.ServerError("UserSignIn: unable to set u2fChallenge in session", err) |
|
||||||
return |
|
||||||
} |
|
||||||
if err := ctx.Session.Release(); err != nil { |
|
||||||
ctx.ServerError("UserSignIn: unable to store session", err) |
|
||||||
} |
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, challenge.SignRequest(regs.ToRegistrations())) |
|
||||||
} |
|
||||||
|
|
||||||
// U2FSign authenticates the user by signResp
|
|
||||||
func U2FSign(ctx *context.Context) { |
|
||||||
signResp := web.GetForm(ctx).(*u2f.SignResponse) |
|
||||||
challSess := ctx.Session.Get("u2fChallenge") |
|
||||||
idSess := ctx.Session.Get("twofaUid") |
|
||||||
if challSess == nil || idSess == nil { |
|
||||||
ctx.ServerError("UserSignIn", errors.New("not in U2F session")) |
|
||||||
return |
|
||||||
} |
|
||||||
challenge := challSess.(*u2f.Challenge) |
|
||||||
id := idSess.(int64) |
|
||||||
regs, err := auth.GetU2FRegistrationsByUID(id) |
|
||||||
if err != nil { |
|
||||||
ctx.ServerError("UserSignIn", err) |
|
||||||
return |
|
||||||
} |
|
||||||
for _, reg := range regs { |
|
||||||
r, err := reg.Parse() |
|
||||||
if err != nil { |
|
||||||
log.Error("parsing u2f registration: %v", err) |
|
||||||
continue |
|
||||||
} |
|
||||||
newCounter, authErr := r.Authenticate(*signResp, *challenge, reg.Counter) |
|
||||||
if authErr == nil { |
|
||||||
reg.Counter = newCounter |
|
||||||
user, err := user_model.GetUserByID(id) |
|
||||||
if err != nil { |
|
||||||
ctx.ServerError("UserSignIn", err) |
|
||||||
return |
|
||||||
} |
|
||||||
remember := ctx.Session.Get("twofaRemember").(bool) |
|
||||||
if err := reg.UpdateCounter(); err != nil { |
|
||||||
ctx.ServerError("UserSignIn", err) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if ctx.Session.Get("linkAccount") != nil { |
|
||||||
if err := externalaccount.LinkAccountFromStore(ctx.Session, user); err != nil { |
|
||||||
ctx.ServerError("UserSignIn", err) |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
redirect := handleSignInFull(ctx, user, remember, false) |
|
||||||
if ctx.Written() { |
|
||||||
return |
|
||||||
} |
|
||||||
if redirect == "" { |
|
||||||
redirect = setting.AppSubURL + "/" |
|
||||||
} |
|
||||||
ctx.PlainText(http.StatusOK, redirect) |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
ctx.Error(http.StatusUnauthorized) |
|
||||||
} |
|
@ -0,0 +1,169 @@ |
|||||||
|
// 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 auth |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/base64" |
||||||
|
"errors" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/auth" |
||||||
|
user_model "code.gitea.io/gitea/models/user" |
||||||
|
wa "code.gitea.io/gitea/modules/auth/webauthn" |
||||||
|
"code.gitea.io/gitea/modules/base" |
||||||
|
"code.gitea.io/gitea/modules/context" |
||||||
|
"code.gitea.io/gitea/modules/log" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
"code.gitea.io/gitea/services/externalaccount" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/protocol" |
||||||
|
"github.com/duo-labs/webauthn/webauthn" |
||||||
|
) |
||||||
|
|
||||||
|
var tplWebAuthn base.TplName = "user/auth/webauthn" |
||||||
|
|
||||||
|
// WebAuthn shows the WebAuthn login page
|
||||||
|
func WebAuthn(ctx *context.Context) { |
||||||
|
ctx.Data["Title"] = ctx.Tr("twofa") |
||||||
|
|
||||||
|
// Check auto-login.
|
||||||
|
if checkAutoLogin(ctx) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
//Ensure user is in a 2FA session.
|
||||||
|
if ctx.Session.Get("twofaUid") == nil { |
||||||
|
ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session")) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
ctx.HTML(200, tplWebAuthn) |
||||||
|
} |
||||||
|
|
||||||
|
// WebAuthnLoginAssertion submits a WebAuthn challenge to the browser
|
||||||
|
func WebAuthnLoginAssertion(ctx *context.Context) { |
||||||
|
// Ensure user is in a WebAuthn session.
|
||||||
|
idSess, ok := ctx.Session.Get("twofaUid").(int64) |
||||||
|
if !ok || idSess == 0 { |
||||||
|
ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session")) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
user, err := user_model.GetUserByID(idSess) |
||||||
|
if err != nil { |
||||||
|
ctx.ServerError("UserSignIn", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
exists, err := auth.ExistsWebAuthnCredentialsForUID(user.ID) |
||||||
|
if err != nil { |
||||||
|
ctx.ServerError("UserSignIn", err) |
||||||
|
return |
||||||
|
} |
||||||
|
if !exists { |
||||||
|
ctx.ServerError("UserSignIn", errors.New("no device registered")) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
assertion, sessionData, err := wa.WebAuthn.BeginLogin((*wa.User)(user), webauthn.WithAssertionExtensions(protocol.AuthenticationExtensions{ |
||||||
|
"appid": setting.U2F.AppID, |
||||||
|
})) |
||||||
|
if err != nil { |
||||||
|
ctx.ServerError("webauthn.BeginLogin", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if err := ctx.Session.Set("webauthnAssertion", sessionData); err != nil { |
||||||
|
ctx.ServerError("Session.Set", err) |
||||||
|
return |
||||||
|
} |
||||||
|
ctx.JSON(http.StatusOK, assertion) |
||||||
|
} |
||||||
|
|
||||||
|
// WebAuthnLoginAssertionPost validates the signature and logs the user in
|
||||||
|
func WebAuthnLoginAssertionPost(ctx *context.Context) { |
||||||
|
idSess, ok := ctx.Session.Get("twofaUid").(int64) |
||||||
|
sessionData, okData := ctx.Session.Get("webauthnAssertion").(*webauthn.SessionData) |
||||||
|
if !ok || !okData || sessionData == nil || idSess == 0 { |
||||||
|
ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session")) |
||||||
|
return |
||||||
|
} |
||||||
|
defer func() { |
||||||
|
_ = ctx.Session.Delete("webauthnAssertion") |
||||||
|
}() |
||||||
|
|
||||||
|
// Load the user from the db
|
||||||
|
user, err := user_model.GetUserByID(idSess) |
||||||
|
if err != nil { |
||||||
|
ctx.ServerError("UserSignIn", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
log.Trace("Finishing webauthn authentication with user: %s", user.Name) |
||||||
|
|
||||||
|
// Now we do the equivalent of webauthn.FinishLogin using a combination of our session data
|
||||||
|
// (from webauthnAssertion) and verify the provided request.0
|
||||||
|
parsedResponse, err := protocol.ParseCredentialRequestResponse(ctx.Req) |
||||||
|
if err != nil { |
||||||
|
// Failed authentication attempt.
|
||||||
|
log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err) |
||||||
|
ctx.Status(http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Validate the parsed response.
|
||||||
|
cred, err := wa.WebAuthn.ValidateLogin((*wa.User)(user), *sessionData, parsedResponse) |
||||||
|
if err != nil { |
||||||
|
// Failed authentication attempt.
|
||||||
|
log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err) |
||||||
|
ctx.Status(http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Ensure that the credential wasn't cloned by checking if CloneWarning is set.
|
||||||
|
// (This is set if the sign counter is less than the one we have stored.)
|
||||||
|
if cred.Authenticator.CloneWarning { |
||||||
|
log.Info("Failed authentication attempt for %s from %s: cloned credential", user.Name, ctx.RemoteAddr()) |
||||||
|
ctx.Status(http.StatusForbidden) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Success! Get the credential and update the sign count with the new value we received.
|
||||||
|
dbCred, err := auth.GetWebAuthnCredentialByCredID(base64.RawStdEncoding.EncodeToString(cred.ID)) |
||||||
|
if err != nil { |
||||||
|
ctx.ServerError("GetWebAuthnCredentialByCredID", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
dbCred.SignCount = cred.Authenticator.SignCount |
||||||
|
if err := dbCred.UpdateSignCount(); err != nil { |
||||||
|
ctx.ServerError("UpdateSignCount", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Now handle account linking if that's requested
|
||||||
|
if ctx.Session.Get("linkAccount") != nil { |
||||||
|
if err := externalaccount.LinkAccountFromStore(ctx.Session, user); err != nil { |
||||||
|
ctx.ServerError("LinkAccountFromStore", err) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
remember := ctx.Session.Get("twofaRemember").(bool) |
||||||
|
redirect := handleSignInFull(ctx, user, remember, false) |
||||||
|
if redirect == "" { |
||||||
|
redirect = setting.AppSubURL + "/" |
||||||
|
} |
||||||
|
_ = ctx.Session.Delete("twofaUid") |
||||||
|
|
||||||
|
// Finally check if the appid extension was used:
|
||||||
|
if value, ok := parsedResponse.ClientExtensionResults["appid"]; ok { |
||||||
|
if appid, ok := value.(bool); ok && appid { |
||||||
|
ctx.Flash.Error(ctx.Tr("webauthn_u2f_deprecated", dbCred.Name)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
ctx.JSON(200, map[string]string{"redirect": redirect}) |
||||||
|
} |
@ -1,111 +0,0 @@ |
|||||||
// 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 security |
|
||||||
|
|
||||||
import ( |
|
||||||
"errors" |
|
||||||
"net/http" |
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/auth" |
|
||||||
"code.gitea.io/gitea/modules/context" |
|
||||||
"code.gitea.io/gitea/modules/log" |
|
||||||
"code.gitea.io/gitea/modules/setting" |
|
||||||
"code.gitea.io/gitea/modules/web" |
|
||||||
"code.gitea.io/gitea/services/forms" |
|
||||||
|
|
||||||
"github.com/tstranex/u2f" |
|
||||||
) |
|
||||||
|
|
||||||
// U2FRegister initializes the u2f registration procedure
|
|
||||||
func U2FRegister(ctx *context.Context) { |
|
||||||
form := web.GetForm(ctx).(*forms.U2FRegistrationForm) |
|
||||||
if form.Name == "" { |
|
||||||
ctx.Error(http.StatusConflict) |
|
||||||
return |
|
||||||
} |
|
||||||
challenge, err := u2f.NewChallenge(setting.U2F.AppID, setting.U2F.TrustedFacets) |
|
||||||
if err != nil { |
|
||||||
ctx.ServerError("NewChallenge", err) |
|
||||||
return |
|
||||||
} |
|
||||||
if err := ctx.Session.Set("u2fChallenge", challenge); err != nil { |
|
||||||
ctx.ServerError("Unable to set session key for u2fChallenge", err) |
|
||||||
return |
|
||||||
} |
|
||||||
regs, err := auth.GetU2FRegistrationsByUID(ctx.User.ID) |
|
||||||
if err != nil { |
|
||||||
ctx.ServerError("GetU2FRegistrationsByUID", err) |
|
||||||
return |
|
||||||
} |
|
||||||
for _, reg := range regs { |
|
||||||
if reg.Name == form.Name { |
|
||||||
ctx.Error(http.StatusConflict, "Name already taken") |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
if err := ctx.Session.Set("u2fName", form.Name); err != nil { |
|
||||||
ctx.ServerError("Unable to set session key for u2fName", err) |
|
||||||
return |
|
||||||
} |
|
||||||
// Here we're just going to try to release the session early
|
|
||||||
if err := ctx.Session.Release(); err != nil { |
|
||||||
// we'll tolerate errors here as they *should* get saved elsewhere
|
|
||||||
log.Error("Unable to save changes to the session: %v", err) |
|
||||||
} |
|
||||||
ctx.JSON(http.StatusOK, u2f.NewWebRegisterRequest(challenge, regs.ToRegistrations())) |
|
||||||
} |
|
||||||
|
|
||||||
// U2FRegisterPost receives the response of the security key
|
|
||||||
func U2FRegisterPost(ctx *context.Context) { |
|
||||||
response := web.GetForm(ctx).(*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 = auth.CreateRegistration(ctx.User.ID, 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 := web.GetForm(ctx).(*forms.U2FDeleteForm) |
|
||||||
reg, err := auth.GetU2FRegistrationByID(form.ID) |
|
||||||
if err != nil { |
|
||||||
if auth.IsErrU2FRegistrationNotExist(err) { |
|
||||||
ctx.Status(200) |
|
||||||
return |
|
||||||
} |
|
||||||
ctx.ServerError("GetU2FRegistrationByID", err) |
|
||||||
return |
|
||||||
} |
|
||||||
if reg.UserID != ctx.User.ID { |
|
||||||
ctx.Status(401) |
|
||||||
return |
|
||||||
} |
|
||||||
if err := auth.DeleteRegistration(reg); err != nil { |
|
||||||
ctx.ServerError("DeleteRegistration", err) |
|
||||||
return |
|
||||||
} |
|
||||||
ctx.JSON(http.StatusOK, map[string]interface{}{ |
|
||||||
"redirect": setting.AppSubURL + "/user/settings/security", |
|
||||||
}) |
|
||||||
} |
|
@ -0,0 +1,119 @@ |
|||||||
|
// 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 security |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/auth" |
||||||
|
wa "code.gitea.io/gitea/modules/auth/webauthn" |
||||||
|
"code.gitea.io/gitea/modules/context" |
||||||
|
"code.gitea.io/gitea/modules/log" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
"code.gitea.io/gitea/modules/web" |
||||||
|
"code.gitea.io/gitea/services/forms" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/protocol" |
||||||
|
"github.com/duo-labs/webauthn/webauthn" |
||||||
|
) |
||||||
|
|
||||||
|
// WebAuthnRegister initializes the webauthn registration procedure
|
||||||
|
func WebAuthnRegister(ctx *context.Context) { |
||||||
|
form := web.GetForm(ctx).(*forms.WebauthnRegistrationForm) |
||||||
|
if form.Name == "" { |
||||||
|
ctx.Error(http.StatusConflict) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
cred, err := auth.GetWebAuthnCredentialByName(ctx.User.ID, form.Name) |
||||||
|
if err != nil && !auth.IsErrWebAuthnCredentialNotExist(err) { |
||||||
|
ctx.ServerError("GetWebAuthnCredentialsByUID", err) |
||||||
|
return |
||||||
|
} |
||||||
|
if cred != nil { |
||||||
|
ctx.Error(http.StatusConflict, "Name already taken") |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
_ = ctx.Session.Delete("registration") |
||||||
|
if err := ctx.Session.Set("WebauthnName", form.Name); err != nil { |
||||||
|
ctx.ServerError("Unable to set session key for WebauthnName", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
credentialOptions, sessionData, err := wa.WebAuthn.BeginRegistration((*wa.User)(ctx.User)) |
||||||
|
if err != nil { |
||||||
|
ctx.ServerError("Unable to BeginRegistration", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Save the session data as marshaled JSON
|
||||||
|
if err = ctx.Session.Set("registration", sessionData); err != nil { |
||||||
|
ctx.ServerError("Unable to set session", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, credentialOptions) |
||||||
|
} |
||||||
|
|
||||||
|
// WebauthnRegisterPost receives the response of the security key
|
||||||
|
func WebauthnRegisterPost(ctx *context.Context) { |
||||||
|
name, ok := ctx.Session.Get("WebauthnName").(string) |
||||||
|
if !ok || name == "" { |
||||||
|
ctx.ServerError("Get WebauthnName", errors.New("no WebauthnName")) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Load the session data
|
||||||
|
sessionData, ok := ctx.Session.Get("registration").(*webauthn.SessionData) |
||||||
|
if !ok || sessionData == nil { |
||||||
|
ctx.ServerError("Get registration", errors.New("no registration")) |
||||||
|
return |
||||||
|
} |
||||||
|
defer func() { |
||||||
|
_ = ctx.Session.Delete("registration") |
||||||
|
}() |
||||||
|
|
||||||
|
// Verify that the challenge succeeded
|
||||||
|
cred, err := wa.WebAuthn.FinishRegistration((*wa.User)(ctx.User), *sessionData, ctx.Req) |
||||||
|
if err != nil { |
||||||
|
if pErr, ok := err.(*protocol.Error); ok { |
||||||
|
log.Error("Unable to finish registration due to error: %v\nDevInfo: %s", pErr, pErr.DevInfo) |
||||||
|
} |
||||||
|
ctx.ServerError("CreateCredential", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
dbCred, err := auth.GetWebAuthnCredentialByName(ctx.User.ID, name) |
||||||
|
if err != nil && !auth.IsErrWebAuthnCredentialNotExist(err) { |
||||||
|
ctx.ServerError("GetWebAuthnCredentialsByUID", err) |
||||||
|
return |
||||||
|
} |
||||||
|
if dbCred != nil { |
||||||
|
ctx.Error(http.StatusConflict, "Name already taken") |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Create the credential
|
||||||
|
_, err = auth.CreateCredential(ctx.User.ID, name, cred) |
||||||
|
if err != nil { |
||||||
|
ctx.ServerError("CreateCredential", err) |
||||||
|
return |
||||||
|
} |
||||||
|
ctx.JSON(http.StatusCreated, cred) |
||||||
|
} |
||||||
|
|
||||||
|
// WebauthnDelete deletes an security key by id
|
||||||
|
func WebauthnDelete(ctx *context.Context) { |
||||||
|
form := web.GetForm(ctx).(*forms.WebauthnDeleteForm) |
||||||
|
if _, err := auth.DeleteCredential(form.ID, ctx.User.ID); err != nil { |
||||||
|
ctx.ServerError("GetWebAuthnCredentialByID", err) |
||||||
|
return |
||||||
|
} |
||||||
|
ctx.JSON(http.StatusOK, map[string]interface{}{ |
||||||
|
"redirect": setting.AppSubURL + "/user/settings/security", |
||||||
|
}) |
||||||
|
} |
@ -1,24 +0,0 @@ |
|||||||
{{template "base/head" .}} |
|
||||||
<div class="page-content 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> |
|
||||||
{{if .TOTPEnrolled}} |
|
||||||
<div class="ui attached segment"> |
|
||||||
<a href="{{AppSubUrl}}/user/two_factor">{{.i18n.Tr "u2f_use_twofa"}}</a> |
|
||||||
</div> |
|
||||||
{{end}} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
{{template "user/auth/u2f_error" .}} |
|
||||||
{{template "base/footer" .}} |
|
@ -1,32 +0,0 @@ |
|||||||
<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,22 @@ |
|||||||
|
{{template "base/head" .}} |
||||||
|
<div class="user signin webauthn-prompt"> |
||||||
|
<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 "webauthn_insert_key"}}</h3> |
||||||
|
{{template "base/alert" .}} |
||||||
|
<p>{{.i18n.Tr "webauthn_sign_in"}}</p> |
||||||
|
</div> |
||||||
|
<div class="ui attached segment"><div class="ui active indeterminate inline loader"></div> {{.i18n.Tr "webauthn_press_button"}} </div> |
||||||
|
<div class="ui attached segment"> |
||||||
|
<a href="{{AppSubUrl}}/user/two_factor">{{.i18n.Tr "webauthn_use_twofa"}}</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{template "user/auth/webauthn_error" .}} |
||||||
|
{{template "base/footer" .}} |
@ -0,0 +1,22 @@ |
|||||||
|
<div class="ui small modal" id="webauthn-error"> |
||||||
|
<div class="header">{{.i18n.Tr "webauthn_error"}}</div> |
||||||
|
<div class="content"> |
||||||
|
<div class="ui negative message"> |
||||||
|
<div class="header"> |
||||||
|
{{.i18n.Tr "webauthn_error"}} |
||||||
|
</div> |
||||||
|
<div class="hide" data-webauthn-error-msg="browser"><p>{{.i18n.Tr "webauthn_unsupported_browser"}}</div> |
||||||
|
<div class="hide" data-webauthn-error-msg="unknown"><p>{{.i18n.Tr "webauthn_error_unknown"}}</div> |
||||||
|
<div class="hide" data-webauthn-error-msg="insecure"><p>{{.i18n.Tr "webauthn_error_insecure"}}</div> |
||||||
|
<div class="hide" data-webauthn-error-msg="unable-to-process"><p>{{.i18n.Tr "webauthn_error_unable_to_process"}}</div> |
||||||
|
<div class="hide" data-webauthn-error-msg="duplicated"><p>{{.i18n.Tr "webauthn_error_duplicated"}}</div> |
||||||
|
<div class="hide" data-webauthn-error-msg="empty"><p>{{.i18n.Tr "webauthn_error_empty"}}</div> |
||||||
|
<div class="hide" data-webauthn-error-msg="timeout"><p>{{.i18n.Tr "webauthn_error_timeout"}}</div> |
||||||
|
<div class="hide" data-webauthn-error-msg="0"></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="actions"> |
||||||
|
<button onclick="window.location.reload()" class="success ui button hide webauthn_error_timeout">{{.i18n.Tr "webauthn_reload"}}</button> |
||||||
|
<div class="ui cancel button">{{.i18n.Tr "cancel"}}</div> |
||||||
|
</div> |
||||||
|
</div> |
@ -1,51 +1,52 @@ |
|||||||
<h4 class="ui top attached header"> |
<h4 class="ui top attached header"> |
||||||
{{.i18n.Tr "settings.u2f"}} |
{{.i18n.Tr "settings.webauthn"}} |
||||||
</h4> |
</h4> |
||||||
<div class="ui attached segment"> |
<div class="ui attached segment"> |
||||||
<p>{{.i18n.Tr "settings.u2f_desc" | Str2html}}</p> |
<p>{{.i18n.Tr "settings.webauthn_desc" | Str2html}}</p> |
||||||
<div class="ui key list"> |
<div class="ui key list"> |
||||||
{{range .U2FRegistrations}} |
{{range .WebAuthnCredentials}} |
||||||
<div class="item"> |
<div class="item"> |
||||||
<div class="right floated content"> |
<div class="right floated content"> |
||||||
<button class="ui red tiny button delete-button" data-modal-id="delete-registration" data-url="{{$.Link}}/u2f/delete" data-id="{{.ID}}"> |
<button class="ui red tiny button delete-button" data-modal-id="delete-registration" data-url="{{$.Link}}/webauthn/delete" data-id="{{.ID}}"> |
||||||
{{$.i18n.Tr "settings.delete_key"}} |
{{$.i18n.Tr "settings.delete_key"}} |
||||||
</button> |
</button> |
||||||
</div> |
</div> |
||||||
<div class="content"> |
<div class="content"> |
||||||
<strong>{{.Name}}</strong> |
<strong>{{.Name}}</strong> |
||||||
</div> |
</div> |
||||||
|
<span class="time">{{TimeSinceUnix .CreatedUnix $.Lang}}</span> |
||||||
</div> |
</div> |
||||||
{{end}} |
{{end}} |
||||||
</div> |
</div> |
||||||
<div class="ui form"> |
<div class="ui form"> |
||||||
{{.CsrfTokenHtml}} |
{{.CsrfTokenHtml}} |
||||||
<div class="required field"> |
<div class="required field"> |
||||||
<label for="nickname">{{.i18n.Tr "settings.u2f_nickname"}}</label> |
<label for="nickname">{{.i18n.Tr "settings.webauthn_nickname"}}</label> |
||||||
<input id="nickname" name="nickname" type="text" required> |
<input id="nickname" name="nickname" type="text" required> |
||||||
</div> |
</div> |
||||||
<button id="register-security-key" class="ui green button">{{svg "octicon-key"}} {{.i18n.Tr "settings.u2f_register_key"}}</button> |
<button id="register-webauthn" class="ui green button">{{svg "octicon-key"}} {{.i18n.Tr "settings.webauthn_register_key"}}</button> |
||||||
</div> |
</div> |
||||||
</div> |
</div> |
||||||
|
|
||||||
<div class="ui small modal" id="register-device"> |
<div class="ui small modal" id="register-device"> |
||||||
<div class="header">{{.i18n.Tr "settings.u2f_register_key"}}</div> |
<div class="header">{{.i18n.Tr "settings.webauthn_register_key"}}</div> |
||||||
<div class="content"> |
<div class="content"> |
||||||
<i class="notched spinner loading icon"></i> {{.i18n.Tr "settings.u2f_press_button"}} |
<i class="notched spinner loading icon"></i> {{.i18n.Tr "settings.webauthn_press_button"}} |
||||||
</div> |
</div> |
||||||
<div class="actions"> |
<div class="actions"> |
||||||
<div class="ui cancel button">{{.i18n.Tr "cancel"}}</div> |
<div class="ui cancel button">{{.i18n.Tr "cancel"}}</div> |
||||||
</div> |
</div> |
||||||
</div> |
</div> |
||||||
|
|
||||||
{{template "user/auth/u2f_error" .}} |
{{template "user/auth/webauthn_error" .}} |
||||||
|
|
||||||
<div class="ui small basic delete modal" id="delete-registration"> |
<div class="ui small basic delete modal" id="delete-registration"> |
||||||
<div class="ui icon header"> |
<div class="ui icon header"> |
||||||
{{svg "octicon-trash"}} |
{{svg "octicon-trash"}} |
||||||
{{.i18n.Tr "settings.u2f_delete_key"}} |
{{.i18n.Tr "settings.webauthn_delete_key"}} |
||||||
</div> |
</div> |
||||||
<div class="content"> |
<div class="content"> |
||||||
<p>{{.i18n.Tr "settings.u2f_delete_key_desc"}}</p> |
<p>{{.i18n.Tr "settings.webauthn_delete_key_desc"}}</p> |
||||||
</div> |
</div> |
||||||
{{template "base/delete_modal_actions" .}} |
{{template "base/delete_modal_actions" .}} |
||||||
</div> |
</div> |
@ -0,0 +1,24 @@ |
|||||||
|
Copyright (c) 2014 CloudFlare Inc. |
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without |
||||||
|
modification, are permitted provided that the following conditions |
||||||
|
are met: |
||||||
|
|
||||||
|
Redistributions of source code must retain the above copyright notice, |
||||||
|
this list of conditions and the following disclaimer. |
||||||
|
|
||||||
|
Redistributions in binary form must reproduce the above copyright notice, |
||||||
|
this list of conditions and the following disclaimer in the documentation |
||||||
|
and/or other materials provided with the distribution. |
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||||||
|
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED |
||||||
|
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,188 @@ |
|||||||
|
// Package pkcs7 implements the subset of the CMS PKCS #7 datatype that is typically
|
||||||
|
// used to package certificates and CRLs. Using openssl, every certificate converted
|
||||||
|
// to PKCS #7 format from another encoding such as PEM conforms to this implementation.
|
||||||
|
// reference: https://www.openssl.org/docs/man1.1.0/apps/crl2pkcs7.html
|
||||||
|
//
|
||||||
|
// PKCS #7 Data type, reference: https://tools.ietf.org/html/rfc2315
|
||||||
|
//
|
||||||
|
// The full pkcs#7 cryptographic message syntax allows for cryptographic enhancements,
|
||||||
|
// for example data can be encrypted and signed and then packaged through pkcs#7 to be
|
||||||
|
// sent over a network and then verified and decrypted. It is asn1, and the type of
|
||||||
|
// PKCS #7 ContentInfo, which comprises the PKCS #7 structure, is:
|
||||||
|
//
|
||||||
|
// ContentInfo ::= SEQUENCE {
|
||||||
|
// contentType ContentType,
|
||||||
|
// content [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// There are 6 possible ContentTypes, data, signedData, envelopedData,
|
||||||
|
// signedAndEnvelopedData, digestedData, and encryptedData. Here signedData, Data, and encrypted
|
||||||
|
// Data are implemented, as the degenerate case of signedData without a signature is the typical
|
||||||
|
// format for transferring certificates and CRLS, and Data and encryptedData are used in PKCS #12
|
||||||
|
// formats.
|
||||||
|
// The ContentType signedData has the form:
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// signedData ::= SEQUENCE {
|
||||||
|
// version Version,
|
||||||
|
// digestAlgorithms DigestAlgorithmIdentifiers,
|
||||||
|
// contentInfo ContentInfo,
|
||||||
|
// certificates [0] IMPLICIT ExtendedCertificatesAndCertificates OPTIONAL
|
||||||
|
// crls [1] IMPLICIT CertificateRevocationLists OPTIONAL,
|
||||||
|
// signerInfos SignerInfos
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// As of yet signerInfos and digestAlgorithms are not parsed, as they are not relevant to
|
||||||
|
// this system's use of PKCS #7 data. Version is an integer type, note that PKCS #7 is
|
||||||
|
// recursive, this second layer of ContentInfo is similar ignored for our degenerate
|
||||||
|
// usage. The ExtendedCertificatesAndCertificates type consists of a sequence of choices
|
||||||
|
// between PKCS #6 extended certificates and x509 certificates. Any sequence consisting
|
||||||
|
// of any number of extended certificates is not yet supported in this implementation.
|
||||||
|
//
|
||||||
|
// The ContentType Data is simply a raw octet string and is parsed directly into a Go []byte slice.
|
||||||
|
//
|
||||||
|
// The ContentType encryptedData is the most complicated and its form can be gathered by
|
||||||
|
// the go type below. It essentially contains a raw octet string of encrypted data and an
|
||||||
|
// algorithm identifier for use in decrypting this data.
|
||||||
|
package pkcs7 |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/x509" |
||||||
|
"crypto/x509/pkix" |
||||||
|
"encoding/asn1" |
||||||
|
"errors" |
||||||
|
|
||||||
|
cferr "github.com/cloudflare/cfssl/errors" |
||||||
|
) |
||||||
|
|
||||||
|
// Types used for asn1 Unmarshaling.
|
||||||
|
|
||||||
|
type signedData struct { |
||||||
|
Version int |
||||||
|
DigestAlgorithms asn1.RawValue |
||||||
|
ContentInfo asn1.RawValue |
||||||
|
Certificates asn1.RawValue `asn1:"optional" asn1:"tag:0"` |
||||||
|
Crls asn1.RawValue `asn1:"optional"` |
||||||
|
SignerInfos asn1.RawValue |
||||||
|
} |
||||||
|
|
||||||
|
type initPKCS7 struct { |
||||||
|
Raw asn1.RawContent |
||||||
|
ContentType asn1.ObjectIdentifier |
||||||
|
Content asn1.RawValue `asn1:"tag:0,explicit,optional"` |
||||||
|
} |
||||||
|
|
||||||
|
// Object identifier strings of the three implemented PKCS7 types.
|
||||||
|
const ( |
||||||
|
ObjIDData = "1.2.840.113549.1.7.1" |
||||||
|
ObjIDSignedData = "1.2.840.113549.1.7.2" |
||||||
|
ObjIDEncryptedData = "1.2.840.113549.1.7.6" |
||||||
|
) |
||||||
|
|
||||||
|
// PKCS7 represents the ASN1 PKCS #7 Content type. It contains one of three
|
||||||
|
// possible types of Content objects, as denoted by the object identifier in
|
||||||
|
// the ContentInfo field, the other two being nil. SignedData
|
||||||
|
// is the degenerate SignedData Content info without signature used
|
||||||
|
// to hold certificates and crls. Data is raw bytes, and EncryptedData
|
||||||
|
// is as defined in PKCS #7 standard.
|
||||||
|
type PKCS7 struct { |
||||||
|
Raw asn1.RawContent |
||||||
|
ContentInfo string |
||||||
|
Content Content |
||||||
|
} |
||||||
|
|
||||||
|
// Content implements three of the six possible PKCS7 data types. Only one is non-nil.
|
||||||
|
type Content struct { |
||||||
|
Data []byte |
||||||
|
SignedData SignedData |
||||||
|
EncryptedData EncryptedData |
||||||
|
} |
||||||
|
|
||||||
|
// SignedData defines the typical carrier of certificates and crls.
|
||||||
|
type SignedData struct { |
||||||
|
Raw asn1.RawContent |
||||||
|
Version int |
||||||
|
Certificates []*x509.Certificate |
||||||
|
Crl *pkix.CertificateList |
||||||
|
} |
||||||
|
|
||||||
|
// Data contains raw bytes. Used as a subtype in PKCS12.
|
||||||
|
type Data struct { |
||||||
|
Bytes []byte |
||||||
|
} |
||||||
|
|
||||||
|
// EncryptedData contains encrypted data. Used as a subtype in PKCS12.
|
||||||
|
type EncryptedData struct { |
||||||
|
Raw asn1.RawContent |
||||||
|
Version int |
||||||
|
EncryptedContentInfo EncryptedContentInfo |
||||||
|
} |
||||||
|
|
||||||
|
// EncryptedContentInfo is a subtype of PKCS7EncryptedData.
|
||||||
|
type EncryptedContentInfo struct { |
||||||
|
Raw asn1.RawContent |
||||||
|
ContentType asn1.ObjectIdentifier |
||||||
|
ContentEncryptionAlgorithm pkix.AlgorithmIdentifier |
||||||
|
EncryptedContent []byte `asn1:"tag:0,optional"` |
||||||
|
} |
||||||
|
|
||||||
|
// ParsePKCS7 attempts to parse the DER encoded bytes of a
|
||||||
|
// PKCS7 structure.
|
||||||
|
func ParsePKCS7(raw []byte) (msg *PKCS7, err error) { |
||||||
|
|
||||||
|
var pkcs7 initPKCS7 |
||||||
|
_, err = asn1.Unmarshal(raw, &pkcs7) |
||||||
|
if err != nil { |
||||||
|
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, err) |
||||||
|
} |
||||||
|
|
||||||
|
msg = new(PKCS7) |
||||||
|
msg.Raw = pkcs7.Raw |
||||||
|
msg.ContentInfo = pkcs7.ContentType.String() |
||||||
|
switch { |
||||||
|
case msg.ContentInfo == ObjIDData: |
||||||
|
msg.ContentInfo = "Data" |
||||||
|
_, err = asn1.Unmarshal(pkcs7.Content.Bytes, &msg.Content.Data) |
||||||
|
if err != nil { |
||||||
|
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, err) |
||||||
|
} |
||||||
|
case msg.ContentInfo == ObjIDSignedData: |
||||||
|
msg.ContentInfo = "SignedData" |
||||||
|
var signedData signedData |
||||||
|
_, err = asn1.Unmarshal(pkcs7.Content.Bytes, &signedData) |
||||||
|
if err != nil { |
||||||
|
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, err) |
||||||
|
} |
||||||
|
if len(signedData.Certificates.Bytes) != 0 { |
||||||
|
msg.Content.SignedData.Certificates, err = x509.ParseCertificates(signedData.Certificates.Bytes) |
||||||
|
if err != nil { |
||||||
|
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, err) |
||||||
|
} |
||||||
|
} |
||||||
|
if len(signedData.Crls.Bytes) != 0 { |
||||||
|
msg.Content.SignedData.Crl, err = x509.ParseDERCRL(signedData.Crls.Bytes) |
||||||
|
if err != nil { |
||||||
|
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, err) |
||||||
|
} |
||||||
|
} |
||||||
|
msg.Content.SignedData.Version = signedData.Version |
||||||
|
msg.Content.SignedData.Raw = pkcs7.Content.Bytes |
||||||
|
case msg.ContentInfo == ObjIDEncryptedData: |
||||||
|
msg.ContentInfo = "EncryptedData" |
||||||
|
var encryptedData EncryptedData |
||||||
|
_, err = asn1.Unmarshal(pkcs7.Content.Bytes, &encryptedData) |
||||||
|
if err != nil { |
||||||
|
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, err) |
||||||
|
} |
||||||
|
if encryptedData.Version != 0 { |
||||||
|
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, errors.New("Only support for PKCS #7 encryptedData version 0")) |
||||||
|
} |
||||||
|
msg.Content.EncryptedData = encryptedData |
||||||
|
|
||||||
|
default: |
||||||
|
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, errors.New("Attempt to parse PKCS# 7 Content not of type data, signed data or encrypted data")) |
||||||
|
} |
||||||
|
|
||||||
|
return msg, nil |
||||||
|
|
||||||
|
} |
@ -0,0 +1,46 @@ |
|||||||
|
/* |
||||||
|
Package errors provides error types returned in CF SSL. |
||||||
|
|
||||||
|
1. Type Error is intended for errors produced by CF SSL packages. |
||||||
|
It formats to a json object that consists of an error message and a 4-digit code for error reasoning. |
||||||
|
|
||||||
|
Example: {"code":1002, "message": "Failed to decode certificate"} |
||||||
|
|
||||||
|
The index of codes are listed below: |
||||||
|
1XXX: CertificateError |
||||||
|
1000: Unknown |
||||||
|
1001: ReadFailed |
||||||
|
1002: DecodeFailed |
||||||
|
1003: ParseFailed |
||||||
|
1100: SelfSigned |
||||||
|
12XX: VerifyFailed |
||||||
|
121X: CertificateInvalid |
||||||
|
1210: NotAuthorizedToSign |
||||||
|
1211: Expired |
||||||
|
1212: CANotAuthorizedForThisName |
||||||
|
1213: TooManyIntermediates |
||||||
|
1214: IncompatibleUsage |
||||||
|
1220: UnknownAuthority |
||||||
|
2XXX: PrivatekeyError |
||||||
|
2000: Unknown |
||||||
|
2001: ReadFailed |
||||||
|
2002: DecodeFailed |
||||||
|
2003: ParseFailed |
||||||
|
2100: Encrypted |
||||||
|
2200: NotRSA |
||||||
|
2300: KeyMismatch |
||||||
|
2400: GenerationFailed |
||||||
|
2500: Unavailable |
||||||
|
3XXX: IntermediatesError |
||||||
|
4XXX: RootError |
||||||
|
5XXX: PolicyError |
||||||
|
5100: NoKeyUsages |
||||||
|
5200: InvalidPolicy |
||||||
|
5300: InvalidRequest |
||||||
|
5400: UnknownProfile |
||||||
|
6XXX: DialError |
||||||
|
|
||||||
|
2. Type HttpError is intended for CF SSL API to consume. It contains a HTTP status code that will be read and returned |
||||||
|
by the API server. |
||||||
|
*/ |
||||||
|
package errors |
@ -0,0 +1,438 @@ |
|||||||
|
package errors |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/x509" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
) |
||||||
|
|
||||||
|
// Error is the error type usually returned by functions in CF SSL package.
|
||||||
|
// It contains a 4-digit error code where the most significant digit
|
||||||
|
// describes the category where the error occurred and the rest 3 digits
|
||||||
|
// describe the specific error reason.
|
||||||
|
type Error struct { |
||||||
|
ErrorCode int `json:"code"` |
||||||
|
Message string `json:"message"` |
||||||
|
} |
||||||
|
|
||||||
|
// Category is the most significant digit of the error code.
|
||||||
|
type Category int |
||||||
|
|
||||||
|
// Reason is the last 3 digits of the error code.
|
||||||
|
type Reason int |
||||||
|
|
||||||
|
const ( |
||||||
|
// Success indicates no error occurred.
|
||||||
|
Success Category = 1000 * iota // 0XXX
|
||||||
|
|
||||||
|
// CertificateError indicates a fault in a certificate.
|
||||||
|
CertificateError // 1XXX
|
||||||
|
|
||||||
|
// PrivateKeyError indicates a fault in a private key.
|
||||||
|
PrivateKeyError // 2XXX
|
||||||
|
|
||||||
|
// IntermediatesError indicates a fault in an intermediate.
|
||||||
|
IntermediatesError // 3XXX
|
||||||
|
|
||||||
|
// RootError indicates a fault in a root.
|
||||||
|
RootError // 4XXX
|
||||||
|
|
||||||
|
// PolicyError indicates an error arising from a malformed or
|
||||||
|
// non-existent policy, or a breach of policy.
|
||||||
|
PolicyError // 5XXX
|
||||||
|
|
||||||
|
// DialError indicates a network fault.
|
||||||
|
DialError // 6XXX
|
||||||
|
|
||||||
|
// APIClientError indicates a problem with the API client.
|
||||||
|
APIClientError // 7XXX
|
||||||
|
|
||||||
|
// OCSPError indicates a problem with OCSP signing
|
||||||
|
OCSPError // 8XXX
|
||||||
|
|
||||||
|
// CSRError indicates a problem with CSR parsing
|
||||||
|
CSRError // 9XXX
|
||||||
|
|
||||||
|
// CTError indicates a problem with the certificate transparency process
|
||||||
|
CTError // 10XXX
|
||||||
|
|
||||||
|
// CertStoreError indicates a problem with the certificate store
|
||||||
|
CertStoreError // 11XXX
|
||||||
|
) |
||||||
|
|
||||||
|
// None is a non-specified error.
|
||||||
|
const ( |
||||||
|
None Reason = iota |
||||||
|
) |
||||||
|
|
||||||
|
// Warning code for a success
|
||||||
|
const ( |
||||||
|
BundleExpiringBit int = 1 << iota // 0x01
|
||||||
|
BundleNotUbiquitousBit // 0x02
|
||||||
|
) |
||||||
|
|
||||||
|
// Parsing errors
|
||||||
|
const ( |
||||||
|
Unknown Reason = iota // X000
|
||||||
|
ReadFailed // X001
|
||||||
|
DecodeFailed // X002
|
||||||
|
ParseFailed // X003
|
||||||
|
) |
||||||
|
|
||||||
|
// The following represent certificate non-parsing errors, and must be
|
||||||
|
// specified along with CertificateError.
|
||||||
|
const ( |
||||||
|
// SelfSigned indicates that a certificate is self-signed and
|
||||||
|
// cannot be used in the manner being attempted.
|
||||||
|
SelfSigned Reason = 100 * (iota + 1) // Code 11XX
|
||||||
|
|
||||||
|
// VerifyFailed is an X.509 verification failure. The least two
|
||||||
|
// significant digits of 12XX is determined as the actual x509
|
||||||
|
// error is examined.
|
||||||
|
VerifyFailed // Code 12XX
|
||||||
|
|
||||||
|
// BadRequest indicates that the certificate request is invalid.
|
||||||
|
BadRequest // Code 13XX
|
||||||
|
|
||||||
|
// MissingSerial indicates that the profile specified
|
||||||
|
// 'ClientProvidesSerialNumbers', but the SignRequest did not include a serial
|
||||||
|
// number.
|
||||||
|
MissingSerial // Code 14XX
|
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
certificateInvalid = 10 * (iota + 1) //121X
|
||||||
|
unknownAuthority //122x
|
||||||
|
) |
||||||
|
|
||||||
|
// The following represent private-key non-parsing errors, and must be
|
||||||
|
// specified with PrivateKeyError.
|
||||||
|
const ( |
||||||
|
// Encrypted indicates that the private key is a PKCS #8 encrypted
|
||||||
|
// private key. At this time, CFSSL does not support decrypting
|
||||||
|
// these keys.
|
||||||
|
Encrypted Reason = 100 * (iota + 1) //21XX
|
||||||
|
|
||||||
|
// NotRSAOrECC indicates that they key is not an RSA or ECC
|
||||||
|
// private key; these are the only two private key types supported
|
||||||
|
// at this time by CFSSL.
|
||||||
|
NotRSAOrECC //22XX
|
||||||
|
|
||||||
|
// KeyMismatch indicates that the private key does not match
|
||||||
|
// the public key or certificate being presented with the key.
|
||||||
|
KeyMismatch //23XX
|
||||||
|
|
||||||
|
// GenerationFailed indicates that a private key could not
|
||||||
|
// be generated.
|
||||||
|
GenerationFailed //24XX
|
||||||
|
|
||||||
|
// Unavailable indicates that a private key mechanism (such as
|
||||||
|
// PKCS #11) was requested but support for that mechanism is
|
||||||
|
// not available.
|
||||||
|
Unavailable |
||||||
|
) |
||||||
|
|
||||||
|
// The following are policy-related non-parsing errors, and must be
|
||||||
|
// specified along with PolicyError.
|
||||||
|
const ( |
||||||
|
// NoKeyUsages indicates that the profile does not permit any
|
||||||
|
// key usages for the certificate.
|
||||||
|
NoKeyUsages Reason = 100 * (iota + 1) // 51XX
|
||||||
|
|
||||||
|
// InvalidPolicy indicates that policy being requested is not
|
||||||
|
// a valid policy or does not exist.
|
||||||
|
InvalidPolicy // 52XX
|
||||||
|
|
||||||
|
// InvalidRequest indicates a certificate request violated the
|
||||||
|
// constraints of the policy being applied to the request.
|
||||||
|
InvalidRequest // 53XX
|
||||||
|
|
||||||
|
// UnknownProfile indicates that the profile does not exist.
|
||||||
|
UnknownProfile // 54XX
|
||||||
|
|
||||||
|
UnmatchedWhitelist // 55xx
|
||||||
|
) |
||||||
|
|
||||||
|
// The following are API client related errors, and should be
|
||||||
|
// specified with APIClientError.
|
||||||
|
const ( |
||||||
|
// AuthenticationFailure occurs when the client is unable
|
||||||
|
// to obtain an authentication token for the request.
|
||||||
|
AuthenticationFailure Reason = 100 * (iota + 1) |
||||||
|
|
||||||
|
// JSONError wraps an encoding/json error.
|
||||||
|
JSONError |
||||||
|
|
||||||
|
// IOError wraps an io/ioutil error.
|
||||||
|
IOError |
||||||
|
|
||||||
|
// ClientHTTPError wraps a net/http error.
|
||||||
|
ClientHTTPError |
||||||
|
|
||||||
|
// ServerRequestFailed covers any other failures from the API
|
||||||
|
// client.
|
||||||
|
ServerRequestFailed |
||||||
|
) |
||||||
|
|
||||||
|
// The following are OCSP related errors, and should be
|
||||||
|
// specified with OCSPError
|
||||||
|
const ( |
||||||
|
// IssuerMismatch ocurs when the certificate in the OCSP signing
|
||||||
|
// request was not issued by the CA that this responder responds for.
|
||||||
|
IssuerMismatch Reason = 100 * (iota + 1) // 81XX
|
||||||
|
|
||||||
|
// InvalidStatus occurs when the OCSP signing requests includes an
|
||||||
|
// invalid value for the certificate status.
|
||||||
|
InvalidStatus |
||||||
|
) |
||||||
|
|
||||||
|
// Certificate transparency related errors specified with CTError
|
||||||
|
const ( |
||||||
|
// PrecertSubmissionFailed occurs when submitting a precertificate to
|
||||||
|
// a log server fails
|
||||||
|
PrecertSubmissionFailed = 100 * (iota + 1) |
||||||
|
// CTClientConstructionFailed occurs when the construction of a new
|
||||||
|
// github.com/google/certificate-transparency client fails.
|
||||||
|
CTClientConstructionFailed |
||||||
|
// PrecertMissingPoison occurs when a precert is passed to SignFromPrecert
|
||||||
|
// and is missing the CT poison extension.
|
||||||
|
PrecertMissingPoison |
||||||
|
// PrecertInvalidPoison occurs when a precert is passed to SignFromPrecert
|
||||||
|
// and has a invalid CT poison extension value or the extension is not
|
||||||
|
// critical.
|
||||||
|
PrecertInvalidPoison |
||||||
|
) |
||||||
|
|
||||||
|
// Certificate persistence related errors specified with CertStoreError
|
||||||
|
const ( |
||||||
|
// InsertionFailed occurs when a SQL insert query failes to complete.
|
||||||
|
InsertionFailed = 100 * (iota + 1) |
||||||
|
// RecordNotFound occurs when a SQL query targeting on one unique
|
||||||
|
// record failes to update the specified row in the table.
|
||||||
|
RecordNotFound |
||||||
|
) |
||||||
|
|
||||||
|
// The error interface implementation, which formats to a JSON object string.
|
||||||
|
func (e *Error) Error() string { |
||||||
|
marshaled, err := json.Marshal(e) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
return string(marshaled) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// New returns an error that contains an error code and message derived from
|
||||||
|
// the given category, reason. Currently, to avoid confusion, it is not
|
||||||
|
// allowed to create an error of category Success
|
||||||
|
func New(category Category, reason Reason) *Error { |
||||||
|
errorCode := int(category) + int(reason) |
||||||
|
var msg string |
||||||
|
switch category { |
||||||
|
case OCSPError: |
||||||
|
switch reason { |
||||||
|
case ReadFailed: |
||||||
|
msg = "No certificate provided" |
||||||
|
case IssuerMismatch: |
||||||
|
msg = "Certificate not issued by this issuer" |
||||||
|
case InvalidStatus: |
||||||
|
msg = "Invalid revocation status" |
||||||
|
} |
||||||
|
case CertificateError: |
||||||
|
switch reason { |
||||||
|
case Unknown: |
||||||
|
msg = "Unknown certificate error" |
||||||
|
case ReadFailed: |
||||||
|
msg = "Failed to read certificate" |
||||||
|
case DecodeFailed: |
||||||
|
msg = "Failed to decode certificate" |
||||||
|
case ParseFailed: |
||||||
|
msg = "Failed to parse certificate" |
||||||
|
case SelfSigned: |
||||||
|
msg = "Certificate is self signed" |
||||||
|
case VerifyFailed: |
||||||
|
msg = "Unable to verify certificate" |
||||||
|
case BadRequest: |
||||||
|
msg = "Invalid certificate request" |
||||||
|
case MissingSerial: |
||||||
|
msg = "Missing serial number in request" |
||||||
|
default: |
||||||
|
panic(fmt.Sprintf("Unsupported CFSSL error reason %d under category CertificateError.", |
||||||
|
reason)) |
||||||
|
|
||||||
|
} |
||||||
|
case PrivateKeyError: |
||||||
|
switch reason { |
||||||
|
case Unknown: |
||||||
|
msg = "Unknown private key error" |
||||||
|
case ReadFailed: |
||||||
|
msg = "Failed to read private key" |
||||||
|
case DecodeFailed: |
||||||
|
msg = "Failed to decode private key" |
||||||
|
case ParseFailed: |
||||||
|
msg = "Failed to parse private key" |
||||||
|
case Encrypted: |
||||||
|
msg = "Private key is encrypted." |
||||||
|
case NotRSAOrECC: |
||||||
|
msg = "Private key algorithm is not RSA or ECC" |
||||||
|
case KeyMismatch: |
||||||
|
msg = "Private key does not match public key" |
||||||
|
case GenerationFailed: |
||||||
|
msg = "Failed to new private key" |
||||||
|
case Unavailable: |
||||||
|
msg = "Private key is unavailable" |
||||||
|
default: |
||||||
|
panic(fmt.Sprintf("Unsupported CFSSL error reason %d under category PrivateKeyError.", |
||||||
|
reason)) |
||||||
|
} |
||||||
|
case IntermediatesError: |
||||||
|
switch reason { |
||||||
|
case Unknown: |
||||||
|
msg = "Unknown intermediate certificate error" |
||||||
|
case ReadFailed: |
||||||
|
msg = "Failed to read intermediate certificate" |
||||||
|
case DecodeFailed: |
||||||
|
msg = "Failed to decode intermediate certificate" |
||||||
|
case ParseFailed: |
||||||
|
msg = "Failed to parse intermediate certificate" |
||||||
|
default: |
||||||
|
panic(fmt.Sprintf("Unsupported CFSSL error reason %d under category IntermediatesError.", |
||||||
|
reason)) |
||||||
|
} |
||||||
|
case RootError: |
||||||
|
switch reason { |
||||||
|
case Unknown: |
||||||
|
msg = "Unknown root certificate error" |
||||||
|
case ReadFailed: |
||||||
|
msg = "Failed to read root certificate" |
||||||
|
case DecodeFailed: |
||||||
|
msg = "Failed to decode root certificate" |
||||||
|
case ParseFailed: |
||||||
|
msg = "Failed to parse root certificate" |
||||||
|
default: |
||||||
|
panic(fmt.Sprintf("Unsupported CFSSL error reason %d under category RootError.", |
||||||
|
reason)) |
||||||
|
} |
||||||
|
case PolicyError: |
||||||
|
switch reason { |
||||||
|
case Unknown: |
||||||
|
msg = "Unknown policy error" |
||||||
|
case NoKeyUsages: |
||||||
|
msg = "Invalid policy: no key usage available" |
||||||
|
case InvalidPolicy: |
||||||
|
msg = "Invalid or unknown policy" |
||||||
|
case InvalidRequest: |
||||||
|
msg = "Policy violation request" |
||||||
|
case UnknownProfile: |
||||||
|
msg = "Unknown policy profile" |
||||||
|
case UnmatchedWhitelist: |
||||||
|
msg = "Request does not match policy whitelist" |
||||||
|
default: |
||||||
|
panic(fmt.Sprintf("Unsupported CFSSL error reason %d under category PolicyError.", |
||||||
|
reason)) |
||||||
|
} |
||||||
|
case DialError: |
||||||
|
switch reason { |
||||||
|
case Unknown: |
||||||
|
msg = "Failed to dial remote server" |
||||||
|
default: |
||||||
|
panic(fmt.Sprintf("Unsupported CFSSL error reason %d under category DialError.", |
||||||
|
reason)) |
||||||
|
} |
||||||
|
case APIClientError: |
||||||
|
switch reason { |
||||||
|
case AuthenticationFailure: |
||||||
|
msg = "API client authentication failure" |
||||||
|
case JSONError: |
||||||
|
msg = "API client JSON config error" |
||||||
|
case ClientHTTPError: |
||||||
|
msg = "API client HTTP error" |
||||||
|
case IOError: |
||||||
|
msg = "API client IO error" |
||||||
|
case ServerRequestFailed: |
||||||
|
msg = "API client error: Server request failed" |
||||||
|
default: |
||||||
|
panic(fmt.Sprintf("Unsupported CFSSL error reason %d under category APIClientError.", |
||||||
|
reason)) |
||||||
|
} |
||||||
|
case CSRError: |
||||||
|
switch reason { |
||||||
|
case Unknown: |
||||||
|
msg = "CSR parsing failed due to unknown error" |
||||||
|
case ReadFailed: |
||||||
|
msg = "CSR file read failed" |
||||||
|
case ParseFailed: |
||||||
|
msg = "CSR Parsing failed" |
||||||
|
case DecodeFailed: |
||||||
|
msg = "CSR Decode failed" |
||||||
|
case BadRequest: |
||||||
|
msg = "CSR Bad request" |
||||||
|
default: |
||||||
|
panic(fmt.Sprintf("Unsupported CF-SSL error reason %d under category APIClientError.", reason)) |
||||||
|
} |
||||||
|
case CTError: |
||||||
|
switch reason { |
||||||
|
case Unknown: |
||||||
|
msg = "Certificate transparency parsing failed due to unknown error" |
||||||
|
case PrecertSubmissionFailed: |
||||||
|
msg = "Certificate transparency precertificate submission failed" |
||||||
|
case PrecertMissingPoison: |
||||||
|
msg = "Precertificate is missing CT poison extension" |
||||||
|
case PrecertInvalidPoison: |
||||||
|
msg = "Precertificate contains an invalid CT poison extension" |
||||||
|
default: |
||||||
|
panic(fmt.Sprintf("Unsupported CF-SSL error reason %d under category CTError.", reason)) |
||||||
|
} |
||||||
|
case CertStoreError: |
||||||
|
switch reason { |
||||||
|
case Unknown: |
||||||
|
msg = "Certificate store action failed due to unknown error" |
||||||
|
default: |
||||||
|
panic(fmt.Sprintf("Unsupported CF-SSL error reason %d under category CertStoreError.", reason)) |
||||||
|
} |
||||||
|
|
||||||
|
default: |
||||||
|
panic(fmt.Sprintf("Unsupported CFSSL error type: %d.", |
||||||
|
category)) |
||||||
|
} |
||||||
|
return &Error{ErrorCode: errorCode, Message: msg} |
||||||
|
} |
||||||
|
|
||||||
|
// Wrap returns an error that contains the given error and an error code derived from
|
||||||
|
// the given category, reason and the error. Currently, to avoid confusion, it is not
|
||||||
|
// allowed to create an error of category Success
|
||||||
|
func Wrap(category Category, reason Reason, err error) *Error { |
||||||
|
errorCode := int(category) + int(reason) |
||||||
|
if err == nil { |
||||||
|
panic("Wrap needs a supplied error to initialize.") |
||||||
|
} |
||||||
|
|
||||||
|
// do not double wrap a error
|
||||||
|
switch err.(type) { |
||||||
|
case *Error: |
||||||
|
panic("Unable to wrap a wrapped error.") |
||||||
|
} |
||||||
|
|
||||||
|
switch category { |
||||||
|
case CertificateError: |
||||||
|
// given VerifyFailed , report the status with more detailed status code
|
||||||
|
// for some certificate errors we care.
|
||||||
|
if reason == VerifyFailed { |
||||||
|
switch errorType := err.(type) { |
||||||
|
case x509.CertificateInvalidError: |
||||||
|
errorCode += certificateInvalid + int(errorType.Reason) |
||||||
|
case x509.UnknownAuthorityError: |
||||||
|
errorCode += unknownAuthority |
||||||
|
} |
||||||
|
} |
||||||
|
case PrivateKeyError, IntermediatesError, RootError, PolicyError, DialError, |
||||||
|
APIClientError, CSRError, CTError, CertStoreError, OCSPError: |
||||||
|
// no-op, just use the error
|
||||||
|
default: |
||||||
|
panic(fmt.Sprintf("Unsupported CFSSL error type: %d.", |
||||||
|
category)) |
||||||
|
} |
||||||
|
|
||||||
|
return &Error{ErrorCode: errorCode, Message: err.Error()} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,47 @@ |
|||||||
|
package errors |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"net/http" |
||||||
|
) |
||||||
|
|
||||||
|
// HTTPError is an augmented error with a HTTP status code.
|
||||||
|
type HTTPError struct { |
||||||
|
StatusCode int |
||||||
|
error |
||||||
|
} |
||||||
|
|
||||||
|
// Error implements the error interface.
|
||||||
|
func (e *HTTPError) Error() string { |
||||||
|
return e.error.Error() |
||||||
|
} |
||||||
|
|
||||||
|
// NewMethodNotAllowed returns an appropriate error in the case that
|
||||||
|
// an HTTP client uses an invalid method (i.e. a GET in place of a POST)
|
||||||
|
// on an API endpoint.
|
||||||
|
func NewMethodNotAllowed(method string) *HTTPError { |
||||||
|
return &HTTPError{http.StatusMethodNotAllowed, errors.New(`Method is not allowed:"` + method + `"`)} |
||||||
|
} |
||||||
|
|
||||||
|
// NewBadRequest creates a HttpError with the given error and error code 400.
|
||||||
|
func NewBadRequest(err error) *HTTPError { |
||||||
|
return &HTTPError{http.StatusBadRequest, err} |
||||||
|
} |
||||||
|
|
||||||
|
// NewBadRequestString returns a HttpError with the supplied message
|
||||||
|
// and error code 400.
|
||||||
|
func NewBadRequestString(s string) *HTTPError { |
||||||
|
return NewBadRequest(errors.New(s)) |
||||||
|
} |
||||||
|
|
||||||
|
// NewBadRequestMissingParameter returns a 400 HttpError as a required
|
||||||
|
// parameter is missing in the HTTP request.
|
||||||
|
func NewBadRequestMissingParameter(s string) *HTTPError { |
||||||
|
return NewBadRequestString(`Missing parameter "` + s + `"`) |
||||||
|
} |
||||||
|
|
||||||
|
// NewBadRequestUnwantedParameter returns a 400 HttpError as a unnecessary
|
||||||
|
// parameter is present in the HTTP request.
|
||||||
|
func NewBadRequestUnwantedParameter(s string) *HTTPError { |
||||||
|
return NewBadRequestString(`Unwanted parameter "` + s + `"`) |
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
// Package derhelpers implements common functionality
|
||||||
|
// on DER encoded data
|
||||||
|
package derhelpers |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto" |
||||||
|
"crypto/ecdsa" |
||||||
|
"crypto/rsa" |
||||||
|
"crypto/x509" |
||||||
|
|
||||||
|
cferr "github.com/cloudflare/cfssl/errors" |
||||||
|
"golang.org/x/crypto/ed25519" |
||||||
|
) |
||||||
|
|
||||||
|
// ParsePrivateKeyDER parses a PKCS #1, PKCS #8, ECDSA, or Ed25519 DER-encoded
|
||||||
|
// private key. The key must not be in PEM format.
|
||||||
|
func ParsePrivateKeyDER(keyDER []byte) (key crypto.Signer, err error) { |
||||||
|
generalKey, err := x509.ParsePKCS8PrivateKey(keyDER) |
||||||
|
if err != nil { |
||||||
|
generalKey, err = x509.ParsePKCS1PrivateKey(keyDER) |
||||||
|
if err != nil { |
||||||
|
generalKey, err = x509.ParseECPrivateKey(keyDER) |
||||||
|
if err != nil { |
||||||
|
generalKey, err = ParseEd25519PrivateKey(keyDER) |
||||||
|
if err != nil { |
||||||
|
// We don't include the actual error into
|
||||||
|
// the final error. The reason might be
|
||||||
|
// we don't want to leak any info about
|
||||||
|
// the private key.
|
||||||
|
return nil, cferr.New(cferr.PrivateKeyError, |
||||||
|
cferr.ParseFailed) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
switch generalKey.(type) { |
||||||
|
case *rsa.PrivateKey: |
||||||
|
return generalKey.(*rsa.PrivateKey), nil |
||||||
|
case *ecdsa.PrivateKey: |
||||||
|
return generalKey.(*ecdsa.PrivateKey), nil |
||||||
|
case ed25519.PrivateKey: |
||||||
|
return generalKey.(ed25519.PrivateKey), nil |
||||||
|
} |
||||||
|
|
||||||
|
// should never reach here
|
||||||
|
return nil, cferr.New(cferr.PrivateKeyError, cferr.ParseFailed) |
||||||
|
} |
@ -0,0 +1,133 @@ |
|||||||
|
package derhelpers |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto" |
||||||
|
"crypto/x509/pkix" |
||||||
|
"encoding/asn1" |
||||||
|
"errors" |
||||||
|
|
||||||
|
"golang.org/x/crypto/ed25519" |
||||||
|
) |
||||||
|
|
||||||
|
var errEd25519WrongID = errors.New("incorrect object identifier") |
||||||
|
var errEd25519WrongKeyType = errors.New("incorrect key type") |
||||||
|
|
||||||
|
// ed25519OID is the OID for the Ed25519 signature scheme: see
|
||||||
|
// https://datatracker.ietf.org/doc/draft-ietf-curdle-pkix-04.
|
||||||
|
var ed25519OID = asn1.ObjectIdentifier{1, 3, 101, 112} |
||||||
|
|
||||||
|
// subjectPublicKeyInfo reflects the ASN.1 object defined in the X.509 standard.
|
||||||
|
//
|
||||||
|
// This is defined in crypto/x509 as "publicKeyInfo".
|
||||||
|
type subjectPublicKeyInfo struct { |
||||||
|
Algorithm pkix.AlgorithmIdentifier |
||||||
|
PublicKey asn1.BitString |
||||||
|
} |
||||||
|
|
||||||
|
// MarshalEd25519PublicKey creates a DER-encoded SubjectPublicKeyInfo for an
|
||||||
|
// ed25519 public key, as defined in
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-curdle-pkix-04. This is analagous to
|
||||||
|
// MarshalPKIXPublicKey in crypto/x509, which doesn't currently support Ed25519.
|
||||||
|
func MarshalEd25519PublicKey(pk crypto.PublicKey) ([]byte, error) { |
||||||
|
pub, ok := pk.(ed25519.PublicKey) |
||||||
|
if !ok { |
||||||
|
return nil, errEd25519WrongKeyType |
||||||
|
} |
||||||
|
|
||||||
|
spki := subjectPublicKeyInfo{ |
||||||
|
Algorithm: pkix.AlgorithmIdentifier{ |
||||||
|
Algorithm: ed25519OID, |
||||||
|
}, |
||||||
|
PublicKey: asn1.BitString{ |
||||||
|
BitLength: len(pub) * 8, |
||||||
|
Bytes: pub, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
return asn1.Marshal(spki) |
||||||
|
} |
||||||
|
|
||||||
|
// ParseEd25519PublicKey returns the Ed25519 public key encoded by the input.
|
||||||
|
func ParseEd25519PublicKey(der []byte) (crypto.PublicKey, error) { |
||||||
|
var spki subjectPublicKeyInfo |
||||||
|
if rest, err := asn1.Unmarshal(der, &spki); err != nil { |
||||||
|
return nil, err |
||||||
|
} else if len(rest) > 0 { |
||||||
|
return nil, errors.New("SubjectPublicKeyInfo too long") |
||||||
|
} |
||||||
|
|
||||||
|
if !spki.Algorithm.Algorithm.Equal(ed25519OID) { |
||||||
|
return nil, errEd25519WrongID |
||||||
|
} |
||||||
|
|
||||||
|
if spki.PublicKey.BitLength != ed25519.PublicKeySize*8 { |
||||||
|
return nil, errors.New("SubjectPublicKeyInfo PublicKey length mismatch") |
||||||
|
} |
||||||
|
|
||||||
|
return ed25519.PublicKey(spki.PublicKey.Bytes), nil |
||||||
|
} |
||||||
|
|
||||||
|
// oneAsymmetricKey reflects the ASN.1 structure for storing private keys in
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-curdle-pkix-04, excluding the optional
|
||||||
|
// fields, which we don't use here.
|
||||||
|
//
|
||||||
|
// This is identical to pkcs8 in crypto/x509.
|
||||||
|
type oneAsymmetricKey struct { |
||||||
|
Version int |
||||||
|
Algorithm pkix.AlgorithmIdentifier |
||||||
|
PrivateKey []byte |
||||||
|
} |
||||||
|
|
||||||
|
// curvePrivateKey is the innter type of the PrivateKey field of
|
||||||
|
// oneAsymmetricKey.
|
||||||
|
type curvePrivateKey []byte |
||||||
|
|
||||||
|
// MarshalEd25519PrivateKey returns a DER encdoing of the input private key as
|
||||||
|
// specified in https://tools.ietf.org/html/draft-ietf-curdle-pkix-04.
|
||||||
|
func MarshalEd25519PrivateKey(sk crypto.PrivateKey) ([]byte, error) { |
||||||
|
priv, ok := sk.(ed25519.PrivateKey) |
||||||
|
if !ok { |
||||||
|
return nil, errEd25519WrongKeyType |
||||||
|
} |
||||||
|
|
||||||
|
// Marshal the innter CurvePrivateKey.
|
||||||
|
curvePrivateKey, err := asn1.Marshal(priv.Seed()) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Marshal the OneAsymmetricKey.
|
||||||
|
asym := oneAsymmetricKey{ |
||||||
|
Version: 0, |
||||||
|
Algorithm: pkix.AlgorithmIdentifier{ |
||||||
|
Algorithm: ed25519OID, |
||||||
|
}, |
||||||
|
PrivateKey: curvePrivateKey, |
||||||
|
} |
||||||
|
return asn1.Marshal(asym) |
||||||
|
} |
||||||
|
|
||||||
|
// ParseEd25519PrivateKey returns the Ed25519 private key encoded by the input.
|
||||||
|
func ParseEd25519PrivateKey(der []byte) (crypto.PrivateKey, error) { |
||||||
|
asym := new(oneAsymmetricKey) |
||||||
|
if rest, err := asn1.Unmarshal(der, asym); err != nil { |
||||||
|
return nil, err |
||||||
|
} else if len(rest) > 0 { |
||||||
|
return nil, errors.New("OneAsymmetricKey too long") |
||||||
|
} |
||||||
|
|
||||||
|
// Check that the key type is correct.
|
||||||
|
if !asym.Algorithm.Algorithm.Equal(ed25519OID) { |
||||||
|
return nil, errEd25519WrongID |
||||||
|
} |
||||||
|
|
||||||
|
// Unmarshal the inner CurvePrivateKey.
|
||||||
|
seed := new(curvePrivateKey) |
||||||
|
if rest, err := asn1.Unmarshal(asym.PrivateKey, seed); err != nil { |
||||||
|
return nil, err |
||||||
|
} else if len(rest) > 0 { |
||||||
|
return nil, errors.New("CurvePrivateKey too long") |
||||||
|
} |
||||||
|
|
||||||
|
return ed25519.NewKeyFromSeed(*seed), nil |
||||||
|
} |
@ -0,0 +1,590 @@ |
|||||||
|
// Package helpers implements utility functionality common to many
|
||||||
|
// CFSSL packages.
|
||||||
|
package helpers |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"crypto" |
||||||
|
"crypto/ecdsa" |
||||||
|
"crypto/elliptic" |
||||||
|
"crypto/rsa" |
||||||
|
"crypto/tls" |
||||||
|
"crypto/x509" |
||||||
|
"crypto/x509/pkix" |
||||||
|
"encoding/asn1" |
||||||
|
"encoding/pem" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io/ioutil" |
||||||
|
"os" |
||||||
|
|
||||||
|
"github.com/google/certificate-transparency-go" |
||||||
|
cttls "github.com/google/certificate-transparency-go/tls" |
||||||
|
ctx509 "github.com/google/certificate-transparency-go/x509" |
||||||
|
"golang.org/x/crypto/ocsp" |
||||||
|
|
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/cloudflare/cfssl/crypto/pkcs7" |
||||||
|
cferr "github.com/cloudflare/cfssl/errors" |
||||||
|
"github.com/cloudflare/cfssl/helpers/derhelpers" |
||||||
|
"github.com/cloudflare/cfssl/log" |
||||||
|
"golang.org/x/crypto/pkcs12" |
||||||
|
) |
||||||
|
|
||||||
|
// OneYear is a time.Duration representing a year's worth of seconds.
|
||||||
|
const OneYear = 8760 * time.Hour |
||||||
|
|
||||||
|
// OneDay is a time.Duration representing a day's worth of seconds.
|
||||||
|
const OneDay = 24 * time.Hour |
||||||
|
|
||||||
|
// InclusiveDate returns the time.Time representation of a date - 1
|
||||||
|
// nanosecond. This allows time.After to be used inclusively.
|
||||||
|
func InclusiveDate(year int, month time.Month, day int) time.Time { |
||||||
|
return time.Date(year, month, day, 0, 0, 0, 0, time.UTC).Add(-1 * time.Nanosecond) |
||||||
|
} |
||||||
|
|
||||||
|
// Jul2012 is the July 2012 CAB Forum deadline for when CAs must stop
|
||||||
|
// issuing certificates valid for more than 5 years.
|
||||||
|
var Jul2012 = InclusiveDate(2012, time.July, 01) |
||||||
|
|
||||||
|
// Apr2015 is the April 2015 CAB Forum deadline for when CAs must stop
|
||||||
|
// issuing certificates valid for more than 39 months.
|
||||||
|
var Apr2015 = InclusiveDate(2015, time.April, 01) |
||||||
|
|
||||||
|
// KeyLength returns the bit size of ECDSA or RSA PublicKey
|
||||||
|
func KeyLength(key interface{}) int { |
||||||
|
if key == nil { |
||||||
|
return 0 |
||||||
|
} |
||||||
|
if ecdsaKey, ok := key.(*ecdsa.PublicKey); ok { |
||||||
|
return ecdsaKey.Curve.Params().BitSize |
||||||
|
} else if rsaKey, ok := key.(*rsa.PublicKey); ok { |
||||||
|
return rsaKey.N.BitLen() |
||||||
|
} |
||||||
|
|
||||||
|
return 0 |
||||||
|
} |
||||||
|
|
||||||
|
// ExpiryTime returns the time when the certificate chain is expired.
|
||||||
|
func ExpiryTime(chain []*x509.Certificate) (notAfter time.Time) { |
||||||
|
if len(chain) == 0 { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
notAfter = chain[0].NotAfter |
||||||
|
for _, cert := range chain { |
||||||
|
if notAfter.After(cert.NotAfter) { |
||||||
|
notAfter = cert.NotAfter |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// MonthsValid returns the number of months for which a certificate is valid.
|
||||||
|
func MonthsValid(c *x509.Certificate) int { |
||||||
|
issued := c.NotBefore |
||||||
|
expiry := c.NotAfter |
||||||
|
years := (expiry.Year() - issued.Year()) |
||||||
|
months := years*12 + int(expiry.Month()) - int(issued.Month()) |
||||||
|
|
||||||
|
// Round up if valid for less than a full month
|
||||||
|
if expiry.Day() > issued.Day() { |
||||||
|
months++ |
||||||
|
} |
||||||
|
return months |
||||||
|
} |
||||||
|
|
||||||
|
// ValidExpiry determines if a certificate is valid for an acceptable
|
||||||
|
// length of time per the CA/Browser Forum baseline requirements.
|
||||||
|
// See https://cabforum.org/wp-content/uploads/CAB-Forum-BR-1.3.0.pdf
|
||||||
|
func ValidExpiry(c *x509.Certificate) bool { |
||||||
|
issued := c.NotBefore |
||||||
|
|
||||||
|
var maxMonths int |
||||||
|
switch { |
||||||
|
case issued.After(Apr2015): |
||||||
|
maxMonths = 39 |
||||||
|
case issued.After(Jul2012): |
||||||
|
maxMonths = 60 |
||||||
|
case issued.Before(Jul2012): |
||||||
|
maxMonths = 120 |
||||||
|
} |
||||||
|
|
||||||
|
if MonthsValid(c) > maxMonths { |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// SignatureString returns the TLS signature string corresponding to
|
||||||
|
// an X509 signature algorithm.
|
||||||
|
func SignatureString(alg x509.SignatureAlgorithm) string { |
||||||
|
switch alg { |
||||||
|
case x509.MD2WithRSA: |
||||||
|
return "MD2WithRSA" |
||||||
|
case x509.MD5WithRSA: |
||||||
|
return "MD5WithRSA" |
||||||
|
case x509.SHA1WithRSA: |
||||||
|
return "SHA1WithRSA" |
||||||
|
case x509.SHA256WithRSA: |
||||||
|
return "SHA256WithRSA" |
||||||
|
case x509.SHA384WithRSA: |
||||||
|
return "SHA384WithRSA" |
||||||
|
case x509.SHA512WithRSA: |
||||||
|
return "SHA512WithRSA" |
||||||
|
case x509.DSAWithSHA1: |
||||||
|
return "DSAWithSHA1" |
||||||
|
case x509.DSAWithSHA256: |
||||||
|
return "DSAWithSHA256" |
||||||
|
case x509.ECDSAWithSHA1: |
||||||
|
return "ECDSAWithSHA1" |
||||||
|
case x509.ECDSAWithSHA256: |
||||||
|
return "ECDSAWithSHA256" |
||||||
|
case x509.ECDSAWithSHA384: |
||||||
|
return "ECDSAWithSHA384" |
||||||
|
case x509.ECDSAWithSHA512: |
||||||
|
return "ECDSAWithSHA512" |
||||||
|
default: |
||||||
|
return "Unknown Signature" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// HashAlgoString returns the hash algorithm name contains in the signature
|
||||||
|
// method.
|
||||||
|
func HashAlgoString(alg x509.SignatureAlgorithm) string { |
||||||
|
switch alg { |
||||||
|
case x509.MD2WithRSA: |
||||||
|
return "MD2" |
||||||
|
case x509.MD5WithRSA: |
||||||
|
return "MD5" |
||||||
|
case x509.SHA1WithRSA: |
||||||
|
return "SHA1" |
||||||
|
case x509.SHA256WithRSA: |
||||||
|
return "SHA256" |
||||||
|
case x509.SHA384WithRSA: |
||||||
|
return "SHA384" |
||||||
|
case x509.SHA512WithRSA: |
||||||
|
return "SHA512" |
||||||
|
case x509.DSAWithSHA1: |
||||||
|
return "SHA1" |
||||||
|
case x509.DSAWithSHA256: |
||||||
|
return "SHA256" |
||||||
|
case x509.ECDSAWithSHA1: |
||||||
|
return "SHA1" |
||||||
|
case x509.ECDSAWithSHA256: |
||||||
|
return "SHA256" |
||||||
|
case x509.ECDSAWithSHA384: |
||||||
|
return "SHA384" |
||||||
|
case x509.ECDSAWithSHA512: |
||||||
|
return "SHA512" |
||||||
|
default: |
||||||
|
return "Unknown Hash Algorithm" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// StringTLSVersion returns underlying enum values from human names for TLS
|
||||||
|
// versions, defaults to current golang default of TLS 1.0
|
||||||
|
func StringTLSVersion(version string) uint16 { |
||||||
|
switch version { |
||||||
|
case "1.2": |
||||||
|
return tls.VersionTLS12 |
||||||
|
case "1.1": |
||||||
|
return tls.VersionTLS11 |
||||||
|
default: |
||||||
|
return tls.VersionTLS10 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// EncodeCertificatesPEM encodes a number of x509 certificates to PEM
|
||||||
|
func EncodeCertificatesPEM(certs []*x509.Certificate) []byte { |
||||||
|
var buffer bytes.Buffer |
||||||
|
for _, cert := range certs { |
||||||
|
pem.Encode(&buffer, &pem.Block{ |
||||||
|
Type: "CERTIFICATE", |
||||||
|
Bytes: cert.Raw, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return buffer.Bytes() |
||||||
|
} |
||||||
|
|
||||||
|
// EncodeCertificatePEM encodes a single x509 certificates to PEM
|
||||||
|
func EncodeCertificatePEM(cert *x509.Certificate) []byte { |
||||||
|
return EncodeCertificatesPEM([]*x509.Certificate{cert}) |
||||||
|
} |
||||||
|
|
||||||
|
// ParseCertificatesPEM parses a sequence of PEM-encoded certificate and returns them,
|
||||||
|
// can handle PEM encoded PKCS #7 structures.
|
||||||
|
func ParseCertificatesPEM(certsPEM []byte) ([]*x509.Certificate, error) { |
||||||
|
var certs []*x509.Certificate |
||||||
|
var err error |
||||||
|
certsPEM = bytes.TrimSpace(certsPEM) |
||||||
|
for len(certsPEM) > 0 { |
||||||
|
var cert []*x509.Certificate |
||||||
|
cert, certsPEM, err = ParseOneCertificateFromPEM(certsPEM) |
||||||
|
if err != nil { |
||||||
|
|
||||||
|
return nil, cferr.New(cferr.CertificateError, cferr.ParseFailed) |
||||||
|
} else if cert == nil { |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
certs = append(certs, cert...) |
||||||
|
} |
||||||
|
if len(certsPEM) > 0 { |
||||||
|
return nil, cferr.New(cferr.CertificateError, cferr.DecodeFailed) |
||||||
|
} |
||||||
|
return certs, nil |
||||||
|
} |
||||||
|
|
||||||
|
// ParseCertificatesDER parses a DER encoding of a certificate object and possibly private key,
|
||||||
|
// either PKCS #7, PKCS #12, or raw x509.
|
||||||
|
func ParseCertificatesDER(certsDER []byte, password string) (certs []*x509.Certificate, key crypto.Signer, err error) { |
||||||
|
certsDER = bytes.TrimSpace(certsDER) |
||||||
|
pkcs7data, err := pkcs7.ParsePKCS7(certsDER) |
||||||
|
if err != nil { |
||||||
|
var pkcs12data interface{} |
||||||
|
certs = make([]*x509.Certificate, 1) |
||||||
|
pkcs12data, certs[0], err = pkcs12.Decode(certsDER, password) |
||||||
|
if err != nil { |
||||||
|
certs, err = x509.ParseCertificates(certsDER) |
||||||
|
if err != nil { |
||||||
|
return nil, nil, cferr.New(cferr.CertificateError, cferr.DecodeFailed) |
||||||
|
} |
||||||
|
} else { |
||||||
|
key = pkcs12data.(crypto.Signer) |
||||||
|
} |
||||||
|
} else { |
||||||
|
if pkcs7data.ContentInfo != "SignedData" { |
||||||
|
return nil, nil, cferr.Wrap(cferr.CertificateError, cferr.DecodeFailed, errors.New("can only extract certificates from signed data content info")) |
||||||
|
} |
||||||
|
certs = pkcs7data.Content.SignedData.Certificates |
||||||
|
} |
||||||
|
if certs == nil { |
||||||
|
return nil, key, cferr.New(cferr.CertificateError, cferr.DecodeFailed) |
||||||
|
} |
||||||
|
return certs, key, nil |
||||||
|
} |
||||||
|
|
||||||
|
// ParseSelfSignedCertificatePEM parses a PEM-encoded certificate and check if it is self-signed.
|
||||||
|
func ParseSelfSignedCertificatePEM(certPEM []byte) (*x509.Certificate, error) { |
||||||
|
cert, err := ParseCertificatePEM(certPEM) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil { |
||||||
|
return nil, cferr.Wrap(cferr.CertificateError, cferr.VerifyFailed, err) |
||||||
|
} |
||||||
|
return cert, nil |
||||||
|
} |
||||||
|
|
||||||
|
// ParseCertificatePEM parses and returns a PEM-encoded certificate,
|
||||||
|
// can handle PEM encoded PKCS #7 structures.
|
||||||
|
func ParseCertificatePEM(certPEM []byte) (*x509.Certificate, error) { |
||||||
|
certPEM = bytes.TrimSpace(certPEM) |
||||||
|
cert, rest, err := ParseOneCertificateFromPEM(certPEM) |
||||||
|
if err != nil { |
||||||
|
// Log the actual parsing error but throw a default parse error message.
|
||||||
|
log.Debugf("Certificate parsing error: %v", err) |
||||||
|
return nil, cferr.New(cferr.CertificateError, cferr.ParseFailed) |
||||||
|
} else if cert == nil { |
||||||
|
return nil, cferr.New(cferr.CertificateError, cferr.DecodeFailed) |
||||||
|
} else if len(rest) > 0 { |
||||||
|
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, errors.New("the PEM file should contain only one object")) |
||||||
|
} else if len(cert) > 1 { |
||||||
|
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, errors.New("the PKCS7 object in the PEM file should contain only one certificate")) |
||||||
|
} |
||||||
|
return cert[0], nil |
||||||
|
} |
||||||
|
|
||||||
|
// ParseOneCertificateFromPEM attempts to parse one PEM encoded certificate object,
|
||||||
|
// either a raw x509 certificate or a PKCS #7 structure possibly containing
|
||||||
|
// multiple certificates, from the top of certsPEM, which itself may
|
||||||
|
// contain multiple PEM encoded certificate objects.
|
||||||
|
func ParseOneCertificateFromPEM(certsPEM []byte) ([]*x509.Certificate, []byte, error) { |
||||||
|
|
||||||
|
block, rest := pem.Decode(certsPEM) |
||||||
|
if block == nil { |
||||||
|
return nil, rest, nil |
||||||
|
} |
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes) |
||||||
|
if err != nil { |
||||||
|
pkcs7data, err := pkcs7.ParsePKCS7(block.Bytes) |
||||||
|
if err != nil { |
||||||
|
return nil, rest, err |
||||||
|
} |
||||||
|
if pkcs7data.ContentInfo != "SignedData" { |
||||||
|
return nil, rest, errors.New("only PKCS #7 Signed Data Content Info supported for certificate parsing") |
||||||
|
} |
||||||
|
certs := pkcs7data.Content.SignedData.Certificates |
||||||
|
if certs == nil { |
||||||
|
return nil, rest, errors.New("PKCS #7 structure contains no certificates") |
||||||
|
} |
||||||
|
return certs, rest, nil |
||||||
|
} |
||||||
|
var certs = []*x509.Certificate{cert} |
||||||
|
return certs, rest, nil |
||||||
|
} |
||||||
|
|
||||||
|
// LoadPEMCertPool loads a pool of PEM certificates from file.
|
||||||
|
func LoadPEMCertPool(certsFile string) (*x509.CertPool, error) { |
||||||
|
if certsFile == "" { |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
pemCerts, err := ioutil.ReadFile(certsFile) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return PEMToCertPool(pemCerts) |
||||||
|
} |
||||||
|
|
||||||
|
// PEMToCertPool concerts PEM certificates to a CertPool.
|
||||||
|
func PEMToCertPool(pemCerts []byte) (*x509.CertPool, error) { |
||||||
|
if len(pemCerts) == 0 { |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
|
||||||
|
certPool := x509.NewCertPool() |
||||||
|
if !certPool.AppendCertsFromPEM(pemCerts) { |
||||||
|
return nil, errors.New("failed to load cert pool") |
||||||
|
} |
||||||
|
|
||||||
|
return certPool, nil |
||||||
|
} |
||||||
|
|
||||||
|
// ParsePrivateKeyPEM parses and returns a PEM-encoded private
|
||||||
|
// key. The private key may be either an unencrypted PKCS#8, PKCS#1,
|
||||||
|
// or elliptic private key.
|
||||||
|
func ParsePrivateKeyPEM(keyPEM []byte) (key crypto.Signer, err error) { |
||||||
|
return ParsePrivateKeyPEMWithPassword(keyPEM, nil) |
||||||
|
} |
||||||
|
|
||||||
|
// ParsePrivateKeyPEMWithPassword parses and returns a PEM-encoded private
|
||||||
|
// key. The private key may be a potentially encrypted PKCS#8, PKCS#1,
|
||||||
|
// or elliptic private key.
|
||||||
|
func ParsePrivateKeyPEMWithPassword(keyPEM []byte, password []byte) (key crypto.Signer, err error) { |
||||||
|
keyDER, err := GetKeyDERFromPEM(keyPEM, password) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return derhelpers.ParsePrivateKeyDER(keyDER) |
||||||
|
} |
||||||
|
|
||||||
|
// GetKeyDERFromPEM parses a PEM-encoded private key and returns DER-format key bytes.
|
||||||
|
func GetKeyDERFromPEM(in []byte, password []byte) ([]byte, error) { |
||||||
|
keyDER, _ := pem.Decode(in) |
||||||
|
if keyDER != nil { |
||||||
|
if procType, ok := keyDER.Headers["Proc-Type"]; ok { |
||||||
|
if strings.Contains(procType, "ENCRYPTED") { |
||||||
|
if password != nil { |
||||||
|
return x509.DecryptPEMBlock(keyDER, password) |
||||||
|
} |
||||||
|
return nil, cferr.New(cferr.PrivateKeyError, cferr.Encrypted) |
||||||
|
} |
||||||
|
} |
||||||
|
return keyDER.Bytes, nil |
||||||
|
} |
||||||
|
|
||||||
|
return nil, cferr.New(cferr.PrivateKeyError, cferr.DecodeFailed) |
||||||
|
} |
||||||
|
|
||||||
|
// ParseCSR parses a PEM- or DER-encoded PKCS #10 certificate signing request.
|
||||||
|
func ParseCSR(in []byte) (csr *x509.CertificateRequest, rest []byte, err error) { |
||||||
|
in = bytes.TrimSpace(in) |
||||||
|
p, rest := pem.Decode(in) |
||||||
|
if p != nil { |
||||||
|
if p.Type != "NEW CERTIFICATE REQUEST" && p.Type != "CERTIFICATE REQUEST" { |
||||||
|
return nil, rest, cferr.New(cferr.CSRError, cferr.BadRequest) |
||||||
|
} |
||||||
|
|
||||||
|
csr, err = x509.ParseCertificateRequest(p.Bytes) |
||||||
|
} else { |
||||||
|
csr, err = x509.ParseCertificateRequest(in) |
||||||
|
} |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
return nil, rest, err |
||||||
|
} |
||||||
|
|
||||||
|
err = csr.CheckSignature() |
||||||
|
if err != nil { |
||||||
|
return nil, rest, err |
||||||
|
} |
||||||
|
|
||||||
|
return csr, rest, nil |
||||||
|
} |
||||||
|
|
||||||
|
// ParseCSRPEM parses a PEM-encoded certificate signing request.
|
||||||
|
// It does not check the signature. This is useful for dumping data from a CSR
|
||||||
|
// locally.
|
||||||
|
func ParseCSRPEM(csrPEM []byte) (*x509.CertificateRequest, error) { |
||||||
|
block, _ := pem.Decode([]byte(csrPEM)) |
||||||
|
if block == nil { |
||||||
|
return nil, cferr.New(cferr.CSRError, cferr.DecodeFailed) |
||||||
|
} |
||||||
|
csrObject, err := x509.ParseCertificateRequest(block.Bytes) |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return csrObject, nil |
||||||
|
} |
||||||
|
|
||||||
|
// SignerAlgo returns an X.509 signature algorithm from a crypto.Signer.
|
||||||
|
func SignerAlgo(priv crypto.Signer) x509.SignatureAlgorithm { |
||||||
|
switch pub := priv.Public().(type) { |
||||||
|
case *rsa.PublicKey: |
||||||
|
bitLength := pub.N.BitLen() |
||||||
|
switch { |
||||||
|
case bitLength >= 4096: |
||||||
|
return x509.SHA512WithRSA |
||||||
|
case bitLength >= 3072: |
||||||
|
return x509.SHA384WithRSA |
||||||
|
case bitLength >= 2048: |
||||||
|
return x509.SHA256WithRSA |
||||||
|
default: |
||||||
|
return x509.SHA1WithRSA |
||||||
|
} |
||||||
|
case *ecdsa.PublicKey: |
||||||
|
switch pub.Curve { |
||||||
|
case elliptic.P521(): |
||||||
|
return x509.ECDSAWithSHA512 |
||||||
|
case elliptic.P384(): |
||||||
|
return x509.ECDSAWithSHA384 |
||||||
|
case elliptic.P256(): |
||||||
|
return x509.ECDSAWithSHA256 |
||||||
|
default: |
||||||
|
return x509.ECDSAWithSHA1 |
||||||
|
} |
||||||
|
default: |
||||||
|
return x509.UnknownSignatureAlgorithm |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// LoadClientCertificate load key/certificate from pem files
|
||||||
|
func LoadClientCertificate(certFile string, keyFile string) (*tls.Certificate, error) { |
||||||
|
if certFile != "" && keyFile != "" { |
||||||
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile) |
||||||
|
if err != nil { |
||||||
|
log.Criticalf("Unable to read client certificate from file: %s or key from file: %s", certFile, keyFile) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
log.Debug("Client certificate loaded ") |
||||||
|
return &cert, nil |
||||||
|
} |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
|
||||||
|
// CreateTLSConfig creates a tls.Config object from certs and roots
|
||||||
|
func CreateTLSConfig(remoteCAs *x509.CertPool, cert *tls.Certificate) *tls.Config { |
||||||
|
var certs []tls.Certificate |
||||||
|
if cert != nil { |
||||||
|
certs = []tls.Certificate{*cert} |
||||||
|
} |
||||||
|
return &tls.Config{ |
||||||
|
Certificates: certs, |
||||||
|
RootCAs: remoteCAs, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// SerializeSCTList serializes a list of SCTs.
|
||||||
|
func SerializeSCTList(sctList []ct.SignedCertificateTimestamp) ([]byte, error) { |
||||||
|
list := ctx509.SignedCertificateTimestampList{} |
||||||
|
for _, sct := range sctList { |
||||||
|
sctBytes, err := cttls.Marshal(sct) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
list.SCTList = append(list.SCTList, ctx509.SerializedSCT{Val: sctBytes}) |
||||||
|
} |
||||||
|
return cttls.Marshal(list) |
||||||
|
} |
||||||
|
|
||||||
|
// DeserializeSCTList deserializes a list of SCTs.
|
||||||
|
func DeserializeSCTList(serializedSCTList []byte) ([]ct.SignedCertificateTimestamp, error) { |
||||||
|
var sctList ctx509.SignedCertificateTimestampList |
||||||
|
rest, err := cttls.Unmarshal(serializedSCTList, &sctList) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if len(rest) != 0 { |
||||||
|
return nil, cferr.Wrap(cferr.CTError, cferr.Unknown, errors.New("serialized SCT list contained trailing garbage")) |
||||||
|
} |
||||||
|
list := make([]ct.SignedCertificateTimestamp, len(sctList.SCTList)) |
||||||
|
for i, serializedSCT := range sctList.SCTList { |
||||||
|
var sct ct.SignedCertificateTimestamp |
||||||
|
rest, err := cttls.Unmarshal(serializedSCT.Val, &sct) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if len(rest) != 0 { |
||||||
|
return nil, cferr.Wrap(cferr.CTError, cferr.Unknown, errors.New("serialized SCT contained trailing garbage")) |
||||||
|
} |
||||||
|
list[i] = sct |
||||||
|
} |
||||||
|
return list, nil |
||||||
|
} |
||||||
|
|
||||||
|
// SCTListFromOCSPResponse extracts the SCTList from an ocsp.Response,
|
||||||
|
// returning an empty list if the SCT extension was not found or could not be
|
||||||
|
// unmarshalled.
|
||||||
|
func SCTListFromOCSPResponse(response *ocsp.Response) ([]ct.SignedCertificateTimestamp, error) { |
||||||
|
// This loop finds the SCTListExtension in the OCSP response.
|
||||||
|
var SCTListExtension, ext pkix.Extension |
||||||
|
for _, ext = range response.Extensions { |
||||||
|
// sctExtOid is the ObjectIdentifier of a Signed Certificate Timestamp.
|
||||||
|
sctExtOid := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 5} |
||||||
|
if ext.Id.Equal(sctExtOid) { |
||||||
|
SCTListExtension = ext |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// This code block extracts the sctList from the SCT extension.
|
||||||
|
var sctList []ct.SignedCertificateTimestamp |
||||||
|
var err error |
||||||
|
if numBytes := len(SCTListExtension.Value); numBytes != 0 { |
||||||
|
var serializedSCTList []byte |
||||||
|
rest := make([]byte, numBytes) |
||||||
|
copy(rest, SCTListExtension.Value) |
||||||
|
for len(rest) != 0 { |
||||||
|
rest, err = asn1.Unmarshal(rest, &serializedSCTList) |
||||||
|
if err != nil { |
||||||
|
return nil, cferr.Wrap(cferr.CTError, cferr.Unknown, err) |
||||||
|
} |
||||||
|
} |
||||||
|
sctList, err = DeserializeSCTList(serializedSCTList) |
||||||
|
} |
||||||
|
return sctList, err |
||||||
|
} |
||||||
|
|
||||||
|
// ReadBytes reads a []byte either from a file or an environment variable.
|
||||||
|
// If valFile has a prefix of 'env:', the []byte is read from the environment
|
||||||
|
// using the subsequent name. If the prefix is 'file:' the []byte is read from
|
||||||
|
// the subsequent file. If no prefix is provided, valFile is assumed to be a
|
||||||
|
// file path.
|
||||||
|
func ReadBytes(valFile string) ([]byte, error) { |
||||||
|
switch splitVal := strings.SplitN(valFile, ":", 2); len(splitVal) { |
||||||
|
case 1: |
||||||
|
return ioutil.ReadFile(valFile) |
||||||
|
case 2: |
||||||
|
switch splitVal[0] { |
||||||
|
case "env": |
||||||
|
return []byte(os.Getenv(splitVal[1])), nil |
||||||
|
case "file": |
||||||
|
return ioutil.ReadFile(splitVal[1]) |
||||||
|
default: |
||||||
|
return nil, fmt.Errorf("unknown prefix: %s", splitVal[0]) |
||||||
|
} |
||||||
|
default: |
||||||
|
return nil, fmt.Errorf("multiple prefixes: %s", |
||||||
|
strings.Join(splitVal[:len(splitVal)-1], ", ")) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,162 @@ |
|||||||
|
// Package log implements a wrapper around the Go standard library's
|
||||||
|
// logging package. Clients should set the current log level; only
|
||||||
|
// messages below that level will actually be logged. For example, if
|
||||||
|
// Level is set to LevelWarning, only log messages at the Warning,
|
||||||
|
// Error, and Critical levels will be logged.
|
||||||
|
package log |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"os" |
||||||
|
) |
||||||
|
|
||||||
|
// The following constants represent logging levels in increasing levels of seriousness.
|
||||||
|
const ( |
||||||
|
// LevelDebug is the log level for Debug statements.
|
||||||
|
LevelDebug = iota |
||||||
|
// LevelInfo is the log level for Info statements.
|
||||||
|
LevelInfo |
||||||
|
// LevelWarning is the log level for Warning statements.
|
||||||
|
LevelWarning |
||||||
|
// LevelError is the log level for Error statements.
|
||||||
|
LevelError |
||||||
|
// LevelCritical is the log level for Critical statements.
|
||||||
|
LevelCritical |
||||||
|
// LevelFatal is the log level for Fatal statements.
|
||||||
|
LevelFatal |
||||||
|
) |
||||||
|
|
||||||
|
var levelPrefix = [...]string{ |
||||||
|
LevelDebug: "DEBUG", |
||||||
|
LevelInfo: "INFO", |
||||||
|
LevelWarning: "WARNING", |
||||||
|
LevelError: "ERROR", |
||||||
|
LevelCritical: "CRITICAL", |
||||||
|
LevelFatal: "FATAL", |
||||||
|
} |
||||||
|
|
||||||
|
// Level stores the current logging level.
|
||||||
|
var Level = LevelInfo |
||||||
|
|
||||||
|
// SyslogWriter specifies the necessary methods for an alternate output
|
||||||
|
// destination passed in via SetLogger.
|
||||||
|
//
|
||||||
|
// SyslogWriter is satisfied by *syslog.Writer.
|
||||||
|
type SyslogWriter interface { |
||||||
|
Debug(string) |
||||||
|
Info(string) |
||||||
|
Warning(string) |
||||||
|
Err(string) |
||||||
|
Crit(string) |
||||||
|
Emerg(string) |
||||||
|
} |
||||||
|
|
||||||
|
// syslogWriter stores the SetLogger() parameter.
|
||||||
|
var syslogWriter SyslogWriter |
||||||
|
|
||||||
|
// SetLogger sets the output used for output by this package.
|
||||||
|
// A *syslog.Writer is a good choice for the logger parameter.
|
||||||
|
// Call with a nil parameter to revert to default behavior.
|
||||||
|
func SetLogger(logger SyslogWriter) { |
||||||
|
syslogWriter = logger |
||||||
|
} |
||||||
|
|
||||||
|
func print(l int, msg string) { |
||||||
|
if l >= Level { |
||||||
|
if syslogWriter != nil { |
||||||
|
switch l { |
||||||
|
case LevelDebug: |
||||||
|
syslogWriter.Debug(msg) |
||||||
|
case LevelInfo: |
||||||
|
syslogWriter.Info(msg) |
||||||
|
case LevelWarning: |
||||||
|
syslogWriter.Warning(msg) |
||||||
|
case LevelError: |
||||||
|
syslogWriter.Err(msg) |
||||||
|
case LevelCritical: |
||||||
|
syslogWriter.Crit(msg) |
||||||
|
case LevelFatal: |
||||||
|
syslogWriter.Emerg(msg) |
||||||
|
} |
||||||
|
} else { |
||||||
|
log.Printf("[%s] %s", levelPrefix[l], msg) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func outputf(l int, format string, v []interface{}) { |
||||||
|
print(l, fmt.Sprintf(format, v...)) |
||||||
|
} |
||||||
|
|
||||||
|
func output(l int, v []interface{}) { |
||||||
|
print(l, fmt.Sprint(v...)) |
||||||
|
} |
||||||
|
|
||||||
|
// Fatalf logs a formatted message at the "fatal" level and then exits. The
|
||||||
|
// arguments are handled in the same manner as fmt.Printf.
|
||||||
|
func Fatalf(format string, v ...interface{}) { |
||||||
|
outputf(LevelFatal, format, v) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
|
||||||
|
// Fatal logs its arguments at the "fatal" level and then exits.
|
||||||
|
func Fatal(v ...interface{}) { |
||||||
|
output(LevelFatal, v) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
|
||||||
|
// Criticalf logs a formatted message at the "critical" level. The
|
||||||
|
// arguments are handled in the same manner as fmt.Printf.
|
||||||
|
func Criticalf(format string, v ...interface{}) { |
||||||
|
outputf(LevelCritical, format, v) |
||||||
|
} |
||||||
|
|
||||||
|
// Critical logs its arguments at the "critical" level.
|
||||||
|
func Critical(v ...interface{}) { |
||||||
|
output(LevelCritical, v) |
||||||
|
} |
||||||
|
|
||||||
|
// Errorf logs a formatted message at the "error" level. The arguments
|
||||||
|
// are handled in the same manner as fmt.Printf.
|
||||||
|
func Errorf(format string, v ...interface{}) { |
||||||
|
outputf(LevelError, format, v) |
||||||
|
} |
||||||
|
|
||||||
|
// Error logs its arguments at the "error" level.
|
||||||
|
func Error(v ...interface{}) { |
||||||
|
output(LevelError, v) |
||||||
|
} |
||||||
|
|
||||||
|
// Warningf logs a formatted message at the "warning" level. The
|
||||||
|
// arguments are handled in the same manner as fmt.Printf.
|
||||||
|
func Warningf(format string, v ...interface{}) { |
||||||
|
outputf(LevelWarning, format, v) |
||||||
|
} |
||||||
|
|
||||||
|
// Warning logs its arguments at the "warning" level.
|
||||||
|
func Warning(v ...interface{}) { |
||||||
|
output(LevelWarning, v) |
||||||
|
} |
||||||
|
|
||||||
|
// Infof logs a formatted message at the "info" level. The arguments
|
||||||
|
// are handled in the same manner as fmt.Printf.
|
||||||
|
func Infof(format string, v ...interface{}) { |
||||||
|
outputf(LevelInfo, format, v) |
||||||
|
} |
||||||
|
|
||||||
|
// Info logs its arguments at the "info" level.
|
||||||
|
func Info(v ...interface{}) { |
||||||
|
output(LevelInfo, v) |
||||||
|
} |
||||||
|
|
||||||
|
// Debugf logs a formatted message at the "debug" level. The arguments
|
||||||
|
// are handled in the same manner as fmt.Printf.
|
||||||
|
func Debugf(format string, v ...interface{}) { |
||||||
|
outputf(LevelDebug, format, v) |
||||||
|
} |
||||||
|
|
||||||
|
// Debug logs its arguments at the "debug" level.
|
||||||
|
func Debug(v ...interface{}) { |
||||||
|
output(LevelDebug, v) |
||||||
|
} |
@ -0,0 +1,336 @@ |
|||||||
|
// Package revoke provides functionality for checking the validity of
|
||||||
|
// a cert. Specifically, the temporal validity of the certificate is
|
||||||
|
// checked first, then any CRL and OCSP url in the cert is checked.
|
||||||
|
package revoke |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"crypto" |
||||||
|
"crypto/x509" |
||||||
|
"crypto/x509/pkix" |
||||||
|
"encoding/base64" |
||||||
|
"encoding/pem" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"io/ioutil" |
||||||
|
"net/http" |
||||||
|
neturl "net/url" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"golang.org/x/crypto/ocsp" |
||||||
|
|
||||||
|
"github.com/cloudflare/cfssl/helpers" |
||||||
|
"github.com/cloudflare/cfssl/log" |
||||||
|
) |
||||||
|
|
||||||
|
// HardFail determines whether the failure to check the revocation
|
||||||
|
// status of a certificate (i.e. due to network failure) causes
|
||||||
|
// verification to fail (a hard failure).
|
||||||
|
var HardFail = false |
||||||
|
|
||||||
|
// CRLSet associates a PKIX certificate list with the URL the CRL is
|
||||||
|
// fetched from.
|
||||||
|
var CRLSet = map[string]*pkix.CertificateList{} |
||||||
|
var crlLock = new(sync.Mutex) |
||||||
|
|
||||||
|
// We can't handle LDAP certificates, so this checks to see if the
|
||||||
|
// URL string points to an LDAP resource so that we can ignore it.
|
||||||
|
func ldapURL(url string) bool { |
||||||
|
u, err := neturl.Parse(url) |
||||||
|
if err != nil { |
||||||
|
log.Warningf("error parsing url %s: %v", url, err) |
||||||
|
return false |
||||||
|
} |
||||||
|
if u.Scheme == "ldap" { |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// revCheck should check the certificate for any revocations. It
|
||||||
|
// returns a pair of booleans: the first indicates whether the certificate
|
||||||
|
// is revoked, the second indicates whether the revocations were
|
||||||
|
// successfully checked.. This leads to the following combinations:
|
||||||
|
//
|
||||||
|
// false, false: an error was encountered while checking revocations.
|
||||||
|
//
|
||||||
|
// false, true: the certificate was checked successfully and
|
||||||
|
// it is not revoked.
|
||||||
|
//
|
||||||
|
// true, true: the certificate was checked successfully and
|
||||||
|
// it is revoked.
|
||||||
|
//
|
||||||
|
// true, false: failure to check revocation status causes
|
||||||
|
// verification to fail
|
||||||
|
func revCheck(cert *x509.Certificate) (revoked, ok bool, err error) { |
||||||
|
for _, url := range cert.CRLDistributionPoints { |
||||||
|
if ldapURL(url) { |
||||||
|
log.Infof("skipping LDAP CRL: %s", url) |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
if revoked, ok, err := certIsRevokedCRL(cert, url); !ok { |
||||||
|
log.Warning("error checking revocation via CRL") |
||||||
|
if HardFail { |
||||||
|
return true, false, err |
||||||
|
} |
||||||
|
return false, false, err |
||||||
|
} else if revoked { |
||||||
|
log.Info("certificate is revoked via CRL") |
||||||
|
return true, true, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if revoked, ok, err := certIsRevokedOCSP(cert, HardFail); !ok { |
||||||
|
log.Warning("error checking revocation via OCSP") |
||||||
|
if HardFail { |
||||||
|
return true, false, err |
||||||
|
} |
||||||
|
return false, false, err |
||||||
|
} else if revoked { |
||||||
|
log.Info("certificate is revoked via OCSP") |
||||||
|
return true, true, err |
||||||
|
} |
||||||
|
|
||||||
|
return false, true, nil |
||||||
|
} |
||||||
|
|
||||||
|
// fetchCRL fetches and parses a CRL.
|
||||||
|
func fetchCRL(url string) (*pkix.CertificateList, error) { |
||||||
|
resp, err := http.Get(url) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} else if resp.StatusCode >= 300 { |
||||||
|
return nil, errors.New("failed to retrieve CRL") |
||||||
|
} |
||||||
|
|
||||||
|
body, err := crlRead(resp.Body) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
resp.Body.Close() |
||||||
|
|
||||||
|
return x509.ParseCRL(body) |
||||||
|
} |
||||||
|
|
||||||
|
func getIssuer(cert *x509.Certificate) *x509.Certificate { |
||||||
|
var issuer *x509.Certificate |
||||||
|
var err error |
||||||
|
for _, issuingCert := range cert.IssuingCertificateURL { |
||||||
|
issuer, err = fetchRemote(issuingCert) |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
return issuer |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// check a cert against a specific CRL. Returns the same bool pair
|
||||||
|
// as revCheck, plus an error if one occurred.
|
||||||
|
func certIsRevokedCRL(cert *x509.Certificate, url string) (revoked, ok bool, err error) { |
||||||
|
crl, ok := CRLSet[url] |
||||||
|
if ok && crl == nil { |
||||||
|
ok = false |
||||||
|
crlLock.Lock() |
||||||
|
delete(CRLSet, url) |
||||||
|
crlLock.Unlock() |
||||||
|
} |
||||||
|
|
||||||
|
var shouldFetchCRL = true |
||||||
|
if ok { |
||||||
|
if !crl.HasExpired(time.Now()) { |
||||||
|
shouldFetchCRL = false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
issuer := getIssuer(cert) |
||||||
|
|
||||||
|
if shouldFetchCRL { |
||||||
|
var err error |
||||||
|
crl, err = fetchCRL(url) |
||||||
|
if err != nil { |
||||||
|
log.Warningf("failed to fetch CRL: %v", err) |
||||||
|
return false, false, err |
||||||
|
} |
||||||
|
|
||||||
|
// check CRL signature
|
||||||
|
if issuer != nil { |
||||||
|
err = issuer.CheckCRLSignature(crl) |
||||||
|
if err != nil { |
||||||
|
log.Warningf("failed to verify CRL: %v", err) |
||||||
|
return false, false, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
crlLock.Lock() |
||||||
|
CRLSet[url] = crl |
||||||
|
crlLock.Unlock() |
||||||
|
} |
||||||
|
|
||||||
|
for _, revoked := range crl.TBSCertList.RevokedCertificates { |
||||||
|
if cert.SerialNumber.Cmp(revoked.SerialNumber) == 0 { |
||||||
|
log.Info("Serial number match: intermediate is revoked.") |
||||||
|
return true, true, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return false, true, err |
||||||
|
} |
||||||
|
|
||||||
|
// VerifyCertificate ensures that the certificate passed in hasn't
|
||||||
|
// expired and checks the CRL for the server.
|
||||||
|
func VerifyCertificate(cert *x509.Certificate) (revoked, ok bool) { |
||||||
|
revoked, ok, _ = VerifyCertificateError(cert) |
||||||
|
return revoked, ok |
||||||
|
} |
||||||
|
|
||||||
|
// VerifyCertificateError ensures that the certificate passed in hasn't
|
||||||
|
// expired and checks the CRL for the server.
|
||||||
|
func VerifyCertificateError(cert *x509.Certificate) (revoked, ok bool, err error) { |
||||||
|
if !time.Now().Before(cert.NotAfter) { |
||||||
|
msg := fmt.Sprintf("Certificate expired %s\n", cert.NotAfter) |
||||||
|
log.Info(msg) |
||||||
|
return true, true, fmt.Errorf(msg) |
||||||
|
} else if !time.Now().After(cert.NotBefore) { |
||||||
|
msg := fmt.Sprintf("Certificate isn't valid until %s\n", cert.NotBefore) |
||||||
|
log.Info(msg) |
||||||
|
return true, true, fmt.Errorf(msg) |
||||||
|
} |
||||||
|
return revCheck(cert) |
||||||
|
} |
||||||
|
|
||||||
|
func fetchRemote(url string) (*x509.Certificate, error) { |
||||||
|
resp, err := http.Get(url) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
in, err := remoteRead(resp.Body) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
resp.Body.Close() |
||||||
|
|
||||||
|
p, _ := pem.Decode(in) |
||||||
|
if p != nil { |
||||||
|
return helpers.ParseCertificatePEM(in) |
||||||
|
} |
||||||
|
|
||||||
|
return x509.ParseCertificate(in) |
||||||
|
} |
||||||
|
|
||||||
|
var ocspOpts = ocsp.RequestOptions{ |
||||||
|
Hash: crypto.SHA1, |
||||||
|
} |
||||||
|
|
||||||
|
func certIsRevokedOCSP(leaf *x509.Certificate, strict bool) (revoked, ok bool, e error) { |
||||||
|
var err error |
||||||
|
|
||||||
|
ocspURLs := leaf.OCSPServer |
||||||
|
if len(ocspURLs) == 0 { |
||||||
|
// OCSP not enabled for this certificate.
|
||||||
|
return false, true, nil |
||||||
|
} |
||||||
|
|
||||||
|
issuer := getIssuer(leaf) |
||||||
|
|
||||||
|
if issuer == nil { |
||||||
|
return false, false, nil |
||||||
|
} |
||||||
|
|
||||||
|
ocspRequest, err := ocsp.CreateRequest(leaf, issuer, &ocspOpts) |
||||||
|
if err != nil { |
||||||
|
return revoked, ok, err |
||||||
|
} |
||||||
|
|
||||||
|
for _, server := range ocspURLs { |
||||||
|
resp, err := sendOCSPRequest(server, ocspRequest, leaf, issuer) |
||||||
|
if err != nil { |
||||||
|
if strict { |
||||||
|
return revoked, ok, err |
||||||
|
} |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
// There wasn't an error fetching the OCSP status.
|
||||||
|
ok = true |
||||||
|
|
||||||
|
if resp.Status != ocsp.Good { |
||||||
|
// The certificate was revoked.
|
||||||
|
revoked = true |
||||||
|
} |
||||||
|
|
||||||
|
return revoked, ok, err |
||||||
|
} |
||||||
|
return revoked, ok, err |
||||||
|
} |
||||||
|
|
||||||
|
// sendOCSPRequest attempts to request an OCSP response from the
|
||||||
|
// server. The error only indicates a failure to *fetch* the
|
||||||
|
// certificate, and *does not* mean the certificate is valid.
|
||||||
|
func sendOCSPRequest(server string, req []byte, leaf, issuer *x509.Certificate) (*ocsp.Response, error) { |
||||||
|
var resp *http.Response |
||||||
|
var err error |
||||||
|
if len(req) > 256 { |
||||||
|
buf := bytes.NewBuffer(req) |
||||||
|
resp, err = http.Post(server, "application/ocsp-request", buf) |
||||||
|
} else { |
||||||
|
reqURL := server + "/" + neturl.QueryEscape(base64.StdEncoding.EncodeToString(req)) |
||||||
|
resp, err = http.Get(reqURL) |
||||||
|
} |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK { |
||||||
|
return nil, errors.New("failed to retrieve OSCP") |
||||||
|
} |
||||||
|
|
||||||
|
body, err := ocspRead(resp.Body) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
resp.Body.Close() |
||||||
|
|
||||||
|
switch { |
||||||
|
case bytes.Equal(body, ocsp.UnauthorizedErrorResponse): |
||||||
|
return nil, errors.New("OSCP unauthorized") |
||||||
|
case bytes.Equal(body, ocsp.MalformedRequestErrorResponse): |
||||||
|
return nil, errors.New("OSCP malformed") |
||||||
|
case bytes.Equal(body, ocsp.InternalErrorErrorResponse): |
||||||
|
return nil, errors.New("OSCP internal error") |
||||||
|
case bytes.Equal(body, ocsp.TryLaterErrorResponse): |
||||||
|
return nil, errors.New("OSCP try later") |
||||||
|
case bytes.Equal(body, ocsp.SigRequredErrorResponse): |
||||||
|
return nil, errors.New("OSCP signature required") |
||||||
|
} |
||||||
|
|
||||||
|
return ocsp.ParseResponseForCert(body, leaf, issuer) |
||||||
|
} |
||||||
|
|
||||||
|
var crlRead = ioutil.ReadAll |
||||||
|
|
||||||
|
// SetCRLFetcher sets the function to use to read from the http response body
|
||||||
|
func SetCRLFetcher(fn func(io.Reader) ([]byte, error)) { |
||||||
|
crlRead = fn |
||||||
|
} |
||||||
|
|
||||||
|
var remoteRead = ioutil.ReadAll |
||||||
|
|
||||||
|
// SetRemoteFetcher sets the function to use to read from the http response body
|
||||||
|
func SetRemoteFetcher(fn func(io.Reader) ([]byte, error)) { |
||||||
|
remoteRead = fn |
||||||
|
} |
||||||
|
|
||||||
|
var ocspRead = ioutil.ReadAll |
||||||
|
|
||||||
|
// SetOCSPFetcher sets the function to use to read from the http response body
|
||||||
|
func SetOCSPFetcher(fn func(io.Reader) ([]byte, error)) { |
||||||
|
ocspRead = fn |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
Copyright (c) 2017 Duo Security, Inc. All rights reserved. |
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without |
||||||
|
modification, are permitted provided that the following conditions |
||||||
|
are met: |
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright |
||||||
|
notice, this list of conditions and the following disclaimer. |
||||||
|
2. Redistributions in binary form must reproduce the above copyright |
||||||
|
notice, this list of conditions and the following disclaimer in the |
||||||
|
documentation and/or other materials provided with the distribution. |
||||||
|
3. Neither the name of the copyright holder nor the names of its |
||||||
|
contributors may be used to endorse or promote products derived from |
||||||
|
this software without specific prior written permission. |
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS |
||||||
|
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||||
|
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
||||||
|
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,617 @@ |
|||||||
|
package metadata |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"crypto" |
||||||
|
"crypto/x509" |
||||||
|
"encoding/base64" |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
"io/ioutil" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/cloudflare/cfssl/revoke" |
||||||
|
"github.com/mitchellh/mapstructure" |
||||||
|
uuid "github.com/satori/go.uuid" |
||||||
|
|
||||||
|
jwt "github.com/golang-jwt/jwt/v4" |
||||||
|
) |
||||||
|
|
||||||
|
// Metadata is a map of authenticator AAGUIDs to corresponding metadata statements
|
||||||
|
var Metadata = make(map[uuid.UUID]MetadataTOCPayloadEntry) |
||||||
|
|
||||||
|
// Conformance indicates if test metadata is currently being used
|
||||||
|
var Conformance = false |
||||||
|
|
||||||
|
// AuthenticatorAttestationType - The ATTESTATION constants are 16 bit long integers indicating the specific attestation that authenticator supports.
|
||||||
|
type AuthenticatorAttestationType uint16 |
||||||
|
|
||||||
|
const ( |
||||||
|
// BasicFull - Indicates full basic attestation, based on an attestation private key shared among a class of authenticators (e.g. same model). Authenticators must provide its attestation signature during the registration process for the same reason. The attestation trust anchor is shared with FIDO Servers out of band (as part of the Metadata). This sharing process shouldt be done according to [UAFMetadataService].
|
||||||
|
BasicFull AuthenticatorAttestationType = 0x3E07 |
||||||
|
// BasicSurrogate - Just syntactically a Basic Attestation. The attestation object self-signed, i.e. it is signed using the UAuth.priv key, i.e. the key corresponding to the UAuth.pub key included in the attestation object. As a consequence it does not provide a cryptographic proof of the security characteristics. But it is the best thing we can do if the authenticator is not able to have an attestation private key.
|
||||||
|
BasicSurrogate |
||||||
|
// Ecdaa - Indicates use of elliptic curve based direct anonymous attestation as defined in [FIDOEcdaaAlgorithm]. Support for this attestation type is optional at this time. It might be required by FIDO Certification.
|
||||||
|
Ecdaa |
||||||
|
// AttCA - Indicates PrivacyCA attestation as defined in [TCG-CMCProfile-AIKCertEnroll]. Support for this attestation type is optional at this time. It might be required by FIDO Certification.
|
||||||
|
AttCA |
||||||
|
) |
||||||
|
|
||||||
|
// AuthenticatorStatus - This enumeration describes the status of an authenticator model as identified by its AAID and potentially some additional information (such as a specific attestation key).
|
||||||
|
type AuthenticatorStatus string |
||||||
|
|
||||||
|
const ( |
||||||
|
// NotFidoCertified - This authenticator is not FIDO certified.
|
||||||
|
NotFidoCertified = "NOT_FIDO_CERTIFIED" |
||||||
|
// FidoCertified - This authenticator has passed FIDO functional certification. This certification scheme is phased out and will be replaced by FIDO_CERTIFIED_L1.
|
||||||
|
FidoCertified = "FIDO_CERTIFIED" |
||||||
|
// UserVerificationBypass - Indicates that malware is able to bypass the user verification. This means that the authenticator could be used without the user's consent and potentially even without the user's knowledge.
|
||||||
|
UserVerificationBypass = "USER_VERIFICATION_BYPASS" |
||||||
|
// AttestationKeyCompromise - Indicates that an attestation key for this authenticator is known to be compromised. Additional data should be supplied, including the key identifier and the date of compromise, if known.
|
||||||
|
AttestationKeyCompromise = "ATTESTATION_KEY_COMPROMISE" |
||||||
|
// UserKeyRemoteCompromise - This authenticator has identified weaknesses that allow registered keys to be compromised and should not be trusted. This would include both, e.g. weak entropy that causes predictable keys to be generated or side channels that allow keys or signatures to be forged, guessed or extracted.
|
||||||
|
UserKeyRemoteCompromise = "USER_KEY_REMOTE_COMPROMISE" |
||||||
|
// UserKeyPhysicalCompromise - This authenticator has known weaknesses in its key protection mechanism(s) that allow user keys to be extracted by an adversary in physical possession of the device.
|
||||||
|
UserKeyPhysicalCompromise = "USER_KEY_PHYSICAL_COMPROMISE" |
||||||
|
// UpdateAvailable - A software or firmware update is available for the device. Additional data should be supplied including a URL where users can obtain an update and the date the update was published.
|
||||||
|
UpdateAvailable = "UPDATE_AVAILABLE" |
||||||
|
// Revoked - The FIDO Alliance has determined that this authenticator should not be trusted for any reason, for example if it is known to be a fraudulent product or contain a deliberate backdoor.
|
||||||
|
Revoked = "REVOKED" |
||||||
|
// SelfAssertionSubmitted - The authenticator vendor has completed and submitted the self-certification checklist to the FIDO Alliance. If this completed checklist is publicly available, the URL will be specified in StatusReport.url.
|
||||||
|
SelfAssertionSubmitted = "SELF_ASSERTION_SUBMITTED" |
||||||
|
// FidoCertifiedL1 - The authenticator has passed FIDO Authenticator certification at level 1. This level is the more strict successor of FIDO_CERTIFIED.
|
||||||
|
FidoCertifiedL1 = "FIDO_CERTIFIED_L1" |
||||||
|
// FidoCertifiedL1plus - The authenticator has passed FIDO Authenticator certification at level 1+. This level is the more than level 1.
|
||||||
|
FidoCertifiedL1plus = "FIDO_CERTIFIED_L1plus" |
||||||
|
// FidoCertifiedL2 - The authenticator has passed FIDO Authenticator certification at level 2. This level is more strict than level 1+.
|
||||||
|
FidoCertifiedL2 = "FIDO_CERTIFIED_L2" |
||||||
|
// FidoCertifiedL2plus - The authenticator has passed FIDO Authenticator certification at level 2+. This level is more strict than level 2.
|
||||||
|
FidoCertifiedL2plus = "FIDO_CERTIFIED_L2plus" |
||||||
|
// FidoCertifiedL3 - The authenticator has passed FIDO Authenticator certification at level 3. This level is more strict than level 2+.
|
||||||
|
FidoCertifiedL3 = "FIDO_CERTIFIED_L3" |
||||||
|
// FidoCertifiedL3plus - The authenticator has passed FIDO Authenticator certification at level 3+. This level is more strict than level 3.
|
||||||
|
FidoCertifiedL3plus = "FIDO_CERTIFIED_L3plus" |
||||||
|
) |
||||||
|
|
||||||
|
// UndesiredAuthenticatorStatus is an array of undesirable authenticator statuses
|
||||||
|
var UndesiredAuthenticatorStatus = [...]AuthenticatorStatus{ |
||||||
|
AttestationKeyCompromise, |
||||||
|
UserVerificationBypass, |
||||||
|
UserKeyRemoteCompromise, |
||||||
|
UserKeyPhysicalCompromise, |
||||||
|
Revoked, |
||||||
|
} |
||||||
|
|
||||||
|
// IsUndesiredAuthenticatorStatus returns whether the supplied authenticator status is desirable or not
|
||||||
|
func IsUndesiredAuthenticatorStatus(status AuthenticatorStatus) bool { |
||||||
|
for _, s := range UndesiredAuthenticatorStatus { |
||||||
|
if s == status { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// StatusReport - Contains the current BiometricStatusReport of one of the authenticator's biometric component.
|
||||||
|
type StatusReport struct { |
||||||
|
// Status of the authenticator. Additional fields MAY be set depending on this value.
|
||||||
|
Status string `json:"status"` |
||||||
|
// ISO-8601 formatted date since when the status code was set, if applicable. If no date is given, the status is assumed to be effective while present.
|
||||||
|
EffectiveDate string `json:"effectiveDate"` |
||||||
|
// Base64-encoded [RFC4648] (not base64url!) DER [ITU-X690-2008] PKIX certificate value related to the current status, if applicable.
|
||||||
|
Certificate string `json:"certificate"` |
||||||
|
// HTTPS URL where additional information may be found related to the current status, if applicable.
|
||||||
|
URL string `json:"url"` |
||||||
|
// Describes the externally visible aspects of the Authenticator Certification evaluation.
|
||||||
|
CertificationDescriptor string `json:"certificationDescriptor"` |
||||||
|
// The unique identifier for the issued Certification.
|
||||||
|
CertificateNumber string `json:"certificateNumber"` |
||||||
|
// The version of the Authenticator Certification Policy the implementation is Certified to, e.g. "1.0.0".
|
||||||
|
CertificationPolicyVersion string `json:"certificationPolicyVersion"` |
||||||
|
// The Document Version of the Authenticator Security Requirements (DV) [FIDOAuthenticatorSecurityRequirements] the implementation is certified to, e.g. "1.2.0".
|
||||||
|
CertificationRequirementsVersion string `json:"certificationRequirementsVersion"` |
||||||
|
} |
||||||
|
|
||||||
|
// BiometricStatusReport - Contains the current BiometricStatusReport of one of the authenticator's biometric component.
|
||||||
|
type BiometricStatusReport struct { |
||||||
|
// Achieved level of the biometric certification of this biometric component of the authenticator
|
||||||
|
CertLevel uint16 `json:"certLevel"` |
||||||
|
// A single USER_VERIFY constant indicating the modality of the biometric component
|
||||||
|
Modality uint32 `json:"modality"` |
||||||
|
// ISO-8601 formatted date since when the certLevel achieved, if applicable. If no date is given, the status is assumed to be effective while present.
|
||||||
|
EffectiveDate string `json:"effectiveDate"` |
||||||
|
// Describes the externally visible aspects of the Biometric Certification evaluation.
|
||||||
|
CertificationDescriptor string `json:"certificationDescriptor"` |
||||||
|
// The unique identifier for the issued Biometric Certification.
|
||||||
|
CertificateNumber string `json:"certificateNumber"` |
||||||
|
// The version of the Biometric Certification Policy the implementation is Certified to, e.g. "1.0.0".
|
||||||
|
CertificationPolicyVersion string `json:"certificationPolicyVersion"` |
||||||
|
// The version of the Biometric Requirements [FIDOBiometricsRequirements] the implementation is certified to, e.g. "1.0.0".
|
||||||
|
CertificationRequirementsVersion string `json:"certificationRequirementsVersion"` |
||||||
|
} |
||||||
|
|
||||||
|
// MetadataTOCPayloadEntry - Represents the MetadataTOCPayloadEntry
|
||||||
|
type MetadataTOCPayloadEntry struct { |
||||||
|
// The AAID of the authenticator this metadata TOC payload entry relates to.
|
||||||
|
Aaid string `json:"aaid"` |
||||||
|
// The Authenticator Attestation GUID.
|
||||||
|
AaGUID string `json:"aaguid"` |
||||||
|
// A list of the attestation certificate public key identifiers encoded as hex string.
|
||||||
|
AttestationCertificateKeyIdentifiers []string `json:"attestationCertificateKeyIdentifiers"` |
||||||
|
// The hash value computed over the base64url encoding of the UTF-8 representation of the JSON encoded metadata statement available at url and as defined in [FIDOMetadataStatement].
|
||||||
|
Hash string `json:"hash"` |
||||||
|
// Uniform resource locator (URL) of the encoded metadata statement for this authenticator model (identified by its AAID, AAGUID or attestationCertificateKeyIdentifier).
|
||||||
|
URL string `json:"url"` |
||||||
|
// Status of the FIDO Biometric Certification of one or more biometric components of the Authenticator
|
||||||
|
BiometricStatusReports []BiometricStatusReport `json:"biometricStatusReports"` |
||||||
|
// An array of status reports applicable to this authenticator.
|
||||||
|
StatusReports []StatusReport `json:"statusReports"` |
||||||
|
// ISO-8601 formatted date since when the status report array was set to the current value.
|
||||||
|
TimeOfLastStatusChange string `json:"timeOfLastStatusChange"` |
||||||
|
// URL of a list of rogue (i.e. untrusted) individual authenticators.
|
||||||
|
RogueListURL string `json:"rogueListURL"` |
||||||
|
// The hash value computed over the Base64url encoding of the UTF-8 representation of the JSON encoded rogueList available at rogueListURL (with type rogueListEntry[]).
|
||||||
|
RogueListHash string `json:"rogueListHash"` |
||||||
|
MetadataStatement MetadataStatement |
||||||
|
} |
||||||
|
|
||||||
|
// RogueListEntry - Contains a list of individual authenticators known to be rogue
|
||||||
|
type RogueListEntry struct { |
||||||
|
// Base64url encoding of the rogue authenticator's secret key
|
||||||
|
Sk string `json:"sk"` |
||||||
|
// ISO-8601 formatted date since when this entry is effective.
|
||||||
|
Date string `json:"date"` |
||||||
|
} |
||||||
|
|
||||||
|
// MetadataTOCPayload - Represents the MetadataTOCPayload
|
||||||
|
type MetadataTOCPayload struct { |
||||||
|
// The legalHeader, if present, contains a legal guide for accessing and using metadata, which itself MAY contain URL(s) pointing to further information, such as a full Terms and Conditions statement.
|
||||||
|
LegalHeader string `json:"legalHeader"` |
||||||
|
// The serial number of this UAF Metadata TOC Payload. Serial numbers MUST be consecutive and strictly monotonic, i.e. the successor TOC will have a no value exactly incremented by one.
|
||||||
|
Number int `json:"no"` |
||||||
|
// ISO-8601 formatted date when the next update will be provided at latest.
|
||||||
|
NextUpdate string `json:"nextUpdate"` |
||||||
|
// List of zero or more MetadataTOCPayloadEntry objects.
|
||||||
|
Entries []MetadataTOCPayloadEntry `json:"entries"` |
||||||
|
} |
||||||
|
|
||||||
|
// Version - Represents a generic version with major and minor fields.
|
||||||
|
type Version struct { |
||||||
|
// Major version.
|
||||||
|
Major uint16 `json:"major"` |
||||||
|
// Minor version.
|
||||||
|
Minor uint16 `json:"minor"` |
||||||
|
} |
||||||
|
|
||||||
|
// CodeAccuracyDescriptor describes the relevant accuracy/complexity aspects of passcode user verification methods.
|
||||||
|
type CodeAccuracyDescriptor struct { |
||||||
|
// The numeric system base (radix) of the code, e.g. 10 in the case of decimal digits.
|
||||||
|
Base uint16 `json:"base"` |
||||||
|
// The minimum number of digits of the given base required for that code, e.g. 4 in the case of 4 digits.
|
||||||
|
MinLength uint16 `json:"minLength"` |
||||||
|
// Maximum number of false attempts before the authenticator will block this method (at least for some time). 0 means it will never block.
|
||||||
|
MaxRetries uint16 `json:"maxRetries"` |
||||||
|
// Enforced minimum number of seconds wait time after blocking (e.g. due to forced reboot or similar).
|
||||||
|
// 0 means this user verification method will be blocked, either permanently or until an alternative user verification method method succeeded.
|
||||||
|
// All alternative user verification methods MUST be specified appropriately in the Metadata in userVerificationDetails.
|
||||||
|
BlockSlowdown uint16 `json:"blockSlowdown"` |
||||||
|
} |
||||||
|
|
||||||
|
// The BiometricAccuracyDescriptor describes relevant accuracy/complexity aspects in the case of a biometric user verification method.
|
||||||
|
type BiometricAccuracyDescriptor struct { |
||||||
|
// The false rejection rate [ISO19795-1] for a single template, i.e. the percentage of verification transactions with truthful claims of identity that are incorrectly denied.
|
||||||
|
SelfAttestedFRR int64 `json:"selfAttestedFRR "` |
||||||
|
// The false acceptance rate [ISO19795-1] for a single template, i.e. the percentage of verification transactions with wrongful claims of identity that are incorrectly confirmed.
|
||||||
|
SelfAttestedFAR int64 `json:"selfAttestedFAR "` |
||||||
|
// Maximum number of alternative templates from different fingers allowed.
|
||||||
|
MaxTemplates uint16 `json:"maxTemplates"` |
||||||
|
// Maximum number of false attempts before the authenticator will block this method (at least for some time). 0 means it will never block.
|
||||||
|
MaxRetries uint16 `json:"maxRetries"` |
||||||
|
// Enforced minimum number of seconds wait time after blocking (e.g. due to forced reboot or similar).
|
||||||
|
// 0 means that this user verification method will be blocked either permanently or until an alternative user verification method succeeded.
|
||||||
|
// All alternative user verification methods MUST be specified appropriately in the metadata in userVerificationDetails.
|
||||||
|
BlockSlowdown uint16 `json:"blockSlowdown"` |
||||||
|
} |
||||||
|
|
||||||
|
// The PatternAccuracyDescriptor describes relevant accuracy/complexity aspects in the case that a pattern is used as the user verification method.
|
||||||
|
type PatternAccuracyDescriptor struct { |
||||||
|
// Number of possible patterns (having the minimum length) out of which exactly one would be the right one, i.e. 1/probability in the case of equal distribution.
|
||||||
|
MinComplexity uint32 `json:"minComplexity"` |
||||||
|
// Maximum number of false attempts before the authenticator will block authentication using this method (at least temporarily). 0 means it will never block.
|
||||||
|
MaxRetries uint16 `json:"maxRetries"` |
||||||
|
// Enforced minimum number of seconds wait time after blocking (due to forced reboot or similar mechanism).
|
||||||
|
// 0 means this user verification method will be blocked, either permanently or until an alternative user verification method method succeeded.
|
||||||
|
// All alternative user verification methods MUST be specified appropriately in the metadata under userVerificationDetails.
|
||||||
|
BlockSlowdown uint16 `json:"blockSlowdown"` |
||||||
|
} |
||||||
|
|
||||||
|
// VerificationMethodDescriptor - A descriptor for a specific base user verification method as implemented by the authenticator.
|
||||||
|
type VerificationMethodDescriptor struct { |
||||||
|
// a single USER_VERIFY constant (see [FIDORegistry]), not a bit flag combination. This value MUST be non-zero.
|
||||||
|
UserVerification uint32 `json:"userVerification"` |
||||||
|
// May optionally be used in the case of method USER_VERIFY_PASSCODE.
|
||||||
|
CaDesc CodeAccuracyDescriptor `json:"caDesc"` |
||||||
|
// May optionally be used in the case of method USER_VERIFY_FINGERPRINT, USER_VERIFY_VOICEPRINT, USER_VERIFY_FACEPRINT, USER_VERIFY_EYEPRINT, or USER_VERIFY_HANDPRINT.
|
||||||
|
BaDesc BiometricAccuracyDescriptor `json:"baDesc"` |
||||||
|
// May optionally be used in case of method USER_VERIFY_PATTERN.
|
||||||
|
PaDesc PatternAccuracyDescriptor `json:"paDesc"` |
||||||
|
} |
||||||
|
|
||||||
|
// VerificationMethodANDCombinations MUST be non-empty. It is a list containing the base user verification methods which must be passed as part of a successful user verification.
|
||||||
|
type VerificationMethodANDCombinations struct { |
||||||
|
//This list will contain only a single entry if using a single user verification method is sufficient.
|
||||||
|
// If this list contains multiple entries, then all of the listed user verification methods MUST be passed as part of the user verification process.
|
||||||
|
VerificationMethodAndCombinations []VerificationMethodDescriptor `json:"verificationMethodANDCombinations"` |
||||||
|
} |
||||||
|
|
||||||
|
// The rgbPaletteEntry is an RGB three-sample tuple palette entry
|
||||||
|
type rgbPaletteEntry struct { |
||||||
|
// Red channel sample value
|
||||||
|
R uint16 `json:"r"` |
||||||
|
// Green channel sample value
|
||||||
|
G uint16 `json:"g"` |
||||||
|
// Blue channel sample value
|
||||||
|
B uint16 `json:"b"` |
||||||
|
} |
||||||
|
|
||||||
|
// The DisplayPNGCharacteristicsDescriptor describes a PNG image characteristics as defined in the PNG [PNG] spec for IHDR (image header) and PLTE (palette table)
|
||||||
|
type DisplayPNGCharacteristicsDescriptor struct { |
||||||
|
// image width
|
||||||
|
Width uint32 `json:"width"` |
||||||
|
// image height
|
||||||
|
Height uint32 `json:"height"` |
||||||
|
// Bit depth - bits per sample or per palette index.
|
||||||
|
BitDepth byte `json:"bitDepth"` |
||||||
|
// Color type defines the PNG image type.
|
||||||
|
ColorType byte `json:"colorType"` |
||||||
|
// Compression method used to compress the image data.
|
||||||
|
Compression byte `json:"compression"` |
||||||
|
// Filter method is the preprocessing method applied to the image data before compression.
|
||||||
|
Filter byte `json:"filter"` |
||||||
|
// Interlace method is the transmission order of the image data.
|
||||||
|
Interlace byte `json:"interlace"` |
||||||
|
// 1 to 256 palette entries
|
||||||
|
Plte []rgbPaletteEntry `json:"plte"` |
||||||
|
} |
||||||
|
|
||||||
|
// EcdaaTrustAnchor - In the case of ECDAA attestation, the ECDAA-Issuer's trust anchor MUST be specified in this field.
|
||||||
|
type EcdaaTrustAnchor struct { |
||||||
|
// base64url encoding of the result of ECPoint2ToB of the ECPoint2 X
|
||||||
|
X string `json:"x"` |
||||||
|
// base64url encoding of the result of ECPoint2ToB of the ECPoint2 Y
|
||||||
|
Y string `json:"y"` |
||||||
|
// base64url encoding of the result of BigNumberToB(c)
|
||||||
|
C string `json:"c"` |
||||||
|
// base64url encoding of the result of BigNumberToB(sx)
|
||||||
|
SX string `json:"sx"` |
||||||
|
// base64url encoding of the result of BigNumberToB(sy)
|
||||||
|
SY string `json:"sy"` |
||||||
|
// Name of the Barreto-Naehrig elliptic curve for G1. "BN_P256", "BN_P638", "BN_ISOP256", and "BN_ISOP512" are supported.
|
||||||
|
G1Curve string `json:"G1Curve"` |
||||||
|
} |
||||||
|
|
||||||
|
// ExtensionDescriptor - This descriptor contains an extension supported by the authenticator.
|
||||||
|
type ExtensionDescriptor struct { |
||||||
|
// Identifies the extension.
|
||||||
|
ID string `json:"id"` |
||||||
|
// The TAG of the extension if this was assigned. TAGs are assigned to extensions if they could appear in an assertion.
|
||||||
|
Tag uint16 `json:"tag"` |
||||||
|
// Contains arbitrary data further describing the extension and/or data needed to correctly process the extension.
|
||||||
|
Data string `json:"data"` |
||||||
|
// Indicates whether unknown extensions must be ignored (false) or must lead to an error (true) when the extension is to be processed by the FIDO Server, FIDO Client, ASM, or FIDO Authenticator.
|
||||||
|
FailIfUnknown bool `json:"fail_if_unknown"` |
||||||
|
} |
||||||
|
|
||||||
|
// MetadataStatement - Authenticator metadata statements are used directly by the FIDO server at a relying party, but the information contained in the authoritative statement is used in several other places.
|
||||||
|
type MetadataStatement struct { |
||||||
|
// The legalHeader, if present, contains a legal guide for accessing and using metadata, which itself MAY contain URL(s) pointing to further information, such as a full Terms and Conditions statement.
|
||||||
|
LegalHeader string `json:"legalHeader"` |
||||||
|
// The Authenticator Attestation ID.
|
||||||
|
Aaid string `json:"aaid"` |
||||||
|
// The Authenticator Attestation GUID.
|
||||||
|
AaGUID string `json:"aaguid"` |
||||||
|
// A list of the attestation certificate public key identifiers encoded as hex string.
|
||||||
|
AttestationCertificateKeyIdentifiers []string `json:"attestationCertificateKeyIdentifiers"` |
||||||
|
// A human-readable, short description of the authenticator, in English.
|
||||||
|
Description string `json:"description"` |
||||||
|
// A list of human-readable short descriptions of the authenticator in different languages.
|
||||||
|
AlternativeDescriptions map[string]string `json:"alternativeDescriptions"` |
||||||
|
// Earliest (i.e. lowest) trustworthy authenticatorVersion meeting the requirements specified in this metadata statement.
|
||||||
|
AuthenticatorVersion uint16 `json:"authenticatorVersion"` |
||||||
|
// The FIDO protocol family. The values "uaf", "u2f", and "fido2" are supported.
|
||||||
|
ProtocolFamily string `json:"protocolFamily"` |
||||||
|
// The FIDO unified protocol version(s) (related to the specific protocol family) supported by this authenticator.
|
||||||
|
Upv []Version `json:"upv"` |
||||||
|
// The assertion scheme supported by the authenticator.
|
||||||
|
AssertionScheme string `json:"assertionScheme"` |
||||||
|
// The preferred authentication algorithm supported by the authenticator.
|
||||||
|
AuthenticationAlgorithm uint16 `json:"authenticationAlgorithm"` |
||||||
|
// The list of authentication algorithms supported by the authenticator.
|
||||||
|
AuthenticationAlgorithms []uint16 `json:"authenticationAlgorithms"` |
||||||
|
// The preferred public key format used by the authenticator during registration operations.
|
||||||
|
PublicKeyAlgAndEncoding uint16 `json:"publicKeyAlgAndEncoding"` |
||||||
|
// The list of public key formats supported by the authenticator during registration operations.
|
||||||
|
PublicKeyAlgAndEncodings []uint16 `json:"publicKeyAlgAndEncodings"` |
||||||
|
// The supported attestation type(s).
|
||||||
|
AttestationTypes []uint16 `json:"attestationTypes"` |
||||||
|
// A list of alternative VerificationMethodANDCombinations.
|
||||||
|
UserVerificationDetails [][]VerificationMethodDescriptor `json:"userVerificationDetails"` |
||||||
|
// A 16-bit number representing the bit fields defined by the KEY_PROTECTION constants in the FIDO Registry of Predefined Values
|
||||||
|
KeyProtection uint16 `json:"keyProtection"` |
||||||
|
// This entry is set to true or it is ommitted, if the Uauth private key is restricted by the authenticator to only sign valid FIDO signature assertions.
|
||||||
|
// This entry is set to false, if the authenticator doesn't restrict the Uauth key to only sign valid FIDO signature assertions.
|
||||||
|
IsKeyRestricted bool `json:"isKeyRestricted"` |
||||||
|
// This entry is set to true or it is ommitted, if Uauth key usage always requires a fresh user verification
|
||||||
|
// This entry is set to false, if the Uauth key can be used without requiring a fresh user verification, e.g. without any additional user interaction, if the user was verified a (potentially configurable) caching time ago.
|
||||||
|
IsFreshUserVerificationRequired bool `json:"isFreshUserVerificationRequired"` |
||||||
|
// A 16-bit number representing the bit fields defined by the MATCHER_PROTECTION constants in the FIDO Registry of Predefined Values
|
||||||
|
MatcherProtection uint16 `json:"matcherProtection"` |
||||||
|
// The authenticator's overall claimed cryptographic strength in bits (sometimes also called security strength or security level).
|
||||||
|
CryptoStrength uint16 `json:"cryptoStrength"` |
||||||
|
// Description of the particular operating environment that is used for the Authenticator.
|
||||||
|
OperatingEnv string `json:"operatingEnv"` |
||||||
|
// A 32-bit number representing the bit fields defined by the ATTACHMENT_HINT constants in the FIDO Registry of Predefined Values
|
||||||
|
AttachmentHint uint32 `json:"attachmentHint"` |
||||||
|
// Indicates if the authenticator is designed to be used only as a second factor, i.e. requiring some other authentication method as a first factor (e.g. username+password).
|
||||||
|
IsSecondFactorOnly bool `json:"isSecondFactorOnly"` |
||||||
|
// A 16-bit number representing a combination of the bit flags defined by the TRANSACTION_CONFIRMATION_DISPLAY constants in the FIDO Registry of Predefined Values
|
||||||
|
TcDisplay uint16 `json:"tcDisplay"` |
||||||
|
// Supported MIME content type [RFC2049] for the transaction confirmation display, such as text/plain or image/png.
|
||||||
|
TcDisplayContentType string `json:"tcDisplayContentType"` |
||||||
|
// A list of alternative DisplayPNGCharacteristicsDescriptor. Each of these entries is one alternative of supported image characteristics for displaying a PNG image.
|
||||||
|
TcDisplayPNGCharacteristics []DisplayPNGCharacteristicsDescriptor `json:"tcDisplayPNGCharacteristics"` |
||||||
|
// Each element of this array represents a PKIX [RFC5280] X.509 certificate that is a valid trust anchor for this authenticator model.
|
||||||
|
// Multiple certificates might be used for different batches of the same model.
|
||||||
|
// The array does not represent a certificate chain, but only the trust anchor of that chain.
|
||||||
|
// A trust anchor can be a root certificate, an intermediate CA certificate or even the attestation certificate itself.
|
||||||
|
AttestationRootCertificates []string `json:"attestationRootCertificates"` |
||||||
|
// A list of trust anchors used for ECDAA attestation. This entry MUST be present if and only if attestationType includes ATTESTATION_ECDAA.
|
||||||
|
EcdaaTrustAnchors []EcdaaTrustAnchor `json:"ecdaaTrustAnchors"` |
||||||
|
// A data: url [RFC2397] encoded PNG [PNG] icon for the Authenticator.
|
||||||
|
Icon string `json:"icon"` |
||||||
|
// List of extensions supported by the authenticator.
|
||||||
|
SupportedExtensions []ExtensionDescriptor `json:"supportedExtensions"` |
||||||
|
} |
||||||
|
|
||||||
|
// MDSGetEndpointsRequest is the request sent to the conformance metadata getEndpoints endpoint
|
||||||
|
type MDSGetEndpointsRequest struct { |
||||||
|
// The URL of the local server endpoint, e.g. https://webauthn.io/
|
||||||
|
Endpoint string `json:"endpoint"` |
||||||
|
} |
||||||
|
|
||||||
|
// MDSGetEndpointsResponse is the response received from a conformance metadata getEndpoints request
|
||||||
|
type MDSGetEndpointsResponse struct { |
||||||
|
// The status of the response
|
||||||
|
Status string `json:"status"` |
||||||
|
// An array of urls, each pointing to a MetadataTOCPayload
|
||||||
|
Result []string `json:"result"` |
||||||
|
} |
||||||
|
|
||||||
|
// ProcessMDSTOC processes a FIDO metadata table of contents object per §3.1.8, steps 1 through 5
|
||||||
|
// FIDO Authenticator Metadata Service
|
||||||
|
// https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-metadata-service-v2.0-rd-20180702.html#metadata-toc-object-processing-rules
|
||||||
|
func ProcessMDSTOC(url string, suffix string, c http.Client) (MetadataTOCPayload, string, error) { |
||||||
|
var tocAlg string |
||||||
|
var payload MetadataTOCPayload |
||||||
|
// 1. The FIDO Server MUST be able to download the latest metadata TOC object from the well-known URL, when appropriate.
|
||||||
|
body, err := downloadBytes(url+suffix, c) |
||||||
|
if err != nil { |
||||||
|
return payload, tocAlg, err |
||||||
|
} |
||||||
|
// Steps 2 - 4 done in unmarshalMDSTOC. Caller is responsible for step 5.
|
||||||
|
return unmarshalMDSTOC(body, c) |
||||||
|
} |
||||||
|
|
||||||
|
func unmarshalMDSTOC(body []byte, c http.Client) (MetadataTOCPayload, string, error) { |
||||||
|
var tocAlg string |
||||||
|
var payload MetadataTOCPayload |
||||||
|
token, err := jwt.Parse(string(body), func(token *jwt.Token) (interface{}, error) { |
||||||
|
// 2. If the x5u attribute is present in the JWT Header, then
|
||||||
|
if _, ok := token.Header["x5u"].([]interface{}); ok { |
||||||
|
// never seen an x5u here, although it is in the spec
|
||||||
|
return nil, errors.New("x5u encountered in header of metadata TOC payload") |
||||||
|
} |
||||||
|
var chain []interface{} |
||||||
|
// 3. If the x5u attribute is missing, the chain should be retrieved from the x5c attribute.
|
||||||
|
|
||||||
|
if x5c, ok := token.Header["x5c"].([]interface{}); !ok { |
||||||
|
// If that attribute is missing as well, Metadata TOC signing trust anchor is considered the TOC signing certificate chain.
|
||||||
|
root, err := getMetdataTOCSigningTrustAnchor(c) |
||||||
|
if nil != err { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
chain[0] = root |
||||||
|
} else { |
||||||
|
chain = x5c |
||||||
|
} |
||||||
|
|
||||||
|
// The certificate chain MUST be verified to properly chain to the metadata TOC signing trust anchor
|
||||||
|
valid, err := validateChain(chain, c) |
||||||
|
if !valid || err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
// chain validated, extract the TOC signing certificate from the chain
|
||||||
|
|
||||||
|
// create a buffer large enough to hold the certificate bytes
|
||||||
|
o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string)))) |
||||||
|
// base64 decode the certificate into the buffer
|
||||||
|
n, err := base64.StdEncoding.Decode(o, []byte(chain[0].(string))) |
||||||
|
// parse the certificate from the buffer
|
||||||
|
cert, err := x509.ParseCertificate(o[:n]) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
// 4. Verify the signature of the Metadata TOC object using the TOC signing certificate chain
|
||||||
|
// jwt.Parse() uses the TOC signing certificate public key internally to verify the signature
|
||||||
|
return cert.PublicKey, err |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
return payload, tocAlg, err |
||||||
|
} |
||||||
|
|
||||||
|
tocAlg = token.Header["alg"].(string) |
||||||
|
err = mapstructure.Decode(token.Claims, &payload) |
||||||
|
|
||||||
|
return payload, tocAlg, err |
||||||
|
} |
||||||
|
|
||||||
|
func getMetdataTOCSigningTrustAnchor(c http.Client) ([]byte, error) { |
||||||
|
rooturl := "" |
||||||
|
if Conformance { |
||||||
|
rooturl = "https://fidoalliance.co.nz/mds/pki/MDSROOT.crt" |
||||||
|
} else { |
||||||
|
rooturl = "https://mds.fidoalliance.org/Root.cer" |
||||||
|
} |
||||||
|
|
||||||
|
return downloadBytes(rooturl, c) |
||||||
|
} |
||||||
|
|
||||||
|
func validateChain(chain []interface{}, c http.Client) (bool, error) { |
||||||
|
root, err := getMetdataTOCSigningTrustAnchor(c) |
||||||
|
if err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
|
||||||
|
roots := x509.NewCertPool() |
||||||
|
|
||||||
|
ok := roots.AppendCertsFromPEM(root) |
||||||
|
if !ok { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
|
||||||
|
o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[1].(string)))) |
||||||
|
n, err := base64.StdEncoding.Decode(o, []byte(chain[1].(string))) |
||||||
|
if err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
intcert, err := x509.ParseCertificate(o[:n]) |
||||||
|
if err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
|
||||||
|
if revoked, ok := revoke.VerifyCertificate(intcert); !ok { |
||||||
|
return false, errCRLUnavailable |
||||||
|
} else if revoked { |
||||||
|
return false, errIntermediateCertRevoked |
||||||
|
} |
||||||
|
|
||||||
|
ints := x509.NewCertPool() |
||||||
|
ints.AddCert(intcert) |
||||||
|
|
||||||
|
l := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string)))) |
||||||
|
n, err = base64.StdEncoding.Decode(l, []byte(chain[0].(string))) |
||||||
|
if err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
leafcert, err := x509.ParseCertificate(l[:n]) |
||||||
|
if err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
if revoked, ok := revoke.VerifyCertificate(leafcert); !ok { |
||||||
|
return false, errCRLUnavailable |
||||||
|
} else if revoked { |
||||||
|
return false, errLeafCertRevoked |
||||||
|
} |
||||||
|
|
||||||
|
opts := x509.VerifyOptions{ |
||||||
|
Roots: roots, |
||||||
|
Intermediates: ints, |
||||||
|
} |
||||||
|
_, err = leafcert.Verify(opts) |
||||||
|
return err == nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// GetMetadataStatement iterates through a list of payload entries within a FIDO metadata table of contents object per §3.1.8, step 6
|
||||||
|
// FIDO Authenticator Metadata Service
|
||||||
|
// https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-metadata-service-v2.0-rd-20180702.html#metadata-toc-object-processing-rules
|
||||||
|
func GetMetadataStatement(entry MetadataTOCPayloadEntry, suffix string, alg string, c http.Client) (MetadataStatement, error) { |
||||||
|
var statement MetadataStatement |
||||||
|
// 1. Ignore the entry if the AAID, AAGUID or attestationCertificateKeyIdentifiers is not relevant to the relying party (e.g. not acceptable by any policy)
|
||||||
|
// Caller is responsible for determining if entry is relevant.
|
||||||
|
|
||||||
|
// 2. Download the metadata statement from the URL specified by the field url.
|
||||||
|
body, err := downloadBytes(entry.URL+suffix, c) |
||||||
|
if err != nil { |
||||||
|
return statement, err |
||||||
|
} |
||||||
|
// 3. Check whether the status report of the authenticator model has changed compared to the cached entry by looking at the fields timeOfLastStatusChange and statusReport.
|
||||||
|
// Caller is responsible for cache
|
||||||
|
|
||||||
|
// step 4 done in unmarshalMetadataStatement, caller is responsible for step 5
|
||||||
|
return unmarshalMetadataStatement(body, entry.Hash) |
||||||
|
} |
||||||
|
|
||||||
|
func unmarshalMetadataStatement(body []byte, hash string) (MetadataStatement, error) { |
||||||
|
// 4. Compute the hash value of the metadata statement downloaded from the URL and verify the hash value to the hash specified in the field hash of the metadata TOC object.
|
||||||
|
var statement MetadataStatement |
||||||
|
|
||||||
|
entryHash, err := base64.URLEncoding.DecodeString(hash) |
||||||
|
if err != nil { |
||||||
|
entryHash, err = base64.RawURLEncoding.DecodeString(hash) |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return statement, err |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: Get hasher based on MDS TOC alg instead of assuming SHA256
|
||||||
|
hasher := crypto.SHA256.New() |
||||||
|
_, _ = hasher.Write(body) |
||||||
|
hashed := hasher.Sum(nil) |
||||||
|
// Ignore the downloaded metadata statement if the hash value doesn't match.
|
||||||
|
if !bytes.Equal(hashed, entryHash) { |
||||||
|
return statement, errHashValueMismatch |
||||||
|
} |
||||||
|
|
||||||
|
// Extract the metadata statement from base64 encoded form
|
||||||
|
n := base64.URLEncoding.DecodedLen(len(body)) |
||||||
|
out := make([]byte, n) |
||||||
|
m, err := base64.URLEncoding.Decode(out, body) |
||||||
|
if err != nil { |
||||||
|
return statement, err |
||||||
|
} |
||||||
|
// Unmarshal the metadata statement into a MetadataStatement structure and return it to caller
|
||||||
|
err = json.Unmarshal(out[:m], &statement) |
||||||
|
return statement, err |
||||||
|
} |
||||||
|
|
||||||
|
func downloadBytes(url string, c http.Client) ([]byte, error) { |
||||||
|
res, err := c.Get(url) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer res.Body.Close() |
||||||
|
body, _ := ioutil.ReadAll(res.Body) |
||||||
|
return body, err |
||||||
|
} |
||||||
|
|
||||||
|
type MetadataError struct { |
||||||
|
// Short name for the type of error that has occurred
|
||||||
|
Type string `json:"type"` |
||||||
|
// Additional details about the error
|
||||||
|
Details string `json:"error"` |
||||||
|
// Information to help debug the error
|
||||||
|
DevInfo string `json:"debug"` |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
errHashValueMismatch = &MetadataError{ |
||||||
|
Type: "hash_mismatch", |
||||||
|
Details: "Hash value mismatch between entry.Hash and downloaded bytes", |
||||||
|
} |
||||||
|
errIntermediateCertRevoked = &MetadataError{ |
||||||
|
Type: "intermediate_revoked", |
||||||
|
Details: "Intermediate certificate is on issuers revocation list", |
||||||
|
} |
||||||
|
errLeafCertRevoked = &MetadataError{ |
||||||
|
Type: "leaf_revoked", |
||||||
|
Details: "Leaf certificate is on issuers revocation list", |
||||||
|
} |
||||||
|
errCRLUnavailable = &MetadataError{ |
||||||
|
Type: "crl_unavailable", |
||||||
|
Details: "Certificate revocation list is unavailable", |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
func (err *MetadataError) Error() string { |
||||||
|
return err.Details |
||||||
|
} |
@ -0,0 +1,155 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/sha256" |
||||||
|
"encoding/base64" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/protocol/webauthncose" |
||||||
|
) |
||||||
|
|
||||||
|
// The raw response returned to us from an authenticator when we request a
|
||||||
|
// credential for login/assertion.
|
||||||
|
type CredentialAssertionResponse struct { |
||||||
|
PublicKeyCredential |
||||||
|
AssertionResponse AuthenticatorAssertionResponse `json:"response"` |
||||||
|
} |
||||||
|
|
||||||
|
// The parsed CredentialAssertionResponse that has been marshalled into a format
|
||||||
|
// that allows us to verify the client and authenticator data inside the response
|
||||||
|
type ParsedCredentialAssertionData struct { |
||||||
|
ParsedPublicKeyCredential |
||||||
|
Response ParsedAssertionResponse |
||||||
|
Raw CredentialAssertionResponse |
||||||
|
} |
||||||
|
|
||||||
|
// The AuthenticatorAssertionResponse contains the raw authenticator assertion data and is parsed into
|
||||||
|
// ParsedAssertionResponse
|
||||||
|
type AuthenticatorAssertionResponse struct { |
||||||
|
AuthenticatorResponse |
||||||
|
AuthenticatorData URLEncodedBase64 `json:"authenticatorData"` |
||||||
|
Signature URLEncodedBase64 `json:"signature"` |
||||||
|
UserHandle URLEncodedBase64 `json:"userHandle,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
// Parsed form of AuthenticatorAssertionResponse
|
||||||
|
type ParsedAssertionResponse struct { |
||||||
|
CollectedClientData CollectedClientData |
||||||
|
AuthenticatorData AuthenticatorData |
||||||
|
Signature []byte |
||||||
|
UserHandle []byte |
||||||
|
} |
||||||
|
|
||||||
|
// Parse the credential request response into a format that is either required by the specification
|
||||||
|
// or makes the assertion verification steps easier to complete. This takes an http.Request that contains
|
||||||
|
// the assertion response data in a raw, mostly base64 encoded format, and parses the data into
|
||||||
|
// manageable structures
|
||||||
|
func ParseCredentialRequestResponse(response *http.Request) (*ParsedCredentialAssertionData, error) { |
||||||
|
if response == nil || response.Body == nil { |
||||||
|
return nil, ErrBadRequest.WithDetails("No response given") |
||||||
|
} |
||||||
|
return ParseCredentialRequestResponseBody(response.Body) |
||||||
|
} |
||||||
|
|
||||||
|
// Parse the credential request response into a format that is either required by the specification
|
||||||
|
// or makes the assertion verification steps easier to complete. This takes an io.Reader that contains
|
||||||
|
// the assertion response data in a raw, mostly base64 encoded format, and parses the data into
|
||||||
|
// manageable structures
|
||||||
|
func ParseCredentialRequestResponseBody(body io.Reader) (*ParsedCredentialAssertionData, error) { |
||||||
|
var car CredentialAssertionResponse |
||||||
|
err := json.NewDecoder(body).Decode(&car) |
||||||
|
if err != nil { |
||||||
|
return nil, ErrBadRequest.WithDetails("Parse error for Assertion") |
||||||
|
} |
||||||
|
|
||||||
|
if car.ID == "" { |
||||||
|
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID missing") |
||||||
|
} |
||||||
|
|
||||||
|
_, err = base64.RawURLEncoding.DecodeString(car.ID) |
||||||
|
if err != nil { |
||||||
|
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID not base64url encoded") |
||||||
|
} |
||||||
|
if car.Type != "public-key" { |
||||||
|
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with bad type") |
||||||
|
} |
||||||
|
var par ParsedCredentialAssertionData |
||||||
|
par.ID, par.RawID, par.Type, par.ClientExtensionResults = car.ID, car.RawID, car.Type, car.ClientExtensionResults |
||||||
|
par.Raw = car |
||||||
|
|
||||||
|
par.Response.Signature = car.AssertionResponse.Signature |
||||||
|
par.Response.UserHandle = car.AssertionResponse.UserHandle |
||||||
|
|
||||||
|
// Step 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
|
||||||
|
// We don't call it cData but this is Step 5 in the spec.
|
||||||
|
err = json.Unmarshal(car.AssertionResponse.ClientDataJSON, &par.Response.CollectedClientData) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
err = par.Response.AuthenticatorData.Unmarshal(car.AssertionResponse.AuthenticatorData) |
||||||
|
if err != nil { |
||||||
|
return nil, ErrParsingData.WithDetails("Error unmarshalling auth data") |
||||||
|
} |
||||||
|
return &par, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Follow the remaining steps outlined in §7.2 Verifying an authentication assertion
|
||||||
|
// (https://www.w3.org/TR/webauthn/#verifying-assertion) and return an error if there
|
||||||
|
// is a failure during each step.
|
||||||
|
func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPartyID, relyingPartyOrigin, appID string, verifyUser bool, credentialBytes []byte) error { |
||||||
|
// Steps 4 through 6 in verifying the assertion data (https://www.w3.org/TR/webauthn/#verifying-assertion) are
|
||||||
|
// "assertive" steps, i.e "Let JSONtext be the result of running UTF-8 decode on the value of cData."
|
||||||
|
// We handle these steps in part as we verify but also beforehand
|
||||||
|
|
||||||
|
// Handle steps 7 through 10 of assertion by verifying stored data against the Collected Client Data
|
||||||
|
// returned by the authenticator
|
||||||
|
validError := p.Response.CollectedClientData.Verify(storedChallenge, AssertCeremony, relyingPartyOrigin) |
||||||
|
if validError != nil { |
||||||
|
return validError |
||||||
|
} |
||||||
|
|
||||||
|
// Begin Step 11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the RP.
|
||||||
|
rpIDHash := sha256.Sum256([]byte(relyingPartyID)) |
||||||
|
|
||||||
|
var appIDHash [32]byte |
||||||
|
if appID != "" { |
||||||
|
appIDHash = sha256.Sum256([]byte(appID)) |
||||||
|
} |
||||||
|
|
||||||
|
// Handle steps 11 through 14, verifying the authenticator data.
|
||||||
|
validError = p.Response.AuthenticatorData.Verify(rpIDHash[:], appIDHash[:], verifyUser) |
||||||
|
if validError != nil { |
||||||
|
return ErrAuthData.WithInfo(validError.Error()) |
||||||
|
} |
||||||
|
|
||||||
|
// allowedUserCredentialIDs := session.AllowedCredentialIDs
|
||||||
|
|
||||||
|
// Step 15. Let hash be the result of computing a hash over the cData using SHA-256.
|
||||||
|
clientDataHash := sha256.Sum256(p.Raw.AssertionResponse.ClientDataJSON) |
||||||
|
|
||||||
|
// Step 16. Using the credential public key looked up in step 3, verify that sig is
|
||||||
|
// a valid signature over the binary concatenation of authData and hash.
|
||||||
|
|
||||||
|
sigData := append(p.Raw.AssertionResponse.AuthenticatorData, clientDataHash[:]...) |
||||||
|
|
||||||
|
var ( |
||||||
|
key interface{} |
||||||
|
err error |
||||||
|
) |
||||||
|
|
||||||
|
if appID == "" { |
||||||
|
key, err = webauthncose.ParsePublicKey(credentialBytes) |
||||||
|
} else { |
||||||
|
key, err = webauthncose.ParseFIDOPublicKey(credentialBytes) |
||||||
|
} |
||||||
|
|
||||||
|
valid, err := webauthncose.VerifySignature(key, sigData, p.Response.Signature) |
||||||
|
if !valid { |
||||||
|
return ErrAssertionSignature.WithDetails(fmt.Sprintf("Error validating the assertion signature: %+v\n", err)) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,156 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/sha256" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"github.com/fxamacker/cbor/v2" |
||||||
|
) |
||||||
|
|
||||||
|
// From §5.2.1 (https://www.w3.org/TR/webauthn/#authenticatorattestationresponse)
|
||||||
|
// "The authenticator's response to a client’s request for the creation
|
||||||
|
// of a new public key credential. It contains information about the new credential
|
||||||
|
// that can be used to identify it for later use, and metadata that can be used by
|
||||||
|
// the WebAuthn Relying Party to assess the characteristics of the credential
|
||||||
|
// during registration."
|
||||||
|
|
||||||
|
// The initial unpacked 'response' object received by the relying party. This
|
||||||
|
// contains the clientDataJSON object, which will be marshalled into
|
||||||
|
// CollectedClientData, and the 'attestationObject', which contains
|
||||||
|
// information about the authenticator, and the newly minted
|
||||||
|
// public key credential. The information in both objects are used
|
||||||
|
// to verify the authenticity of the ceremony and new credential
|
||||||
|
type AuthenticatorAttestationResponse struct { |
||||||
|
// The byte slice of clientDataJSON, which becomes CollectedClientData
|
||||||
|
AuthenticatorResponse |
||||||
|
// The byte slice version of AttestationObject
|
||||||
|
// This attribute contains an attestation object, which is opaque to, and
|
||||||
|
// cryptographically protected against tampering by, the client. The
|
||||||
|
// attestation object contains both authenticator data and an attestation
|
||||||
|
// statement. The former contains the AAGUID, a unique credential ID, and
|
||||||
|
// the credential public key. The contents of the attestation statement are
|
||||||
|
// determined by the attestation statement format used by the authenticator.
|
||||||
|
// It also contains any additional information that the Relying Party's server
|
||||||
|
// requires to validate the attestation statement, as well as to decode and
|
||||||
|
// validate the authenticator data along with the JSON-serialized client data.
|
||||||
|
AttestationObject URLEncodedBase64 `json:"attestationObject"` |
||||||
|
} |
||||||
|
|
||||||
|
// The parsed out version of AuthenticatorAttestationResponse.
|
||||||
|
type ParsedAttestationResponse struct { |
||||||
|
CollectedClientData CollectedClientData |
||||||
|
AttestationObject AttestationObject |
||||||
|
} |
||||||
|
|
||||||
|
// From §6.4. Authenticators MUST also provide some form of attestation. The basic requirement is that the
|
||||||
|
// authenticator can produce, for each credential public key, an attestation statement verifiable by the
|
||||||
|
// WebAuthn Relying Party. Typically, this attestation statement contains a signature by an attestation
|
||||||
|
// private key over the attested credential public key and a challenge, as well as a certificate or similar
|
||||||
|
// data providing provenance information for the attestation public key, enabling the Relying Party to make
|
||||||
|
// a trust decision. However, if an attestation key pair is not available, then the authenticator MUST
|
||||||
|
// perform self attestation of the credential public key with the corresponding credential private key.
|
||||||
|
// All this information is returned by authenticators any time a new public key credential is generated, in
|
||||||
|
// the overall form of an attestation object. (https://www.w3.org/TR/webauthn/#attestation-object)
|
||||||
|
//
|
||||||
|
type AttestationObject struct { |
||||||
|
// The authenticator data, including the newly created public key. See AuthenticatorData for more info
|
||||||
|
AuthData AuthenticatorData |
||||||
|
// The byteform version of the authenticator data, used in part for signature validation
|
||||||
|
RawAuthData []byte `json:"authData"` |
||||||
|
// The format of the Attestation data.
|
||||||
|
Format string `json:"fmt"` |
||||||
|
// The attestation statement data sent back if attestation is requested.
|
||||||
|
AttStatement map[string]interface{} `json:"attStmt,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
type attestationFormatValidationHandler func(AttestationObject, []byte) (string, []interface{}, error) |
||||||
|
|
||||||
|
var attestationRegistry = make(map[string]attestationFormatValidationHandler) |
||||||
|
|
||||||
|
// Using one of the locally registered attestation formats, handle validating the attestation
|
||||||
|
// data provided by the authenticator (and in some cases its manufacturer)
|
||||||
|
func RegisterAttestationFormat(format string, handler attestationFormatValidationHandler) { |
||||||
|
attestationRegistry[format] = handler |
||||||
|
} |
||||||
|
|
||||||
|
// Parse the values returned in the authenticator response and perform attestation verification
|
||||||
|
// Step 8. This returns a fully decoded struct with the data put into a format that can be
|
||||||
|
// used to verify the user and credential that was created
|
||||||
|
func (ccr *AuthenticatorAttestationResponse) Parse() (*ParsedAttestationResponse, error) { |
||||||
|
var p ParsedAttestationResponse |
||||||
|
|
||||||
|
err := json.Unmarshal(ccr.ClientDataJSON, &p.CollectedClientData) |
||||||
|
if err != nil { |
||||||
|
return nil, ErrParsingData.WithInfo(err.Error()) |
||||||
|
} |
||||||
|
|
||||||
|
err = cbor.Unmarshal(ccr.AttestationObject, &p.AttestationObject) |
||||||
|
if err != nil { |
||||||
|
return nil, ErrParsingData.WithInfo(err.Error()) |
||||||
|
} |
||||||
|
|
||||||
|
// Step 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse
|
||||||
|
// structure to obtain the attestation statement format fmt, the authenticator data authData, and
|
||||||
|
// the attestation statement attStmt.
|
||||||
|
err = p.AttestationObject.AuthData.Unmarshal(p.AttestationObject.RawAuthData) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("error decoding auth data: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
if !p.AttestationObject.AuthData.Flags.HasAttestedCredentialData() { |
||||||
|
return nil, ErrAttestationFormat.WithInfo("Attestation missing attested credential data flag") |
||||||
|
} |
||||||
|
|
||||||
|
return &p, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Verify - Perform Steps 9 through 14 of registration verification, delegating Steps
|
||||||
|
func (attestationObject *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, verificationRequired bool) error { |
||||||
|
// Steps 9 through 12 are verified against the auth data.
|
||||||
|
// These steps are identical to 11 through 14 for assertion
|
||||||
|
// so we handle them with AuthData
|
||||||
|
|
||||||
|
// Begin Step 9. Verify that the rpIdHash in authData is
|
||||||
|
// the SHA-256 hash of the RP ID expected by the RP.
|
||||||
|
rpIDHash := sha256.Sum256([]byte(relyingPartyID)) |
||||||
|
// Handle Steps 9 through 12
|
||||||
|
authDataVerificationError := attestationObject.AuthData.Verify(rpIDHash[:], nil, verificationRequired) |
||||||
|
if authDataVerificationError != nil { |
||||||
|
return authDataVerificationError |
||||||
|
} |
||||||
|
|
||||||
|
// Step 13. Determine the attestation statement format by performing a
|
||||||
|
// USASCII case-sensitive match on fmt against the set of supported
|
||||||
|
// WebAuthn Attestation Statement Format Identifier values. The up-to-date
|
||||||
|
// list of registered WebAuthn Attestation Statement Format Identifier
|
||||||
|
// values is maintained in the IANA registry of the same name
|
||||||
|
// [WebAuthn-Registries] (https://www.w3.org/TR/webauthn/#biblio-webauthn-registries).
|
||||||
|
|
||||||
|
// Since there is not an active registry yet, we'll check it against our internal
|
||||||
|
// Supported types.
|
||||||
|
|
||||||
|
// But first let's make sure attestation is present. If it isn't, we don't need to handle
|
||||||
|
// any of the following steps
|
||||||
|
if attestationObject.Format == "none" { |
||||||
|
if len(attestationObject.AttStatement) != 0 { |
||||||
|
return ErrAttestationFormat.WithInfo("Attestation format none with attestation present") |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
formatHandler, valid := attestationRegistry[attestationObject.Format] |
||||||
|
if !valid { |
||||||
|
return ErrAttestationFormat.WithInfo(fmt.Sprintf("Attestation format %s is unsupported", attestationObject.Format)) |
||||||
|
} |
||||||
|
|
||||||
|
// Step 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, by using
|
||||||
|
// the attestation statement format fmt’s verification procedure given attStmt, authData and the hash of the serialized
|
||||||
|
// client data computed in step 7.
|
||||||
|
attestationType, _, err := formatHandler(*attestationObject, clientDataHash) |
||||||
|
if err != nil { |
||||||
|
return err.(*Error).WithInfo(attestationType) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,227 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"crypto/x509" |
||||||
|
"encoding/asn1" |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/protocol/webauthncose" |
||||||
|
) |
||||||
|
|
||||||
|
var androidAttestationKey = "android-key" |
||||||
|
|
||||||
|
func init() { |
||||||
|
RegisterAttestationFormat(androidAttestationKey, verifyAndroidKeyFormat) |
||||||
|
} |
||||||
|
|
||||||
|
// From §8.4. https://www.w3.org/TR/webauthn/#android-key-attestation
|
||||||
|
// The android-key attestation statement looks like:
|
||||||
|
// $$attStmtType //= (
|
||||||
|
// fmt: "android-key",
|
||||||
|
// attStmt: androidStmtFormat
|
||||||
|
// )
|
||||||
|
// androidStmtFormat = {
|
||||||
|
// alg: COSEAlgorithmIdentifier,
|
||||||
|
// sig: bytes,
|
||||||
|
// x5c: [ credCert: bytes, * (caCert: bytes) ]
|
||||||
|
// }
|
||||||
|
func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) { |
||||||
|
// Given the verification procedure inputs attStmt, authenticatorData and clientDataHash, the verification procedure is as follows:
|
||||||
|
// §8.4.1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract
|
||||||
|
// the contained fields.
|
||||||
|
|
||||||
|
// Get the alg value - A COSEAlgorithmIdentifier containing the identifier of the algorithm
|
||||||
|
// used to generate the attestation signature.
|
||||||
|
alg, present := att.AttStatement["alg"].(int64) |
||||||
|
if !present { |
||||||
|
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving alg value") |
||||||
|
} |
||||||
|
|
||||||
|
// Get the sig value - A byte string containing the attestation signature.
|
||||||
|
sig, present := att.AttStatement["sig"].([]byte) |
||||||
|
if !present { |
||||||
|
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving sig value") |
||||||
|
} |
||||||
|
|
||||||
|
// If x5c is not present, return an error
|
||||||
|
x5c, x509present := att.AttStatement["x5c"].([]interface{}) |
||||||
|
if !x509present { |
||||||
|
// Handle Basic Attestation steps for the x509 Certificate
|
||||||
|
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving x5c value") |
||||||
|
} |
||||||
|
|
||||||
|
// §8.4.2. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
|
||||||
|
// using the public key in the first certificate in x5c with the algorithm specified in alg.
|
||||||
|
attCertBytes, valid := x5c[0].([]byte) |
||||||
|
if !valid { |
||||||
|
return androidAttestationKey, nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain") |
||||||
|
} |
||||||
|
|
||||||
|
signatureData := append(att.RawAuthData, clientDataHash...) |
||||||
|
|
||||||
|
attCert, err := x509.ParseCertificate(attCertBytes) |
||||||
|
if err != nil { |
||||||
|
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err)) |
||||||
|
} |
||||||
|
|
||||||
|
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg) |
||||||
|
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg) |
||||||
|
err = attCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), signatureData, sig) |
||||||
|
if err != nil { |
||||||
|
return androidAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err)) |
||||||
|
} |
||||||
|
// Verify that the public key in the first certificate in x5c matches the credentialPublicKey in the attestedCredentialData in authenticatorData.
|
||||||
|
pubKey, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey) |
||||||
|
if err != nil { |
||||||
|
return androidAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err)) |
||||||
|
} |
||||||
|
e := pubKey.(webauthncose.EC2PublicKeyData) |
||||||
|
valid, err = e.Verify(signatureData, sig) |
||||||
|
if err != nil || valid != true { |
||||||
|
return androidAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err)) |
||||||
|
} |
||||||
|
// §8.4.3. Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
|
||||||
|
// attCert.Extensions
|
||||||
|
var attExtBytes []byte |
||||||
|
for _, ext := range attCert.Extensions { |
||||||
|
if ext.Id.Equal([]int{1, 3, 6, 1, 4, 1, 11129, 2, 1, 17}) { |
||||||
|
attExtBytes = ext.Value |
||||||
|
} |
||||||
|
} |
||||||
|
if len(attExtBytes) == 0 { |
||||||
|
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions missing 1.3.6.1.4.1.11129.2.1.17") |
||||||
|
} |
||||||
|
// As noted in §8.4.1 (https://w3c.github.io/webauthn/#key-attstn-cert-requirements) the Android Key Attestation attestation certificate's
|
||||||
|
// android key attestation certificate extension data is identified by the OID "1.3.6.1.4.1.11129.2.1.17".
|
||||||
|
decoded := keyDescription{} |
||||||
|
_, err = asn1.Unmarshal([]byte(attExtBytes), &decoded) |
||||||
|
if err != nil { |
||||||
|
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to parse Android key attestation certificate extensions") |
||||||
|
} |
||||||
|
// Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
|
||||||
|
if 0 != bytes.Compare(decoded.AttestationChallenge, clientDataHash) { |
||||||
|
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation challenge not equal to clientDataHash") |
||||||
|
} |
||||||
|
// The AuthorizationList.allApplications field is not present on either authorization list (softwareEnforced nor teeEnforced), since PublicKeyCredential MUST be scoped to the RP ID.
|
||||||
|
if nil != decoded.SoftwareEnforced.AllApplications || nil != decoded.TeeEnforced.AllApplications { |
||||||
|
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains all applications field") |
||||||
|
} |
||||||
|
// For the following, use only the teeEnforced authorization list if the RP wants to accept only keys from a trusted execution environment, otherwise use the union of teeEnforced and softwareEnforced.
|
||||||
|
// The value in the AuthorizationList.origin field is equal to KM_ORIGIN_GENERATED. (which == 0)
|
||||||
|
if KM_ORIGIN_GENERATED != decoded.SoftwareEnforced.Origin || KM_ORIGIN_GENERATED != decoded.TeeEnforced.Origin { |
||||||
|
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains authorization list with origin not equal KM_ORIGIN_GENERATED") |
||||||
|
} |
||||||
|
// The value in the AuthorizationList.purpose field is equal to KM_PURPOSE_SIGN. (which == 2)
|
||||||
|
if !contains(decoded.SoftwareEnforced.Purpose, KM_PURPOSE_SIGN) && !contains(decoded.TeeEnforced.Purpose, KM_PURPOSE_SIGN) { |
||||||
|
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains authorization list with purpose not equal KM_PURPOSE_SIGN") |
||||||
|
} |
||||||
|
return androidAttestationKey, x5c, err |
||||||
|
} |
||||||
|
|
||||||
|
func contains(s []int, e int) bool { |
||||||
|
for _, a := range s { |
||||||
|
if a == e { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
type keyDescription struct { |
||||||
|
AttestationVersion int |
||||||
|
AttestationSecurityLevel asn1.Enumerated |
||||||
|
KeymasterVersion int |
||||||
|
KeymasterSecurityLevel asn1.Enumerated |
||||||
|
AttestationChallenge []byte |
||||||
|
UniqueID []byte |
||||||
|
SoftwareEnforced authorizationList |
||||||
|
TeeEnforced authorizationList |
||||||
|
} |
||||||
|
|
||||||
|
type authorizationList struct { |
||||||
|
Purpose []int `asn1:"tag:1,explicit,set,optional"` |
||||||
|
Algorithm int `asn1:"tag:2,explicit,optional"` |
||||||
|
KeySize int `asn1:"tag:3,explicit,optional"` |
||||||
|
Digest []int `asn1:"tag:5,explicit,set,optional"` |
||||||
|
Padding []int `asn1:"tag:6,explicit,set,optional"` |
||||||
|
EcCurve int `asn1:"tag:10,explicit,optional"` |
||||||
|
RsaPublicExponent int `asn1:"tag:200,explicit,optional"` |
||||||
|
RollbackResistance interface{} `asn1:"tag:303,explicit,optional"` |
||||||
|
ActiveDateTime int `asn1:"tag:400,explicit,optional"` |
||||||
|
OriginationExpireDateTime int `asn1:"tag:401,explicit,optional"` |
||||||
|
UsageExpireDateTime int `asn1:"tag:402,explicit,optional"` |
||||||
|
NoAuthRequired interface{} `asn1:"tag:503,explicit,optional"` |
||||||
|
UserAuthType int `asn1:"tag:504,explicit,optional"` |
||||||
|
AuthTimeout int `asn1:"tag:505,explicit,optional"` |
||||||
|
AllowWhileOnBody interface{} `asn1:"tag:506,explicit,optional"` |
||||||
|
TrustedUserPresenceRequired interface{} `asn1:"tag:507,explicit,optional"` |
||||||
|
TrustedConfirmationRequired interface{} `asn1:"tag:508,explicit,optional"` |
||||||
|
UnlockedDeviceRequired interface{} `asn1:"tag:509,explicit,optional"` |
||||||
|
AllApplications interface{} `asn1:"tag:600,explicit,optional"` |
||||||
|
ApplicationID interface{} `asn1:"tag:601,explicit,optional"` |
||||||
|
CreationDateTime int `asn1:"tag:701,explicit,optional"` |
||||||
|
Origin int `asn1:"tag:702,explicit,optional"` |
||||||
|
RootOfTrust rootOfTrust `asn1:"tag:704,explicit,optional"` |
||||||
|
OsVersion int `asn1:"tag:705,explicit,optional"` |
||||||
|
OsPatchLevel int `asn1:"tag:706,explicit,optional"` |
||||||
|
AttestationApplicationID []byte `asn1:"tag:709,explicit,optional"` |
||||||
|
AttestationIDBrand []byte `asn1:"tag:710,explicit,optional"` |
||||||
|
AttestationIDDevice []byte `asn1:"tag:711,explicit,optional"` |
||||||
|
AttestationIDProduct []byte `asn1:"tag:712,explicit,optional"` |
||||||
|
AttestationIDSerial []byte `asn1:"tag:713,explicit,optional"` |
||||||
|
AttestationIDImei []byte `asn1:"tag:714,explicit,optional"` |
||||||
|
AttestationIDMeid []byte `asn1:"tag:715,explicit,optional"` |
||||||
|
AttestationIDManufacturer []byte `asn1:"tag:716,explicit,optional"` |
||||||
|
AttestationIDModel []byte `asn1:"tag:717,explicit,optional"` |
||||||
|
VendorPatchLevel int `asn1:"tag:718,explicit,optional"` |
||||||
|
BootPatchLevel int `asn1:"tag:719,explicit,optional"` |
||||||
|
} |
||||||
|
|
||||||
|
type rootOfTrust struct { |
||||||
|
verifiedBootKey []byte |
||||||
|
deviceLocked bool |
||||||
|
verifiedBootState verifiedBootState |
||||||
|
verifiedBootHash []byte |
||||||
|
} |
||||||
|
|
||||||
|
type verifiedBootState int |
||||||
|
|
||||||
|
const ( |
||||||
|
Verified verifiedBootState = iota |
||||||
|
SelfSigned |
||||||
|
Unverified |
||||||
|
Failed |
||||||
|
) |
||||||
|
|
||||||
|
/** |
||||||
|
* The origin of a key (or pair), i.e. where it was generated. Note that KM_TAG_ORIGIN can be found |
||||||
|
* in either the hardware-enforced or software-enforced list for a key, indicating whether the key |
||||||
|
* is hardware or software-based. Specifically, a key with KM_ORIGIN_GENERATED in the |
||||||
|
* hardware-enforced list is guaranteed never to have existed outide the secure hardware. |
||||||
|
*/ |
||||||
|
type KM_KEY_ORIGIN int |
||||||
|
|
||||||
|
const ( |
||||||
|
KM_ORIGIN_GENERATED = iota /* Generated in keymaster. Should not exist outside the TEE. */ |
||||||
|
KM_ORIGIN_DERIVED /* Derived inside keymaster. Likely exists off-device. */ |
||||||
|
KM_ORIGIN_IMPORTED /* Imported into keymaster. Existed as cleartext in Android. */ |
||||||
|
KM_ORIGIN_UNKNOWN /* Keymaster did not record origin. This value can only be seen on |
||||||
|
* keys in a keymaster0 implementation. The keymaster0 adapter uses |
||||||
|
* this value to document the fact that it is unkown whether the key |
||||||
|
* was generated inside or imported into keymaster. */ |
||||||
|
) |
||||||
|
|
||||||
|
/** |
||||||
|
* Possible purposes of a key (or pair). |
||||||
|
*/ |
||||||
|
type KM_PURPOSE int |
||||||
|
|
||||||
|
const ( |
||||||
|
KM_PURPOSE_ENCRYPT = iota /* Usable with RSA, EC and AES keys. */ |
||||||
|
KM_PURPOSE_DECRYPT /* Usable with RSA, EC and AES keys. */ |
||||||
|
KM_PURPOSE_SIGN /* Usable with RSA, EC and HMAC keys. */ |
||||||
|
KM_PURPOSE_VERIFY /* Usable with RSA, EC and HMAC keys. */ |
||||||
|
KM_PURPOSE_DERIVE_KEY /* Usable with EC keys. */ |
||||||
|
KM_PURPOSE_WRAP /* Usable with wrapped keys. */ |
||||||
|
) |
@ -0,0 +1,104 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"crypto/ecdsa" |
||||||
|
"crypto/elliptic" |
||||||
|
"crypto/sha256" |
||||||
|
"crypto/x509" |
||||||
|
"encoding/asn1" |
||||||
|
"fmt" |
||||||
|
"math/big" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/protocol/webauthncose" |
||||||
|
) |
||||||
|
|
||||||
|
var appleAttestationKey = "apple" |
||||||
|
|
||||||
|
func init() { |
||||||
|
RegisterAttestationFormat(appleAttestationKey, verifyAppleKeyFormat) |
||||||
|
} |
||||||
|
|
||||||
|
// From §8.8. https://www.w3.org/TR/webauthn-2/#sctn-apple-anonymous-attestation
|
||||||
|
// The apple attestation statement looks like:
|
||||||
|
// $$attStmtType //= (
|
||||||
|
// fmt: "apple",
|
||||||
|
// attStmt: appleStmtFormat
|
||||||
|
// )
|
||||||
|
// appleStmtFormat = {
|
||||||
|
// x5c: [ credCert: bytes, * (caCert: bytes) ]
|
||||||
|
// }
|
||||||
|
func verifyAppleKeyFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) { |
||||||
|
|
||||||
|
// Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined
|
||||||
|
// above and perform CBOR decoding on it to extract the contained fields.
|
||||||
|
|
||||||
|
// If x5c is not present, return an error
|
||||||
|
x5c, x509present := att.AttStatement["x5c"].([]interface{}) |
||||||
|
if !x509present { |
||||||
|
// Handle Basic Attestation steps for the x509 Certificate
|
||||||
|
return appleAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving x5c value") |
||||||
|
} |
||||||
|
|
||||||
|
credCertBytes, valid := x5c[0].([]byte) |
||||||
|
if !valid { |
||||||
|
return appleAttestationKey, nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain") |
||||||
|
} |
||||||
|
|
||||||
|
credCert, err := x509.ParseCertificate(credCertBytes) |
||||||
|
if err != nil { |
||||||
|
return appleAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err)) |
||||||
|
} |
||||||
|
|
||||||
|
// Step 2. Concatenate authenticatorData and clientDataHash to form nonceToHash.
|
||||||
|
nonceToHash := append(att.RawAuthData, clientDataHash...) |
||||||
|
|
||||||
|
// Step 3. Perform SHA-256 hash of nonceToHash to produce nonce.
|
||||||
|
nonce := sha256.Sum256(nonceToHash) |
||||||
|
|
||||||
|
// Step 4. Verify that nonce equals the value of the extension with OID 1.2.840.113635.100.8.2 in credCert.
|
||||||
|
var attExtBytes []byte |
||||||
|
for _, ext := range credCert.Extensions { |
||||||
|
if ext.Id.Equal([]int{1, 2, 840, 113635, 100, 8, 2}) { |
||||||
|
attExtBytes = ext.Value |
||||||
|
} |
||||||
|
} |
||||||
|
if len(attExtBytes) == 0 { |
||||||
|
return appleAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions missing 1.2.840.113635.100.8.2") |
||||||
|
} |
||||||
|
|
||||||
|
decoded := AppleAnonymousAttestation{} |
||||||
|
_, err = asn1.Unmarshal([]byte(attExtBytes), &decoded) |
||||||
|
if err != nil { |
||||||
|
return appleAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to parse apple attestation certificate extensions") |
||||||
|
} |
||||||
|
|
||||||
|
if !bytes.Equal(decoded.Nonce, nonce[:]) || err != nil { |
||||||
|
return appleAttestationKey, nil, ErrInvalidAttestation.WithDetails("Attestation certificate does not contain expected nonce") |
||||||
|
} |
||||||
|
|
||||||
|
// Step 5. Verify that the credential public key equals the Subject Public Key of credCert.
|
||||||
|
// TODO: Probably move this part to webauthncose.go
|
||||||
|
pubKey, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey) |
||||||
|
if err != nil { |
||||||
|
return appleAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err)) |
||||||
|
} |
||||||
|
credPK := pubKey.(webauthncose.EC2PublicKeyData) |
||||||
|
subjectPK := credCert.PublicKey.(*ecdsa.PublicKey) |
||||||
|
credPKInfo := &ecdsa.PublicKey{ |
||||||
|
Curve: elliptic.P256(), |
||||||
|
X: big.NewInt(0).SetBytes(credPK.XCoord), |
||||||
|
Y: big.NewInt(0).SetBytes(credPK.YCoord), |
||||||
|
} |
||||||
|
if !credPKInfo.Equal(subjectPK) { |
||||||
|
return appleAttestationKey, nil, ErrInvalidAttestation.WithDetails("Certificate public key does not match public key in authData") |
||||||
|
} |
||||||
|
|
||||||
|
// Step 6. If successful, return implementation-specific values representing attestation type Anonymization CA and attestation trust path x5c.
|
||||||
|
return appleAttestationKey, x5c, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Apple has not yet publish schema for the extension(as of JULY 2021.)
|
||||||
|
type AppleAnonymousAttestation struct { |
||||||
|
Nonce []byte `asn1:"tag:1,explicit"` |
||||||
|
} |
@ -0,0 +1,278 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"crypto/x509" |
||||||
|
"encoding/asn1" |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/metadata" |
||||||
|
uuid "github.com/satori/go.uuid" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/protocol/webauthncose" |
||||||
|
) |
||||||
|
|
||||||
|
var packedAttestationKey = "packed" |
||||||
|
|
||||||
|
func init() { |
||||||
|
RegisterAttestationFormat(packedAttestationKey, verifyPackedFormat) |
||||||
|
} |
||||||
|
|
||||||
|
// From §8.2. https://www.w3.org/TR/webauthn/#packed-attestation
|
||||||
|
// The packed attestation statement looks like:
|
||||||
|
// packedStmtFormat = {
|
||||||
|
// alg: COSEAlgorithmIdentifier,
|
||||||
|
// sig: bytes,
|
||||||
|
// x5c: [ attestnCert: bytes, * (caCert: bytes) ]
|
||||||
|
// } OR
|
||||||
|
// {
|
||||||
|
// alg: COSEAlgorithmIdentifier, (-260 for ED256 / -261 for ED512)
|
||||||
|
// sig: bytes,
|
||||||
|
// ecdaaKeyId: bytes
|
||||||
|
// } OR
|
||||||
|
// {
|
||||||
|
// alg: COSEAlgorithmIdentifier
|
||||||
|
// sig: bytes,
|
||||||
|
// }
|
||||||
|
func verifyPackedFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) { |
||||||
|
// Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined
|
||||||
|
// above and perform CBOR decoding on it to extract the contained fields.
|
||||||
|
|
||||||
|
// Get the alg value - A COSEAlgorithmIdentifier containing the identifier of the algorithm
|
||||||
|
// used to generate the attestation signature.
|
||||||
|
|
||||||
|
alg, present := att.AttStatement["alg"].(int64) |
||||||
|
if !present { |
||||||
|
return packedAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving alg value") |
||||||
|
} |
||||||
|
|
||||||
|
// Get the sig value - A byte string containing the attestation signature.
|
||||||
|
sig, present := att.AttStatement["sig"].([]byte) |
||||||
|
if !present { |
||||||
|
return packedAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving sig value") |
||||||
|
} |
||||||
|
|
||||||
|
// Step 2. If x5c is present, this indicates that the attestation type is not ECDAA.
|
||||||
|
x5c, x509present := att.AttStatement["x5c"].([]interface{}) |
||||||
|
if x509present { |
||||||
|
// Handle Basic Attestation steps for the x509 Certificate
|
||||||
|
return handleBasicAttestation(sig, clientDataHash, att.RawAuthData, att.AuthData.AttData.AAGUID, alg, x5c) |
||||||
|
} |
||||||
|
|
||||||
|
// Step 3. If ecdaaKeyId is present, then the attestation type is ECDAA.
|
||||||
|
// Also make sure the we did not have an x509 then
|
||||||
|
ecdaaKeyID, ecdaaKeyPresent := att.AttStatement["ecdaaKeyId"].([]byte) |
||||||
|
if ecdaaKeyPresent { |
||||||
|
// Handle ECDAA Attestation steps for the x509 Certificate
|
||||||
|
return handleECDAAAttesation(sig, clientDataHash, ecdaaKeyID) |
||||||
|
} |
||||||
|
|
||||||
|
// Step 4. If neither x5c nor ecdaaKeyId is present, self attestation is in use.
|
||||||
|
return handleSelfAttestation(alg, att.AuthData.AttData.CredentialPublicKey, att.RawAuthData, clientDataHash, sig) |
||||||
|
} |
||||||
|
|
||||||
|
// Handle the attestation steps laid out in
|
||||||
|
func handleBasicAttestation(signature, clientDataHash, authData, aaguid []byte, alg int64, x5c []interface{}) (string, []interface{}, error) { |
||||||
|
// Step 2.1. Verify that sig is a valid signature over the concatenation of authenticatorData
|
||||||
|
// and clientDataHash using the attestation public key in attestnCert with the algorithm specified in alg.
|
||||||
|
attestationType := "Packed (Basic)" |
||||||
|
|
||||||
|
for _, c := range x5c { |
||||||
|
cb, cv := c.([]byte) |
||||||
|
if !cv { |
||||||
|
return attestationType, x5c, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain") |
||||||
|
} |
||||||
|
ct, err := x509.ParseCertificate(cb) |
||||||
|
if err != nil { |
||||||
|
return attestationType, x5c, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err)) |
||||||
|
} |
||||||
|
if ct.NotBefore.After(time.Now()) || ct.NotAfter.Before(time.Now()) { |
||||||
|
return attestationType, x5c, ErrAttestationFormat.WithDetails("Cert in chain not time valid") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
attCertBytes, valid := x5c[0].([]byte) |
||||||
|
if !valid { |
||||||
|
return attestationType, x5c, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain") |
||||||
|
} |
||||||
|
|
||||||
|
signatureData := append(authData, clientDataHash...) |
||||||
|
|
||||||
|
attCert, err := x509.ParseCertificate(attCertBytes) |
||||||
|
if err != nil { |
||||||
|
return attestationType, x5c, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err)) |
||||||
|
} |
||||||
|
|
||||||
|
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg) |
||||||
|
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg) |
||||||
|
err = attCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), signatureData, signature) |
||||||
|
if err != nil { |
||||||
|
return attestationType, x5c, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err)) |
||||||
|
} |
||||||
|
|
||||||
|
// Step 2.2 Verify that attestnCert meets the requirements in §8.2.1 Packed attestation statement certificate requirements.
|
||||||
|
// §8.2.1 can be found here https://www.w3.org/TR/webauthn/#packed-attestation-cert-requirements
|
||||||
|
|
||||||
|
// Step 2.2.1 (from §8.2.1) Version MUST be set to 3 (which is indicated by an ASN.1 INTEGER with value 2).
|
||||||
|
if attCert.Version != 3 { |
||||||
|
return attestationType, x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate is incorrect version") |
||||||
|
} |
||||||
|
|
||||||
|
// Step 2.2.2 (from §8.2.1) Subject field MUST be set to:
|
||||||
|
|
||||||
|
// Subject-C
|
||||||
|
// ISO 3166 code specifying the country where the Authenticator vendor is incorporated (PrintableString)
|
||||||
|
|
||||||
|
// TODO: Find a good, useable, country code library. For now, check stringy-ness
|
||||||
|
subjectString := strings.Join(attCert.Subject.Country, "") |
||||||
|
if subjectString == "" { |
||||||
|
return attestationType, x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Country Code is invalid") |
||||||
|
} |
||||||
|
|
||||||
|
// Subject-O
|
||||||
|
// Legal name of the Authenticator vendor (UTF8String)
|
||||||
|
subjectString = strings.Join(attCert.Subject.Organization, "") |
||||||
|
if subjectString == "" { |
||||||
|
return attestationType, x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Organization is invalid") |
||||||
|
} |
||||||
|
|
||||||
|
// Subject-OU
|
||||||
|
// Literal string “Authenticator Attestation” (UTF8String)
|
||||||
|
subjectString = strings.Join(attCert.Subject.OrganizationalUnit, " ") |
||||||
|
if subjectString != "Authenticator Attestation" { |
||||||
|
// TODO: Implement a return error when I'm more certain this is general practice
|
||||||
|
} |
||||||
|
|
||||||
|
// Subject-CN
|
||||||
|
// A UTF8String of the vendor’s choosing
|
||||||
|
subjectString = attCert.Subject.CommonName |
||||||
|
if subjectString == "" { |
||||||
|
return attestationType, x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Common Name not set") |
||||||
|
} |
||||||
|
// TODO: And then what
|
||||||
|
|
||||||
|
// Step 2.2.3 (from §8.2.1) If the related attestation root certificate is used for multiple authenticator models,
|
||||||
|
// the Extension OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) MUST be present, containing the
|
||||||
|
// AAGUID as a 16-byte OCTET STRING. The extension MUST NOT be marked as critical.
|
||||||
|
|
||||||
|
idFido := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 45724, 1, 1, 4} |
||||||
|
var foundAAGUID []byte |
||||||
|
for _, extension := range attCert.Extensions { |
||||||
|
if extension.Id.Equal(idFido) { |
||||||
|
if extension.Critical { |
||||||
|
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Attestation certificate FIDO extension marked as critical") |
||||||
|
} |
||||||
|
foundAAGUID = extension.Value |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// We validate the AAGUID as mentioned above
|
||||||
|
// This is not well defined in§8.2.1 but mentioned in step 2.3: we validate the AAGUID if it is present within the certificate
|
||||||
|
// and make sure it matches the auth data AAGUID
|
||||||
|
// Note that an X.509 Extension encodes the DER-encoding of the value in an OCTET STRING. Thus, the
|
||||||
|
// AAGUID MUST be wrapped in two OCTET STRINGS to be valid.
|
||||||
|
if len(foundAAGUID) > 0 { |
||||||
|
unMarshalledAAGUID := []byte{} |
||||||
|
asn1.Unmarshal(foundAAGUID, &unMarshalledAAGUID) |
||||||
|
if !bytes.Equal(aaguid, unMarshalledAAGUID) { |
||||||
|
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Certificate AAGUID does not match Auth Data certificate") |
||||||
|
} |
||||||
|
} |
||||||
|
uuid, err := uuid.FromBytes(aaguid) |
||||||
|
|
||||||
|
if meta, ok := metadata.Metadata[uuid]; ok { |
||||||
|
for _, s := range meta.StatusReports { |
||||||
|
if metadata.IsUndesiredAuthenticatorStatus(metadata.AuthenticatorStatus(s.Status)) { |
||||||
|
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Authenticator with undesirable status encountered") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if attCert.Subject.CommonName != attCert.Issuer.CommonName { |
||||||
|
var hasBasicFull = false |
||||||
|
for _, a := range meta.MetadataStatement.AttestationTypes { |
||||||
|
if metadata.AuthenticatorAttestationType(a) == metadata.AuthenticatorAttestationType(metadata.BasicFull) { |
||||||
|
hasBasicFull = true |
||||||
|
} |
||||||
|
} |
||||||
|
if !hasBasicFull { |
||||||
|
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Attestation with full attestation from authentictor that does not support full attestation") |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
if metadata.Conformance { |
||||||
|
return attestationType, x5c, ErrInvalidAttestation.WithDetails("AAGUID not found in metadata during conformance testing") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Step 2.2.4 The Basic Constraints extension MUST have the CA component set to false.
|
||||||
|
if attCert.IsCA { |
||||||
|
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Attestation certificate's Basic Constraints marked as CA") |
||||||
|
} |
||||||
|
|
||||||
|
// Note for 2.2.5 An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL
|
||||||
|
// Distribution Point extension [RFC5280](https://www.w3.org/TR/webauthn/#biblio-rfc5280) are
|
||||||
|
// both OPTIONAL as the status of many attestation certificates is available through authenticator
|
||||||
|
// metadata services. See, for example, the FIDO Metadata Service
|
||||||
|
// [FIDOMetadataService] (https://www.w3.org/TR/webauthn/#biblio-fidometadataservice)
|
||||||
|
|
||||||
|
// Step 2.4 If successful, return attestation type Basic and attestation trust path x5c.
|
||||||
|
// We don't handle trust paths yet but we're done
|
||||||
|
return attestationType, x5c, nil |
||||||
|
} |
||||||
|
|
||||||
|
func handleECDAAAttesation(signature, clientDataHash, ecdaaKeyID []byte) (string, []interface{}, error) { |
||||||
|
return "Packed (ECDAA)", nil, ErrNotSpecImplemented |
||||||
|
} |
||||||
|
|
||||||
|
func handleSelfAttestation(alg int64, pubKey, authData, clientDataHash, signature []byte) (string, []interface{}, error) { |
||||||
|
attestationType := "Packed (Self)" |
||||||
|
// §4.1 Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData.
|
||||||
|
|
||||||
|
// §4.2 Verify that sig is a valid signature over the concatenation of authenticatorData and
|
||||||
|
// clientDataHash using the credential public key with alg.
|
||||||
|
verificationData := append(authData, clientDataHash...) |
||||||
|
|
||||||
|
key, err := webauthncose.ParsePublicKey(pubKey) |
||||||
|
if err != nil { |
||||||
|
return attestationType, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the public key: %+v\n", err)) |
||||||
|
} |
||||||
|
|
||||||
|
switch key.(type) { |
||||||
|
case webauthncose.OKPPublicKeyData: |
||||||
|
k := key.(webauthncose.OKPPublicKeyData) |
||||||
|
err := verifyKeyAlgorithm(k.Algorithm, alg) |
||||||
|
if err != nil { |
||||||
|
return attestationType, nil, err |
||||||
|
} |
||||||
|
case webauthncose.EC2PublicKeyData: |
||||||
|
k := key.(webauthncose.EC2PublicKeyData) |
||||||
|
err := verifyKeyAlgorithm(k.Algorithm, alg) |
||||||
|
if err != nil { |
||||||
|
return attestationType, nil, err |
||||||
|
} |
||||||
|
case webauthncose.RSAPublicKeyData: |
||||||
|
k := key.(webauthncose.RSAPublicKeyData) |
||||||
|
err := verifyKeyAlgorithm(k.Algorithm, alg) |
||||||
|
if err != nil { |
||||||
|
return attestationType, nil, err |
||||||
|
} |
||||||
|
default: |
||||||
|
return attestationType, nil, ErrInvalidAttestation.WithDetails("Error verifying the public key data") |
||||||
|
} |
||||||
|
|
||||||
|
valid, err := webauthncose.VerifySignature(key, verificationData, signature) |
||||||
|
if !valid && err == nil { |
||||||
|
return attestationType, nil, ErrInvalidAttestation.WithDetails("Unabled to verify signature") |
||||||
|
} |
||||||
|
|
||||||
|
return attestationType, nil, err |
||||||
|
} |
||||||
|
|
||||||
|
func verifyKeyAlgorithm(keyAlgorithm, attestedAlgorithm int64) error { |
||||||
|
if keyAlgorithm != attestedAlgorithm { |
||||||
|
return ErrInvalidAttestation.WithDetails("Public key algorithm does not equal att statement algorithm") |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,143 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"crypto/sha256" |
||||||
|
"crypto/x509" |
||||||
|
"encoding/base64" |
||||||
|
"fmt" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/metadata" |
||||||
|
|
||||||
|
jwt "github.com/golang-jwt/jwt/v4" |
||||||
|
"github.com/mitchellh/mapstructure" |
||||||
|
) |
||||||
|
|
||||||
|
var safetyNetAttestationKey = "android-safetynet" |
||||||
|
|
||||||
|
func init() { |
||||||
|
RegisterAttestationFormat(safetyNetAttestationKey, verifySafetyNetFormat) |
||||||
|
} |
||||||
|
|
||||||
|
type SafetyNetResponse struct { |
||||||
|
Nonce string `json:"nonce"` |
||||||
|
TimestampMs int64 `json:"timestampMs"` |
||||||
|
ApkPackageName string `json:"apkPackageName"` |
||||||
|
ApkDigestSha256 string `json:"apkDigestSha256"` |
||||||
|
CtsProfileMatch bool `json:"ctsProfileMatch"` |
||||||
|
ApkCertificateDigestSha256 []interface{} `json:"apkCertificateDigestSha256"` |
||||||
|
BasicIntegrity bool `json:"basicIntegrity"` |
||||||
|
} |
||||||
|
|
||||||
|
// Thanks to @koesie10 and @herrjemand for outlining how to support this type really well
|
||||||
|
|
||||||
|
// §8.5. Android SafetyNet Attestation Statement Format https://w3c.github.io/webauthn/#android-safetynet-attestation
|
||||||
|
// When the authenticator in question is a platform-provided Authenticator on certain Android platforms, the attestation
|
||||||
|
// statement is based on the SafetyNet API. In this case the authenticator data is completely controlled by the caller of
|
||||||
|
// the SafetyNet API (typically an application running on the Android platform) and the attestation statement only provides
|
||||||
|
// some statements about the health of the platform and the identity of the calling application. This attestation does not
|
||||||
|
// provide information regarding provenance of the authenticator and its associated data. Therefore platform-provided
|
||||||
|
// authenticators SHOULD make use of the Android Key Attestation when available, even if the SafetyNet API is also present.
|
||||||
|
func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) { |
||||||
|
// The syntax of an Android Attestation statement is defined as follows:
|
||||||
|
// $$attStmtType //= (
|
||||||
|
// fmt: "android-safetynet",
|
||||||
|
// attStmt: safetynetStmtFormat
|
||||||
|
// )
|
||||||
|
|
||||||
|
// safetynetStmtFormat = {
|
||||||
|
// ver: text,
|
||||||
|
// response: bytes
|
||||||
|
// }
|
||||||
|
|
||||||
|
// §8.5.1 Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract
|
||||||
|
// the contained fields.
|
||||||
|
|
||||||
|
// We have done this
|
||||||
|
// §8.5.2 Verify that response is a valid SafetyNet response of version ver.
|
||||||
|
version, present := att.AttStatement["ver"].(string) |
||||||
|
if !present { |
||||||
|
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to find the version of SafetyNet") |
||||||
|
} |
||||||
|
|
||||||
|
if version == "" { |
||||||
|
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Not a proper version for SafetyNet") |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: provide user the ability to designate their supported versions
|
||||||
|
|
||||||
|
response, present := att.AttStatement["response"].([]byte) |
||||||
|
if !present { |
||||||
|
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to find the SafetyNet response") |
||||||
|
} |
||||||
|
|
||||||
|
token, err := jwt.Parse(string(response), func(token *jwt.Token) (interface{}, error) { |
||||||
|
chain := token.Header["x5c"].([]interface{}) |
||||||
|
o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string)))) |
||||||
|
n, err := base64.StdEncoding.Decode(o, []byte(chain[0].(string))) |
||||||
|
cert, err := x509.ParseCertificate(o[:n]) |
||||||
|
return cert.PublicKey, err |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)) |
||||||
|
} |
||||||
|
|
||||||
|
// marshall the JWT payload into the safetynet response json
|
||||||
|
var safetyNetResponse SafetyNetResponse |
||||||
|
err = mapstructure.Decode(token.Claims, &safetyNetResponse) |
||||||
|
if err != nil { |
||||||
|
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the SafetyNet response: %+v", err)) |
||||||
|
} |
||||||
|
|
||||||
|
// §8.5.3 Verify that the nonce in the response is identical to the Base64 encoding of the SHA-256 hash of the concatenation
|
||||||
|
// of authenticatorData and clientDataHash.
|
||||||
|
nonceBuffer := sha256.Sum256(append(att.RawAuthData, clientDataHash...)) |
||||||
|
nonceBytes, err := base64.StdEncoding.DecodeString(safetyNetResponse.Nonce) |
||||||
|
if !bytes.Equal(nonceBuffer[:], nonceBytes) || err != nil { |
||||||
|
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails("Invalid nonce for in SafetyNet response") |
||||||
|
} |
||||||
|
|
||||||
|
// §8.5.4 Let attestationCert be the attestation certificate (https://www.w3.org/TR/webauthn/#attestation-certificate)
|
||||||
|
certChain := token.Header["x5c"].([]interface{}) |
||||||
|
l := make([]byte, base64.StdEncoding.DecodedLen(len(certChain[0].(string)))) |
||||||
|
n, err := base64.StdEncoding.Decode(l, []byte(certChain[0].(string))) |
||||||
|
if err != nil { |
||||||
|
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)) |
||||||
|
} |
||||||
|
attestationCert, err := x509.ParseCertificate(l[:n]) |
||||||
|
if err != nil { |
||||||
|
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)) |
||||||
|
} |
||||||
|
|
||||||
|
// §8.5.5 Verify that attestationCert is issued to the hostname "attest.android.com"
|
||||||
|
err = attestationCert.VerifyHostname("attest.android.com") |
||||||
|
if err != nil { |
||||||
|
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)) |
||||||
|
} |
||||||
|
|
||||||
|
// §8.5.6 Verify that the ctsProfileMatch attribute in the payload of response is true.
|
||||||
|
if !safetyNetResponse.CtsProfileMatch { |
||||||
|
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails("ctsProfileMatch attribute of the JWT payload is false") |
||||||
|
} |
||||||
|
|
||||||
|
// Verify sanity of timestamp in the payload
|
||||||
|
now := time.Now() |
||||||
|
oneMinuteAgo := now.Add(-time.Minute) |
||||||
|
t := time.Unix(safetyNetResponse.TimestampMs/1000, 0) |
||||||
|
if t.After(now) { |
||||||
|
// zero tolerance for post-dated timestamps
|
||||||
|
return "Basic attestation with SafetyNet", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp after current time") |
||||||
|
} else if t.Before(oneMinuteAgo) { |
||||||
|
// allow old timestamp for testing purposes
|
||||||
|
// TODO: Make this user configurable
|
||||||
|
msg := "SafetyNet response with timestamp before one minute ago" |
||||||
|
if metadata.Conformance { |
||||||
|
return "Basic attestation with SafetyNet", nil, ErrInvalidAttestation.WithDetails(msg) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// §8.5.7 If successful, return implementation-specific values representing attestation type Basic and attestation
|
||||||
|
// trust path attestationCert.
|
||||||
|
return "Basic attestation with SafetyNet", nil, nil |
||||||
|
} |
@ -0,0 +1,349 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"crypto/x509" |
||||||
|
"crypto/x509/pkix" |
||||||
|
"encoding/asn1" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"math/big" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/protocol/webauthncose" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/protocol/googletpm" |
||||||
|
) |
||||||
|
|
||||||
|
var tpmAttestationKey = "tpm" |
||||||
|
|
||||||
|
func init() { |
||||||
|
RegisterAttestationFormat(tpmAttestationKey, verifyTPMFormat) |
||||||
|
googletpm.UseTPM20LengthPrefixSize() |
||||||
|
} |
||||||
|
|
||||||
|
func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) { |
||||||
|
// Given the verification procedure inputs attStmt, authenticatorData
|
||||||
|
// and clientDataHash, the verification procedure is as follows
|
||||||
|
|
||||||
|
// Verify that attStmt is valid CBOR conforming to the syntax defined
|
||||||
|
// above and perform CBOR decoding on it to extract the contained fields
|
||||||
|
|
||||||
|
ver, present := att.AttStatement["ver"].(string) |
||||||
|
if !present { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving ver value") |
||||||
|
} |
||||||
|
|
||||||
|
if ver != "2.0" { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("WebAuthn only supports TPM 2.0 currently") |
||||||
|
} |
||||||
|
|
||||||
|
alg, present := att.AttStatement["alg"].(int64) |
||||||
|
if !present { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving alg value") |
||||||
|
} |
||||||
|
|
||||||
|
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg) |
||||||
|
|
||||||
|
x5c, x509present := att.AttStatement["x5c"].([]interface{}) |
||||||
|
if !x509present { |
||||||
|
// Handle Basic Attestation steps for the x509 Certificate
|
||||||
|
return tpmAttestationKey, nil, ErrNotImplemented |
||||||
|
} |
||||||
|
|
||||||
|
_, ecdaaKeyPresent := att.AttStatement["ecdaaKeyId"].([]byte) |
||||||
|
if ecdaaKeyPresent { |
||||||
|
return tpmAttestationKey, nil, ErrNotImplemented |
||||||
|
} |
||||||
|
|
||||||
|
sigBytes, present := att.AttStatement["sig"].([]byte) |
||||||
|
if !present { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving sig value") |
||||||
|
} |
||||||
|
|
||||||
|
certInfoBytes, present := att.AttStatement["certInfo"].([]byte) |
||||||
|
if !present { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving certInfo value") |
||||||
|
} |
||||||
|
|
||||||
|
pubAreaBytes, present := att.AttStatement["pubArea"].([]byte) |
||||||
|
if !present { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving pubArea value") |
||||||
|
} |
||||||
|
|
||||||
|
// Verify that the public key specified by the parameters and unique fields of pubArea
|
||||||
|
// is identical to the credentialPublicKey in the attestedCredentialData in authenticatorData.
|
||||||
|
pubArea, err := googletpm.DecodePublic(pubAreaBytes) |
||||||
|
if err != nil { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to decode TPMT_PUBLIC in attestation statement") |
||||||
|
} |
||||||
|
|
||||||
|
key, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey) |
||||||
|
switch key.(type) { |
||||||
|
case webauthncose.EC2PublicKeyData: |
||||||
|
e := key.(webauthncose.EC2PublicKeyData) |
||||||
|
if pubArea.ECCParameters.CurveID != googletpm.EllipticCurve(e.Curve) || |
||||||
|
0 != pubArea.ECCParameters.Point.X.Cmp(new(big.Int).SetBytes(e.XCoord)) || |
||||||
|
0 != pubArea.ECCParameters.Point.Y.Cmp(new(big.Int).SetBytes(e.YCoord)) { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Mismatch between ECCParameters in pubArea and credentialPublicKey") |
||||||
|
} |
||||||
|
case webauthncose.RSAPublicKeyData: |
||||||
|
r := key.(webauthncose.RSAPublicKeyData) |
||||||
|
mod := new(big.Int).SetBytes(r.Modulus) |
||||||
|
exp := uint32(r.Exponent[0]) + uint32(r.Exponent[1])<<8 + uint32(r.Exponent[2])<<16 |
||||||
|
if 0 != pubArea.RSAParameters.Modulus.Cmp(mod) || |
||||||
|
pubArea.RSAParameters.Exponent != exp { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Mismatch between RSAParameters in pubArea and credentialPublicKey") |
||||||
|
} |
||||||
|
default: |
||||||
|
return "", nil, ErrUnsupportedKey |
||||||
|
} |
||||||
|
|
||||||
|
// Concatenate authenticatorData and clientDataHash to form attToBeSigned
|
||||||
|
attToBeSigned := append(att.RawAuthData, clientDataHash...) |
||||||
|
|
||||||
|
// Validate that certInfo is valid:
|
||||||
|
certInfo, err := googletpm.DecodeAttestationData(certInfoBytes) |
||||||
|
// 1/4 Verify that magic is set to TPM_GENERATED_VALUE.
|
||||||
|
if certInfo.Magic != 0xff544347 { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Magic is not set to TPM_GENERATED_VALUE") |
||||||
|
} |
||||||
|
// 2/4 Verify that type is set to TPM_ST_ATTEST_CERTIFY.
|
||||||
|
if certInfo.Type != googletpm.TagAttestCertify { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Type is not set to TPM_ST_ATTEST_CERTIFY") |
||||||
|
} |
||||||
|
// 3/4 Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg".
|
||||||
|
f := webauthncose.HasherFromCOSEAlg(coseAlg) |
||||||
|
h := f() |
||||||
|
h.Write(attToBeSigned) |
||||||
|
if 0 != bytes.Compare(certInfo.ExtraData, h.Sum(nil)) { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("ExtraData is not set to hash of attToBeSigned") |
||||||
|
} |
||||||
|
// 4/4 Verify that attested contains a TPMS_CERTIFY_INFO structure as specified in
|
||||||
|
// [TPMv2-Part2] section 10.12.3, whose name field contains a valid Name for pubArea,
|
||||||
|
// as computed using the algorithm in the nameAlg field of pubArea
|
||||||
|
// using the procedure specified in [TPMv2-Part1] section 16.
|
||||||
|
f, err = certInfo.AttestedCertifyInfo.Name.Digest.Alg.HashConstructor() |
||||||
|
h = f() |
||||||
|
h.Write(pubAreaBytes) |
||||||
|
if 0 != bytes.Compare(h.Sum(nil), certInfo.AttestedCertifyInfo.Name.Digest.Value) { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Hash value mismatch attested and pubArea") |
||||||
|
} |
||||||
|
|
||||||
|
// Note that the remaining fields in the "Standard Attestation Structure"
|
||||||
|
// [TPMv2-Part1] section 31.2, i.e., qualifiedSigner, clockInfo and firmwareVersion
|
||||||
|
// are ignored. These fields MAY be used as an input to risk engines.
|
||||||
|
|
||||||
|
// If x5c is present, this indicates that the attestation type is not ECDAA.
|
||||||
|
if x509present { |
||||||
|
// In this case:
|
||||||
|
// Verify the sig is a valid signature over certInfo using the attestation public key in aikCert with the algorithm specified in alg.
|
||||||
|
aikCertBytes, valid := x5c[0].([]byte) |
||||||
|
if !valid { |
||||||
|
return tpmAttestationKey, nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain") |
||||||
|
} |
||||||
|
|
||||||
|
aikCert, err := x509.ParseCertificate(aikCertBytes) |
||||||
|
if err != nil { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error parsing certificate from ASN.1") |
||||||
|
} |
||||||
|
|
||||||
|
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg) |
||||||
|
|
||||||
|
err = aikCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), certInfoBytes, sigBytes) |
||||||
|
if err != nil { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err)) |
||||||
|
} |
||||||
|
// Verify that aikCert meets the requirements in §8.3.1 TPM Attestation Statement Certificate Requirements
|
||||||
|
|
||||||
|
// 1/6 Version MUST be set to 3.
|
||||||
|
if aikCert.Version != 3 { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate version must be 3") |
||||||
|
} |
||||||
|
// 2/6 Subject field MUST be set to empty.
|
||||||
|
if aikCert.Subject.String() != "" { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate subject must be empty") |
||||||
|
} |
||||||
|
|
||||||
|
// 3/6 The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9{}
|
||||||
|
var manufacturer, model, version string |
||||||
|
for _, ext := range aikCert.Extensions { |
||||||
|
if ext.Id.Equal([]int{2, 5, 29, 17}) { |
||||||
|
manufacturer, model, version, err = parseSANExtension(ext.Value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if manufacturer == "" || model == "" || version == "" { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Invalid SAN data in AIK certificate") |
||||||
|
} |
||||||
|
|
||||||
|
if false == isValidTPMManufacturer(manufacturer) { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Invalid TPM manufacturer") |
||||||
|
} |
||||||
|
|
||||||
|
// 4/6 The Extended Key Usage extension MUST contain the "joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)" OID.
|
||||||
|
var ekuValid = false |
||||||
|
var eku []asn1.ObjectIdentifier |
||||||
|
for _, ext := range aikCert.Extensions { |
||||||
|
if ext.Id.Equal([]int{2, 5, 29, 37}) { |
||||||
|
rest, err := asn1.Unmarshal(ext.Value, &eku) |
||||||
|
if len(rest) != 0 || err != nil || !eku[0].Equal(tcgKpAIKCertificate) { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate EKU missing 2.23.133.8.3") |
||||||
|
} |
||||||
|
ekuValid = true |
||||||
|
} |
||||||
|
} |
||||||
|
if false == ekuValid { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate missing EKU") |
||||||
|
} |
||||||
|
|
||||||
|
// 5/6 The Basic Constraints extension MUST have the CA component set to false.
|
||||||
|
type basicConstraints struct { |
||||||
|
IsCA bool `asn1:"optional"` |
||||||
|
MaxPathLen int `asn1:"optional,default:-1"` |
||||||
|
} |
||||||
|
var constraints basicConstraints |
||||||
|
for _, ext := range aikCert.Extensions { |
||||||
|
if ext.Id.Equal([]int{2, 5, 29, 19}) { |
||||||
|
if rest, err := asn1.Unmarshal(ext.Value, &constraints); err != nil { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints malformed") |
||||||
|
} else if len(rest) != 0 { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints contains extra data") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if constraints.IsCA != false { |
||||||
|
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints missing or CA is true") |
||||||
|
} |
||||||
|
// 6/6 An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point
|
||||||
|
// extension [RFC5280] are both OPTIONAL as the status of many attestation certificates is available
|
||||||
|
// through metadata services. See, for example, the FIDO Metadata Service.
|
||||||
|
} |
||||||
|
|
||||||
|
return tpmAttestationKey, x5c, err |
||||||
|
} |
||||||
|
func forEachSAN(extension []byte, callback func(tag int, data []byte) error) error { |
||||||
|
// RFC 5280, 4.2.1.6
|
||||||
|
|
||||||
|
// SubjectAltName ::= GeneralNames
|
||||||
|
//
|
||||||
|
// GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
|
||||||
|
//
|
||||||
|
// GeneralName ::= CHOICE {
|
||||||
|
// otherName [0] OtherName,
|
||||||
|
// rfc822Name [1] IA5String,
|
||||||
|
// dNSName [2] IA5String,
|
||||||
|
// x400Address [3] ORAddress,
|
||||||
|
// directoryName [4] Name,
|
||||||
|
// ediPartyName [5] EDIPartyName,
|
||||||
|
// uniformResourceIdentifier [6] IA5String,
|
||||||
|
// iPAddress [7] OCTET STRING,
|
||||||
|
// registeredID [8] OBJECT IDENTIFIER }
|
||||||
|
var seq asn1.RawValue |
||||||
|
rest, err := asn1.Unmarshal(extension, &seq) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} else if len(rest) != 0 { |
||||||
|
return errors.New("x509: trailing data after X.509 extension") |
||||||
|
} |
||||||
|
if !seq.IsCompound || seq.Tag != 16 || seq.Class != 0 { |
||||||
|
return asn1.StructuralError{Msg: "bad SAN sequence"} |
||||||
|
} |
||||||
|
|
||||||
|
rest = seq.Bytes |
||||||
|
for len(rest) > 0 { |
||||||
|
var v asn1.RawValue |
||||||
|
rest, err = asn1.Unmarshal(rest, &v) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if err := callback(v.Tag, v.Bytes); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
const ( |
||||||
|
nameTypeDN = 4 |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
tcgKpAIKCertificate = asn1.ObjectIdentifier{2, 23, 133, 8, 3} |
||||||
|
tcgAtTpmManufacturer = asn1.ObjectIdentifier{2, 23, 133, 2, 1} |
||||||
|
tcgAtTpmModel = asn1.ObjectIdentifier{2, 23, 133, 2, 2} |
||||||
|
tcgAtTpmVersion = asn1.ObjectIdentifier{2, 23, 133, 2, 3} |
||||||
|
) |
||||||
|
|
||||||
|
func parseSANExtension(value []byte) (manufacturer string, model string, version string, err error) { |
||||||
|
err = forEachSAN(value, func(tag int, data []byte) error { |
||||||
|
switch tag { |
||||||
|
case nameTypeDN: |
||||||
|
tpmDeviceAttributes := pkix.RDNSequence{} |
||||||
|
_, err := asn1.Unmarshal(data, &tpmDeviceAttributes) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
for _, rdn := range tpmDeviceAttributes { |
||||||
|
if len(rdn) == 0 { |
||||||
|
continue |
||||||
|
} |
||||||
|
for _, atv := range rdn { |
||||||
|
value, ok := atv.Value.(string) |
||||||
|
if !ok { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
if atv.Type.Equal(tcgAtTpmManufacturer) { |
||||||
|
manufacturer = strings.TrimPrefix(value, "id:") |
||||||
|
} |
||||||
|
if atv.Type.Equal(tcgAtTpmModel) { |
||||||
|
model = value |
||||||
|
} |
||||||
|
if atv.Type.Equal(tcgAtTpmVersion) { |
||||||
|
version = strings.TrimPrefix(value, "id:") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
}) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
var tpmManufacturers = []struct { |
||||||
|
id string |
||||||
|
name string |
||||||
|
code string |
||||||
|
}{ |
||||||
|
{"414D4400", "AMD", "AMD"}, |
||||||
|
{"41544D4C", "Atmel", "ATML"}, |
||||||
|
{"4252434D", "Broadcom", "BRCM"}, |
||||||
|
{"49424d00", "IBM", "IBM"}, |
||||||
|
{"49465800", "Infineon", "IFX"}, |
||||||
|
{"494E5443", "Intel", "INTC"}, |
||||||
|
{"4C454E00", "Lenovo", "LEN"}, |
||||||
|
{"4E534D20", "National Semiconductor", "NSM"}, |
||||||
|
{"4E545A00", "Nationz", "NTZ"}, |
||||||
|
{"4E544300", "Nuvoton Technology", "NTC"}, |
||||||
|
{"51434F4D", "Qualcomm", "QCOM"}, |
||||||
|
{"534D5343", "SMSC", "SMSC"}, |
||||||
|
{"53544D20", "ST Microelectronics", "STM"}, |
||||||
|
{"534D534E", "Samsung", "SMSN"}, |
||||||
|
{"534E5300", "Sinosun", "SNS"}, |
||||||
|
{"54584E00", "Texas Instruments", "TXN"}, |
||||||
|
{"57454300", "Winbond", "WEC"}, |
||||||
|
{"524F4343", "Fuzhouk Rockchip", "ROCC"}, |
||||||
|
{"FFFFF1D0", "FIDO Alliance Conformance Testing", "FIDO"}, |
||||||
|
} |
||||||
|
|
||||||
|
func isValidTPMManufacturer(id string) bool { |
||||||
|
for _, m := range tpmManufacturers { |
||||||
|
if m.id == id { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
@ -0,0 +1,135 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"crypto/ecdsa" |
||||||
|
"crypto/elliptic" |
||||||
|
"crypto/x509" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/protocol/webauthncose" |
||||||
|
"github.com/fxamacker/cbor/v2" |
||||||
|
) |
||||||
|
|
||||||
|
var u2fAttestationKey = "fido-u2f" |
||||||
|
|
||||||
|
func init() { |
||||||
|
RegisterAttestationFormat(u2fAttestationKey, verifyU2FFormat) |
||||||
|
} |
||||||
|
|
||||||
|
// verifyU2FFormat - Follows verification steps set out by https://www.w3.org/TR/webauthn/#fido-u2f-attestation
|
||||||
|
func verifyU2FFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) { |
||||||
|
|
||||||
|
if !bytes.Equal(att.AuthData.AttData.AAGUID, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) { |
||||||
|
return u2fAttestationKey, nil, ErrUnsupportedAlgorithm.WithDetails("U2F attestation format AAGUID not set to 0x00") |
||||||
|
} |
||||||
|
// Signing procedure step - If the credential public key of the given credential is not of
|
||||||
|
// algorithm -7 ("ES256"), stop and return an error.
|
||||||
|
key := webauthncose.EC2PublicKeyData{} |
||||||
|
cbor.Unmarshal(att.AuthData.AttData.CredentialPublicKey, &key) |
||||||
|
|
||||||
|
if webauthncose.COSEAlgorithmIdentifier(key.PublicKeyData.Algorithm) != webauthncose.AlgES256 { |
||||||
|
return u2fAttestationKey, nil, ErrUnsupportedAlgorithm.WithDetails("Non-ES256 Public Key algorithm used") |
||||||
|
} |
||||||
|
|
||||||
|
// U2F Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined above
|
||||||
|
// and perform CBOR decoding on it to extract the contained fields.
|
||||||
|
|
||||||
|
// The Format/syntax is
|
||||||
|
// u2fStmtFormat = {
|
||||||
|
// x5c: [ attestnCert: bytes ],
|
||||||
|
// sig: bytes
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Check for "x5c" which is a single element array containing the attestation certificate in X.509 format.
|
||||||
|
x5c, present := att.AttStatement["x5c"].([]interface{}) |
||||||
|
if !present { |
||||||
|
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Missing properly formatted x5c data") |
||||||
|
} |
||||||
|
|
||||||
|
// Check for "sig" which is The attestation signature. The signature was calculated over the (raw) U2F
|
||||||
|
// registration response message https://www.w3.org/TR/webauthn/#biblio-fido-u2f-message-formats]
|
||||||
|
// received by the client from the authenticator.
|
||||||
|
signature, present := att.AttStatement["sig"].([]byte) |
||||||
|
if !present { |
||||||
|
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Missing sig data") |
||||||
|
} |
||||||
|
|
||||||
|
// U2F Step 2. (1) Check that x5c has exactly one element and let attCert be that element. (2) Let certificate public
|
||||||
|
// key be the public key conveyed by attCert. (3) If certificate public key is not an Elliptic Curve (EC) public
|
||||||
|
// key over the P-256 curve, terminate this algorithm and return an appropriate error.
|
||||||
|
|
||||||
|
// Step 2.1
|
||||||
|
if len(x5c) > 1 { |
||||||
|
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Received more than one element in x5c values") |
||||||
|
} |
||||||
|
|
||||||
|
// Note: Packed Attestation, FIDO U2F Attestation, and Assertion Signatures support ASN.1,but it is recommended
|
||||||
|
// that any new attestation formats defined not use ASN.1 encodings, but instead represent signatures as equivalent
|
||||||
|
// fixed-length byte arrays without internal structure, using the same representations as used by COSE signatures
|
||||||
|
// as defined in RFC8152 (https://www.w3.org/TR/webauthn/#biblio-rfc8152)
|
||||||
|
// and RFC8230 (https://www.w3.org/TR/webauthn/#biblio-rfc8230).
|
||||||
|
|
||||||
|
// Step 2.2
|
||||||
|
asn1Bytes, decoded := x5c[0].([]byte) |
||||||
|
if !decoded { |
||||||
|
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Error decoding ASN.1 data from x5c") |
||||||
|
} |
||||||
|
|
||||||
|
attCert, err := x509.ParseCertificate(asn1Bytes) |
||||||
|
if err != nil { |
||||||
|
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Error parsing certificate from ASN.1 data into certificate") |
||||||
|
} |
||||||
|
|
||||||
|
// Step 2.3
|
||||||
|
if attCert.PublicKeyAlgorithm != x509.ECDSA && attCert.PublicKey.(*ecdsa.PublicKey).Curve != elliptic.P256() { |
||||||
|
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate is in invalid format") |
||||||
|
} |
||||||
|
|
||||||
|
// Step 3. Extract the claimed rpIdHash from authenticatorData, and the claimed credentialId and credentialPublicKey
|
||||||
|
// from authenticatorData.attestedCredentialData.
|
||||||
|
|
||||||
|
rpIdHash := att.AuthData.RPIDHash |
||||||
|
|
||||||
|
credentialID := att.AuthData.AttData.CredentialID |
||||||
|
|
||||||
|
// credentialPublicKey handled earlier
|
||||||
|
|
||||||
|
// Step 4. Convert the COSE_KEY formatted credentialPublicKey (see Section 7 of RFC8152 [https://www.w3.org/TR/webauthn/#biblio-rfc8152])
|
||||||
|
// to Raw ANSI X9.62 public key format (see ALG_KEY_ECC_X962_RAW in Section 3.6.2 Public Key
|
||||||
|
// Representation Formats of FIDO-Registry [https://www.w3.org/TR/webauthn/#biblio-fido-registry]).
|
||||||
|
|
||||||
|
// Let x be the value corresponding to the "-2" key (representing x coordinate) in credentialPublicKey, and confirm
|
||||||
|
// its size to be of 32 bytes. If size differs or "-2" key is not found, terminate this algorithm and
|
||||||
|
// return an appropriate error.
|
||||||
|
|
||||||
|
// Let y be the value corresponding to the "-3" key (representing y coordinate) in credentialPublicKey, and confirm
|
||||||
|
// its size to be of 32 bytes. If size differs or "-3" key is not found, terminate this algorithm and
|
||||||
|
// return an appropriate error.
|
||||||
|
|
||||||
|
if len(key.XCoord) > 32 || len(key.YCoord) > 32 { |
||||||
|
return u2fAttestationKey, nil, ErrAttestation.WithDetails("X or Y Coordinate for key is invalid length") |
||||||
|
} |
||||||
|
|
||||||
|
// Let publicKeyU2F be the concatenation 0x04 || x || y.
|
||||||
|
publicKeyU2F := bytes.NewBuffer([]byte{0x04}) |
||||||
|
publicKeyU2F.Write(key.XCoord) |
||||||
|
publicKeyU2F.Write(key.YCoord) |
||||||
|
|
||||||
|
// Step 5. Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F)
|
||||||
|
// (see §4.3 of FIDO-U2F-Message-Formats [https://www.w3.org/TR/webauthn/#biblio-fido-u2f-message-formats]).
|
||||||
|
|
||||||
|
verificationData := bytes.NewBuffer([]byte{0x00}) |
||||||
|
verificationData.Write(rpIdHash) |
||||||
|
verificationData.Write(clientDataHash) |
||||||
|
verificationData.Write(credentialID) |
||||||
|
verificationData.Write(publicKeyU2F.Bytes()) |
||||||
|
|
||||||
|
// Step 6. Verify the sig using verificationData and certificate public key per SEC1[https://www.w3.org/TR/webauthn/#biblio-sec1].
|
||||||
|
sigErr := attCert.CheckSignature(x509.ECDSAWithSHA256, verificationData.Bytes(), signature) |
||||||
|
if sigErr != nil { |
||||||
|
return u2fAttestationKey, nil, sigErr |
||||||
|
} |
||||||
|
|
||||||
|
// Step 7. If successful, return attestation type Basic with the attestation trust path set to x5c.
|
||||||
|
return "Fido U2F Basic", x5c, sigErr |
||||||
|
} |
@ -0,0 +1,256 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/binary" |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"github.com/fxamacker/cbor/v2" |
||||||
|
) |
||||||
|
|
||||||
|
var minAuthDataLength = 37 |
||||||
|
|
||||||
|
// Authenticators respond to Relying Party requests by returning an object derived from the
|
||||||
|
// AuthenticatorResponse interface. See §5.2. Authenticator Responses
|
||||||
|
// https://www.w3.org/TR/webauthn/#iface-authenticatorresponse
|
||||||
|
type AuthenticatorResponse struct { |
||||||
|
// From the spec https://www.w3.org/TR/webauthn/#dom-authenticatorresponse-clientdatajson
|
||||||
|
// This attribute contains a JSON serialization of the client data passed to the authenticator
|
||||||
|
// by the client in its call to either create() or get().
|
||||||
|
ClientDataJSON URLEncodedBase64 `json:"clientDataJSON"` |
||||||
|
} |
||||||
|
|
||||||
|
// AuthenticatorData From §6.1 of the spec.
|
||||||
|
// The authenticator data structure encodes contextual bindings made by the authenticator. These bindings
|
||||||
|
// are controlled by the authenticator itself, and derive their trust from the WebAuthn Relying Party's
|
||||||
|
// assessment of the security properties of the authenticator. In one extreme case, the authenticator
|
||||||
|
// may be embedded in the client, and its bindings may be no more trustworthy than the client data.
|
||||||
|
// At the other extreme, the authenticator may be a discrete entity with high-security hardware and
|
||||||
|
// software, connected to the client over a secure channel. In both cases, the Relying Party receives
|
||||||
|
// the authenticator data in the same format, and uses its knowledge of the authenticator to make
|
||||||
|
// trust decisions.
|
||||||
|
//
|
||||||
|
// The authenticator data, at least during attestation, contains the Public Key that the RP stores
|
||||||
|
// and will associate with the user attempting to register.
|
||||||
|
type AuthenticatorData struct { |
||||||
|
RPIDHash []byte `json:"rpid"` |
||||||
|
Flags AuthenticatorFlags `json:"flags"` |
||||||
|
Counter uint32 `json:"sign_count"` |
||||||
|
AttData AttestedCredentialData `json:"att_data"` |
||||||
|
ExtData []byte `json:"ext_data"` |
||||||
|
} |
||||||
|
|
||||||
|
type AttestedCredentialData struct { |
||||||
|
AAGUID []byte `json:"aaguid"` |
||||||
|
CredentialID []byte `json:"credential_id"` |
||||||
|
// The raw credential public key bytes received from the attestation data
|
||||||
|
CredentialPublicKey []byte `json:"public_key"` |
||||||
|
} |
||||||
|
|
||||||
|
// AuthenticatorAttachment https://www.w3.org/TR/webauthn/#platform-attachment
|
||||||
|
type AuthenticatorAttachment string |
||||||
|
|
||||||
|
const ( |
||||||
|
// Platform - A platform authenticator is attached using a client device-specific transport, called
|
||||||
|
// platform attachment, and is usually not removable from the client device. A public key credential
|
||||||
|
// bound to a platform authenticator is called a platform credential.
|
||||||
|
Platform AuthenticatorAttachment = "platform" |
||||||
|
// CrossPlatform A roaming authenticator is attached using cross-platform transports, called
|
||||||
|
// cross-platform attachment. Authenticators of this class are removable from, and can "roam"
|
||||||
|
// among, client devices. A public key credential bound to a roaming authenticator is called a
|
||||||
|
// roaming credential.
|
||||||
|
CrossPlatform AuthenticatorAttachment = "cross-platform" |
||||||
|
) |
||||||
|
|
||||||
|
// Authenticators may implement various transports for communicating with clients. This enumeration defines
|
||||||
|
// hints as to how clients might communicate with a particular authenticator in order to obtain an assertion
|
||||||
|
// for a specific credential. Note that these hints represent the WebAuthn Relying Party's best belief as to
|
||||||
|
// how an authenticator may be reached. A Relying Party may obtain a list of transports hints from some
|
||||||
|
// attestation statement formats or via some out-of-band mechanism; it is outside the scope of this
|
||||||
|
// specification to define that mechanism.
|
||||||
|
// See §5.10.4. Authenticator Transport https://www.w3.org/TR/webauthn/#transport
|
||||||
|
type AuthenticatorTransport string |
||||||
|
|
||||||
|
const ( |
||||||
|
// USB The authenticator should transport information over USB
|
||||||
|
USB AuthenticatorTransport = "usb" |
||||||
|
// NFC The authenticator should transport information over Near Field Communication Protocol
|
||||||
|
NFC AuthenticatorTransport = "nfc" |
||||||
|
// BLE The authenticator should transport information over Bluetooth
|
||||||
|
BLE AuthenticatorTransport = "ble" |
||||||
|
// Internal the client should use an internal source like a TPM or SE
|
||||||
|
Internal AuthenticatorTransport = "internal" |
||||||
|
) |
||||||
|
|
||||||
|
// A WebAuthn Relying Party may require user verification for some of its operations but not for others,
|
||||||
|
// and may use this type to express its needs.
|
||||||
|
// See §5.10.6. User Verification Requirement Enumeration https://www.w3.org/TR/webauthn/#userVerificationRequirement
|
||||||
|
type UserVerificationRequirement string |
||||||
|
|
||||||
|
const ( |
||||||
|
// VerificationRequired User verification is required to create/release a credential
|
||||||
|
VerificationRequired UserVerificationRequirement = "required" |
||||||
|
// VerificationPreferred User verification is preferred to create/release a credential
|
||||||
|
VerificationPreferred UserVerificationRequirement = "preferred" // This is the default
|
||||||
|
// VerificationDiscouraged The authenticator should not verify the user for the credential
|
||||||
|
VerificationDiscouraged UserVerificationRequirement = "discouraged" |
||||||
|
) |
||||||
|
|
||||||
|
// AuthenticatorFlags A byte of information returned during during ceremonies in the
|
||||||
|
// authenticatorData that contains bits that give us information about the
|
||||||
|
// whether the user was present and/or verified during authentication, and whether
|
||||||
|
// there is attestation or extension data present. Bit 0 is the least significant bit.
|
||||||
|
type AuthenticatorFlags byte |
||||||
|
|
||||||
|
// The bits that do not have flags are reserved for future use.
|
||||||
|
const ( |
||||||
|
// FlagUserPresent Bit 00000001 in the byte sequence. Tells us if user is present
|
||||||
|
FlagUserPresent AuthenticatorFlags = 1 << iota // Referred to as UP
|
||||||
|
_ // Reserved
|
||||||
|
// FlagUserVerified Bit 00000100 in the byte sequence. Tells us if user is verified
|
||||||
|
// by the authenticator using a biometric or PIN
|
||||||
|
FlagUserVerified // Referred to as UV
|
||||||
|
_ // Reserved
|
||||||
|
_ // Reserved
|
||||||
|
_ // Reserved
|
||||||
|
// FlagAttestedCredentialData Bit 01000000 in the byte sequence. Indicates whether
|
||||||
|
// the authenticator added attested credential data.
|
||||||
|
FlagAttestedCredentialData // Referred to as AT
|
||||||
|
// FlagHasExtension Bit 10000000 in the byte sequence. Indicates if the authenticator data has extensions.
|
||||||
|
FlagHasExtensions // Referred to as ED
|
||||||
|
) |
||||||
|
|
||||||
|
// UserPresent returns if the UP flag was set
|
||||||
|
func (flag AuthenticatorFlags) UserPresent() bool { |
||||||
|
return (flag & FlagUserPresent) == FlagUserPresent |
||||||
|
} |
||||||
|
|
||||||
|
// UserVerified returns if the UV flag was set
|
||||||
|
func (flag AuthenticatorFlags) UserVerified() bool { |
||||||
|
return (flag & FlagUserVerified) == FlagUserVerified |
||||||
|
} |
||||||
|
|
||||||
|
// HasAttestedCredentialData returns if the AT flag was set
|
||||||
|
func (flag AuthenticatorFlags) HasAttestedCredentialData() bool { |
||||||
|
return (flag & FlagAttestedCredentialData) == FlagAttestedCredentialData |
||||||
|
} |
||||||
|
|
||||||
|
// HasExtensions returns if the ED flag was set
|
||||||
|
func (flag AuthenticatorFlags) HasExtensions() bool { |
||||||
|
return (flag & FlagHasExtensions) == FlagHasExtensions |
||||||
|
} |
||||||
|
|
||||||
|
// Unmarshal will take the raw Authenticator Data and marshalls it into AuthenticatorData for further validation.
|
||||||
|
// The authenticator data has a compact but extensible encoding. This is desired since authenticators can be
|
||||||
|
// devices with limited capabilities and low power requirements, with much simpler software stacks than the client platform.
|
||||||
|
// The authenticator data structure is a byte array of 37 bytes or more, and is laid out in this table:
|
||||||
|
// https://www.w3.org/TR/webauthn/#table-authData
|
||||||
|
func (a *AuthenticatorData) Unmarshal(rawAuthData []byte) error { |
||||||
|
if minAuthDataLength > len(rawAuthData) { |
||||||
|
err := ErrBadRequest.WithDetails("Authenticator data length too short") |
||||||
|
info := fmt.Sprintf("Expected data greater than %d bytes. Got %d bytes\n", minAuthDataLength, len(rawAuthData)) |
||||||
|
return err.WithInfo(info) |
||||||
|
} |
||||||
|
|
||||||
|
a.RPIDHash = rawAuthData[:32] |
||||||
|
a.Flags = AuthenticatorFlags(rawAuthData[32]) |
||||||
|
a.Counter = binary.BigEndian.Uint32(rawAuthData[33:37]) |
||||||
|
|
||||||
|
remaining := len(rawAuthData) - minAuthDataLength |
||||||
|
|
||||||
|
if a.Flags.HasAttestedCredentialData() { |
||||||
|
if len(rawAuthData) > minAuthDataLength { |
||||||
|
a.unmarshalAttestedData(rawAuthData) |
||||||
|
attDataLen := len(a.AttData.AAGUID) + 2 + len(a.AttData.CredentialID) + len(a.AttData.CredentialPublicKey) |
||||||
|
remaining = remaining - attDataLen |
||||||
|
} else { |
||||||
|
return ErrBadRequest.WithDetails("Attested credential flag set but data is missing") |
||||||
|
} |
||||||
|
} else { |
||||||
|
if !a.Flags.HasExtensions() && len(rawAuthData) != 37 { |
||||||
|
return ErrBadRequest.WithDetails("Attested credential flag not set") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if a.Flags.HasExtensions() { |
||||||
|
if remaining != 0 { |
||||||
|
a.ExtData = rawAuthData[len(rawAuthData)-remaining:] |
||||||
|
remaining -= len(a.ExtData) |
||||||
|
} else { |
||||||
|
return ErrBadRequest.WithDetails("Extensions flag set but extensions data is missing") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if remaining != 0 { |
||||||
|
return ErrBadRequest.WithDetails("Leftover bytes decoding AuthenticatorData") |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// If Attestation Data is present, unmarshall that into the appropriate public key structure
|
||||||
|
func (a *AuthenticatorData) unmarshalAttestedData(rawAuthData []byte) { |
||||||
|
a.AttData.AAGUID = rawAuthData[37:53] |
||||||
|
idLength := binary.BigEndian.Uint16(rawAuthData[53:55]) |
||||||
|
a.AttData.CredentialID = rawAuthData[55 : 55+idLength] |
||||||
|
a.AttData.CredentialPublicKey = unmarshalCredentialPublicKey(rawAuthData[55+idLength:]) |
||||||
|
} |
||||||
|
|
||||||
|
// Unmarshall the credential's Public Key into CBOR encoding
|
||||||
|
func unmarshalCredentialPublicKey(keyBytes []byte) []byte { |
||||||
|
var m interface{} |
||||||
|
cbor.Unmarshal(keyBytes, &m) |
||||||
|
rawBytes, _ := cbor.Marshal(m) |
||||||
|
return rawBytes |
||||||
|
} |
||||||
|
|
||||||
|
// ResidentKeyRequired - Require that the key be private key resident to the client device
|
||||||
|
func ResidentKeyRequired() *bool { |
||||||
|
required := true |
||||||
|
return &required |
||||||
|
} |
||||||
|
|
||||||
|
// ResidentKeyUnrequired - Do not require that the private key be resident to the client device.
|
||||||
|
func ResidentKeyUnrequired() *bool { |
||||||
|
required := false |
||||||
|
return &required |
||||||
|
} |
||||||
|
|
||||||
|
// Verify on AuthenticatorData handles Steps 9 through 12 for Registration
|
||||||
|
// and Steps 11 through 14 for Assertion.
|
||||||
|
func (a *AuthenticatorData) Verify(rpIdHash, appIDHash []byte, userVerificationRequired bool) error { |
||||||
|
|
||||||
|
// Registration Step 9 & Assertion Step 11
|
||||||
|
// Verify that the RP ID hash in authData is indeed the SHA-256
|
||||||
|
// hash of the RP ID expected by the RP.
|
||||||
|
if !bytes.Equal(a.RPIDHash[:], rpIdHash) && !bytes.Equal(a.RPIDHash[:], appIDHash) { |
||||||
|
return ErrVerification.WithInfo(fmt.Sprintf("RP Hash mismatch. Expected %s and Received %s\n", a.RPIDHash, rpIdHash)) |
||||||
|
} |
||||||
|
|
||||||
|
// Registration Step 10 & Assertion Step 12
|
||||||
|
// Verify that the User Present bit of the flags in authData is set.
|
||||||
|
if !a.Flags.UserPresent() { |
||||||
|
return ErrVerification.WithInfo(fmt.Sprintln("User presence flag not set by authenticator")) |
||||||
|
} |
||||||
|
|
||||||
|
// Registration Step 11 & Assertion Step 13
|
||||||
|
// If user verification is required for this assertion, verify that
|
||||||
|
// the User Verified bit of the flags in authData is set.
|
||||||
|
if userVerificationRequired && !a.Flags.UserVerified() { |
||||||
|
return ErrVerification.WithInfo(fmt.Sprintln("User verification required but flag not set by authenticator")) |
||||||
|
} |
||||||
|
|
||||||
|
// Registration Step 12 & Assertion Step 14
|
||||||
|
// Verify that the values of the client extension outputs in clientExtensionResults
|
||||||
|
// and the authenticator extension outputs in the extensions in authData are as
|
||||||
|
// expected, considering the client extension input values that were given as the
|
||||||
|
// extensions option in the create() call. In particular, any extension identifier
|
||||||
|
// values in the clientExtensionResults and the extensions in authData MUST be also be
|
||||||
|
// present as extension identifier values in the extensions member of options, i.e., no
|
||||||
|
// extensions are present that were not requested. In the general case, the meaning
|
||||||
|
// of "are as expected" is specific to the Relying Party and which extensions are in use.
|
||||||
|
|
||||||
|
// This is not yet fully implemented by the spec or by browsers
|
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/base64" |
||||||
|
"reflect" |
||||||
|
) |
||||||
|
|
||||||
|
// URLEncodedBase64 represents a byte slice holding URL-encoded base64 data.
|
||||||
|
// When fields of this type are unmarshaled from JSON, the data is base64
|
||||||
|
// decoded into a byte slice.
|
||||||
|
type URLEncodedBase64 []byte |
||||||
|
|
||||||
|
// UnmarshalJSON base64 decodes a URL-encoded value, storing the result in the
|
||||||
|
// provided byte slice.
|
||||||
|
func (dest *URLEncodedBase64) UnmarshalJSON(data []byte) error { |
||||||
|
if bytes.Equal(data, []byte("null")) { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Trim the leading spaces
|
||||||
|
data = bytes.Trim(data, "\"") |
||||||
|
out := make([]byte, base64.RawURLEncoding.DecodedLen(len(data))) |
||||||
|
n, err := base64.RawURLEncoding.Decode(out, data) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
v := reflect.ValueOf(dest).Elem() |
||||||
|
v.SetBytes(out[:n]) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// MarshalJSON base64 encodes a non URL-encoded value, storing the result in the
|
||||||
|
// provided byte slice.
|
||||||
|
func (data URLEncodedBase64) MarshalJSON() ([]byte, error) { |
||||||
|
if data == nil { |
||||||
|
return []byte("null"), nil |
||||||
|
} |
||||||
|
return []byte(`"` + base64.RawURLEncoding.EncodeToString(data) + `"`), nil |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/rand" |
||||||
|
"encoding/base64" |
||||||
|
) |
||||||
|
|
||||||
|
// ChallengeLength - Length of bytes to generate for a challenge
|
||||||
|
const ChallengeLength = 32 |
||||||
|
|
||||||
|
// Challenge that should be signed and returned by the authenticator
|
||||||
|
type Challenge URLEncodedBase64 |
||||||
|
|
||||||
|
// Create a new challenge to be sent to the authenticator. The spec recommends using
|
||||||
|
// at least 16 bytes with 100 bits of entropy. We use 32 bytes.
|
||||||
|
func CreateChallenge() (Challenge, error) { |
||||||
|
challenge := make([]byte, ChallengeLength) |
||||||
|
_, err := rand.Read(challenge) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return challenge, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (c Challenge) String() string { |
||||||
|
return base64.RawURLEncoding.EncodeToString(c) |
||||||
|
} |
@ -0,0 +1,112 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net/url" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// CollectedClientData represents the contextual bindings of both the WebAuthn Relying Party
|
||||||
|
// and the client. It is a key-value mapping whose keys are strings. Values can be any type
|
||||||
|
// that has a valid encoding in JSON. Its structure is defined by the following Web IDL.
|
||||||
|
// https://www.w3.org/TR/webauthn/#sec-client-data
|
||||||
|
type CollectedClientData struct { |
||||||
|
// Type the string "webauthn.create" when creating new credentials,
|
||||||
|
// and "webauthn.get" when getting an assertion from an existing credential. The
|
||||||
|
// purpose of this member is to prevent certain types of signature confusion attacks
|
||||||
|
//(where an attacker substitutes one legitimate signature for another).
|
||||||
|
Type CeremonyType `json:"type"` |
||||||
|
Challenge string `json:"challenge"` |
||||||
|
Origin string `json:"origin"` |
||||||
|
TokenBinding *TokenBinding `json:"tokenBinding,omitempty"` |
||||||
|
// Chromium (Chrome) returns a hint sometimes about how to handle clientDataJSON in a safe manner
|
||||||
|
Hint string `json:"new_keys_may_be_added_here,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
type CeremonyType string |
||||||
|
|
||||||
|
const ( |
||||||
|
CreateCeremony CeremonyType = "webauthn.create" |
||||||
|
AssertCeremony CeremonyType = "webauthn.get" |
||||||
|
) |
||||||
|
|
||||||
|
type TokenBinding struct { |
||||||
|
Status TokenBindingStatus `json:"status"` |
||||||
|
ID string `json:"id,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
type TokenBindingStatus string |
||||||
|
|
||||||
|
const ( |
||||||
|
// Indicates token binding was used when communicating with the
|
||||||
|
// Relying Party. In this case, the id member MUST be present.
|
||||||
|
Present TokenBindingStatus = "present" |
||||||
|
// Indicates token binding was used when communicating with the
|
||||||
|
// negotiated when communicating with the Relying Party.
|
||||||
|
Supported TokenBindingStatus = "supported" |
||||||
|
// Indicates token binding not supported
|
||||||
|
// when communicating with the Relying Party.
|
||||||
|
NotSupported TokenBindingStatus = "not-supported" |
||||||
|
) |
||||||
|
|
||||||
|
// Returns the origin per the HTML spec: (scheme)://(host)[:(port)]
|
||||||
|
func FullyQualifiedOrigin(u *url.URL) string { |
||||||
|
return fmt.Sprintf("%s://%s", u.Scheme, u.Host) |
||||||
|
} |
||||||
|
|
||||||
|
// Handles steps 3 through 6 of verfying the registering client data of a
|
||||||
|
// new credential and steps 7 through 10 of verifying an authentication assertion
|
||||||
|
// See https://www.w3.org/TR/webauthn/#registering-a-new-credential
|
||||||
|
// and https://www.w3.org/TR/webauthn/#verifying-assertion
|
||||||
|
func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyType, relyingPartyOrigin string) error { |
||||||
|
|
||||||
|
// Registration Step 3. Verify that the value of C.type is webauthn.create.
|
||||||
|
|
||||||
|
// Assertion Step 7. Verify that the value of C.type is the string webauthn.get.
|
||||||
|
if c.Type != ceremony { |
||||||
|
err := ErrVerification.WithDetails("Error validating ceremony type") |
||||||
|
err.WithInfo(fmt.Sprintf("Expected Value: %s\n Received: %s\n", ceremony, c.Type)) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Registration Step 4. Verify that the value of C.challenge matches the challenge
|
||||||
|
// that was sent to the authenticator in the create() call.
|
||||||
|
|
||||||
|
// Assertion Step 8. Verify that the value of C.challenge matches the challenge
|
||||||
|
// that was sent to the authenticator in the PublicKeyCredentialRequestOptions
|
||||||
|
// passed to the get() call.
|
||||||
|
|
||||||
|
challenge := c.Challenge |
||||||
|
if 0 != strings.Compare(storedChallenge, challenge) { |
||||||
|
err := ErrVerification.WithDetails("Error validating challenge") |
||||||
|
return err.WithInfo(fmt.Sprintf("Expected b Value: %#v\nReceived b: %#v\n", storedChallenge, challenge)) |
||||||
|
} |
||||||
|
|
||||||
|
// Registration Step 5 & Assertion Step 9. Verify that the value of C.origin matches
|
||||||
|
// the Relying Party's origin.
|
||||||
|
clientDataOrigin, err := url.Parse(c.Origin) |
||||||
|
if err != nil { |
||||||
|
return ErrParsingData.WithDetails("Error decoding clientData origin as URL") |
||||||
|
} |
||||||
|
|
||||||
|
if !strings.EqualFold(FullyQualifiedOrigin(clientDataOrigin), relyingPartyOrigin) { |
||||||
|
err := ErrVerification.WithDetails("Error validating origin") |
||||||
|
return err.WithInfo(fmt.Sprintf("Expected Value: %s\n Received: %s\n", relyingPartyOrigin, FullyQualifiedOrigin(clientDataOrigin))) |
||||||
|
} |
||||||
|
|
||||||
|
// Registration Step 6 and Assertion Step 10. Verify that the value of C.tokenBinding.status
|
||||||
|
// matches the state of Token Binding for the TLS connection over which the assertion was
|
||||||
|
// obtained. If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id
|
||||||
|
// matches the base64url encoding of the Token Binding ID for the connection.
|
||||||
|
if c.TokenBinding != nil { |
||||||
|
if c.TokenBinding.Status == "" { |
||||||
|
return ErrParsingData.WithDetails("Error decoding clientData, token binding present without status") |
||||||
|
} |
||||||
|
if c.TokenBinding.Status != Present && c.TokenBinding.Status != Supported && c.TokenBinding.Status != NotSupported { |
||||||
|
return ErrParsingData.WithDetails("Error decoding clientData, token binding present with invalid status").WithInfo(fmt.Sprintf("Got: %s\n", c.TokenBinding.Status)) |
||||||
|
} |
||||||
|
} |
||||||
|
// Not yet fully implemented by the spec, browsers, and me.
|
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,216 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/sha256" |
||||||
|
"encoding/base64" |
||||||
|
"encoding/json" |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
) |
||||||
|
|
||||||
|
// The basic credential type that is inherited by WebAuthn's
|
||||||
|
// PublicKeyCredential type
|
||||||
|
// https://w3c.github.io/webappsec-credential-management/#credential
|
||||||
|
type Credential struct { |
||||||
|
// ID is The credential’s identifier. The requirements for the
|
||||||
|
// identifier are distinct for each type of credential. It might
|
||||||
|
// represent a username for username/password tuples, for example.
|
||||||
|
ID string `json:"id"` |
||||||
|
// Type is the value of the object’s interface object's [[type]] slot,
|
||||||
|
// which specifies the credential type represented by this object.
|
||||||
|
// This should be type "public-key" for Webauthn credentials.
|
||||||
|
Type string `json:"type"` |
||||||
|
} |
||||||
|
|
||||||
|
// The PublicKeyCredential interface inherits from Credential, and contains
|
||||||
|
// the attributes that are returned to the caller when a new credential
|
||||||
|
// is created, or a new assertion is requested.
|
||||||
|
type ParsedCredential struct { |
||||||
|
ID string `cbor:"id"` |
||||||
|
Type string `cbor:"type"` |
||||||
|
} |
||||||
|
|
||||||
|
type PublicKeyCredential struct { |
||||||
|
Credential |
||||||
|
RawID URLEncodedBase64 `json:"rawId"` |
||||||
|
ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
type ParsedPublicKeyCredential struct { |
||||||
|
ParsedCredential |
||||||
|
RawID []byte `json:"rawId"` |
||||||
|
ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
type CredentialCreationResponse struct { |
||||||
|
PublicKeyCredential |
||||||
|
AttestationResponse AuthenticatorAttestationResponse `json:"response"` |
||||||
|
} |
||||||
|
|
||||||
|
type ParsedCredentialCreationData struct { |
||||||
|
ParsedPublicKeyCredential |
||||||
|
Response ParsedAttestationResponse |
||||||
|
Raw CredentialCreationResponse |
||||||
|
} |
||||||
|
|
||||||
|
func ParseCredentialCreationResponse(response *http.Request) (*ParsedCredentialCreationData, error) { |
||||||
|
if response == nil || response.Body == nil { |
||||||
|
return nil, ErrBadRequest.WithDetails("No response given") |
||||||
|
} |
||||||
|
return ParseCredentialCreationResponseBody(response.Body) |
||||||
|
} |
||||||
|
|
||||||
|
func ParseCredentialCreationResponseBody(body io.Reader) (*ParsedCredentialCreationData, error) { |
||||||
|
var ccr CredentialCreationResponse |
||||||
|
err := json.NewDecoder(body).Decode(&ccr) |
||||||
|
if err != nil { |
||||||
|
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo(err.Error()) |
||||||
|
} |
||||||
|
|
||||||
|
if ccr.ID == "" { |
||||||
|
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Missing ID") |
||||||
|
} |
||||||
|
|
||||||
|
testB64, err := base64.RawURLEncoding.DecodeString(ccr.ID) |
||||||
|
if err != nil || !(len(testB64) > 0) { |
||||||
|
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("ID not base64.RawURLEncoded") |
||||||
|
} |
||||||
|
|
||||||
|
if ccr.PublicKeyCredential.Credential.Type == "" { |
||||||
|
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Missing type") |
||||||
|
} |
||||||
|
|
||||||
|
if ccr.PublicKeyCredential.Credential.Type != "public-key" { |
||||||
|
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Type not public-key") |
||||||
|
} |
||||||
|
|
||||||
|
var pcc ParsedCredentialCreationData |
||||||
|
pcc.ID, pcc.RawID, pcc.Type = ccr.ID, ccr.RawID, ccr.Type |
||||||
|
pcc.Raw = ccr |
||||||
|
|
||||||
|
parsedAttestationResponse, err := ccr.AttestationResponse.Parse() |
||||||
|
if err != nil { |
||||||
|
return nil, ErrParsingData.WithDetails("Error parsing attestation response") |
||||||
|
} |
||||||
|
|
||||||
|
pcc.Response = *parsedAttestationResponse |
||||||
|
|
||||||
|
return &pcc, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Verifies the Client and Attestation data as laid out by §7.1. Registering a new credential
|
||||||
|
// https://www.w3.org/TR/webauthn/#registering-a-new-credential
|
||||||
|
func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUser bool, relyingPartyID, relyingPartyOrigin string) error { |
||||||
|
|
||||||
|
// Handles steps 3 through 6 - Verifying the Client Data against the Relying Party's stored data
|
||||||
|
verifyError := pcc.Response.CollectedClientData.Verify(storedChallenge, CreateCeremony, relyingPartyOrigin) |
||||||
|
if verifyError != nil { |
||||||
|
return verifyError |
||||||
|
} |
||||||
|
|
||||||
|
// Step 7. Compute the hash of response.clientDataJSON using SHA-256.
|
||||||
|
clientDataHash := sha256.Sum256(pcc.Raw.AttestationResponse.ClientDataJSON) |
||||||
|
|
||||||
|
// Step 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse
|
||||||
|
// structure to obtain the attestation statement format fmt, the authenticator data authData, and the
|
||||||
|
// attestation statement attStmt. is handled while
|
||||||
|
|
||||||
|
// We do the above step while parsing and decoding the CredentialCreationResponse
|
||||||
|
// Handle steps 9 through 14 - This verifies the attestaion object and
|
||||||
|
verifyError = pcc.Response.AttestationObject.Verify(relyingPartyID, clientDataHash[:], verifyUser) |
||||||
|
if verifyError != nil { |
||||||
|
return verifyError |
||||||
|
} |
||||||
|
|
||||||
|
// Step 15. If validation is successful, obtain a list of acceptable trust anchors (attestation root
|
||||||
|
// certificates or ECDAA-Issuer public keys) for that attestation type and attestation statement
|
||||||
|
// format fmt, from a trusted source or from policy. For example, the FIDO Metadata Service provides
|
||||||
|
// one way to obtain such information, using the aaguid in the attestedCredentialData in authData.
|
||||||
|
// [https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-metadata-service-v2.0-id-20180227.html]
|
||||||
|
|
||||||
|
// TODO: There are no valid AAGUIDs yet or trust sources supported. We could implement policy for the RP in
|
||||||
|
// the future, however.
|
||||||
|
|
||||||
|
// Step 16. Assess the attestation trustworthiness using outputs of the verification procedure in step 14, as follows:
|
||||||
|
// - If self attestation was used, check if self attestation is acceptable under Relying Party policy.
|
||||||
|
// - If ECDAA was used, verify that the identifier of the ECDAA-Issuer public key used is included in
|
||||||
|
// the set of acceptable trust anchors obtained in step 15.
|
||||||
|
// - Otherwise, use the X.509 certificates returned by the verification procedure to verify that the
|
||||||
|
// attestation public key correctly chains up to an acceptable root certificate.
|
||||||
|
|
||||||
|
// TODO: We're not supporting trust anchors, self-attestation policy, or acceptable root certs yet
|
||||||
|
|
||||||
|
// Step 17. Check that the credentialId is not yet registered to any other user. If registration is
|
||||||
|
// requested for a credential that is already registered to a different user, the Relying Party SHOULD
|
||||||
|
// fail this registration ceremony, or it MAY decide to accept the registration, e.g. while deleting
|
||||||
|
// the older registration.
|
||||||
|
|
||||||
|
// TODO: We can't support this in the code's current form, the Relying Party would need to check for this
|
||||||
|
// against their database
|
||||||
|
|
||||||
|
// Step 18 If the attestation statement attStmt verified successfully and is found to be trustworthy, then
|
||||||
|
// register the new credential with the account that was denoted in the options.user passed to create(), by
|
||||||
|
// associating it with the credentialId and credentialPublicKey in the attestedCredentialData in authData, as
|
||||||
|
// appropriate for the Relying Party's system.
|
||||||
|
|
||||||
|
// Step 19. If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above,
|
||||||
|
// the Relying Party SHOULD fail the registration ceremony.
|
||||||
|
|
||||||
|
// TODO: Not implemented for the reasons mentioned under Step 16
|
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetAppID takes a AuthenticationExtensions object or nil. It then performs the following checks in order:
|
||||||
|
//
|
||||||
|
// 1. Check that the Session Data's AuthenticationExtensions has been provided and return a blank appid if it hasn't been.
|
||||||
|
// 2. Check that the AuthenticationExtensionsClientOutputs contains the extensions output and return a blank appid if it doesn't.
|
||||||
|
// 3. Check that the Credential AttestationType is `fido-u2f` and return a blank appid if it isn't.
|
||||||
|
// 4. Check that the AuthenticationExtensionsClientOutputs contains the appid key and return a blank appid if it doesn't.
|
||||||
|
// 5. Check that the AuthenticationExtensionsClientOutputs appid is a bool and return an error if it isn't.
|
||||||
|
// 6. Check that the appid output is true and return a blank appid if it isn't.
|
||||||
|
// 7. Check that the Session Data has an appid extension defined and return an error if it doesn't.
|
||||||
|
// 8. Check that the appid extension in Session Data is a string and return an error if it isn't.
|
||||||
|
// 9. Return the appid extension value from the Session Data.
|
||||||
|
func (ppkc ParsedPublicKeyCredential) GetAppID(authExt AuthenticationExtensions, credentialAttestationType string) (appID string, err error) { |
||||||
|
var ( |
||||||
|
value, clientValue interface{} |
||||||
|
enableAppID, ok bool |
||||||
|
) |
||||||
|
|
||||||
|
if authExt == nil { |
||||||
|
return "", nil |
||||||
|
} |
||||||
|
|
||||||
|
if ppkc.ClientExtensionResults == nil { |
||||||
|
return "", nil |
||||||
|
} |
||||||
|
|
||||||
|
// If the credential does not have the correct attestation type it is assumed to NOT be a fido-u2f credential.
|
||||||
|
// https://w3c.github.io/webauthn/#sctn-fido-u2f-attestation
|
||||||
|
if credentialAttestationType != "fido-u2f" { |
||||||
|
return "", nil |
||||||
|
} |
||||||
|
|
||||||
|
if clientValue, ok = ppkc.ClientExtensionResults["appid"]; !ok { |
||||||
|
return "", nil |
||||||
|
} |
||||||
|
|
||||||
|
if enableAppID, ok = clientValue.(bool); !ok { |
||||||
|
return "", ErrBadRequest.WithDetails("Client Output appid did not have the expected type") |
||||||
|
} |
||||||
|
|
||||||
|
if !enableAppID { |
||||||
|
return "", nil |
||||||
|
} |
||||||
|
|
||||||
|
if value, ok = authExt["appid"]; !ok { |
||||||
|
return "", ErrBadRequest.WithDetails("Session Data does not have an appid but Client Output indicates it should be set") |
||||||
|
} |
||||||
|
|
||||||
|
if appID, ok = value.(string); !ok { |
||||||
|
return "", ErrBadRequest.WithDetails("Session Data appid did not have the expected type") |
||||||
|
} |
||||||
|
|
||||||
|
return appID, nil |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
// The protocol package contains data structures and validation functionality
|
||||||
|
// outlined in the Web Authnentication specification (https://www.w3.org/TR/webauthn).
|
||||||
|
// The data structures here attempt to conform as much as possible to their definitions,
|
||||||
|
// but some structs (like those that are used as part of validation steps) contain
|
||||||
|
// additional fields that help us unpack and validate the data we unmarshall.
|
||||||
|
// When implementing this library, most developers will primarily be using the API
|
||||||
|
// outlined in the webauthn package.
|
||||||
|
package protocol |
@ -0,0 +1,48 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
// From §5.4.1 (https://www.w3.org/TR/webauthn/#dictionary-pkcredentialentity).
|
||||||
|
// PublicKeyCredentialEntity describes a user account, or a WebAuthn Relying Party,
|
||||||
|
// with which a public key credential is associated.
|
||||||
|
type CredentialEntity struct { |
||||||
|
// A human-palatable name for the entity. Its function depends on what the PublicKeyCredentialEntity represents:
|
||||||
|
//
|
||||||
|
// When inherited by PublicKeyCredentialRpEntity it is a human-palatable identifier for the Relying Party,
|
||||||
|
// intended only for display. For example, "ACME Corporation", "Wonderful Widgets, Inc." or "ОАО Примертех".
|
||||||
|
//
|
||||||
|
// When inherited by PublicKeyCredentialUserEntity, it is a human-palatable identifier for a user account. It is
|
||||||
|
// intended only for display, i.e., aiding the user in determining the difference between user accounts with similar
|
||||||
|
// displayNames. For example, "alexm", "alex.p.mueller@example.com" or "+14255551234".
|
||||||
|
Name string `json:"name"` |
||||||
|
// A serialized URL which resolves to an image associated with the entity. For example,
|
||||||
|
// this could be a user’s avatar or a Relying Party's logo. This URL MUST be an a priori
|
||||||
|
// authenticated URL. Authenticators MUST accept and store a 128-byte minimum length for
|
||||||
|
// an icon member’s value. Authenticators MAY ignore an icon member’s value if its length
|
||||||
|
// is greater than 128 bytes. The URL’s scheme MAY be "data" to avoid fetches of the URL,
|
||||||
|
// at the cost of needing more storage.
|
||||||
|
Icon string `json:"icon,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
// From §5.4.2 (https://www.w3.org/TR/webauthn/#sctn-rp-credential-params).
|
||||||
|
// The PublicKeyCredentialRpEntity is used to supply additional
|
||||||
|
// Relying Party attributes when creating a new credential.
|
||||||
|
type RelyingPartyEntity struct { |
||||||
|
CredentialEntity |
||||||
|
// A unique identifier for the Relying Party entity, which sets the RP ID.
|
||||||
|
ID string `json:"id"` |
||||||
|
} |
||||||
|
|
||||||
|
// From §5.4.3 (https://www.w3.org/TR/webauthn/#sctn-user-credential-params).
|
||||||
|
// The PublicKeyCredentialUserEntity is used to supply additional
|
||||||
|
// user account attributes when creating a new credential.
|
||||||
|
type UserEntity struct { |
||||||
|
CredentialEntity |
||||||
|
// A human-palatable name for the user account, intended only for display.
|
||||||
|
// For example, "Alex P. Müller" or "田中 倫". The Relying Party SHOULD let
|
||||||
|
// the user choose this, and SHOULD NOT restrict the choice more than necessary.
|
||||||
|
DisplayName string `json:"displayName,omitempty"` |
||||||
|
// ID is the user handle of the user account entity. To ensure secure operation,
|
||||||
|
// authentication and authorization decisions MUST be made on the basis of this id
|
||||||
|
// member, not the displayName nor name members. See Section 6.1 of
|
||||||
|
// [RFC8266](https://www.w3.org/TR/webauthn/#biblio-rfc8266).
|
||||||
|
ID []byte `json:"id"` |
||||||
|
} |
@ -0,0 +1,85 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
type Error struct { |
||||||
|
// Short name for the type of error that has occurred
|
||||||
|
Type string `json:"type"` |
||||||
|
// Additional details about the error
|
||||||
|
Details string `json:"error"` |
||||||
|
// Information to help debug the error
|
||||||
|
DevInfo string `json:"debug"` |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
ErrBadRequest = &Error{ |
||||||
|
Type: "invalid_request", |
||||||
|
Details: "Error reading the requst data", |
||||||
|
} |
||||||
|
ErrChallengeMismatch = &Error{ |
||||||
|
Type: "challenge_mismatch", |
||||||
|
Details: "Stored challenge and received challenge do not match", |
||||||
|
} |
||||||
|
ErrParsingData = &Error{ |
||||||
|
Type: "parse_error", |
||||||
|
Details: "Error parsing the authenticator response", |
||||||
|
} |
||||||
|
ErrAuthData = &Error{ |
||||||
|
Type: "auth_data", |
||||||
|
Details: "Error verifying the authenticator data", |
||||||
|
} |
||||||
|
ErrVerification = &Error{ |
||||||
|
Type: "verification_error", |
||||||
|
Details: "Error validating the authenticator response", |
||||||
|
} |
||||||
|
ErrAttestation = &Error{ |
||||||
|
Type: "attesation_error", |
||||||
|
Details: "Error validating the attestation data provided", |
||||||
|
} |
||||||
|
ErrInvalidAttestation = &Error{ |
||||||
|
Type: "invalid_attestation", |
||||||
|
Details: "Invalid attestation data", |
||||||
|
} |
||||||
|
ErrAttestationFormat = &Error{ |
||||||
|
Type: "invalid_attestation", |
||||||
|
Details: "Invalid attestation format", |
||||||
|
} |
||||||
|
ErrAttestationCertificate = &Error{ |
||||||
|
Type: "invalid_certificate", |
||||||
|
Details: "Invalid attestation certificate", |
||||||
|
} |
||||||
|
ErrAssertionSignature = &Error{ |
||||||
|
Type: "invalid_signature", |
||||||
|
Details: "Assertion Signature against auth data and client hash is not valid", |
||||||
|
} |
||||||
|
ErrUnsupportedKey = &Error{ |
||||||
|
Type: "invalid_key_type", |
||||||
|
Details: "Unsupported Public Key Type", |
||||||
|
} |
||||||
|
ErrUnsupportedAlgorithm = &Error{ |
||||||
|
Type: "unsupported_key_algorithm", |
||||||
|
Details: "Unsupported public key algorithm", |
||||||
|
} |
||||||
|
ErrNotSpecImplemented = &Error{ |
||||||
|
Type: "spec_unimplemented", |
||||||
|
Details: "This field is not yet supported by the WebAuthn spec", |
||||||
|
} |
||||||
|
ErrNotImplemented = &Error{ |
||||||
|
Type: "not_implemented", |
||||||
|
Details: "This field is not yet supported by this library", |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
func (err *Error) Error() string { |
||||||
|
return err.Details |
||||||
|
} |
||||||
|
|
||||||
|
func (passedError *Error) WithDetails(details string) *Error { |
||||||
|
err := *passedError |
||||||
|
err.Details = details |
||||||
|
return &err |
||||||
|
} |
||||||
|
|
||||||
|
func (passedError *Error) WithInfo(info string) *Error { |
||||||
|
err := *passedError |
||||||
|
err.DevInfo = info |
||||||
|
return &err |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
// Extensions are discussed in §9. WebAuthn Extensions (https://www.w3.org/TR/webauthn/#extensions).
|
||||||
|
|
||||||
|
// For a list of commonly supported extenstions, see §10. Defined Extensions
|
||||||
|
// (https://www.w3.org/TR/webauthn/#sctn-defined-extensions).
|
||||||
|
|
||||||
|
type AuthenticationExtensionsClientOutputs map[string]interface{} |
@ -0,0 +1,282 @@ |
|||||||
|
package googletpm |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"crypto/sha1" |
||||||
|
"crypto/sha256" |
||||||
|
"crypto/sha512" |
||||||
|
"fmt" |
||||||
|
"hash" |
||||||
|
) |
||||||
|
|
||||||
|
// DecodeAttestationData decode a TPMS_ATTEST message. No error is returned if
|
||||||
|
// the input has extra trailing data.
|
||||||
|
func DecodeAttestationData(in []byte) (*AttestationData, error) { |
||||||
|
buf := bytes.NewBuffer(in) |
||||||
|
|
||||||
|
var ad AttestationData |
||||||
|
if err := UnpackBuf(buf, &ad.Magic, &ad.Type); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Magic/Type: %v", err) |
||||||
|
} |
||||||
|
n, err := decodeName(buf) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("decoding QualifiedSigner: %v", err) |
||||||
|
} |
||||||
|
ad.QualifiedSigner = *n |
||||||
|
if err := UnpackBuf(buf, &ad.ExtraData, &ad.ClockInfo, &ad.FirmwareVersion); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding ExtraData/ClockInfo/FirmwareVersion: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// The spec specifies several other types of attestation data. We only need
|
||||||
|
// parsing of Certify & Creation attestation data for now. If you need
|
||||||
|
// support for other attestation types, add them here.
|
||||||
|
switch ad.Type { |
||||||
|
case TagAttestCertify: |
||||||
|
if ad.AttestedCertifyInfo, err = decodeCertifyInfo(buf); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding AttestedCertifyInfo: %v", err) |
||||||
|
} |
||||||
|
case TagAttestCreation: |
||||||
|
if ad.AttestedCreationInfo, err = decodeCreationInfo(buf); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding AttestedCreationInfo: %v", err) |
||||||
|
} |
||||||
|
case TagAttestQuote: |
||||||
|
if ad.AttestedQuoteInfo, err = decodeQuoteInfo(buf); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding AttestedQuoteInfo: %v", err) |
||||||
|
} |
||||||
|
default: |
||||||
|
return nil, fmt.Errorf("only Certify & Creation attestation structures are supported, got type 0x%x", ad.Type) |
||||||
|
} |
||||||
|
|
||||||
|
return &ad, nil |
||||||
|
} |
||||||
|
|
||||||
|
// AttestationData contains data attested by TPM commands (like Certify).
|
||||||
|
type AttestationData struct { |
||||||
|
Magic uint32 |
||||||
|
Type Tag |
||||||
|
QualifiedSigner Name |
||||||
|
ExtraData []byte |
||||||
|
ClockInfo ClockInfo |
||||||
|
FirmwareVersion uint64 |
||||||
|
AttestedCertifyInfo *CertifyInfo |
||||||
|
AttestedQuoteInfo *QuoteInfo |
||||||
|
AttestedCreationInfo *CreationInfo |
||||||
|
} |
||||||
|
|
||||||
|
// Tag is a command tag.
|
||||||
|
type Tag uint16 |
||||||
|
|
||||||
|
type Name struct { |
||||||
|
Handle *Handle |
||||||
|
Digest *HashValue |
||||||
|
} |
||||||
|
|
||||||
|
// A Handle is a reference to a TPM object.
|
||||||
|
type Handle uint32 |
||||||
|
type HashValue struct { |
||||||
|
Alg Algorithm |
||||||
|
Value []byte |
||||||
|
} |
||||||
|
|
||||||
|
// ClockInfo contains TPM state info included in AttestationData.
|
||||||
|
type ClockInfo struct { |
||||||
|
Clock uint64 |
||||||
|
ResetCount uint32 |
||||||
|
RestartCount uint32 |
||||||
|
Safe byte |
||||||
|
} |
||||||
|
|
||||||
|
// CertifyInfo contains Certify-specific data for TPMS_ATTEST.
|
||||||
|
type CertifyInfo struct { |
||||||
|
Name Name |
||||||
|
QualifiedName Name |
||||||
|
} |
||||||
|
|
||||||
|
// QuoteInfo represents a TPMS_QUOTE_INFO structure.
|
||||||
|
type QuoteInfo struct { |
||||||
|
PCRSelection PCRSelection |
||||||
|
PCRDigest []byte |
||||||
|
} |
||||||
|
|
||||||
|
// PCRSelection contains a slice of PCR indexes and a hash algorithm used in
|
||||||
|
// them.
|
||||||
|
type PCRSelection struct { |
||||||
|
Hash Algorithm |
||||||
|
PCRs []int |
||||||
|
} |
||||||
|
|
||||||
|
// CreationInfo contains Creation-specific data for TPMS_ATTEST.
|
||||||
|
type CreationInfo struct { |
||||||
|
Name Name |
||||||
|
// Most TPM2B_Digest structures contain a TPMU_HA structure
|
||||||
|
// and get parsed to HashValue. This is never the case for the
|
||||||
|
// digest in TPMS_CREATION_INFO.
|
||||||
|
OpaqueDigest []byte |
||||||
|
} |
||||||
|
|
||||||
|
func decodeName(in *bytes.Buffer) (*Name, error) { |
||||||
|
var nameBuf []byte |
||||||
|
if err := UnpackBuf(in, &nameBuf); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
name := new(Name) |
||||||
|
switch len(nameBuf) { |
||||||
|
case 0: |
||||||
|
// No name is present.
|
||||||
|
case 4: |
||||||
|
name.Handle = new(Handle) |
||||||
|
if err := UnpackBuf(bytes.NewBuffer(nameBuf), name.Handle); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Handle: %v", err) |
||||||
|
} |
||||||
|
default: |
||||||
|
var err error |
||||||
|
name.Digest, err = decodeHashValue(bytes.NewBuffer(nameBuf)) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Digest: %v", err) |
||||||
|
} |
||||||
|
} |
||||||
|
return name, nil |
||||||
|
} |
||||||
|
|
||||||
|
func decodeHashValue(in *bytes.Buffer) (*HashValue, error) { |
||||||
|
var hv HashValue |
||||||
|
if err := UnpackBuf(in, &hv.Alg); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Alg: %v", err) |
||||||
|
} |
||||||
|
hfn, ok := hashConstructors[hv.Alg] |
||||||
|
if !ok { |
||||||
|
return nil, fmt.Errorf("unsupported hash algorithm type 0x%x", hv.Alg) |
||||||
|
} |
||||||
|
hv.Value = make([]byte, hfn().Size()) |
||||||
|
if _, err := in.Read(hv.Value); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Value: %v", err) |
||||||
|
} |
||||||
|
return &hv, nil |
||||||
|
} |
||||||
|
|
||||||
|
// HashConstructor returns a function that can be used to make a
|
||||||
|
// hash.Hash using the specified algorithm. An error is returned
|
||||||
|
// if the algorithm is not a hash algorithm.
|
||||||
|
func (a Algorithm) HashConstructor() (func() hash.Hash, error) { |
||||||
|
c, ok := hashConstructors[a] |
||||||
|
if !ok { |
||||||
|
return nil, fmt.Errorf("algorithm not supported: 0x%x", a) |
||||||
|
} |
||||||
|
return c, nil |
||||||
|
} |
||||||
|
|
||||||
|
var hashConstructors = map[Algorithm]func() hash.Hash{ |
||||||
|
AlgSHA1: sha1.New, |
||||||
|
AlgSHA256: sha256.New, |
||||||
|
AlgSHA384: sha512.New384, |
||||||
|
AlgSHA512: sha512.New, |
||||||
|
} |
||||||
|
|
||||||
|
// TPM Structure Tags. Tags are used to disambiguate structures, similar to Alg
|
||||||
|
// values: tag value defines what kind of data lives in a nested field.
|
||||||
|
const ( |
||||||
|
TagNull Tag = 0x8000 |
||||||
|
TagNoSessions Tag = 0x8001 |
||||||
|
TagSessions Tag = 0x8002 |
||||||
|
TagAttestCertify Tag = 0x8017 |
||||||
|
TagAttestQuote Tag = 0x8018 |
||||||
|
TagAttestCreation Tag = 0x801a |
||||||
|
TagHashCheck Tag = 0x8024 |
||||||
|
) |
||||||
|
|
||||||
|
func decodeCertifyInfo(in *bytes.Buffer) (*CertifyInfo, error) { |
||||||
|
var ci CertifyInfo |
||||||
|
|
||||||
|
n, err := decodeName(in) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Name: %v", err) |
||||||
|
} |
||||||
|
ci.Name = *n |
||||||
|
|
||||||
|
n, err = decodeName(in) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("decoding QualifiedName: %v", err) |
||||||
|
} |
||||||
|
ci.QualifiedName = *n |
||||||
|
|
||||||
|
return &ci, nil |
||||||
|
} |
||||||
|
|
||||||
|
func decodeCreationInfo(in *bytes.Buffer) (*CreationInfo, error) { |
||||||
|
var ci CreationInfo |
||||||
|
|
||||||
|
n, err := decodeName(in) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Name: %v", err) |
||||||
|
} |
||||||
|
ci.Name = *n |
||||||
|
|
||||||
|
if err := UnpackBuf(in, &ci.OpaqueDigest); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Digest: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
return &ci, nil |
||||||
|
} |
||||||
|
|
||||||
|
func decodeQuoteInfo(in *bytes.Buffer) (*QuoteInfo, error) { |
||||||
|
var out QuoteInfo |
||||||
|
sel, err := decodeTPMLPCRSelection(in) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("decoding PCRSelection: %v", err) |
||||||
|
} |
||||||
|
out.PCRSelection = sel |
||||||
|
if err := UnpackBuf(in, &out.PCRDigest); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding PCRDigest: %v", err) |
||||||
|
} |
||||||
|
return &out, nil |
||||||
|
} |
||||||
|
|
||||||
|
func decodeTPMLPCRSelection(buf *bytes.Buffer) (PCRSelection, error) { |
||||||
|
var count uint32 |
||||||
|
var sel PCRSelection |
||||||
|
if err := UnpackBuf(buf, &count); err != nil { |
||||||
|
return sel, err |
||||||
|
} |
||||||
|
switch count { |
||||||
|
case 0: |
||||||
|
sel.Hash = AlgUnknown |
||||||
|
return sel, nil |
||||||
|
case 1: // We only support decoding of a single PCRSelection.
|
||||||
|
default: |
||||||
|
return sel, fmt.Errorf("decoding TPML_PCR_SELECTION list longer than 1 is not supported (got length %d)", count) |
||||||
|
} |
||||||
|
|
||||||
|
// See comment in encodeTPMLPCRSelection for details on this format.
|
||||||
|
var ts tpmsPCRSelection |
||||||
|
if err := UnpackBuf(buf, &ts.Hash, &ts.Size); err != nil { |
||||||
|
return sel, err |
||||||
|
} |
||||||
|
ts.PCRs = make([]byte, ts.Size) |
||||||
|
if _, err := buf.Read(ts.PCRs); err != nil { |
||||||
|
return sel, err |
||||||
|
} |
||||||
|
|
||||||
|
sel.Hash = ts.Hash |
||||||
|
for i := 0; i < int(ts.Size); i++ { |
||||||
|
for j := 0; j < 8; j++ { |
||||||
|
set := ts.PCRs[i] & byte(1<<byte(j)) |
||||||
|
if set == 0 { |
||||||
|
continue |
||||||
|
} |
||||||
|
sel.PCRs = append(sel.PCRs, 8*i+j) |
||||||
|
} |
||||||
|
} |
||||||
|
return sel, nil |
||||||
|
} |
||||||
|
|
||||||
|
type tpmsPCRSelection struct { |
||||||
|
Hash Algorithm |
||||||
|
Size byte |
||||||
|
PCRs RawBytes |
||||||
|
} |
||||||
|
|
||||||
|
// RawBytes is for Pack and RunCommand arguments that are already encoded.
|
||||||
|
// Compared to []byte, RawBytes will not be prepended with slice length during
|
||||||
|
// encoding.
|
||||||
|
type RawBytes []byte |
@ -0,0 +1,152 @@ |
|||||||
|
package googletpm |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/binary" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"reflect" |
||||||
|
) |
||||||
|
|
||||||
|
// From github.com/google/go-tpm
|
||||||
|
// Portions of existing package conflicted with existing build environment
|
||||||
|
// and only needed very small amount of code for pubarea and certinfo structs
|
||||||
|
// so copied them out to this package
|
||||||
|
|
||||||
|
// Supported Algorithms.
|
||||||
|
const ( |
||||||
|
AlgUnknown Algorithm = 0x0000 |
||||||
|
AlgRSA Algorithm = 0x0001 |
||||||
|
AlgSHA1 Algorithm = 0x0004 |
||||||
|
AlgAES Algorithm = 0x0006 |
||||||
|
AlgKeyedHash Algorithm = 0x0008 |
||||||
|
AlgSHA256 Algorithm = 0x000B |
||||||
|
AlgSHA384 Algorithm = 0x000C |
||||||
|
AlgSHA512 Algorithm = 0x000D |
||||||
|
AlgNull Algorithm = 0x0010 |
||||||
|
AlgRSASSA Algorithm = 0x0014 |
||||||
|
AlgRSAES Algorithm = 0x0015 |
||||||
|
AlgRSAPSS Algorithm = 0x0016 |
||||||
|
AlgOAEP Algorithm = 0x0017 |
||||||
|
AlgECDSA Algorithm = 0x0018 |
||||||
|
AlgECDH Algorithm = 0x0019 |
||||||
|
AlgECDAA Algorithm = 0x001A |
||||||
|
AlgKDF2 Algorithm = 0x0021 |
||||||
|
AlgECC Algorithm = 0x0023 |
||||||
|
AlgCTR Algorithm = 0x0040 |
||||||
|
AlgOFB Algorithm = 0x0041 |
||||||
|
AlgCBC Algorithm = 0x0042 |
||||||
|
AlgCFB Algorithm = 0x0043 |
||||||
|
AlgECB Algorithm = 0x0044 |
||||||
|
) |
||||||
|
|
||||||
|
// UnpackBuf recursively unpacks types from a reader just as encoding/binary
|
||||||
|
// does under binary.BigEndian, but with one difference: it unpacks a byte
|
||||||
|
// slice by first reading an integer with lengthPrefixSize bytes, then reading
|
||||||
|
// that many bytes. It assumes that incoming values are pointers to values so
|
||||||
|
// that, e.g., underlying slices can be resized as needed.
|
||||||
|
func UnpackBuf(buf io.Reader, elts ...interface{}) error { |
||||||
|
for _, e := range elts { |
||||||
|
v := reflect.ValueOf(e) |
||||||
|
k := v.Kind() |
||||||
|
if k != reflect.Ptr { |
||||||
|
return fmt.Errorf("all values passed to Unpack must be pointers, got %v", k) |
||||||
|
} |
||||||
|
|
||||||
|
if v.IsNil() { |
||||||
|
return errors.New("can't fill a nil pointer") |
||||||
|
} |
||||||
|
|
||||||
|
iv := reflect.Indirect(v) |
||||||
|
switch iv.Kind() { |
||||||
|
case reflect.Struct: |
||||||
|
// Decompose the struct and copy over the values.
|
||||||
|
for i := 0; i < iv.NumField(); i++ { |
||||||
|
if err := UnpackBuf(buf, iv.Field(i).Addr().Interface()); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
case reflect.Slice: |
||||||
|
var size int |
||||||
|
_, isHandles := e.(*[]Handle) |
||||||
|
|
||||||
|
switch { |
||||||
|
// []Handle always uses 2-byte length, even with TPM 1.2.
|
||||||
|
case isHandles: |
||||||
|
var tmpSize uint16 |
||||||
|
if err := binary.Read(buf, binary.BigEndian, &tmpSize); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
size = int(tmpSize) |
||||||
|
// TPM 2.0
|
||||||
|
case lengthPrefixSize == tpm20PrefixSize: |
||||||
|
var tmpSize uint16 |
||||||
|
if err := binary.Read(buf, binary.BigEndian, &tmpSize); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
size = int(tmpSize) |
||||||
|
// TPM 1.2
|
||||||
|
case lengthPrefixSize == tpm12PrefixSize: |
||||||
|
var tmpSize uint32 |
||||||
|
if err := binary.Read(buf, binary.BigEndian, &tmpSize); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
size = int(tmpSize) |
||||||
|
default: |
||||||
|
return fmt.Errorf("lengthPrefixSize is %d, must be either 2 or 4", lengthPrefixSize) |
||||||
|
} |
||||||
|
|
||||||
|
// A zero size is used by the TPM to signal that certain elements
|
||||||
|
// are not present.
|
||||||
|
if size == 0 { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
// Make len(e) match size exactly.
|
||||||
|
switch b := e.(type) { |
||||||
|
case *[]byte: |
||||||
|
if len(*b) >= size { |
||||||
|
*b = (*b)[:size] |
||||||
|
} else { |
||||||
|
*b = append(*b, make([]byte, size-len(*b))...) |
||||||
|
} |
||||||
|
case *[]Handle: |
||||||
|
if len(*b) >= size { |
||||||
|
*b = (*b)[:size] |
||||||
|
} else { |
||||||
|
*b = append(*b, make([]Handle, size-len(*b))...) |
||||||
|
} |
||||||
|
default: |
||||||
|
return fmt.Errorf("can't fill pointer to %T, only []byte or []Handle slices", e) |
||||||
|
} |
||||||
|
|
||||||
|
if err := binary.Read(buf, binary.BigEndian, e); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
default: |
||||||
|
if err := binary.Read(buf, binary.BigEndian, e); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// lengthPrefixSize is the size in bytes of length prefix for byte slices.
|
||||||
|
//
|
||||||
|
// In TPM 1.2 this is 4 bytes.
|
||||||
|
// In TPM 2.0 this is 2 bytes.
|
||||||
|
var lengthPrefixSize int |
||||||
|
|
||||||
|
const ( |
||||||
|
tpm12PrefixSize = 4 |
||||||
|
tpm20PrefixSize = 2 |
||||||
|
) |
||||||
|
|
||||||
|
// UseTPM20LengthPrefixSize makes Pack/Unpack use TPM 2.0 encoding for byte
|
||||||
|
// arrays.
|
||||||
|
func UseTPM20LengthPrefixSize() { |
||||||
|
lengthPrefixSize = tpm20PrefixSize |
||||||
|
} |
@ -0,0 +1,240 @@ |
|||||||
|
package googletpm |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"math/big" |
||||||
|
) |
||||||
|
|
||||||
|
// DecodePublic decodes a TPMT_PUBLIC message. No error is returned if
|
||||||
|
// the input has extra trailing data.
|
||||||
|
func DecodePublic(buf []byte) (Public, error) { |
||||||
|
in := bytes.NewBuffer(buf) |
||||||
|
var pub Public |
||||||
|
var err error |
||||||
|
if err = UnpackBuf(in, &pub.Type, &pub.NameAlg, &pub.Attributes, &pub.AuthPolicy); err != nil { |
||||||
|
return pub, fmt.Errorf("decoding TPMT_PUBLIC: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
switch pub.Type { |
||||||
|
case AlgRSA: |
||||||
|
pub.RSAParameters, err = decodeRSAParams(in) |
||||||
|
case AlgECC: |
||||||
|
pub.ECCParameters, err = decodeECCParams(in) |
||||||
|
default: |
||||||
|
err = fmt.Errorf("unsupported type in TPMT_PUBLIC: %v", pub.Type) |
||||||
|
} |
||||||
|
return pub, err |
||||||
|
} |
||||||
|
|
||||||
|
// Public contains the public area of an object.
|
||||||
|
type Public struct { |
||||||
|
Type Algorithm |
||||||
|
NameAlg Algorithm |
||||||
|
Attributes KeyProp |
||||||
|
AuthPolicy []byte |
||||||
|
|
||||||
|
// If Type is AlgKeyedHash, then do not set these.
|
||||||
|
// Otherwise, only one of the Parameters fields should be set. When encoding/decoding,
|
||||||
|
// one will be picked based on Type.
|
||||||
|
RSAParameters *RSAParams |
||||||
|
ECCParameters *ECCParams |
||||||
|
} |
||||||
|
|
||||||
|
// Algorithm represents a TPM_ALG_ID value.
|
||||||
|
type Algorithm uint16 |
||||||
|
|
||||||
|
// KeyProp is a bitmask used in Attributes field of key templates. Individual
|
||||||
|
// flags should be OR-ed to form a full mask.
|
||||||
|
type KeyProp uint32 |
||||||
|
|
||||||
|
// Key properties.
|
||||||
|
const ( |
||||||
|
FlagFixedTPM KeyProp = 0x00000002 |
||||||
|
FlagFixedParent KeyProp = 0x00000010 |
||||||
|
FlagSensitiveDataOrigin KeyProp = 0x00000020 |
||||||
|
FlagUserWithAuth KeyProp = 0x00000040 |
||||||
|
FlagAdminWithPolicy KeyProp = 0x00000080 |
||||||
|
FlagNoDA KeyProp = 0x00000400 |
||||||
|
FlagRestricted KeyProp = 0x00010000 |
||||||
|
FlagDecrypt KeyProp = 0x00020000 |
||||||
|
FlagSign KeyProp = 0x00040000 |
||||||
|
|
||||||
|
FlagSealDefault = FlagFixedTPM | FlagFixedParent |
||||||
|
FlagSignerDefault = FlagSign | FlagRestricted | FlagFixedTPM | |
||||||
|
FlagFixedParent | FlagSensitiveDataOrigin | FlagUserWithAuth |
||||||
|
FlagStorageDefault = FlagDecrypt | FlagRestricted | FlagFixedTPM | |
||||||
|
FlagFixedParent | FlagSensitiveDataOrigin | FlagUserWithAuth |
||||||
|
) |
||||||
|
|
||||||
|
func decodeRSAParams(in *bytes.Buffer) (*RSAParams, error) { |
||||||
|
var params RSAParams |
||||||
|
var err error |
||||||
|
|
||||||
|
if params.Symmetric, err = decodeSymScheme(in); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Symmetric: %v", err) |
||||||
|
} |
||||||
|
if params.Sign, err = decodeSigScheme(in); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Sign: %v", err) |
||||||
|
} |
||||||
|
var modBytes []byte |
||||||
|
if err := UnpackBuf(in, ¶ms.KeyBits, ¶ms.Exponent, &modBytes); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding KeyBits, Exponent, Modulus: %v", err) |
||||||
|
} |
||||||
|
if params.Exponent == 0 { |
||||||
|
params.encodeDefaultExponentAsZero = true |
||||||
|
params.Exponent = defaultRSAExponent |
||||||
|
} |
||||||
|
params.Modulus = new(big.Int).SetBytes(modBytes) |
||||||
|
return ¶ms, nil |
||||||
|
} |
||||||
|
|
||||||
|
const defaultRSAExponent = 1<<16 + 1 |
||||||
|
|
||||||
|
// RSAParams represents parameters of an RSA key pair.
|
||||||
|
//
|
||||||
|
// Symmetric and Sign may be nil, depending on key Attributes in Public.
|
||||||
|
//
|
||||||
|
// One of Modulus and ModulusRaw must always be non-nil. Modulus takes
|
||||||
|
// precedence. ModulusRaw is used for key templates where the field named
|
||||||
|
// "unique" must be a byte array of all zeroes.
|
||||||
|
type RSAParams struct { |
||||||
|
Symmetric *SymScheme |
||||||
|
Sign *SigScheme |
||||||
|
KeyBits uint16 |
||||||
|
// The default Exponent (65537) has two representations; the
|
||||||
|
// 0 value, and the value 65537.
|
||||||
|
// If encodeDefaultExponentAsZero is set, an exponent of 65537
|
||||||
|
// will be encoded as zero. This is necessary to produce an identical
|
||||||
|
// encoded bitstream, so Name digest calculations will be correct.
|
||||||
|
encodeDefaultExponentAsZero bool |
||||||
|
Exponent uint32 |
||||||
|
ModulusRaw []byte |
||||||
|
Modulus *big.Int |
||||||
|
} |
||||||
|
|
||||||
|
// SymScheme represents a symmetric encryption scheme.
|
||||||
|
type SymScheme struct { |
||||||
|
Alg Algorithm |
||||||
|
KeyBits uint16 |
||||||
|
Mode Algorithm |
||||||
|
} // SigScheme represents a signing scheme.
|
||||||
|
type SigScheme struct { |
||||||
|
Alg Algorithm |
||||||
|
Hash Algorithm |
||||||
|
Count uint32 |
||||||
|
} |
||||||
|
|
||||||
|
func decodeSigScheme(in *bytes.Buffer) (*SigScheme, error) { |
||||||
|
var scheme SigScheme |
||||||
|
if err := UnpackBuf(in, &scheme.Alg); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Alg: %v", err) |
||||||
|
} |
||||||
|
if scheme.Alg == AlgNull { |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
if err := UnpackBuf(in, &scheme.Hash); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Hash: %v", err) |
||||||
|
} |
||||||
|
if scheme.Alg.UsesCount() { |
||||||
|
if err := UnpackBuf(in, &scheme.Count); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Count: %v", err) |
||||||
|
} |
||||||
|
} |
||||||
|
return &scheme, nil |
||||||
|
} |
||||||
|
|
||||||
|
// UsesCount returns true if a signature algorithm uses count value.
|
||||||
|
func (a Algorithm) UsesCount() bool { |
||||||
|
return a == AlgECDAA |
||||||
|
} |
||||||
|
|
||||||
|
func decodeKDFScheme(in *bytes.Buffer) (*KDFScheme, error) { |
||||||
|
var scheme KDFScheme |
||||||
|
if err := UnpackBuf(in, &scheme.Alg); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Alg: %v", err) |
||||||
|
} |
||||||
|
if scheme.Alg == AlgNull { |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
if err := UnpackBuf(in, &scheme.Hash); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Hash: %v", err) |
||||||
|
} |
||||||
|
return &scheme, nil |
||||||
|
} |
||||||
|
func decodeSymScheme(in *bytes.Buffer) (*SymScheme, error) { |
||||||
|
var scheme SymScheme |
||||||
|
if err := UnpackBuf(in, &scheme.Alg); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Alg: %v", err) |
||||||
|
} |
||||||
|
if scheme.Alg == AlgNull { |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
if err := UnpackBuf(in, &scheme.KeyBits, &scheme.Mode); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding KeyBits, Mode: %v", err) |
||||||
|
} |
||||||
|
return &scheme, nil |
||||||
|
} |
||||||
|
func decodeECCParams(in *bytes.Buffer) (*ECCParams, error) { |
||||||
|
var params ECCParams |
||||||
|
var err error |
||||||
|
|
||||||
|
if params.Symmetric, err = decodeSymScheme(in); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Symmetric: %v", err) |
||||||
|
} |
||||||
|
if params.Sign, err = decodeSigScheme(in); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Sign: %v", err) |
||||||
|
} |
||||||
|
if err := UnpackBuf(in, ¶ms.CurveID); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding CurveID: %v", err) |
||||||
|
} |
||||||
|
if params.KDF, err = decodeKDFScheme(in); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding KDF: %v", err) |
||||||
|
} |
||||||
|
var x, y []byte |
||||||
|
if err := UnpackBuf(in, &x, &y); err != nil { |
||||||
|
return nil, fmt.Errorf("decoding Point: %v", err) |
||||||
|
} |
||||||
|
params.Point.X = new(big.Int).SetBytes(x) |
||||||
|
params.Point.Y = new(big.Int).SetBytes(y) |
||||||
|
return ¶ms, nil |
||||||
|
} |
||||||
|
|
||||||
|
// ECCParams represents parameters of an ECC key pair.
|
||||||
|
//
|
||||||
|
// Symmetric, Sign and KDF may be nil, depending on key Attributes in Public.
|
||||||
|
type ECCParams struct { |
||||||
|
Symmetric *SymScheme |
||||||
|
Sign *SigScheme |
||||||
|
CurveID EllipticCurve |
||||||
|
KDF *KDFScheme |
||||||
|
Point ECPoint |
||||||
|
} |
||||||
|
|
||||||
|
// EllipticCurve identifies specific EC curves.
|
||||||
|
type EllipticCurve uint16 |
||||||
|
|
||||||
|
// ECC curves supported by TPM 2.0 spec.
|
||||||
|
const ( |
||||||
|
CurveNISTP192 = EllipticCurve(iota + 1) |
||||||
|
CurveNISTP224 |
||||||
|
CurveNISTP256 |
||||||
|
CurveNISTP384 |
||||||
|
CurveNISTP521 |
||||||
|
|
||||||
|
CurveBNP256 = EllipticCurve(iota + 10) |
||||||
|
CurveBNP638 |
||||||
|
|
||||||
|
CurveSM2P256 = EllipticCurve(0x0020) |
||||||
|
) |
||||||
|
|
||||||
|
// ECPoint represents a ECC coordinates for a point.
|
||||||
|
type ECPoint struct { |
||||||
|
X, Y *big.Int |
||||||
|
} |
||||||
|
|
||||||
|
// KDFScheme represents a KDF (Key Derivation Function) scheme.
|
||||||
|
type KDFScheme struct { |
||||||
|
Alg Algorithm |
||||||
|
Hash Algorithm |
||||||
|
} |
@ -0,0 +1,136 @@ |
|||||||
|
package protocol |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/duo-labs/webauthn/protocol/webauthncose" |
||||||
|
) |
||||||
|
|
||||||
|
type CredentialCreation struct { |
||||||
|
Response PublicKeyCredentialCreationOptions `json:"publicKey"` |
||||||
|
} |
||||||
|
|
||||||
|
type CredentialAssertion struct { |
||||||
|
Response PublicKeyCredentialRequestOptions `json:"publicKey"` |
||||||
|
} |
||||||
|
|
||||||
|
// In order to create a Credential via create(), the caller specifies a few parameters in a CredentialCreationOptions object.
|
||||||
|
// See §5.4. Options for Credential Creation https://www.w3.org/TR/webauthn/#dictionary-makecredentialoptions
|
||||||
|
type PublicKeyCredentialCreationOptions struct { |
||||||
|
Challenge Challenge `json:"challenge"` |
||||||
|
RelyingParty RelyingPartyEntity `json:"rp"` |
||||||
|
User UserEntity `json:"user"` |
||||||
|
Parameters []CredentialParameter `json:"pubKeyCredParams,omitempty"` |
||||||
|
AuthenticatorSelection AuthenticatorSelection `json:"authenticatorSelection,omitempty"` |
||||||
|
Timeout int `json:"timeout,omitempty"` |
||||||
|
CredentialExcludeList []CredentialDescriptor `json:"excludeCredentials,omitempty"` |
||||||
|
Extensions AuthenticationExtensions `json:"extensions,omitempty"` |
||||||
|
Attestation ConveyancePreference `json:"attestation,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
// The PublicKeyCredentialRequestOptions dictionary supplies get() with the data it needs to generate an assertion.
|
||||||
|
// Its challenge member MUST be present, while its other members are OPTIONAL.
|
||||||
|
// See §5.5. Options for Assertion Generation https://www.w3.org/TR/webauthn/#assertion-options
|
||||||
|
type PublicKeyCredentialRequestOptions struct { |
||||||
|
Challenge Challenge `json:"challenge"` |
||||||
|
Timeout int `json:"timeout,omitempty"` |
||||||
|
RelyingPartyID string `json:"rpId,omitempty"` |
||||||
|
AllowedCredentials []CredentialDescriptor `json:"allowCredentials,omitempty"` |
||||||
|
UserVerification UserVerificationRequirement `json:"userVerification,omitempty"` // Default is "preferred"
|
||||||
|
Extensions AuthenticationExtensions `json:"extensions,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
// This dictionary contains the attributes that are specified by a caller when referring to a public
|
||||||
|
// key credential as an input parameter to the create() or get() methods. It mirrors the fields of
|
||||||
|
// the PublicKeyCredential object returned by the latter methods.
|
||||||
|
// See §5.10.3. Credential Descriptor https://www.w3.org/TR/webauthn/#credential-dictionary
|
||||||
|
type CredentialDescriptor struct { |
||||||
|
// The valid credential types.
|
||||||
|
Type CredentialType `json:"type"` |
||||||
|
// CredentialID The ID of a credential to allow/disallow
|
||||||
|
CredentialID []byte `json:"id"` |
||||||
|
// The authenticator transports that can be used
|
||||||
|
Transport []AuthenticatorTransport `json:"transports,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
// CredentialParameter is the credential type and algorithm
|
||||||
|
// that the relying party wants the authenticator to create
|
||||||
|
type CredentialParameter struct { |
||||||
|
Type CredentialType `json:"type"` |
||||||
|
Algorithm webauthncose.COSEAlgorithmIdentifier `json:"alg"` |
||||||
|
} |
||||||
|
|
||||||
|
// This enumeration defines the valid credential types.
|
||||||
|
// It is an extension point; values can be added to it in the future, as
|
||||||
|
// more credential types are defined. The values of this enumeration are used
|
||||||
|
// for versioning the Authentication Assertion and attestation structures according
|
||||||
|
// to the type of the authenticator.
|
||||||
|
// See §5.10.3. Credential Descriptor https://www.w3.org/TR/webauthn/#credentialType
|
||||||
|
type CredentialType string |
||||||
|
|
||||||
|
const ( |
||||||
|
// PublicKeyCredentialType - Currently one credential type is defined, namely "public-key".
|
||||||
|
PublicKeyCredentialType CredentialType = "public-key" |
||||||
|
) |
||||||
|
|
||||||
|
// AuthenticationExtensions - referred to as AuthenticationExtensionsClientInputs in the
|
||||||
|
// spec document, this member contains additional parameters requesting additional processing
|
||||||
|
// by the client and authenticator.
|
||||||
|
// This is currently under development
|
||||||
|
type AuthenticationExtensions map[string]interface{} |
||||||
|
|
||||||
|
// WebAuthn Relying Parties may use the AuthenticatorSelectionCriteria dictionary to specify their requirements
|
||||||
|
// regarding authenticator attributes. See §5.4.4. Authenticator Selection Criteria
|
||||||
|
// https://www.w3.org/TR/webauthn/#authenticatorSelection
|
||||||
|
type AuthenticatorSelection struct { |
||||||
|
// AuthenticatorAttachment If this member is present, eligible authenticators are filtered to only
|
||||||
|
// authenticators attached with the specified AuthenticatorAttachment enum
|
||||||
|
AuthenticatorAttachment AuthenticatorAttachment `json:"authenticatorAttachment,omitempty"` |
||||||
|
// RequireResidentKey this member describes the Relying Party's requirements regarding resident
|
||||||
|
// credentials. If the parameter is set to true, the authenticator MUST create a client-side-resident
|
||||||
|
// public key credential source when creating a public key credential.
|
||||||
|
RequireResidentKey *bool `json:"requireResidentKey,omitempty"` |
||||||
|
// UserVerification This member describes the Relying Party's requirements regarding user verification for
|
||||||
|
// the create() operation. Eligible authenticators are filtered to only those capable of satisfying this
|
||||||
|
// requirement.
|
||||||
|
UserVerification UserVerificationRequirement `json:"userVerification,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
// WebAuthn Relying Parties may use AttestationConveyancePreference to specify their preference regarding
|
||||||
|
// attestation conveyance during credential generation. See §5.4.6. https://www.w3.org/TR/webauthn/#attestation-convey
|
||||||
|
type ConveyancePreference string |
||||||
|
|
||||||
|
const ( |
||||||
|
// The default value. This value indicates that the Relying Party is not interested in authenticator attestation. For example,
|
||||||
|
// in order to potentially avoid having to obtain user consent to relay identifying information to the Relying Party, or to
|
||||||
|
// save a roundtrip to an Attestation CA.
|
||||||
|
PreferNoAttestation ConveyancePreference = "none" |
||||||
|
// This value indicates that the Relying Party prefers an attestation conveyance yielding verifiable attestation
|
||||||
|
// statements, but allows the client to decide how to obtain such attestation statements. The client MAY replace
|
||||||
|
// the authenticator-generated attestation statements with attestation statements generated by an Anonymization
|
||||||
|
// CA, in order to protect the user’s privacy, or to assist Relying Parties with attestation verification in a
|
||||||
|
// heterogeneous ecosystem.
|
||||||
|
PreferIndirectAttestation ConveyancePreference = "indirect" |
||||||
|
// This value indicates that the Relying Party wants to receive the attestation statement as generated by the authenticator.
|
||||||
|
PreferDirectAttestation ConveyancePreference = "direct" |
||||||
|
) |
||||||
|
|
||||||
|
func (a *PublicKeyCredentialRequestOptions) GetAllowedCredentialIDs() [][]byte { |
||||||
|
var allowedCredentialIDs = make([][]byte, len(a.AllowedCredentials)) |
||||||
|
for i, credential := range a.AllowedCredentials { |
||||||
|
allowedCredentialIDs[i] = credential.CredentialID |
||||||
|
} |
||||||
|
return allowedCredentialIDs |
||||||
|
} |
||||||
|
|
||||||
|
type Extensions interface{} |
||||||
|
|
||||||
|
type ServerResponse struct { |
||||||
|
Status ServerResponseStatus `json:"status"` |
||||||
|
Message string `json:"errorMessage"` |
||||||
|
} |
||||||
|
|
||||||
|
type ServerResponseStatus string |
||||||
|
|
||||||
|
const ( |
||||||
|
StatusOk ServerResponseStatus = "ok" |
||||||
|
StatusFailed ServerResponseStatus = "failed" |
||||||
|
) |
@ -0,0 +1 @@ |
|||||||
|
package protocol |
@ -0,0 +1,12 @@ |
|||||||
|
// +build go1.13
|
||||||
|
|
||||||
|
package webauthncose |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/ed25519" |
||||||
|
"crypto/x509" |
||||||
|
) |
||||||
|
|
||||||
|
func marshalEd25519PublicKey(pub ed25519.PublicKey) ([]byte, error) { |
||||||
|
return x509.MarshalPKIXPublicKey(pub) |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
// +build !go1.13
|
||||||
|
|
||||||
|
package webauthncose |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/x509/pkix" |
||||||
|
"encoding/asn1" |
||||||
|
|
||||||
|
"golang.org/x/crypto/ed25519" |
||||||
|
) |
||||||
|
|
||||||
|
var oidSignatureEd25519 = asn1.ObjectIdentifier{1, 3, 101, 112} |
||||||
|
|
||||||
|
type pkixPublicKey struct { |
||||||
|
Algo pkix.AlgorithmIdentifier |
||||||
|
BitString asn1.BitString |
||||||
|
} |
||||||
|
|
||||||
|
// marshalEd25519PublicKey is a backport of the functionality introduced in
|
||||||
|
// Go v1.13.
|
||||||
|
// Ref: https://golang.org/doc/go1.13#crypto/ed25519
|
||||||
|
// Ref: https://golang.org/doc/go1.13#crypto/x509
|
||||||
|
func marshalEd25519PublicKey(pub ed25519.PublicKey) ([]byte, error) { |
||||||
|
publicKeyBytes := pub |
||||||
|
var publicKeyAlgorithm pkix.AlgorithmIdentifier |
||||||
|
publicKeyAlgorithm.Algorithm = oidSignatureEd25519 |
||||||
|
|
||||||
|
pkix := pkixPublicKey{ |
||||||
|
Algo: publicKeyAlgorithm, |
||||||
|
BitString: asn1.BitString{ |
||||||
|
Bytes: publicKeyBytes, |
||||||
|
BitLength: 8 * len(publicKeyBytes), |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
ret, _ := asn1.Marshal(pkix) |
||||||
|
return ret, nil |
||||||
|
} |
@ -0,0 +1,400 @@ |
|||||||
|
package webauthncose |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto" |
||||||
|
"crypto/ecdsa" |
||||||
|
"crypto/elliptic" |
||||||
|
"crypto/rsa" |
||||||
|
"crypto/x509" |
||||||
|
"encoding/asn1" |
||||||
|
"encoding/pem" |
||||||
|
"fmt" |
||||||
|
"hash" |
||||||
|
"math/big" |
||||||
|
|
||||||
|
"github.com/fxamacker/cbor/v2" |
||||||
|
"golang.org/x/crypto/ed25519" |
||||||
|
) |
||||||
|
|
||||||
|
// PublicKeyData The public key portion of a Relying Party-specific credential key pair, generated
|
||||||
|
// by an authenticator and returned to a Relying Party at registration time. We unpack this object
|
||||||
|
// using fxamacker's cbor library ("github.com/fxamacker/cbor/v2") which is why there are cbor tags
|
||||||
|
// included. The tag field values correspond to the IANA COSE keys that give their respective
|
||||||
|
// values.
|
||||||
|
// See §6.4.1.1 https://www.w3.org/TR/webauthn/#sctn-encoded-credPubKey-examples for examples of this
|
||||||
|
// COSE data.
|
||||||
|
type PublicKeyData struct { |
||||||
|
// Decode the results to int by default.
|
||||||
|
_struct bool `cbor:",keyasint" json:"public_key"` |
||||||
|
// The type of key created. Should be OKP, EC2, or RSA.
|
||||||
|
KeyType int64 `cbor:"1,keyasint" json:"kty"` |
||||||
|
// A COSEAlgorithmIdentifier for the algorithm used to derive the key signature.
|
||||||
|
Algorithm int64 `cbor:"3,keyasint" json:"alg"` |
||||||
|
} |
||||||
|
type EC2PublicKeyData struct { |
||||||
|
PublicKeyData |
||||||
|
// If the key type is EC2, the curve on which we derive the signature from.
|
||||||
|
Curve int64 `cbor:"-1,keyasint,omitempty" json:"crv"` |
||||||
|
// A byte string 32 bytes in length that holds the x coordinate of the key.
|
||||||
|
XCoord []byte `cbor:"-2,keyasint,omitempty" json:"x"` |
||||||
|
// A byte string 32 bytes in length that holds the y coordinate of the key.
|
||||||
|
YCoord []byte `cbor:"-3,keyasint,omitempty" json:"y"` |
||||||
|
} |
||||||
|
|
||||||
|
type RSAPublicKeyData struct { |
||||||
|
PublicKeyData |
||||||
|
// Represents the modulus parameter for the RSA algorithm
|
||||||
|
Modulus []byte `cbor:"-1,keyasint,omitempty" json:"n"` |
||||||
|
// Represents the exponent parameter for the RSA algorithm
|
||||||
|
Exponent []byte `cbor:"-2,keyasint,omitempty" json:"e"` |
||||||
|
} |
||||||
|
|
||||||
|
type OKPPublicKeyData struct { |
||||||
|
PublicKeyData |
||||||
|
Curve int64 |
||||||
|
// A byte string that holds the x coordinate of the key.
|
||||||
|
XCoord []byte `cbor:"-2,keyasint,omitempty" json:"x"` |
||||||
|
} |
||||||
|
|
||||||
|
// Verify Octet Key Pair (OKP) Public Key Signature
|
||||||
|
func (k *OKPPublicKeyData) Verify(data []byte, sig []byte) (bool, error) { |
||||||
|
var key ed25519.PublicKey = make([]byte, ed25519.PublicKeySize) |
||||||
|
copy(key, k.XCoord) |
||||||
|
return ed25519.Verify(key, data, sig), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Verify Elliptic Curce Public Key Signature
|
||||||
|
func (k *EC2PublicKeyData) Verify(data []byte, sig []byte) (bool, error) { |
||||||
|
var curve elliptic.Curve |
||||||
|
switch COSEAlgorithmIdentifier(k.Algorithm) { |
||||||
|
case AlgES512: // IANA COSE code for ECDSA w/ SHA-512
|
||||||
|
curve = elliptic.P521() |
||||||
|
case AlgES384: // IANA COSE code for ECDSA w/ SHA-384
|
||||||
|
curve = elliptic.P384() |
||||||
|
case AlgES256: // IANA COSE code for ECDSA w/ SHA-256
|
||||||
|
curve = elliptic.P256() |
||||||
|
default: |
||||||
|
return false, ErrUnsupportedAlgorithm |
||||||
|
} |
||||||
|
|
||||||
|
pubkey := &ecdsa.PublicKey{ |
||||||
|
Curve: curve, |
||||||
|
X: big.NewInt(0).SetBytes(k.XCoord), |
||||||
|
Y: big.NewInt(0).SetBytes(k.YCoord), |
||||||
|
} |
||||||
|
|
||||||
|
type ECDSASignature struct { |
||||||
|
R, S *big.Int |
||||||
|
} |
||||||
|
|
||||||
|
e := &ECDSASignature{} |
||||||
|
f := HasherFromCOSEAlg(COSEAlgorithmIdentifier(k.PublicKeyData.Algorithm)) |
||||||
|
h := f() |
||||||
|
h.Write(data) |
||||||
|
_, err := asn1.Unmarshal(sig, e) |
||||||
|
if err != nil { |
||||||
|
return false, ErrSigNotProvidedOrInvalid |
||||||
|
} |
||||||
|
return ecdsa.Verify(pubkey, h.Sum(nil), e.R, e.S), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Verify RSA Public Key Signature
|
||||||
|
func (k *RSAPublicKeyData) Verify(data []byte, sig []byte) (bool, error) { |
||||||
|
pubkey := &rsa.PublicKey{ |
||||||
|
N: big.NewInt(0).SetBytes(k.Modulus), |
||||||
|
E: int(uint(k.Exponent[2]) | uint(k.Exponent[1])<<8 | uint(k.Exponent[0])<<16), |
||||||
|
} |
||||||
|
|
||||||
|
f := HasherFromCOSEAlg(COSEAlgorithmIdentifier(k.PublicKeyData.Algorithm)) |
||||||
|
h := f() |
||||||
|
h.Write(data) |
||||||
|
|
||||||
|
var hash crypto.Hash |
||||||
|
switch COSEAlgorithmIdentifier(k.PublicKeyData.Algorithm) { |
||||||
|
case AlgRS1: |
||||||
|
hash = crypto.SHA1 |
||||||
|
case AlgPS256, AlgRS256: |
||||||
|
hash = crypto.SHA256 |
||||||
|
case AlgPS384, AlgRS384: |
||||||
|
hash = crypto.SHA384 |
||||||
|
case AlgPS512, AlgRS512: |
||||||
|
hash = crypto.SHA512 |
||||||
|
default: |
||||||
|
return false, ErrUnsupportedAlgorithm |
||||||
|
} |
||||||
|
switch COSEAlgorithmIdentifier(k.PublicKeyData.Algorithm) { |
||||||
|
case AlgPS256, AlgPS384, AlgPS512: |
||||||
|
err := rsa.VerifyPSS(pubkey, hash, h.Sum(nil), sig, nil) |
||||||
|
return err == nil, err |
||||||
|
|
||||||
|
case AlgRS1, AlgRS256, AlgRS384, AlgRS512: |
||||||
|
err := rsa.VerifyPKCS1v15(pubkey, hash, h.Sum(nil), sig) |
||||||
|
return err == nil, err |
||||||
|
default: |
||||||
|
return false, ErrUnsupportedAlgorithm |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Return which signature algorithm is being used from the COSE Key
|
||||||
|
func SigAlgFromCOSEAlg(coseAlg COSEAlgorithmIdentifier) SignatureAlgorithm { |
||||||
|
for _, details := range SignatureAlgorithmDetails { |
||||||
|
if details.coseAlg == coseAlg { |
||||||
|
return details.algo |
||||||
|
} |
||||||
|
} |
||||||
|
return UnknownSignatureAlgorithm |
||||||
|
} |
||||||
|
|
||||||
|
// Return the Hashing interface to be used for a given COSE Algorithm
|
||||||
|
func HasherFromCOSEAlg(coseAlg COSEAlgorithmIdentifier) func() hash.Hash { |
||||||
|
for _, details := range SignatureAlgorithmDetails { |
||||||
|
if details.coseAlg == coseAlg { |
||||||
|
return details.hasher |
||||||
|
} |
||||||
|
} |
||||||
|
// default to SHA256? Why not.
|
||||||
|
return crypto.SHA256.New |
||||||
|
} |
||||||
|
|
||||||
|
// Figure out what kind of COSE material was provided and create the data for the new key
|
||||||
|
func ParsePublicKey(keyBytes []byte) (interface{}, error) { |
||||||
|
pk := PublicKeyData{} |
||||||
|
cbor.Unmarshal(keyBytes, &pk) |
||||||
|
switch COSEKeyType(pk.KeyType) { |
||||||
|
case OctetKey: |
||||||
|
var o OKPPublicKeyData |
||||||
|
cbor.Unmarshal(keyBytes, &o) |
||||||
|
o.PublicKeyData = pk |
||||||
|
return o, nil |
||||||
|
case EllipticKey: |
||||||
|
var e EC2PublicKeyData |
||||||
|
cbor.Unmarshal(keyBytes, &e) |
||||||
|
e.PublicKeyData = pk |
||||||
|
return e, nil |
||||||
|
case RSAKey: |
||||||
|
var r RSAPublicKeyData |
||||||
|
cbor.Unmarshal(keyBytes, &r) |
||||||
|
r.PublicKeyData = pk |
||||||
|
return r, nil |
||||||
|
default: |
||||||
|
return nil, ErrUnsupportedKey |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ParseFIDOPublicKey is only used when the appID extension is configured by the assertion response.
|
||||||
|
func ParseFIDOPublicKey(keyBytes []byte) (EC2PublicKeyData, error) { |
||||||
|
x, y := elliptic.Unmarshal(elliptic.P256(), keyBytes) |
||||||
|
|
||||||
|
return EC2PublicKeyData{ |
||||||
|
PublicKeyData: PublicKeyData{ |
||||||
|
Algorithm: int64(AlgES256), |
||||||
|
}, |
||||||
|
XCoord: x.Bytes(), |
||||||
|
YCoord: y.Bytes(), |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
// COSEAlgorithmIdentifier From §5.10.5. A number identifying a cryptographic algorithm. The algorithm
|
||||||
|
// identifiers SHOULD be values registered in the IANA COSE Algorithms registry
|
||||||
|
// [https://www.w3.org/TR/webauthn/#biblio-iana-cose-algs-reg], for instance, -7 for "ES256"
|
||||||
|
// and -257 for "RS256".
|
||||||
|
type COSEAlgorithmIdentifier int |
||||||
|
|
||||||
|
const ( |
||||||
|
// AlgES256 ECDSA with SHA-256
|
||||||
|
AlgES256 COSEAlgorithmIdentifier = -7 |
||||||
|
// AlgES384 ECDSA with SHA-384
|
||||||
|
AlgES384 COSEAlgorithmIdentifier = -35 |
||||||
|
// AlgES512 ECDSA with SHA-512
|
||||||
|
AlgES512 COSEAlgorithmIdentifier = -36 |
||||||
|
// AlgRS1 RSASSA-PKCS1-v1_5 with SHA-1
|
||||||
|
AlgRS1 COSEAlgorithmIdentifier = -65535 |
||||||
|
// AlgRS256 RSASSA-PKCS1-v1_5 with SHA-256
|
||||||
|
AlgRS256 COSEAlgorithmIdentifier = -257 |
||||||
|
// AlgRS384 RSASSA-PKCS1-v1_5 with SHA-384
|
||||||
|
AlgRS384 COSEAlgorithmIdentifier = -258 |
||||||
|
// AlgRS512 RSASSA-PKCS1-v1_5 with SHA-512
|
||||||
|
AlgRS512 COSEAlgorithmIdentifier = -259 |
||||||
|
// AlgPS256 RSASSA-PSS with SHA-256
|
||||||
|
AlgPS256 COSEAlgorithmIdentifier = -37 |
||||||
|
// AlgPS384 RSASSA-PSS with SHA-384
|
||||||
|
AlgPS384 COSEAlgorithmIdentifier = -38 |
||||||
|
// AlgPS512 RSASSA-PSS with SHA-512
|
||||||
|
AlgPS512 COSEAlgorithmIdentifier = -39 |
||||||
|
// AlgEdDSA EdDSA
|
||||||
|
AlgEdDSA COSEAlgorithmIdentifier = -8 |
||||||
|
) |
||||||
|
|
||||||
|
// The Key Type derived from the IANA COSE AuthData
|
||||||
|
type COSEKeyType int |
||||||
|
|
||||||
|
const ( |
||||||
|
// OctetKey is an Octet Key
|
||||||
|
OctetKey COSEKeyType = 1 |
||||||
|
// EllipticKey is an Elliptic Curve Public Key
|
||||||
|
EllipticKey COSEKeyType = 2 |
||||||
|
// RSAKey is an RSA Public Key
|
||||||
|
RSAKey COSEKeyType = 3 |
||||||
|
) |
||||||
|
|
||||||
|
func VerifySignature(key interface{}, data []byte, sig []byte) (bool, error) { |
||||||
|
|
||||||
|
switch key.(type) { |
||||||
|
case OKPPublicKeyData: |
||||||
|
o := key.(OKPPublicKeyData) |
||||||
|
return o.Verify(data, sig) |
||||||
|
case EC2PublicKeyData: |
||||||
|
e := key.(EC2PublicKeyData) |
||||||
|
return e.Verify(data, sig) |
||||||
|
case RSAPublicKeyData: |
||||||
|
r := key.(RSAPublicKeyData) |
||||||
|
return r.Verify(data, sig) |
||||||
|
default: |
||||||
|
return false, ErrUnsupportedKey |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func DisplayPublicKey(cpk []byte) string { |
||||||
|
parsedKey, err := ParsePublicKey(cpk) |
||||||
|
if err != nil { |
||||||
|
return "Cannot display key" |
||||||
|
} |
||||||
|
switch parsedKey.(type) { |
||||||
|
case RSAPublicKeyData: |
||||||
|
pKey := parsedKey.(RSAPublicKeyData) |
||||||
|
rKey := &rsa.PublicKey{ |
||||||
|
N: big.NewInt(0).SetBytes(pKey.Modulus), |
||||||
|
E: int(uint(pKey.Exponent[2]) | uint(pKey.Exponent[1])<<8 | uint(pKey.Exponent[0])<<16), |
||||||
|
} |
||||||
|
data, err := x509.MarshalPKIXPublicKey(rKey) |
||||||
|
if err != nil { |
||||||
|
return "Cannot display key" |
||||||
|
} |
||||||
|
pemBytes := pem.EncodeToMemory(&pem.Block{ |
||||||
|
Type: "RSA PUBLIC KEY", |
||||||
|
Bytes: data, |
||||||
|
}) |
||||||
|
return fmt.Sprintf("%s", pemBytes) |
||||||
|
case EC2PublicKeyData: |
||||||
|
pKey := parsedKey.(EC2PublicKeyData) |
||||||
|
var curve elliptic.Curve |
||||||
|
switch COSEAlgorithmIdentifier(pKey.Algorithm) { |
||||||
|
case AlgES256: |
||||||
|
curve = elliptic.P256() |
||||||
|
case AlgES384: |
||||||
|
curve = elliptic.P384() |
||||||
|
case AlgES512: |
||||||
|
curve = elliptic.P521() |
||||||
|
default: |
||||||
|
return "Cannot display key" |
||||||
|
} |
||||||
|
eKey := &ecdsa.PublicKey{ |
||||||
|
Curve: curve, |
||||||
|
X: big.NewInt(0).SetBytes(pKey.XCoord), |
||||||
|
Y: big.NewInt(0).SetBytes(pKey.YCoord), |
||||||
|
} |
||||||
|
data, err := x509.MarshalPKIXPublicKey(eKey) |
||||||
|
if err != nil { |
||||||
|
return "Cannot display key" |
||||||
|
} |
||||||
|
pemBytes := pem.EncodeToMemory(&pem.Block{ |
||||||
|
Type: "PUBLIC KEY", |
||||||
|
Bytes: data, |
||||||
|
}) |
||||||
|
return fmt.Sprintf("%s", pemBytes) |
||||||
|
case OKPPublicKeyData: |
||||||
|
pKey := parsedKey.(OKPPublicKeyData) |
||||||
|
if len(pKey.XCoord) != ed25519.PublicKeySize { |
||||||
|
return "Cannot display key" |
||||||
|
} |
||||||
|
var oKey ed25519.PublicKey = make([]byte, ed25519.PublicKeySize) |
||||||
|
copy(oKey, pKey.XCoord) |
||||||
|
data, err := marshalEd25519PublicKey(oKey) |
||||||
|
if err != nil { |
||||||
|
return "Cannot display key" |
||||||
|
} |
||||||
|
pemBytes := pem.EncodeToMemory(&pem.Block{ |
||||||
|
Type: "PUBLIC KEY", |
||||||
|
Bytes: data, |
||||||
|
}) |
||||||
|
return fmt.Sprintf("%s", pemBytes) |
||||||
|
|
||||||
|
default: |
||||||
|
return "Cannot display key of this type" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Algorithm enumerations used for
|
||||||
|
type SignatureAlgorithm int |
||||||
|
|
||||||
|
const ( |
||||||
|
UnknownSignatureAlgorithm SignatureAlgorithm = iota |
||||||
|
MD2WithRSA |
||||||
|
MD5WithRSA |
||||||
|
SHA1WithRSA |
||||||
|
SHA256WithRSA |
||||||
|
SHA384WithRSA |
||||||
|
SHA512WithRSA |
||||||
|
DSAWithSHA1 |
||||||
|
DSAWithSHA256 |
||||||
|
ECDSAWithSHA1 |
||||||
|
ECDSAWithSHA256 |
||||||
|
ECDSAWithSHA384 |
||||||
|
ECDSAWithSHA512 |
||||||
|
SHA256WithRSAPSS |
||||||
|
SHA384WithRSAPSS |
||||||
|
SHA512WithRSAPSS |
||||||
|
) |
||||||
|
|
||||||
|
var SignatureAlgorithmDetails = []struct { |
||||||
|
algo SignatureAlgorithm |
||||||
|
coseAlg COSEAlgorithmIdentifier |
||||||
|
name string |
||||||
|
hasher func() hash.Hash |
||||||
|
}{ |
||||||
|
{SHA1WithRSA, AlgRS1, "SHA1-RSA", crypto.SHA1.New}, |
||||||
|
{SHA256WithRSA, AlgRS256, "SHA256-RSA", crypto.SHA256.New}, |
||||||
|
{SHA384WithRSA, AlgRS384, "SHA384-RSA", crypto.SHA384.New}, |
||||||
|
{SHA512WithRSA, AlgRS512, "SHA512-RSA", crypto.SHA512.New}, |
||||||
|
{SHA256WithRSAPSS, AlgPS256, "SHA256-RSAPSS", crypto.SHA256.New}, |
||||||
|
{SHA384WithRSAPSS, AlgPS384, "SHA384-RSAPSS", crypto.SHA384.New}, |
||||||
|
{SHA512WithRSAPSS, AlgPS512, "SHA512-RSAPSS", crypto.SHA512.New}, |
||||||
|
{ECDSAWithSHA256, AlgES256, "ECDSA-SHA256", crypto.SHA256.New}, |
||||||
|
{ECDSAWithSHA384, AlgES384, "ECDSA-SHA384", crypto.SHA384.New}, |
||||||
|
{ECDSAWithSHA512, AlgES512, "ECDSA-SHA512", crypto.SHA512.New}, |
||||||
|
{UnknownSignatureAlgorithm, AlgEdDSA, "EdDSA", crypto.SHA512.New}, |
||||||
|
} |
||||||
|
|
||||||
|
type Error struct { |
||||||
|
// Short name for the type of error that has occurred
|
||||||
|
Type string `json:"type"` |
||||||
|
// Additional details about the error
|
||||||
|
Details string `json:"error"` |
||||||
|
// Information to help debug the error
|
||||||
|
DevInfo string `json:"debug"` |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
ErrUnsupportedKey = &Error{ |
||||||
|
Type: "invalid_key_type", |
||||||
|
Details: "Unsupported Public Key Type", |
||||||
|
} |
||||||
|
ErrUnsupportedAlgorithm = &Error{ |
||||||
|
Type: "unsupported_key_algorithm", |
||||||
|
Details: "Unsupported public key algorithm", |
||||||
|
} |
||||||
|
ErrSigNotProvidedOrInvalid = &Error{ |
||||||
|
Type: "signature_not_provided_or_invalid", |
||||||
|
Details: "Signature invalid or not provided", |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
func (err *Error) Error() string { |
||||||
|
return err.Details |
||||||
|
} |
||||||
|
|
||||||
|
func (passedError *Error) WithDetails(details string) *Error { |
||||||
|
err := *passedError |
||||||
|
err.Details = details |
||||||
|
return &err |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
package webauthn |
||||||
|
|
||||||
|
import ( |
||||||
|
p "github.com/duo-labs/webauthn/protocol" |
||||||
|
) |
||||||
|
|
||||||
|
type Authenticator struct { |
||||||
|
// The AAGUID of the authenticator. An AAGUID is defined as an array containing the globally unique
|
||||||
|
// identifier of the authenticator model being sought.
|
||||||
|
AAGUID []byte |
||||||
|
// SignCount -Upon a new login operation, the Relying Party compares the stored signature counter value
|
||||||
|
// with the new signCount value returned in the assertion’s authenticator data. If this new
|
||||||
|
// signCount value is less than or equal to the stored value, a cloned authenticator may
|
||||||
|
// exist, or the authenticator may be malfunctioning.
|
||||||
|
SignCount uint32 |
||||||
|
// CloneWarning - This is a signal that the authenticator may be cloned, i.e. at least two copies of the
|
||||||
|
// credential private key may exist and are being used in parallel. Relying Parties should incorporate
|
||||||
|
// this information into their risk scoring. Whether the Relying Party updates the stored signature
|
||||||
|
// counter value in this case, or not, or fails the authentication ceremony or not, is Relying Party-specific.
|
||||||
|
CloneWarning bool |
||||||
|
} |
||||||
|
|
||||||
|
// Allow for easy marhsalling of authenticator options that are provided to the user
|
||||||
|
func SelectAuthenticator(att string, rrk *bool, uv string) p.AuthenticatorSelection { |
||||||
|
return p.AuthenticatorSelection{ |
||||||
|
AuthenticatorAttachment: p.AuthenticatorAttachment(att), |
||||||
|
RequireResidentKey: rrk, |
||||||
|
UserVerification: p.UserVerificationRequirement(uv), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// VerifyCounter
|
||||||
|
// Step 17 of §7.2. about verifying attestation. If the signature counter value authData.signCount
|
||||||
|
// is nonzero or the value stored in conjunction with credential’s id attribute is nonzero, then
|
||||||
|
// run the following sub-step:
|
||||||
|
//
|
||||||
|
// If the signature counter value authData.signCount is
|
||||||
|
//
|
||||||
|
// → Greater than the signature counter value stored in conjunction with credential’s id attribute.
|
||||||
|
// Update the stored signature counter value, associated with credential’s id attribute, to be the value of
|
||||||
|
// authData.signCount.
|
||||||
|
//
|
||||||
|
// → Less than or equal to the signature counter value stored in conjunction with credential’s id attribute.
|
||||||
|
// This is a signal that the authenticator may be cloned, see CloneWarning above for more information.
|
||||||
|
func (a *Authenticator) UpdateCounter(authDataCount uint32) { |
||||||
|
if authDataCount <= a.SignCount && (authDataCount != 0 || a.SignCount != 0) { |
||||||
|
a.CloneWarning = true |
||||||
|
return |
||||||
|
} |
||||||
|
a.SignCount = authDataCount |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
package webauthn |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/duo-labs/webauthn/protocol" |
||||||
|
) |
||||||
|
|
||||||
|
// Credential contains all needed information about a WebAuthn credential for storage
|
||||||
|
type Credential struct { |
||||||
|
// A probabilistically-unique byte sequence identifying a public key credential source and its authentication assertions.
|
||||||
|
ID []byte |
||||||
|
// The public key portion of a Relying Party-specific credential key pair, generated by an authenticator and returned to
|
||||||
|
// a Relying Party at registration time (see also public key credential). The private key portion of the credential key
|
||||||
|
// pair is known as the credential private key. Note that in the case of self attestation, the credential key pair is also
|
||||||
|
// used as the attestation key pair, see self attestation for details.
|
||||||
|
PublicKey []byte |
||||||
|
// The attestation format used (if any) by the authenticator when creating the credential.
|
||||||
|
AttestationType string |
||||||
|
// The Authenticator information for a given certificate
|
||||||
|
Authenticator Authenticator |
||||||
|
} |
||||||
|
|
||||||
|
// MakeNewCredential will return a credential pointer on successful validation of a registration response
|
||||||
|
func MakeNewCredential(c *protocol.ParsedCredentialCreationData) (*Credential, error) { |
||||||
|
newCredential := &Credential{ |
||||||
|
ID: c.Response.AttestationObject.AuthData.AttData.CredentialID, |
||||||
|
PublicKey: c.Response.AttestationObject.AuthData.AttData.CredentialPublicKey, |
||||||
|
AttestationType: c.Response.AttestationObject.Format, |
||||||
|
Authenticator: Authenticator{ |
||||||
|
AAGUID: c.Response.AttestationObject.AuthData.AttData.AAGUID, |
||||||
|
SignCount: c.Response.AttestationObject.AuthData.Counter, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
return newCredential, nil |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
// Contains the API functionality of the library. After creating and configuring a webauthn object, users can
|
||||||
|
// call the object to create and validate web authentication credentials.
|
||||||
|
package webauthn |
@ -0,0 +1,188 @@ |
|||||||
|
package webauthn |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/base64" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/protocol" |
||||||
|
) |
||||||
|
|
||||||
|
// BEGIN REGISTRATION
|
||||||
|
// These objects help us creat the CredentialCreationOptions
|
||||||
|
// that will be passed to the authenticator via the user client
|
||||||
|
|
||||||
|
// LoginOption is used to provide parameters that modify the default Credential Assertion Payload that is sent to the user.
|
||||||
|
type LoginOption func(*protocol.PublicKeyCredentialRequestOptions) |
||||||
|
|
||||||
|
// Creates the CredentialAssertion data payload that should be sent to the user agent for beginning the
|
||||||
|
// login/assertion process. The format of this data can be seen in §5.5 of the WebAuthn specification
|
||||||
|
// (https://www.w3.org/TR/webauthn/#assertion-options). These default values can be amended by providing
|
||||||
|
// additional LoginOption parameters. This function also returns sessionData, that must be stored by the
|
||||||
|
// RP in a secure manner and then provided to the FinishLogin function. This data helps us verify the
|
||||||
|
// ownership of the credential being retreived.
|
||||||
|
func (webauthn *WebAuthn) BeginLogin(user User, opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) { |
||||||
|
challenge, err := protocol.CreateChallenge() |
||||||
|
if err != nil { |
||||||
|
return nil, nil, err |
||||||
|
} |
||||||
|
|
||||||
|
credentials := user.WebAuthnCredentials() |
||||||
|
|
||||||
|
if len(credentials) == 0 { // If the user does not have any credentials, we cannot do login
|
||||||
|
return nil, nil, protocol.ErrBadRequest.WithDetails("Found no credentials for user") |
||||||
|
} |
||||||
|
|
||||||
|
var allowedCredentials = make([]protocol.CredentialDescriptor, len(credentials)) |
||||||
|
|
||||||
|
for i, credential := range credentials { |
||||||
|
var credentialDescriptor protocol.CredentialDescriptor |
||||||
|
credentialDescriptor.CredentialID = credential.ID |
||||||
|
credentialDescriptor.Type = protocol.PublicKeyCredentialType |
||||||
|
allowedCredentials[i] = credentialDescriptor |
||||||
|
} |
||||||
|
|
||||||
|
requestOptions := protocol.PublicKeyCredentialRequestOptions{ |
||||||
|
Challenge: challenge, |
||||||
|
Timeout: webauthn.Config.Timeout, |
||||||
|
RelyingPartyID: webauthn.Config.RPID, |
||||||
|
UserVerification: webauthn.Config.AuthenticatorSelection.UserVerification, |
||||||
|
AllowedCredentials: allowedCredentials, |
||||||
|
} |
||||||
|
|
||||||
|
for _, setter := range opts { |
||||||
|
setter(&requestOptions) |
||||||
|
} |
||||||
|
|
||||||
|
newSessionData := SessionData{ |
||||||
|
Challenge: base64.RawURLEncoding.EncodeToString(challenge), |
||||||
|
UserID: user.WebAuthnID(), |
||||||
|
AllowedCredentialIDs: requestOptions.GetAllowedCredentialIDs(), |
||||||
|
UserVerification: requestOptions.UserVerification, |
||||||
|
Extensions: requestOptions.Extensions, |
||||||
|
} |
||||||
|
|
||||||
|
response := protocol.CredentialAssertion{requestOptions} |
||||||
|
|
||||||
|
return &response, &newSessionData, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Updates the allowed credential list with Credential Descripiptors, discussed in §5.10.3
|
||||||
|
// (https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor) with user-supplied values
|
||||||
|
func WithAllowedCredentials(allowList []protocol.CredentialDescriptor) LoginOption { |
||||||
|
return func(cco *protocol.PublicKeyCredentialRequestOptions) { |
||||||
|
cco.AllowedCredentials = allowList |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Request a user verification preference
|
||||||
|
func WithUserVerification(userVerification protocol.UserVerificationRequirement) LoginOption { |
||||||
|
return func(cco *protocol.PublicKeyCredentialRequestOptions) { |
||||||
|
cco.UserVerification = userVerification |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Request additional extensions for assertion
|
||||||
|
func WithAssertionExtensions(extensions protocol.AuthenticationExtensions) LoginOption { |
||||||
|
return func(cco *protocol.PublicKeyCredentialRequestOptions) { |
||||||
|
cco.Extensions = extensions |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Take the response from the client and validate it against the user credentials and stored session data
|
||||||
|
func (webauthn *WebAuthn) FinishLogin(user User, session SessionData, response *http.Request) (*Credential, error) { |
||||||
|
parsedResponse, err := protocol.ParseCredentialRequestResponse(response) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return webauthn.ValidateLogin(user, session, parsedResponse) |
||||||
|
} |
||||||
|
|
||||||
|
// ValidateLogin takes a parsed response and validates it against the user credentials and session data
|
||||||
|
func (webauthn *WebAuthn) ValidateLogin(user User, session SessionData, parsedResponse *protocol.ParsedCredentialAssertionData) (*Credential, error) { |
||||||
|
if !bytes.Equal(user.WebAuthnID(), session.UserID) { |
||||||
|
return nil, protocol.ErrBadRequest.WithDetails("ID mismatch for User and Session") |
||||||
|
} |
||||||
|
|
||||||
|
// Step 1. If the allowCredentials option was given when this authentication ceremony was initiated,
|
||||||
|
// verify that credential.id identifies one of the public key credentials that were listed in
|
||||||
|
// allowCredentials.
|
||||||
|
|
||||||
|
// NON-NORMATIVE Prior Step: Verify that the allowCredentials for the session are owned by the user provided
|
||||||
|
userCredentials := user.WebAuthnCredentials() |
||||||
|
var credentialFound bool |
||||||
|
if len(session.AllowedCredentialIDs) > 0 { |
||||||
|
var credentialsOwned bool |
||||||
|
for _, allowedCredentialID := range session.AllowedCredentialIDs { |
||||||
|
for _, userCredential := range userCredentials { |
||||||
|
if bytes.Equal(userCredential.ID, allowedCredentialID) { |
||||||
|
credentialsOwned = true |
||||||
|
break |
||||||
|
} |
||||||
|
credentialsOwned = false |
||||||
|
} |
||||||
|
} |
||||||
|
if !credentialsOwned { |
||||||
|
return nil, protocol.ErrBadRequest.WithDetails("User does not own all credentials from the allowedCredentialList") |
||||||
|
} |
||||||
|
for _, allowedCredentialID := range session.AllowedCredentialIDs { |
||||||
|
if bytes.Equal(parsedResponse.RawID, allowedCredentialID) { |
||||||
|
credentialFound = true |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
if !credentialFound { |
||||||
|
return nil, protocol.ErrBadRequest.WithDetails("User does not own the credential returned") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Step 2. If credential.response.userHandle is present, verify that the user identified by this value is
|
||||||
|
// the owner of the public key credential identified by credential.id.
|
||||||
|
|
||||||
|
// This is in part handled by our Step 1
|
||||||
|
|
||||||
|
userHandle := parsedResponse.Response.UserHandle |
||||||
|
if userHandle != nil && len(userHandle) > 0 { |
||||||
|
if !bytes.Equal(userHandle, user.WebAuthnID()) { |
||||||
|
return nil, protocol.ErrBadRequest.WithDetails("userHandle and User ID do not match") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Step 3. Using credential’s id attribute (or the corresponding rawId, if base64url encoding is inappropriate
|
||||||
|
// for your use case), look up the corresponding credential public key.
|
||||||
|
var loginCredential Credential |
||||||
|
for _, cred := range userCredentials { |
||||||
|
if bytes.Equal(cred.ID, parsedResponse.RawID) { |
||||||
|
loginCredential = cred |
||||||
|
credentialFound = true |
||||||
|
break |
||||||
|
} |
||||||
|
credentialFound = false |
||||||
|
} |
||||||
|
|
||||||
|
if !credentialFound { |
||||||
|
return nil, protocol.ErrBadRequest.WithDetails("Unable to find the credential for the returned credential ID") |
||||||
|
} |
||||||
|
|
||||||
|
shouldVerifyUser := session.UserVerification == protocol.VerificationRequired |
||||||
|
|
||||||
|
rpID := webauthn.Config.RPID |
||||||
|
rpOrigin := webauthn.Config.RPOrigin |
||||||
|
|
||||||
|
appID, err := parsedResponse.GetAppID(session.Extensions, loginCredential.AttestationType) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Handle steps 4 through 16
|
||||||
|
validError := parsedResponse.Verify(session.Challenge, rpID, rpOrigin, appID, shouldVerifyUser, loginCredential.PublicKey) |
||||||
|
if validError != nil { |
||||||
|
return nil, validError |
||||||
|
} |
||||||
|
|
||||||
|
// Handle step 17
|
||||||
|
loginCredential.Authenticator.UpdateCounter(parsedResponse.Response.AuthenticatorData.Counter) |
||||||
|
|
||||||
|
return &loginCredential, nil |
||||||
|
} |
@ -0,0 +1,71 @@ |
|||||||
|
package webauthn |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net/url" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/protocol" |
||||||
|
) |
||||||
|
|
||||||
|
var defaultTimeout = 60000 |
||||||
|
|
||||||
|
// WebAuthn is the primary interface of this package and contains the request handlers that should be called.
|
||||||
|
type WebAuthn struct { |
||||||
|
Config *Config |
||||||
|
} |
||||||
|
|
||||||
|
// The config values required for proper
|
||||||
|
type Config struct { |
||||||
|
RPDisplayName string |
||||||
|
RPID string |
||||||
|
RPOrigin string |
||||||
|
RPIcon string |
||||||
|
// Defaults for generating options
|
||||||
|
AttestationPreference protocol.ConveyancePreference |
||||||
|
AuthenticatorSelection protocol.AuthenticatorSelection |
||||||
|
|
||||||
|
Timeout int |
||||||
|
Debug bool |
||||||
|
} |
||||||
|
|
||||||
|
// Validate that the config flags in Config are properly set
|
||||||
|
func (config *Config) validate() error { |
||||||
|
if len(config.RPDisplayName) == 0 { |
||||||
|
return fmt.Errorf("Missing RPDisplayName") |
||||||
|
} |
||||||
|
|
||||||
|
if len(config.RPID) == 0 { |
||||||
|
return fmt.Errorf("Missing RPID") |
||||||
|
} |
||||||
|
|
||||||
|
_, err := url.Parse(config.RPID) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("RPID not valid URI: %+v", err) |
||||||
|
} |
||||||
|
|
||||||
|
if config.Timeout == 0 { |
||||||
|
config.Timeout = defaultTimeout |
||||||
|
} |
||||||
|
|
||||||
|
if config.RPOrigin == "" { |
||||||
|
config.RPOrigin = config.RPID |
||||||
|
} else { |
||||||
|
u, err := url.Parse(config.RPOrigin) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("RPOrigin not valid URL: %+v", err) |
||||||
|
} |
||||||
|
config.RPOrigin = protocol.FullyQualifiedOrigin(u) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Create a new WebAuthn object given the proper config flags
|
||||||
|
func New(config *Config) (*WebAuthn, error) { |
||||||
|
if err := config.validate(); err != nil { |
||||||
|
return nil, fmt.Errorf("Configuration error: %+v", err) |
||||||
|
} |
||||||
|
return &WebAuthn{ |
||||||
|
config, |
||||||
|
}, nil |
||||||
|
} |
@ -0,0 +1,170 @@ |
|||||||
|
package webauthn |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/base64" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/duo-labs/webauthn/protocol" |
||||||
|
"github.com/duo-labs/webauthn/protocol/webauthncose" |
||||||
|
) |
||||||
|
|
||||||
|
// BEGIN REGISTRATION
|
||||||
|
// These objects help us creat the CredentialCreationOptions
|
||||||
|
// that will be passed to the authenticator via the user client
|
||||||
|
|
||||||
|
type RegistrationOption func(*protocol.PublicKeyCredentialCreationOptions) |
||||||
|
|
||||||
|
// Generate a new set of registration data to be sent to the client and authenticator.
|
||||||
|
func (webauthn *WebAuthn) BeginRegistration(user User, opts ...RegistrationOption) (*protocol.CredentialCreation, *SessionData, error) { |
||||||
|
challenge, err := protocol.CreateChallenge() |
||||||
|
if err != nil { |
||||||
|
return nil, nil, err |
||||||
|
} |
||||||
|
|
||||||
|
webAuthnUser := protocol.UserEntity{ |
||||||
|
ID: user.WebAuthnID(), |
||||||
|
DisplayName: user.WebAuthnDisplayName(), |
||||||
|
CredentialEntity: protocol.CredentialEntity{ |
||||||
|
Name: user.WebAuthnName(), |
||||||
|
Icon: user.WebAuthnIcon(), |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
relyingParty := protocol.RelyingPartyEntity{ |
||||||
|
ID: webauthn.Config.RPID, |
||||||
|
CredentialEntity: protocol.CredentialEntity{ |
||||||
|
Name: webauthn.Config.RPDisplayName, |
||||||
|
Icon: webauthn.Config.RPIcon, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
credentialParams := defaultRegistrationCredentialParameters() |
||||||
|
|
||||||
|
creationOptions := protocol.PublicKeyCredentialCreationOptions{ |
||||||
|
Challenge: challenge, |
||||||
|
RelyingParty: relyingParty, |
||||||
|
User: webAuthnUser, |
||||||
|
Parameters: credentialParams, |
||||||
|
AuthenticatorSelection: webauthn.Config.AuthenticatorSelection, |
||||||
|
Timeout: webauthn.Config.Timeout, |
||||||
|
Attestation: webauthn.Config.AttestationPreference, |
||||||
|
} |
||||||
|
|
||||||
|
for _, setter := range opts { |
||||||
|
setter(&creationOptions) |
||||||
|
} |
||||||
|
|
||||||
|
response := protocol.CredentialCreation{Response: creationOptions} |
||||||
|
newSessionData := SessionData{ |
||||||
|
Challenge: base64.RawURLEncoding.EncodeToString(challenge), |
||||||
|
UserID: user.WebAuthnID(), |
||||||
|
UserVerification: creationOptions.AuthenticatorSelection.UserVerification, |
||||||
|
} |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
return nil, nil, protocol.ErrParsingData.WithDetails("Error packing session data") |
||||||
|
} |
||||||
|
|
||||||
|
return &response, &newSessionData, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Provide non-default parameters regarding the authenticator to select.
|
||||||
|
func WithAuthenticatorSelection(authenticatorSelection protocol.AuthenticatorSelection) RegistrationOption { |
||||||
|
return func(cco *protocol.PublicKeyCredentialCreationOptions) { |
||||||
|
cco.AuthenticatorSelection = authenticatorSelection |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Provide non-default parameters regarding credentials to exclude from retrieval.
|
||||||
|
func WithExclusions(excludeList []protocol.CredentialDescriptor) RegistrationOption { |
||||||
|
return func(cco *protocol.PublicKeyCredentialCreationOptions) { |
||||||
|
cco.CredentialExcludeList = excludeList |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Provide non-default parameters regarding whether the authenticator should attest to the credential.
|
||||||
|
func WithConveyancePreference(preference protocol.ConveyancePreference) RegistrationOption { |
||||||
|
return func(cco *protocol.PublicKeyCredentialCreationOptions) { |
||||||
|
cco.Attestation = preference |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Provide extension parameter to registration options
|
||||||
|
func WithExtensions(extension protocol.AuthenticationExtensions) RegistrationOption { |
||||||
|
return func(cco *protocol.PublicKeyCredentialCreationOptions) { |
||||||
|
cco.Extensions = extension |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Take the response from the authenticator and client and verify the credential against the user's credentials and
|
||||||
|
// session data.
|
||||||
|
func (webauthn *WebAuthn) FinishRegistration(user User, session SessionData, response *http.Request) (*Credential, error) { |
||||||
|
parsedResponse, err := protocol.ParseCredentialCreationResponse(response) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return webauthn.CreateCredential(user, session, parsedResponse) |
||||||
|
} |
||||||
|
|
||||||
|
// CreateCredential verifies a parsed response against the user's credentials and session data.
|
||||||
|
func (webauthn *WebAuthn) CreateCredential(user User, session SessionData, parsedResponse *protocol.ParsedCredentialCreationData) (*Credential, error) { |
||||||
|
if !bytes.Equal(user.WebAuthnID(), session.UserID) { |
||||||
|
return nil, protocol.ErrBadRequest.WithDetails("ID mismatch for User and Session") |
||||||
|
} |
||||||
|
|
||||||
|
shouldVerifyUser := session.UserVerification == protocol.VerificationRequired |
||||||
|
|
||||||
|
invalidErr := parsedResponse.Verify(session.Challenge, shouldVerifyUser, webauthn.Config.RPID, webauthn.Config.RPOrigin) |
||||||
|
if invalidErr != nil { |
||||||
|
return nil, invalidErr |
||||||
|
} |
||||||
|
|
||||||
|
return MakeNewCredential(parsedResponse) |
||||||
|
} |
||||||
|
|
||||||
|
func defaultRegistrationCredentialParameters() []protocol.CredentialParameter { |
||||||
|
return []protocol.CredentialParameter{ |
||||||
|
protocol.CredentialParameter{ |
||||||
|
Type: protocol.PublicKeyCredentialType, |
||||||
|
Algorithm: webauthncose.AlgES256, |
||||||
|
}, |
||||||
|
protocol.CredentialParameter{ |
||||||
|
Type: protocol.PublicKeyCredentialType, |
||||||
|
Algorithm: webauthncose.AlgES384, |
||||||
|
}, |
||||||
|
protocol.CredentialParameter{ |
||||||
|
Type: protocol.PublicKeyCredentialType, |
||||||
|
Algorithm: webauthncose.AlgES512, |
||||||
|
}, |
||||||
|
protocol.CredentialParameter{ |
||||||
|
Type: protocol.PublicKeyCredentialType, |
||||||
|
Algorithm: webauthncose.AlgRS256, |
||||||
|
}, |
||||||
|
protocol.CredentialParameter{ |
||||||
|
Type: protocol.PublicKeyCredentialType, |
||||||
|
Algorithm: webauthncose.AlgRS384, |
||||||
|
}, |
||||||
|
protocol.CredentialParameter{ |
||||||
|
Type: protocol.PublicKeyCredentialType, |
||||||
|
Algorithm: webauthncose.AlgRS512, |
||||||
|
}, |
||||||
|
protocol.CredentialParameter{ |
||||||
|
Type: protocol.PublicKeyCredentialType, |
||||||
|
Algorithm: webauthncose.AlgPS256, |
||||||
|
}, |
||||||
|
protocol.CredentialParameter{ |
||||||
|
Type: protocol.PublicKeyCredentialType, |
||||||
|
Algorithm: webauthncose.AlgPS384, |
||||||
|
}, |
||||||
|
protocol.CredentialParameter{ |
||||||
|
Type: protocol.PublicKeyCredentialType, |
||||||
|
Algorithm: webauthncose.AlgPS512, |
||||||
|
}, |
||||||
|
protocol.CredentialParameter{ |
||||||
|
Type: protocol.PublicKeyCredentialType, |
||||||
|
Algorithm: webauthncose.AlgEdDSA, |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
package webauthn |
||||||
|
|
||||||
|
import "github.com/duo-labs/webauthn/protocol" |
||||||
|
|
||||||
|
// SessionData is the data that should be stored by the Relying Party for
|
||||||
|
// the duration of the web authentication ceremony
|
||||||
|
type SessionData struct { |
||||||
|
Challenge string `json:"challenge"` |
||||||
|
UserID []byte `json:"user_id"` |
||||||
|
AllowedCredentialIDs [][]byte `json:"allowed_credentials,omitempty"` |
||||||
|
UserVerification protocol.UserVerificationRequirement `json:"userVerification"` |
||||||
|
Extensions protocol.AuthenticationExtensions `json:"extensions,omitempty"` |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
package webauthn |
||||||
|
|
||||||
|
// User is built to interface with the Relying Party's User entry and
|
||||||
|
// elaborate the fields and methods needed for WebAuthn
|
||||||
|
type User interface { |
||||||
|
// User ID according to the Relying Party
|
||||||
|
WebAuthnID() []byte |
||||||
|
// User Name according to the Relying Party
|
||||||
|
WebAuthnName() string |
||||||
|
// Display Name of the user
|
||||||
|
WebAuthnDisplayName() string |
||||||
|
// User's icon url
|
||||||
|
WebAuthnIcon() string |
||||||
|
// Credentials owned by the user
|
||||||
|
WebAuthnCredentials() []Credential |
||||||
|
} |
||||||
|
|
||||||
|
type defaultUser struct { |
||||||
|
id []byte |
||||||
|
} |
||||||
|
|
||||||
|
var _ User = (*defaultUser)(nil) |
||||||
|
|
||||||
|
func (user *defaultUser) WebAuthnID() []byte { |
||||||
|
return user.id |
||||||
|
} |
||||||
|
|
||||||
|
func (user *defaultUser) WebAuthnName() string { |
||||||
|
return "newUser" |
||||||
|
} |
||||||
|
|
||||||
|
func (user *defaultUser) WebAuthnDisplayName() string { |
||||||
|
return "New User" |
||||||
|
} |
||||||
|
|
||||||
|
func (user *defaultUser) WebAuthnIcon() string { |
||||||
|
return "https://pics.com/avatar.png" |
||||||
|
} |
||||||
|
|
||||||
|
func (user *defaultUser) WebAuthnCredentials() []Credential { |
||||||
|
return []Credential{} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
# Binaries for programs and plugins |
||||||
|
*.exe |
||||||
|
*.exe~ |
||||||
|
*.dll |
||||||
|
*.so |
||||||
|
*.dylib |
||||||
|
|
||||||
|
# Test binary, build with `go test -c` |
||||||
|
*.test |
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE |
||||||
|
*.out |
@ -0,0 +1,86 @@ |
|||||||
|
# Do not delete linter settings. Linters like gocritic can be enabled on the command line. |
||||||
|
|
||||||
|
linters-settings: |
||||||
|
dupl: |
||||||
|
threshold: 100 |
||||||
|
funlen: |
||||||
|
lines: 100 |
||||||
|
statements: 50 |
||||||
|
goconst: |
||||||
|
min-len: 2 |
||||||
|
min-occurrences: 3 |
||||||
|
gocritic: |
||||||
|
enabled-tags: |
||||||
|
- diagnostic |
||||||
|
- experimental |
||||||
|
- opinionated |
||||||
|
- performance |
||||||
|
- style |
||||||
|
disabled-checks: |
||||||
|
- dupImport # https://github.com/go-critic/go-critic/issues/845 |
||||||
|
- ifElseChain |
||||||
|
- octalLiteral |
||||||
|
- paramTypeCombine |
||||||
|
- whyNoLint |
||||||
|
- wrapperFunc |
||||||
|
gofmt: |
||||||
|
simplify: false |
||||||
|
goimports: |
||||||
|
local-prefixes: github.com/fxamacker/cbor |
||||||
|
golint: |
||||||
|
min-confidence: 0 |
||||||
|
govet: |
||||||
|
check-shadowing: true |
||||||
|
lll: |
||||||
|
line-length: 140 |
||||||
|
maligned: |
||||||
|
suggest-new: true |
||||||
|
misspell: |
||||||
|
locale: US |
||||||
|
|
||||||
|
linters: |
||||||
|
disable-all: true |
||||||
|
enable: |
||||||
|
- deadcode |
||||||
|
- errcheck |
||||||
|
- goconst |
||||||
|
- gocyclo |
||||||
|
- gofmt |
||||||
|
- goimports |
||||||
|
- golint |
||||||
|
- gosec |
||||||
|
- govet |
||||||
|
- ineffassign |
||||||
|
- maligned |
||||||
|
- misspell |
||||||
|
- staticcheck |
||||||
|
- structcheck |
||||||
|
- typecheck |
||||||
|
- unconvert |
||||||
|
- unused |
||||||
|
- varcheck |
||||||
|
|
||||||
|
|
||||||
|
issues: |
||||||
|
# max-issues-per-linter default is 50. Set to 0 to disable limit. |
||||||
|
max-issues-per-linter: 0 |
||||||
|
# max-same-issues default is 3. Set to 0 to disable limit. |
||||||
|
max-same-issues: 0 |
||||||
|
# Excluding configuration per-path, per-linter, per-text and per-source |
||||||
|
exclude-rules: |
||||||
|
- path: _test\.go |
||||||
|
linters: |
||||||
|
- goconst |
||||||
|
- dupl |
||||||
|
- gomnd |
||||||
|
- lll |
||||||
|
- path: doc\.go |
||||||
|
linters: |
||||||
|
- goimports |
||||||
|
- gomnd |
||||||
|
- lll |
||||||
|
|
||||||
|
# golangci.com configuration |
||||||
|
# https://github.com/golangci/golangci/wiki/Configuration |
||||||
|
service: |
||||||
|
golangci-lint-version: 1.23.x # use the fixed version to not introduce new linters unexpectedly |
@ -0,0 +1,264 @@ |
|||||||
|
# CBOR Benchmarks for fxamacker/cbor |
||||||
|
|
||||||
|
See [bench_test.go](bench_test.go). |
||||||
|
|
||||||
|
Benchmarks on Feb. 22, 2020 with cbor v2.2.0: |
||||||
|
* [Go builtin types](#go-builtin-types) |
||||||
|
* [Go structs](#go-structs) |
||||||
|
* [Go structs with "keyasint" struct tag](#go-structs-with-keyasint-struct-tag) |
||||||
|
* [Go structs with "toarray" struct tag](#go-structs-with-toarray-struct-tag) |
||||||
|
* [COSE data](#cose-data) |
||||||
|
* [CWT claims data](#cwt-claims-data) |
||||||
|
* [SenML data](#SenML-data) |
||||||
|
|
||||||
|
## Go builtin types |
||||||
|
|
||||||
|
Benchmarks use data representing the following values: |
||||||
|
|
||||||
|
* Boolean: `true` |
||||||
|
* Positive integer: `18446744073709551615` |
||||||
|
* Negative integer: `-1000` |
||||||
|
* Float: `-4.1` |
||||||
|
* Byte string: `h'0102030405060708090a0b0c0d0e0f101112131415161718191a'` |
||||||
|
* Text string: `"The quick brown fox jumps over the lazy dog"` |
||||||
|
* Array: `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]` |
||||||
|
* Map: `{"a": "A", "b": "B", "c": "C", "d": "D", "e": "E", "f": "F", "g": "G", "h": "H", "i": "I", "j": "J", "l": "L", "m": "M", "n": "N"}}` |
||||||
|
|
||||||
|
Decoding Benchmark | Time | Memory | Allocs |
||||||
|
--- | ---: | ---: | ---: |
||||||
|
BenchmarkUnmarshal/CBOR_bool_to_Go_interface_{}-2 | 110 ns/op | 16 B/op | 1 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_bool_to_Go_bool-2 | 99.3 ns/op | 1 B/op | 1 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_positive_int_to_Go_interface_{}-2 | 135 ns/op | 24 B/op | 2 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_positive_int_to_Go_uint64-2 | 116 ns/op | 8 B/op | 1 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_negative_int_to_Go_interface_{}-2 | 133 ns/op | 24 B/op | 2 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_negative_int_to_Go_int64-2 | 113 ns/op | 8 B/op | 1 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_float_to_Go_interface_{}-2 | 137 ns/op | 24 B/op | 2 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_float_to_Go_float64-2 | 115 ns/op | 8 B/op | 1 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_bytes_to_Go_interface_{}-2 | 179 ns/op | 80 B/op | 3 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_bytes_to_Go_[]uint8-2 | 194 ns/op | 64 B/op | 2 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_text_to_Go_interface_{}-2 | 209 ns/op | 80 B/op | 3 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_text_to_Go_string-2 | 193 ns/op | 64 B/op | 2 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_array_to_Go_interface_{}-2 |1068 ns/op | 672 B/op | 29 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_array_to_Go_[]int-2 | 1073 ns/op | 272 B/op | 3 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_map_to_Go_interface_{}-2 | 2926 ns/op | 1420 B/op | 30 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_map_to_Go_map[string]interface_{}-2 | 3755 ns/op | 965 B/op | 19 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_map_to_Go_map[string]string-2 | 2586 ns/op | 740 B/op | 5 allocs/op |
||||||
|
|
||||||
|
Encoding Benchmark | Time | Memory | Allocs |
||||||
|
--- | ---: | ---: | ---: |
||||||
|
BenchmarkMarshal/Go_bool_to_CBOR_bool-2 | 86.1 ns/op | 1 B/op | 1 allocs/op |
||||||
|
BenchmarkMarshal/Go_uint64_to_CBOR_positive_int-2 | 97.0 ns/op | 16 B/op | 1 allocs/op |
||||||
|
BenchmarkMarshal/Go_int64_to_CBOR_negative_int-2 | 90.3 ns/op | 3 B/op | 1 allocs/op |
||||||
|
BenchmarkMarshal/Go_float64_to_CBOR_float-2 | 97.9 ns/op | 16 B/op | 1 allocs/op |
||||||
|
BenchmarkMarshal/Go_[]uint8_to_CBOR_bytes-2 | 121 ns/op | 32 B/op | 1 allocs/op |
||||||
|
BenchmarkMarshal/Go_string_to_CBOR_text-2 | 115 ns/op | 48 B/op | 1 allocs/op |
||||||
|
BenchmarkMarshal/Go_[]int_to_CBOR_array-2 | 529 ns/op | 32 B/op | 1 allocs/op |
||||||
|
BenchmarkMarshal/Go_map[string]string_to_CBOR_map-2 | 2115 ns/op | 576 B/op | 28 allocs/op |
||||||
|
|
||||||
|
## Go structs |
||||||
|
|
||||||
|
Benchmarks use struct and map[string]interface{} representing the following value: |
||||||
|
|
||||||
|
``` |
||||||
|
{ |
||||||
|
"T": true, |
||||||
|
"Ui": uint(18446744073709551615), |
||||||
|
"I": -1000, |
||||||
|
"F": -4.1, |
||||||
|
"B": []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26}, |
||||||
|
"S": "The quick brown fox jumps over the lazy dog", |
||||||
|
"Slci": []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26}, |
||||||
|
"Mss": map[string]string{"a": "A", "b": "B", "c": "C", "d": "D", "e": "E", "f": "F", "g": "G", "h": "H", "i": "I", "j": "J", "l": "L", "m": "M", "n": "N"}, |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Decoding Benchmark | Time | Memory | Allocs |
||||||
|
--- | ---: | ---: | ---: |
||||||
|
BenchmarkUnmarshal/CBOR_map_to_Go_map[string]interface{}-2 | 6221 ns/op | 2621 B/op | 73 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_map_to_Go_struct-2 | 4458 ns/op | 1172 B/op | 10 allocs/op |
||||||
|
|
||||||
|
Encoding Benchmark | Time | Memory | Allocs |
||||||
|
--- | ---: | ---: | ---: |
||||||
|
BenchmarkMarshal/Go_map[string]interface{}_to_CBOR_map-2 | 4441 ns/op | 1072 B/op | 45 allocs/op |
||||||
|
BenchmarkMarshal/Go_struct_to_CBOR_map-2 | 2866 ns/op | 720 B/op | 28 allocs/op |
||||||
|
|
||||||
|
## Go structs with "keyasint" struct tag |
||||||
|
|
||||||
|
Benchmarks use struct (with keyasint struct tag) and map[int]interface{} representing the following value: |
||||||
|
|
||||||
|
``` |
||||||
|
{ |
||||||
|
1: true, |
||||||
|
2: uint(18446744073709551615), |
||||||
|
3: -1000, |
||||||
|
4: -4.1, |
||||||
|
5: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26}, |
||||||
|
6: "The quick brown fox jumps over the lazy dog", |
||||||
|
7: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26}, |
||||||
|
8: map[string]string{"a": "A", "b": "B", "c": "C", "d": "D", "e": "E", "f": "F", "g": "G", "h": "H", "i": "I", "j": "J", "l": "L", "m": "M", "n": "N"}, |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Struct type with keyasint struct tag is used to handle CBOR map with integer keys. |
||||||
|
|
||||||
|
``` |
||||||
|
type T struct { |
||||||
|
T bool `cbor:"1,keyasint"` |
||||||
|
Ui uint `cbor:"2,keyasint"` |
||||||
|
I int `cbor:"3,keyasint"` |
||||||
|
F float64 `cbor:"4,keyasint"` |
||||||
|
B []byte `cbor:"5,keyasint"` |
||||||
|
S string `cbor:"6,keyasint"` |
||||||
|
Slci []int `cbor:"7,keyasint"` |
||||||
|
Mss map[string]string `cbor:"8,keyasint"` |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Decoding Benchmark | Time | Memory | Allocs |
||||||
|
--- | ---: | ---: | ---: |
||||||
|
BenchmarkUnmarshal/CBOR_map_to_Go_map[int]interface{}-2| 6030 ns/op | 2517 B/op | 70 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_map_to_Go_struct_keyasint-2 | 4332 ns/op | 1173 B/op | 10 allocs/op |
||||||
|
|
||||||
|
Encoding Benchmark | Time | Memory | Allocs |
||||||
|
--- | ---: | ---: | ---: |
||||||
|
BenchmarkMarshal/Go_map[int]interface{}_to_CBOR_map-2 | 4348 ns/op | 992 B/op | 45 allocs/op |
||||||
|
BenchmarkMarshal/Go_struct_keyasint_to_CBOR_map-2 | 2847 ns/op | 704 B/op | 28 allocs/op |
||||||
|
|
||||||
|
## Go structs with "toarray" struct tag |
||||||
|
|
||||||
|
Benchmarks use struct (with toarray struct tag) and []interface{} representing the following value: |
||||||
|
|
||||||
|
``` |
||||||
|
[ |
||||||
|
true, |
||||||
|
uint(18446744073709551615), |
||||||
|
-1000, |
||||||
|
-4.1, |
||||||
|
[]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26}, |
||||||
|
"The quick brown fox jumps over the lazy dog", |
||||||
|
[]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26}, |
||||||
|
map[string]string{"a": "A", "b": "B", "c": "C", "d": "D", "e": "E", "f": "F", "g": "G", "h": "H", "i": "I", "j": "J", "l": "L", "m": "M", "n": "N"} |
||||||
|
] |
||||||
|
``` |
||||||
|
|
||||||
|
Struct type with toarray struct tag is used to handle CBOR array. |
||||||
|
|
||||||
|
``` |
||||||
|
type T struct { |
||||||
|
_ struct{} `cbor:",toarray"` |
||||||
|
T bool |
||||||
|
Ui uint |
||||||
|
I int |
||||||
|
F float64 |
||||||
|
B []byte |
||||||
|
S string |
||||||
|
Slci []int |
||||||
|
Mss map[string]string |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Decoding Benchmark | Time | Memory | Allocs |
||||||
|
--- | ---: | ---: | ---: |
||||||
|
BenchmarkUnmarshal/CBOR_array_to_Go_[]interface{}-2 | 4863 ns/op | 2404 B/op | 67 allocs/op |
||||||
|
BenchmarkUnmarshal/CBOR_array_to_Go_struct_toarray-2 | 4173 ns/op | 1164 B/op | 9 allocs/op |
||||||
|
|
||||||
|
Encoding Benchmark | Time | Memory | Allocs |
||||||
|
--- | ---: | ---: | ---: |
||||||
|
BenchmarkMarshal/Go_[]interface{}_to_CBOR_map-2 | 3240 ns/op | 704 B/op | 28 allocs/op |
||||||
|
BenchmarkMarshal/Go_struct_toarray_to_CBOR_array-2 | 2823 ns/op | 704 B/op | 28 allocs/op |
||||||
|
|
||||||
|
## COSE data |
||||||
|
|
||||||
|
Benchmarks use COSE data from https://tools.ietf.org/html/rfc8392#appendix-A section A.2 |
||||||
|
|
||||||
|
``` |
||||||
|
// 128-Bit Symmetric COSE_Key |
||||||
|
{ |
||||||
|
/ k / -1: h'231f4c4d4d3051fdc2ec0a3851d5b383' |
||||||
|
/ kty / 1: 4 / Symmetric /, |
||||||
|
/ kid / 2: h'53796d6d6574726963313238' / 'Symmetric128' /, |
||||||
|
/ alg / 3: 10 / AES-CCM-16-64-128 / |
||||||
|
} |
||||||
|
// 256-Bit Symmetric COSE_Key |
||||||
|
{ |
||||||
|
/ k / -1: h'403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1 |
||||||
|
ec99192d79569388' |
||||||
|
/ kty / 1: 4 / Symmetric /, |
||||||
|
/ kid / 4: h'53796d6d6574726963323536' / 'Symmetric256' /, |
||||||
|
/ alg / 3: 4 / HMAC 256/64 / |
||||||
|
} |
||||||
|
// ECDSA 256-Bit COSE Key |
||||||
|
{ |
||||||
|
/ d / -4: h'6c1382765aec5358f117733d281c1c7bdc39884d04a45a1e |
||||||
|
6c67c858bc206c19', |
||||||
|
/ y / -3: h'60f7f1a780d8a783bfb7a2dd6b2796e8128dbbcef9d3d168 |
||||||
|
db9529971a36e7b9', |
||||||
|
/ x / -2: h'143329cce7868e416927599cf65a34f3ce2ffda55a7eca69 |
||||||
|
ed8919a394d42f0f', |
||||||
|
/ crv / -1: 1 / P-256 /, |
||||||
|
/ kty / 1: 2 / EC2 /, |
||||||
|
/ kid / 2: h'4173796d6d657472696345434453413 |
||||||
|
23536' / 'AsymmetricECDSA256' /, |
||||||
|
/ alg / 3: -7 / ECDSA 256 / |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Decoding Benchmark | Time | Memory | Allocs |
||||||
|
--- | ---: | ---: | ---: |
||||||
|
BenchmarkUnmarshalCOSE/128-Bit_Symmetric_Key-2 | 562 ns/op | 240 B/op | 4 allocs/op |
||||||
|
BenchmarkUnmarshalCOSE/256-Bit_Symmetric_Key-2 | 568 ns/op | 256 B/op | 4 allocs/op |
||||||
|
BenchmarkUnmarshalCOSE/ECDSA_P256_256-Bit_Key-2 | 968 ns/op | 360 B/op | 7 allocs/op |
||||||
|
|
||||||
|
Encoding Benchmark | Time | Memory | Allocs |
||||||
|
--- | ---: | ---: | ---: |
||||||
|
BenchmarkMarshalCOSE/128-Bit_Symmetric_Key-2 | 523 ns/op | 224 B/op | 2 allocs/op |
||||||
|
BenchmarkMarshalCOSE/256-Bit_Symmetric_Key-2 | 521 ns/op | 240 B/op | 2 allocs/op |
||||||
|
BenchmarkMarshalCOSE/ECDSA_P256_256-Bit_Key-2 | 668 ns/op | 320 B/op | 2 allocs/op |
||||||
|
|
||||||
|
## CWT claims data |
||||||
|
|
||||||
|
Benchmarks use CTW claims data from https://tools.ietf.org/html/rfc8392#appendix-A section A.1 |
||||||
|
|
||||||
|
``` |
||||||
|
{ |
||||||
|
/ iss / 1: "coap://as.example.com", |
||||||
|
/ sub / 2: "erikw", |
||||||
|
/ aud / 3: "coap://light.example.com", |
||||||
|
/ exp / 4: 1444064944, |
||||||
|
/ nbf / 5: 1443944944, |
||||||
|
/ iat / 6: 1443944944, |
||||||
|
/ cti / 7: h'0b71' |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Decoding Benchmark | Time | Memory | Allocs |
||||||
|
--- | ---: | ---: | ---: |
||||||
|
BenchmarkUnmarshalCWTClaims-2 | 765 ns/op | 176 B/op | 6 allocs/op |
||||||
|
|
||||||
|
Encoding Benchmark | Time | Memory | Allocs |
||||||
|
--- | ---: | ---: | ---: |
||||||
|
BenchmarkMarshalCWTClaims-2 | 451 ns/op | 176 B/op | 2 allocs/op |
||||||
|
|
||||||
|
## SenML data |
||||||
|
|
||||||
|
Benchmarks use SenML data from https://tools.ietf.org/html/rfc8428#section-6 |
||||||
|
|
||||||
|
``` |
||||||
|
[ |
||||||
|
{-2: "urn:dev:ow:10e2073a0108006:", -3: 1276020076.001, -4: "A", -1: 5, 0: "voltage", 1: "V", 2: 120.1}, |
||||||
|
{0: "current", 6: -5, 2: 1.2}, |
||||||
|
{0: "current", 6: -4, 2: 1.3}, |
||||||
|
{0: "current", 6: -3, 2: 1.4}, |
||||||
|
{0: "current", 6: -2, 2: 1.5}, |
||||||
|
{0: "current", 6: -1, 2: 1.6}, |
||||||
|
{0: "current", 6: 0, 2: 1.7} |
||||||
|
] |
||||||
|
``` |
||||||
|
|
||||||
|
Decoding Benchmark | Time | Memory | Allocs |
||||||
|
--- | ---: | ---: | ---: |
||||||
|
BenchmarkUnmarshalSenML-2 | 3106 ns/op | 1544 B/op | 18 allocs/op |
||||||
|
|
||||||
|
Encoding Benchmark | Time | Memory | Allocs |
||||||
|
--- | ---: | ---: | ---: |
||||||
|
BenchmarkMarshalSenML-2 | 2976 ns/op | 272 B/op | 2 allocs/op |
@ -0,0 +1,32 @@ |
|||||||
|
👉 [Comparisons](https://github.com/fxamacker/cbor#comparisons) • [Status](https://github.com/fxamacker/cbor#current-status) • [Design Goals](https://github.com/fxamacker/cbor#design-goals) • [Features](https://github.com/fxamacker/cbor#features) • [Standards](https://github.com/fxamacker/cbor#standards) • [Fuzzing](https://github.com/fxamacker/cbor#fuzzing-and-code-coverage) • [Usage](https://github.com/fxamacker/cbor#usage) • [Security Policy](https://github.com/fxamacker/cbor#security-policy) • [License](https://github.com/fxamacker/cbor#license) |
||||||
|
|
||||||
|
# CBOR |
||||||
|
[CBOR](https://en.wikipedia.org/wiki/CBOR) is a data format designed to allow small code size and small message size. CBOR is defined in [RFC 7049 Concise Binary Object Representation](https://tools.ietf.org/html/rfc7049), an [IETF](http://ietf.org/) Internet Standards Document. |
||||||
|
|
||||||
|
CBOR is also designed to be stable for decades, be extensible without need for version negotiation, and not require a schema. |
||||||
|
|
||||||
|
While JSON uses text, CBOR uses binary. CDDL can be used to express CBOR (and JSON) in an easy and unambiguous way. CDDL is defined in (RFC 8610 Concise Data Definition Language). |
||||||
|
|
||||||
|
## CBOR in Golang (Go) |
||||||
|
[Golang](https://golang.org/) is a nickname for the Go programming language. Go is specified in [The Go Programming Language Specification](https://golang.org/ref/spec). |
||||||
|
|
||||||
|
__[fxamacker/cbor](https://github.com/fxamacker/cbor)__ is a library (written in Go) that encodes and decodes CBOR. The API design of fxamacker/cbor is based on Go's [`encoding/json`](https://golang.org/pkg/encoding/json/). The design and reliability of fxamacker/cbor makes it ideal for encoding and decoding COSE. |
||||||
|
|
||||||
|
## COSE |
||||||
|
COSE is a protocol using CBOR for basic security services. COSE is defined in ([RFC 8152 CBOR Object Signing and Encryption](https://tools.ietf.org/html/rfc8152)). |
||||||
|
|
||||||
|
COSE describes how to create and process signatures, message authentication codes, and encryption using CBOR for serialization. COSE specification also describes how to represent cryptographic keys using CBOR. COSE is used by WebAuthn. |
||||||
|
|
||||||
|
## CWT |
||||||
|
CBOR Web Token (CWT) is defined in [RFC 8392](http://tools.ietf.org/html/rfc8392). CWT is based on COSE and was derived in part from JSON Web Token (JWT). CWT is a compact way to securely represent claims to be transferred between two parties. |
||||||
|
|
||||||
|
## WebAuthn |
||||||
|
[WebAuthn](https://en.wikipedia.org/wiki/WebAuthn) (Web Authentication) is a web standard for authenticating users to web-based apps and services. It's a core component of FIDO2, the successor of FIDO U2F legacy protocol. |
||||||
|
|
||||||
|
__[fxamacker/webauthn](https://github.com/fxamacker/webauthn)__ is a library (written in Go) that performs server-side authentication for clients using FIDO2 keys, legacy FIDO U2F keys, tpm, and etc. |
||||||
|
|
||||||
|
Copyright (c) Faye Amacker and contributors. |
||||||
|
|
||||||
|
<hr> |
||||||
|
|
||||||
|
👉 [Comparisons](https://github.com/fxamacker/cbor#comparisons) • [Status](https://github.com/fxamacker/cbor#current-status) • [Design Goals](https://github.com/fxamacker/cbor#design-goals) • [Features](https://github.com/fxamacker/cbor#features) • [Standards](https://github.com/fxamacker/cbor#standards) • [Fuzzing](https://github.com/fxamacker/cbor#fuzzing-and-code-coverage) • [Usage](https://github.com/fxamacker/cbor#usage) • [Security Policy](https://github.com/fxamacker/cbor#security-policy) • [License](https://github.com/fxamacker/cbor#license) |
@ -0,0 +1,76 @@ |
|||||||
|
# Contributor Covenant Code of Conduct |
||||||
|
|
||||||
|
## Our Pledge |
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as |
||||||
|
contributors and maintainers pledge to making participation in our project and |
||||||
|
our community a harassment-free experience for everyone, regardless of age, body |
||||||
|
size, disability, ethnicity, sex characteristics, gender identity and expression, |
||||||
|
level of experience, education, socio-economic status, nationality, personal |
||||||
|
appearance, race, religion, or sexual identity and orientation. |
||||||
|
|
||||||
|
## Our Standards |
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment |
||||||
|
include: |
||||||
|
|
||||||
|
* Using welcoming and inclusive language |
||||||
|
* Being respectful of differing viewpoints and experiences |
||||||
|
* Gracefully accepting constructive criticism |
||||||
|
* Focusing on what is best for the community |
||||||
|
* Showing empathy towards other community members |
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include: |
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or |
||||||
|
advances |
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks |
||||||
|
* Public or private harassment |
||||||
|
* Publishing others' private information, such as a physical or electronic |
||||||
|
address, without explicit permission |
||||||
|
* Other conduct which could reasonably be considered inappropriate in a |
||||||
|
professional setting |
||||||
|
|
||||||
|
## Our Responsibilities |
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable |
||||||
|
behavior and are expected to take appropriate and fair corrective action in |
||||||
|
response to any instances of unacceptable behavior. |
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or |
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions |
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or |
||||||
|
permanently any contributor for other behaviors that they deem inappropriate, |
||||||
|
threatening, offensive, or harmful. |
||||||
|
|
||||||
|
## Scope |
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces |
||||||
|
when an individual is representing the project or its community. Examples of |
||||||
|
representing a project or community include using an official project e-mail |
||||||
|
address, posting via an official social media account, or acting as an appointed |
||||||
|
representative at an online or offline event. Representation of a project may be |
||||||
|
further defined and clarified by project maintainers. |
||||||
|
|
||||||
|
## Enforcement |
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be |
||||||
|
reported by contacting the project team at faye.github@gmail.com. All |
||||||
|
complaints will be reviewed and investigated and will result in a response that |
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is |
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident. |
||||||
|
Further details of specific enforcement policies may be posted separately. |
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good |
||||||
|
faith may face temporary or permanent repercussions as determined by other |
||||||
|
members of the project's leadership. |
||||||
|
|
||||||
|
## Attribution |
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, |
||||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html |
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org |
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see |
||||||
|
https://www.contributor-covenant.org/faq |
@ -0,0 +1,47 @@ |
|||||||
|
# How to contribute |
||||||
|
|
||||||
|
This project started because I needed an easy, small, and crash-proof CBOR library for my [WebAuthn (FIDO2) server library](https://github.com/fxamacker/webauthn). I believe this was the first and still only standalone CBOR library (in Go) that is fuzz tested as of November 10, 2019. |
||||||
|
|
||||||
|
To my surprise, Stefan Tatschner (rumpelsepp) submitted the first 2 issues when I didn't expect this project to be noticed. So I decided to make it more full-featured for others by announcing releases and asking for feedback. Even this document exists because Montgomery Edwards⁴⁴⁸ (x448) opened [issue #22](https://github.com/fxamacker/cbor/issues/22). In other words, you can contribute by opening an issue that helps the project improve. Especially in the early stages. |
||||||
|
|
||||||
|
When I announced v1.2 on Go Forum, Jakob Borg (calmh) responded with a thumbs up and encouragement. Another project of equal priority needed my time and Jakob's kind words tipped the scale for me to work on this one (speedups for [milestone v1.3](https://github.com/fxamacker/cbor/issues?q=is%3Aopen+is%3Aissue+milestone%3Av1.3.0).) So words of appreciation or encouragement is nice way to contribute to open source projects. |
||||||
|
|
||||||
|
Another way is by using this library in your project. It can lead to features that benefit both projects, which is what happened when oasislabs/oasis-core switched to this CBOR libary -- thanks Yawning Angel (yawning) for requesting BinaryMarshaler/BinaryUnmarshaler and Jernej Kos (kostco) for requesting RawMessage! |
||||||
|
|
||||||
|
If you'd like to contribute code or send CBOR data, please read on (it can save you time!) |
||||||
|
|
||||||
|
## Private reports |
||||||
|
Usually, all issues are tracked publicly on [GitHub](https://github.com/fxamacker/cbor/issues). |
||||||
|
|
||||||
|
To report security vulnerabilities, please email faye.github@gmail.com and allow time for the problem to be resolved before disclosing it to the public. For more info, see [Security Policy](https://github.com/fxamacker/cbor#security-policy). |
||||||
|
|
||||||
|
Please do not send data that might contain personally identifiable information, even if you think you have permission. That type of support requires payment and a contract where I'm indemnified, held harmless, and defended for any data you send to me. |
||||||
|
|
||||||
|
## Prerequisites to pull requests |
||||||
|
Please [create an issue](https://github.com/fxamacker/cbor/issues/new/choose), if one doesn't already exist, and describe your concern. You'll need a [GitHub account](https://github.com/signup/free) to do this. |
||||||
|
|
||||||
|
If you submit a pull request without creating an issue and getting a response, you risk having your work unused because the bugfix or feature was already done by others and being reviewed before reaching Github. |
||||||
|
|
||||||
|
## Describe your issue |
||||||
|
Clearly describe the issue: |
||||||
|
* If it's a bug, please provide: **version of this library** and **Go** (`go version`), **unmodified error message**, and describe **how to reproduce it**. Also state **what you expected to happen** instead of the error. |
||||||
|
* If you propose a change or addition, try to give an example how the improved code could look like or how to use it. |
||||||
|
* If you found a compilation error, please confirm you're using a supported version of Go. If you are, then provide the output of `go version` first, followed by the complete error message. |
||||||
|
|
||||||
|
## Please don't |
||||||
|
Please don't send data containing personally identifiable information, even if you think you have permission. That type of support requires payment and a contract where I'm indemnified, held harmless, and defended for any data you send to me. |
||||||
|
|
||||||
|
Please don't send CBOR data larger than 512 bytes. If you want to send crash-producing CBOR data > 512 bytes, please get my permission before sending it to me. |
||||||
|
|
||||||
|
## Wanted |
||||||
|
* Opening issues that are helpful to the project |
||||||
|
* Using this library in your project and letting me know |
||||||
|
* Sending well-formed CBOR data (<= 512 bytes) that causes crashes (none found yet). |
||||||
|
* Sending malformed CBOR data (<= 512 bytes) that causes crashes (none found yet, but bad actors are better than me at breaking things). |
||||||
|
* Sending tests or data for unit tests that increase code coverage (currently at 97.8% for v1.2.) |
||||||
|
* Pull requests with small changes that are well-documented and easily understandable. |
||||||
|
* Sponsors, donations, bounties, subscriptions: I'd like to run uninterrupted fuzzing between releases on a server with dedicated CPUs (after v1.3 or v1.4.) |
||||||
|
|
||||||
|
## Credits |
||||||
|
This guide used nlohmann/json contribution guidelines for inspiration as suggested in issue #22. |
||||||
|
|
@ -0,0 +1,21 @@ |
|||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2019 - present Faye Amacker |
||||||
|
|
||||||
|
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,308 @@ |
|||||||
|
// Copyright (c) Faye Amacker. All rights reserved.
|
||||||
|
// Licensed under the MIT License. See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
package cbor |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"errors" |
||||||
|
"reflect" |
||||||
|
"sort" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
decodingStructTypeCache sync.Map // map[reflect.Type]*decodingStructType
|
||||||
|
encodingStructTypeCache sync.Map // map[reflect.Type]*encodingStructType
|
||||||
|
encodeFuncCache sync.Map // map[reflect.Type]encodeFunc
|
||||||
|
typeInfoCache sync.Map // map[reflect.Type]*typeInfo
|
||||||
|
) |
||||||
|
|
||||||
|
type specialType int |
||||||
|
|
||||||
|
const ( |
||||||
|
specialTypeNone specialType = iota |
||||||
|
specialTypeUnmarshalerIface |
||||||
|
specialTypeEmptyIface |
||||||
|
specialTypeTag |
||||||
|
specialTypeTime |
||||||
|
) |
||||||
|
|
||||||
|
type typeInfo struct { |
||||||
|
elemTypeInfo *typeInfo |
||||||
|
keyTypeInfo *typeInfo |
||||||
|
typ reflect.Type |
||||||
|
kind reflect.Kind |
||||||
|
nonPtrType reflect.Type |
||||||
|
nonPtrKind reflect.Kind |
||||||
|
spclType specialType |
||||||
|
} |
||||||
|
|
||||||
|
func newTypeInfo(t reflect.Type) *typeInfo { |
||||||
|
tInfo := typeInfo{typ: t, kind: t.Kind()} |
||||||
|
|
||||||
|
for t.Kind() == reflect.Ptr { |
||||||
|
t = t.Elem() |
||||||
|
} |
||||||
|
|
||||||
|
k := t.Kind() |
||||||
|
|
||||||
|
tInfo.nonPtrType = t |
||||||
|
tInfo.nonPtrKind = k |
||||||
|
|
||||||
|
if k == reflect.Interface && t.NumMethod() == 0 { |
||||||
|
tInfo.spclType = specialTypeEmptyIface |
||||||
|
} else if t == typeTag { |
||||||
|
tInfo.spclType = specialTypeTag |
||||||
|
} else if t == typeTime { |
||||||
|
tInfo.spclType = specialTypeTime |
||||||
|
} else if reflect.PtrTo(t).Implements(typeUnmarshaler) { |
||||||
|
tInfo.spclType = specialTypeUnmarshalerIface |
||||||
|
} |
||||||
|
|
||||||
|
switch k { |
||||||
|
case reflect.Array, reflect.Slice: |
||||||
|
tInfo.elemTypeInfo = getTypeInfo(t.Elem()) |
||||||
|
case reflect.Map: |
||||||
|
tInfo.keyTypeInfo = getTypeInfo(t.Key()) |
||||||
|
tInfo.elemTypeInfo = getTypeInfo(t.Elem()) |
||||||
|
} |
||||||
|
|
||||||
|
return &tInfo |
||||||
|
} |
||||||
|
|
||||||
|
type decodingStructType struct { |
||||||
|
fields fields |
||||||
|
err error |
||||||
|
toArray bool |
||||||
|
} |
||||||
|
|
||||||
|
func getDecodingStructType(t reflect.Type) *decodingStructType { |
||||||
|
if v, _ := decodingStructTypeCache.Load(t); v != nil { |
||||||
|
return v.(*decodingStructType) |
||||||
|
} |
||||||
|
|
||||||
|
flds, structOptions := getFields(t) |
||||||
|
|
||||||
|
toArray := hasToArrayOption(structOptions) |
||||||
|
|
||||||
|
var err error |
||||||
|
for i := 0; i < len(flds); i++ { |
||||||
|
if flds[i].keyAsInt { |
||||||
|
nameAsInt, numErr := strconv.Atoi(flds[i].name) |
||||||
|
if numErr != nil { |
||||||
|
err = errors.New("cbor: failed to parse field name \"" + flds[i].name + "\" to int (" + numErr.Error() + ")") |
||||||
|
break |
||||||
|
} |
||||||
|
flds[i].nameAsInt = int64(nameAsInt) |
||||||
|
} |
||||||
|
|
||||||
|
flds[i].typInfo = getTypeInfo(flds[i].typ) |
||||||
|
} |
||||||
|
|
||||||
|
structType := &decodingStructType{fields: flds, err: err, toArray: toArray} |
||||||
|
decodingStructTypeCache.Store(t, structType) |
||||||
|
return structType |
||||||
|
} |
||||||
|
|
||||||
|
type encodingStructType struct { |
||||||
|
fields fields |
||||||
|
bytewiseFields fields |
||||||
|
lengthFirstFields fields |
||||||
|
err error |
||||||
|
toArray bool |
||||||
|
omitEmpty bool |
||||||
|
hasAnonymousField bool |
||||||
|
} |
||||||
|
|
||||||
|
func (st *encodingStructType) getFields(em *encMode) fields { |
||||||
|
if em.sort == SortNone { |
||||||
|
return st.fields |
||||||
|
} |
||||||
|
if em.sort == SortLengthFirst { |
||||||
|
return st.lengthFirstFields |
||||||
|
} |
||||||
|
return st.bytewiseFields |
||||||
|
} |
||||||
|
|
||||||
|
type bytewiseFieldSorter struct { |
||||||
|
fields fields |
||||||
|
} |
||||||
|
|
||||||
|
func (x *bytewiseFieldSorter) Len() int { |
||||||
|
return len(x.fields) |
||||||
|
} |
||||||
|
|
||||||
|
func (x *bytewiseFieldSorter) Swap(i, j int) { |
||||||
|
x.fields[i], x.fields[j] = x.fields[j], x.fields[i] |
||||||
|
} |
||||||
|
|
||||||
|
func (x *bytewiseFieldSorter) Less(i, j int) bool { |
||||||
|
return bytes.Compare(x.fields[i].cborName, x.fields[j].cborName) <= 0 |
||||||
|
} |
||||||
|
|
||||||
|
type lengthFirstFieldSorter struct { |
||||||
|
fields fields |
||||||
|
} |
||||||
|
|
||||||
|
func (x *lengthFirstFieldSorter) Len() int { |
||||||
|
return len(x.fields) |
||||||
|
} |
||||||
|
|
||||||
|
func (x *lengthFirstFieldSorter) Swap(i, j int) { |
||||||
|
x.fields[i], x.fields[j] = x.fields[j], x.fields[i] |
||||||
|
} |
||||||
|
|
||||||
|
func (x *lengthFirstFieldSorter) Less(i, j int) bool { |
||||||
|
if len(x.fields[i].cborName) != len(x.fields[j].cborName) { |
||||||
|
return len(x.fields[i].cborName) < len(x.fields[j].cborName) |
||||||
|
} |
||||||
|
return bytes.Compare(x.fields[i].cborName, x.fields[j].cborName) <= 0 |
||||||
|
} |
||||||
|
|
||||||
|
func getEncodingStructType(t reflect.Type) *encodingStructType { |
||||||
|
if v, _ := encodingStructTypeCache.Load(t); v != nil { |
||||||
|
return v.(*encodingStructType) |
||||||
|
} |
||||||
|
|
||||||
|
flds, structOptions := getFields(t) |
||||||
|
|
||||||
|
if hasToArrayOption(structOptions) { |
||||||
|
return getEncodingStructToArrayType(t, flds) |
||||||
|
} |
||||||
|
|
||||||
|
var err error |
||||||
|
var omitEmpty bool |
||||||
|
var hasAnonymousField bool |
||||||
|
var hasKeyAsInt bool |
||||||
|
var hasKeyAsStr bool |
||||||
|
e := getEncodeState() |
||||||
|
for i := 0; i < len(flds); i++ { |
||||||
|
// Get field's encodeFunc
|
||||||
|
flds[i].ef = getEncodeFunc(flds[i].typ) |
||||||
|
if flds[i].ef == nil { |
||||||
|
err = &UnsupportedTypeError{t} |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
// Encode field name
|
||||||
|
if flds[i].keyAsInt { |
||||||
|
nameAsInt, numErr := strconv.Atoi(flds[i].name) |
||||||
|
if numErr != nil { |
||||||
|
err = errors.New("cbor: failed to parse field name \"" + flds[i].name + "\" to int (" + numErr.Error() + ")") |
||||||
|
break |
||||||
|
} |
||||||
|
flds[i].nameAsInt = int64(nameAsInt) |
||||||
|
if nameAsInt >= 0 { |
||||||
|
encodeHead(e, byte(cborTypePositiveInt), uint64(nameAsInt)) |
||||||
|
} else { |
||||||
|
n := nameAsInt*(-1) - 1 |
||||||
|
encodeHead(e, byte(cborTypeNegativeInt), uint64(n)) |
||||||
|
} |
||||||
|
flds[i].cborName = make([]byte, e.Len()) |
||||||
|
copy(flds[i].cborName, e.Bytes()) |
||||||
|
e.Reset() |
||||||
|
|
||||||
|
hasKeyAsInt = true |
||||||
|
} else { |
||||||
|
encodeHead(e, byte(cborTypeTextString), uint64(len(flds[i].name))) |
||||||
|
flds[i].cborName = make([]byte, e.Len()+len(flds[i].name)) |
||||||
|
n := copy(flds[i].cborName, e.Bytes()) |
||||||
|
copy(flds[i].cborName[n:], flds[i].name) |
||||||
|
e.Reset() |
||||||
|
|
||||||
|
hasKeyAsStr = true |
||||||
|
} |
||||||
|
|
||||||
|
// Check if field is from embedded struct
|
||||||
|
if len(flds[i].idx) > 1 { |
||||||
|
hasAnonymousField = true |
||||||
|
} |
||||||
|
|
||||||
|
// Check if field can be omitted when empty
|
||||||
|
if flds[i].omitEmpty { |
||||||
|
omitEmpty = true |
||||||
|
} |
||||||
|
} |
||||||
|
putEncodeState(e) |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
structType := &encodingStructType{err: err} |
||||||
|
encodingStructTypeCache.Store(t, structType) |
||||||
|
return structType |
||||||
|
} |
||||||
|
|
||||||
|
// Sort fields by canonical order
|
||||||
|
bytewiseFields := make(fields, len(flds)) |
||||||
|
copy(bytewiseFields, flds) |
||||||
|
sort.Sort(&bytewiseFieldSorter{bytewiseFields}) |
||||||
|
|
||||||
|
lengthFirstFields := bytewiseFields |
||||||
|
if hasKeyAsInt && hasKeyAsStr { |
||||||
|
lengthFirstFields = make(fields, len(flds)) |
||||||
|
copy(lengthFirstFields, flds) |
||||||
|
sort.Sort(&lengthFirstFieldSorter{lengthFirstFields}) |
||||||
|
} |
||||||
|
|
||||||
|
structType := &encodingStructType{ |
||||||
|
fields: flds, |
||||||
|
bytewiseFields: bytewiseFields, |
||||||
|
lengthFirstFields: lengthFirstFields, |
||||||
|
omitEmpty: omitEmpty, |
||||||
|
hasAnonymousField: hasAnonymousField, |
||||||
|
} |
||||||
|
encodingStructTypeCache.Store(t, structType) |
||||||
|
return structType |
||||||
|
} |
||||||
|
|
||||||
|
func getEncodingStructToArrayType(t reflect.Type, flds fields) *encodingStructType { |
||||||
|
var hasAnonymousField bool |
||||||
|
for i := 0; i < len(flds); i++ { |
||||||
|
// Get field's encodeFunc
|
||||||
|
flds[i].ef = getEncodeFunc(flds[i].typ) |
||||||
|
if flds[i].ef == nil { |
||||||
|
structType := &encodingStructType{err: &UnsupportedTypeError{t}} |
||||||
|
encodingStructTypeCache.Store(t, structType) |
||||||
|
return structType |
||||||
|
} |
||||||
|
|
||||||
|
// Check if field is from embedded struct
|
||||||
|
if len(flds[i].idx) > 1 { |
||||||
|
hasAnonymousField = true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
structType := &encodingStructType{ |
||||||
|
fields: flds, |
||||||
|
toArray: true, |
||||||
|
hasAnonymousField: hasAnonymousField, |
||||||
|
} |
||||||
|
encodingStructTypeCache.Store(t, structType) |
||||||
|
return structType |
||||||
|
} |
||||||
|
|
||||||
|
func getEncodeFunc(t reflect.Type) encodeFunc { |
||||||
|
if v, _ := encodeFuncCache.Load(t); v != nil { |
||||||
|
return v.(encodeFunc) |
||||||
|
} |
||||||
|
f := getEncodeFuncInternal(t) |
||||||
|
encodeFuncCache.Store(t, f) |
||||||
|
return f |
||||||
|
} |
||||||
|
|
||||||
|
func getTypeInfo(t reflect.Type) *typeInfo { |
||||||
|
if v, _ := typeInfoCache.Load(t); v != nil { |
||||||
|
return v.(*typeInfo) |
||||||
|
} |
||||||
|
tInfo := newTypeInfo(t) |
||||||
|
typeInfoCache.Store(t, tInfo) |
||||||
|
return tInfo |
||||||
|
} |
||||||
|
|
||||||
|
func hasToArrayOption(tag string) bool { |
||||||
|
s := ",toarray" |
||||||
|
idx := strings.Index(tag, s) |
||||||
|
return idx >= 0 && (len(tag) == idx+len(s) || tag[idx+len(s)] == ',') |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue