Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Webauthn HID client #12

Open
wants to merge 34 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1684339
add go.mod
daeMOn63 Sep 9, 2020
1a80ea5
add new HID commands and CBOR command support
daeMOn63 Sep 9, 2020
7a3d30e
add ctap2token getInfo()
daeMOn63 Sep 9, 2020
6dacead
add ctap2token example
daeMOn63 Sep 9, 2020
070ceb5
add CTAP2 makeCredential support
daeMOn63 Sep 11, 2020
d60d73f
add complete ctap2/webauthn flow example
daeMOn63 Sep 15, 2020
45e882f
add reset command
daeMOn63 Sep 16, 2020
e701732
add GetNextAssertion
daeMOn63 Sep 16, 2020
e706c46
fix tests
daeMOn63 Sep 16, 2020
8ee9dd2
extracted pin handling to dedicated package
daeMOn63 Sep 16, 2020
c405ddc
add webauthn registeration handling
daeMOn63 Sep 18, 2020
acdfab1
add webauthn ctap2 authenticate
daeMOn63 Sep 18, 2020
acab0e9
examples cleanup
daeMOn63 Sep 18, 2020
8a1578c
fix webauthn demos and cleanup encoding issues
daeMOn63 Sep 22, 2020
f755187
u2ftoken: parse register response
daeMOn63 Sep 22, 2020
187936d
tidy mod
daeMOn63 Sep 22, 2020
c79788d
webauthn: ctap1 register support
daeMOn63 Sep 22, 2020
e374739
webauthn: ctap1 authenticate support
daeMOn63 Sep 22, 2020
b53d4e0
webauthn: ctap1 handle excluded credentials
daeMOn63 Sep 22, 2020
1027f71
webauthn: ctap1 fix authenticate allowed credentials
daeMOn63 Sep 22, 2020
368d6db
add device selection doc
daeMOn63 Sep 23, 2020
b2043b8
add CLI webauthn implementation
daeMOn63 Sep 28, 2020
f3f30b5
cleanup
daeMOn63 Sep 28, 2020
f2f9cc1
update device capability detection
daeMOn63 Sep 29, 2020
91a9b6b
improve request cancelation handling
daeMOn63 Sep 29, 2020
4f13633
renamed device selection document
daeMOn63 Sep 29, 2020
f026a3e
improve pin input
daeMOn63 Sep 29, 2020
1897ff2
improve cancel on CTAP2 devices
daeMOn63 Sep 30, 2020
f65213a
fix review comments
daeMOn63 Jan 22, 2021
5558a12
Apply suggestions from code review
daeMOn63 Feb 8, 2021
4a1739f
rename Webauthn -> WebAuthn and split RegisterRequest
daeMOn63 Feb 8, 2021
50ac7cd
remove and clarify todos
daeMOn63 Feb 8, 2021
b785fc1
fix #14 getAssertion issue with HyperSecu Mini
daeMOn63 Feb 26, 2021
840ab5f
add extra check for pin before input
daeMOn63 Feb 27, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions crypto/cose.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package crypto

import (
"github.com/fxamacker/cbor/v2"
)

// COSEKey, as defined per https://tools.ietf.org/html/rfc8152#section-7.1
// Only supports Elliptic Curve Public keys.
type COSEKey struct {
Y []byte `cbor:"-3,keyasint,omitempty"`
X []byte `cbor:"-2,keyasint,omitempty"`
Curve CurveType `cbor:"-1,keyasint,omitempty"`

KeyType KeyType `cbor:"1,keyasint"`
KeyID []byte `cbor:"2,keyasint,omitempty"`
Alg Alg `cbor:"3,keyasint,omitempty"`
KeyOps []KeyOperation `cbor:"4,keyasint,omitempty"`
BaseIV []byte `cbor:"5,keyasint,omitempty"`
}

func (k *COSEKey) CBOREncode() ([]byte, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (k *COSEKey) CBOREncode() ([]byte, error) {
func (k *COSEKey) MarshalCBOR() ([]byte, error) {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enc, err := cbor.CTAP2EncOptions().EncMode()
if err != nil {
return nil, err
}

return enc.Marshal(k)
}

// KeyType defines a key type from https://tools.ietf.org/html/rfc8152#section-13
type KeyType int

const (
// OKP is an Octet Key Pair
OKP KeyType = 0x01
// EC2 is an Elliptic Curve Key
EC2 KeyType = 0x02
)

type CurveType int

const (
P256 CurveType = 0x01
P384 CurveType = 0x02
P521 CurveType = 0x03
X25519 CurveType = 0x04
X448 CurveType = 0x05
Ed25519 CurveType = 0x06
Ed448 CurveType = 0x07
)

type KeyOperation int

const (
Sign KeyOperation = iota + 1
Verify
Encrypt
Decrypt
WrapKey
UnwrapKey
DeriveKey
DeriveBits
MACCreate
MACVerify
)

// Alg must be the value of one of the algorithms registered in
// https://www.iana.org/assignments/cose/cose.xhtml#algorithms.
type Alg int

const (
RS256 Alg = -257 // RSASSA-PKCS1-v1_5 using SHA-256
PS256 Alg = -37 // RSASSA-PSS w/ SHA-256
ECDHES_HKDF256 Alg = -25 // ECDH-ES + HKDF-256
ES256 Alg = -7 // ECDSA w/ SHA-256
)
175 changes: 175 additions & 0 deletions ctap2token/example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package main

import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"errors"
"fmt"
"math/big"

"github.com/flynn/u2f/ctap2token"
"github.com/flynn/u2f/ctap2token/pin"
"github.com/flynn/u2f/u2fhid"
)

func main() {
devices, err := u2fhid.Devices()
if err != nil {
panic(err)
}

for _, d := range devices {
dev, err := u2fhid.Open(d)
if err != nil {
panic(err)
}

token := ctap2token.NewToken(dev)

info, err := token.GetInfo()
if err != nil {
fmt.Printf("failed to retrieve token info (%v), does the token support CTAP2 ?\n", err)
continue
}
fmt.Printf("Token info:\n%#v\n", info)

clientDataHash := make([]byte, 32)
if _, err := rand.Read(clientDataHash); err != nil {
panic(err)
}

userID := make([]byte, 32)
if _, err := rand.Read(userID); err != nil {
panic(err)
}

req := &ctap2token.MakeCredentialRequest{
ClientDataHash: clientDataHash,
RP: ctap2token.CredentialRpEntity{
ID: "example.com",
Name: "Acme",
},
User: ctap2token.CredentialUserEntity{
ID: userID,
Icon: "https://pics.example.com/00/p/aBjjjpqPb.png",
Name: "[email protected]",
DisplayName: "John P. Smith",
},
PubKeyCredParams: []ctap2token.CredentialParam{
ctap2token.PublicKeyES256,
ctap2token.PublicKeyRS256,
},
}

// first try without user verification
fmt.Println("Sending makeCredential request, please press authenticator button...")
resp, err := token.MakeCredential(req)
if err != nil {
// retry but with user verification
if errors.Unwrap(err) != ctap2token.ErrPinRequired {
panic(err)
}

// HyperSecu Mini returns CTAP2_ERR_PIN_REQUIRED instead of CTAP2_ERR_ACTION_TIMEOUT
// so we need an extra check to ensure the Pin is set on the token before asking the user input.
if pinEnabled, ok := info.Options["clientPin"]; !ok || !pinEnabled {
panic(fmt.Errorf("Got %s error from token but pin is not set.", ctap2token.ErrPinRequired))
}

pinHandler := pin.NewInteractiveHandler()
userPIN, err := pinHandler.ReadPIN()
if err != nil {
panic(err)
}

pinAuth, err := pin.ExchangeUserPin(token, userPIN, clientDataHash)
if err != nil {
panic(err)
}
req.PinUVAuth = &pinAuth
req.PinUVAuthProtocol = ctap2token.PinProtoV1

resp, err = token.MakeCredential(req)
if err != nil {
panic(err)
}
}
fmt.Println("Successfully created credential")

// Verify signature with the X509 certificate from the attestation statement
x509certs, ok := resp.AttSmt["x5c"]
if !ok {
panic("no x5c field")
}

x509cert := x509certs.([]interface{})[0].([]byte)
cert, err := x509.ParseCertificate(x509cert)
if err != nil {
panic(err)
}

signed := append(resp.AuthData, clientDataHash...)
if err := cert.CheckSignature(x509.ECDSAWithSHA256, signed, resp.AttSmt["sig"].([]byte)); err != nil {
panic(err)
}
fmt.Println("MakeCredentials signature is valid!")

mcpAuthData, err := resp.AuthData.Parse()
if err != nil {
panic(err)
}
fmt.Printf("credentialID: %x\n", mcpAuthData.AttestedCredentialData.CredentialID)

fmt.Println("Sending GetAssertion request, please press authenticator button...")
getAssertionResp, err := token.GetAssertion(&ctap2token.GetAssertionRequest{
RPID: "example.com",
AllowList: []*ctap2token.CredentialDescriptor{
{
ID: mcpAuthData.AttestedCredentialData.CredentialID,
Type: ctap2token.PublicKey,
},
},
ClientDataHash: clientDataHash,
// enable UserVerified flag
// PinUVAuth: pinAuth,
// PinUVAuthProtocol: ctap2token.PinProtoV1,
})
if err != nil {
panic(err)
}

if !bytes.Equal(getAssertionResp.Credential.ID, mcpAuthData.AttestedCredentialData.CredentialID) {
panic("CredentialID mismatch")
}
fmt.Printf("Found credential %x\n", getAssertionResp.Credential.ID)

// Verify signature with the public key from MakeCredential
pubX := new(big.Int)
pubX.SetBytes(mcpAuthData.AttestedCredentialData.CredentialPublicKey.X)
pubY := new(big.Int)
pubY.SetBytes(mcpAuthData.AttestedCredentialData.CredentialPublicKey.Y)

pubkey := &ecdsa.PublicKey{
Curve: elliptic.P256(),
X: pubX,
Y: pubY,
}

hash := sha256.New()
if _, err := hash.Write(getAssertionResp.AuthData); err != nil {
panic(err)
}
if _, err := hash.Write(clientDataHash); err != nil {
panic(err)
}

if !ecdsa.VerifyASN1(pubkey, hash.Sum(nil), getAssertionResp.Signature) {
panic("invalid signature")
}
fmt.Println("Signature verified!")
}
}
Loading