-
Notifications
You must be signed in to change notification settings - Fork 23
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
daeMOn63
wants to merge
34
commits into
master
Choose a base branch
from
webauthn
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
1684339
add go.mod
daeMOn63 1a80ea5
add new HID commands and CBOR command support
daeMOn63 7a3d30e
add ctap2token getInfo()
daeMOn63 6dacead
add ctap2token example
daeMOn63 070ceb5
add CTAP2 makeCredential support
daeMOn63 d60d73f
add complete ctap2/webauthn flow example
daeMOn63 45e882f
add reset command
daeMOn63 e701732
add GetNextAssertion
daeMOn63 e706c46
fix tests
daeMOn63 8ee9dd2
extracted pin handling to dedicated package
daeMOn63 c405ddc
add webauthn registeration handling
daeMOn63 acdfab1
add webauthn ctap2 authenticate
daeMOn63 acab0e9
examples cleanup
daeMOn63 8a1578c
fix webauthn demos and cleanup encoding issues
daeMOn63 f755187
u2ftoken: parse register response
daeMOn63 187936d
tidy mod
daeMOn63 c79788d
webauthn: ctap1 register support
daeMOn63 e374739
webauthn: ctap1 authenticate support
daeMOn63 b53d4e0
webauthn: ctap1 handle excluded credentials
daeMOn63 1027f71
webauthn: ctap1 fix authenticate allowed credentials
daeMOn63 368d6db
add device selection doc
daeMOn63 b2043b8
add CLI webauthn implementation
daeMOn63 f3f30b5
cleanup
daeMOn63 f2f9cc1
update device capability detection
daeMOn63 91a9b6b
improve request cancelation handling
daeMOn63 4f13633
renamed device selection document
daeMOn63 f026a3e
improve pin input
daeMOn63 1897ff2
improve cancel on CTAP2 devices
daeMOn63 f65213a
fix review comments
daeMOn63 5558a12
Apply suggestions from code review
daeMOn63 4a1739f
rename Webauthn -> WebAuthn and split RegisterRequest
daeMOn63 50ac7cd
remove and clarify todos
daeMOn63 b785fc1
fix #14 getAssertion issue with HyperSecu Mini
daeMOn63 840ab5f
add extra check for pin before input
daeMOn63 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!") | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't rename, this cause infinite loop as MarshalCBOR() makes the struct implement cbor.Marshaler interface (see https://github.com/fxamacker/cbor/blob/ee962ff86de23bc964f19d8e4a1a23171b05e58b/encode.go#L28-L29 and https://github.com/fxamacker/cbor/blob/ee962ff86de23bc964f19d8e4a1a23171b05e58b/encode.go#L99-L101)