├── LICENSE ├── README.md ├── cmd ├── ls.go ├── reg.go ├── root.go ├── sig.go ├── ver.go └── ver_test.go └── main.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Mark Percival 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # u2fcli 2 | 3 | u2fcli is a tool designed to handle registering, signing, and verifying U2F tokens on the command line. 4 | 5 | ## Install 6 | 7 | `go get github.com/mdp/u2fcli` 8 | 9 | ## Usage 10 | 11 | 12 | ### Registration 13 | 14 | Choose a challenge and App ID to send to the U2F device 15 | 16 | ``` 17 | [mdp u2fcli]$ u2fcli reg --challenge complexChallengeGoesHere \ 18 | --appid https://mdp.im 19 | Registering, press the button on your U2F device 20 | 21 | { 22 | "KeyHandle": "0JGeJ3MhvDzK_YjKhK4VkPOegGn0x3wxJENJ8J1JanozbSr8Elz2KRcARLh2sF__l_Vof2xiydPw6CEicpzs0A", 23 | "PublicKey": "BPQPBz7NV3LwksVwjbGdn7ODP5omKHt8CetrHnDZeUUxmFChHcKuYNHgLm0HdtsSD6p7cjrFZdb9mNOLg3huRcI", 24 | "RegisteredData": "0JGeJ3MhvDzK_YjKhK4VkPOegGn0x3wxJENJ8J1JanozbSr8Elz2KRcARLh2sF__l_Vof2xiydPw6CEicpzs0DCCAkQwggEuoAMCAQICBFVivqAwCwYJKoZIhvcNAQELMC4xLDAqBgNVBAMTI1l1YmljbyBVMkYgUm9vdCBDQSBTZXJpYWwgNDU3MjAwNjMxMCAXDTE0MDgwMTAwMDAwMFoYDzIwNTAwOTA0MDAwMDAwWjAqMSgwJgYDVQQDDB9ZdWJpY28gVTJGIEVFIFNlcmlhbCAxNDMyNTM0Njg4MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESzMfdz2BRLmZXL5FhVF-F1g6pHYjaVy-haxILIAZ8sm5RnrgRbDmbxMbLqMkPJH9pgLjGPP8XY0qerrnK9FDCaM7MDkwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjUwEwYLKwYBBAGC5RwCAQEEBAMCBSAwCwYJKoZIhvcNAQELA4IBAQCsFtmzbrazqbdtdZSzT1n09z7byf3rKTXra0Ucq_QdJdPnFhTXRyYEynKleOMj7bdgBGhfBefRub4F226UQPrFz8kypsr66FKZdy7bAnggIDzUFB0-629qLOmeOVeAMmOrq41uxICn3whK0sunt9bXfJTD68CxZvlgV8r1_jpjHqJqQzdio2--z0z0RQliX9WvEEmqfIvHaJpmWemvXejw1ywoglF0xQ4Gq39qB5CDe22zKr_cvKg1y7sJDvHw2Z4Iab_p5WdkxCMObAV3KbAQ3g7F-czkyRwoJiGOqAgau5aRUewWclryqNled5W8qiJ6m5RDIMQnYZyq-FTZgpjXMEUCIEwCqGbDrEYu0F2vVl6IC_5u3M3WVZGm3A6efdh55j1aAiEAooYy3e8q5eakgpXC8FPy0VdGphzV6sjbpuExuJdCqlk" 25 | } 26 | ``` 27 | 28 | ### Signing 29 | 30 | Using the `Key Handle` from above, we can ask the U2F token to sign a challenge 31 | 32 | ``` 33 | [mdp u2fcli]$ u2fcli sig --appid https://mdp.im \ 34 | --challenge anotherChallenge \ 35 | --keyhandle 0JGeJ3MhvDzK_YjKhK4VkPOegGn0x3wxJENJ8J1JanozbSr8Elz2KRcARLh2sF__l_Vof2xiydPw6CEicpzs0A 36 | Authenticating, press the button on your U2F device 37 | 38 | { 39 | "Counter": 33, 40 | "Signature": "AQAAACEwRQIgetEQfx2p2SB7ch2JtvDYxjqTekMfZuDPjrJ0deNTXysCIQD5LehJ4gXf1vpJ37_XWefnSkRzwfwZ3Uffq7jWWTZYkw" 41 | } 42 | ``` 43 | 44 | ### Verifying 45 | 46 | Finally, we can verify the `Signature` from above by providing it to u2fcli along with the `PublicKey` we recieved at registration. 47 | 48 | ``` 49 | [mdp u2fcli]$ u2fcli ver --appid https://mdp.im \ 50 | --challenge anotherChallenge \ 51 | --publickey BPQPBz7NV3LwksVwjbGdn7ODP5omKHt8CetrHnDZeUUxmFChHcKuYNHgLm0HdtsSD6p7cjrFZdb9mNOLg3huRcI \ 52 | --signature AQAAACEwRQIgetEQfx2p2SB7ch2JtvDYxjqTekMfZuDPjrJ0deNTXysCIQD5LehJ4gXf1vpJ37_XWefnSkRzwfwZ3Uffq7jWWTZYkw 53 | 54 | Signature verified 55 | ``` 56 | 57 | 58 | ## Credit 59 | 60 | This tool wouldn't be possible without the work of https://github.com/flynn/u2f which 61 | handles all the interactions with the hardware and USB HID devices 62 | 63 | -------------------------------------------------------------------------------- /cmd/ls.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // lsCmd respresents the ls command 10 | var lsCmd = &cobra.Command{ 11 | Use: "ls", 12 | Short: "List u2f devices attached", 13 | Long: `List of all u2f devices attached`, 14 | Run: func(cmd *cobra.Command, args []string) { 15 | 16 | devices := getOrderedDevices() 17 | 18 | for i, d := range devices { 19 | fmt.Printf("[%d]: manufacturer = %q, product = %q, vid = 0x%04x, pid = 0x%04x\n", i+1, d.Manufacturer, d.Product, d.ProductID, d.VendorID) 20 | } 21 | }, 22 | } 23 | 24 | func init() { 25 | RootCmd.AddCommand(lsCmd) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/reg.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "time" 9 | 10 | "github.com/flynn/u2f/u2fhid" 11 | "github.com/flynn/u2f/u2ftoken" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // regCmd respresents the reg command 16 | var regCmd = &cobra.Command{ 17 | Use: "reg", 18 | Short: "Register with a U2F device", 19 | Long: `Register with a U2F device 20 | Requires a challege and appID. For example: 21 | 22 | u2fcli reg --challenge MyChallenge --appid https://mysite.com`, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | device := getDevice() 25 | 26 | if challengeFlag == "" { 27 | fmt.Println(os.Stderr, "Please supply the challenge using -challenge option.") 28 | return 29 | } 30 | if appIDFlag == "" { 31 | fmt.Fprintln(os.Stderr, "Please supply the appID using -appid option.") 32 | return 33 | } 34 | 35 | appIDHash := sum256(appIDFlag) 36 | challengeHash := sum256(challengeFlag) 37 | 38 | dev, err := u2fhid.Open(device) 39 | defer dev.Close() 40 | 41 | if err != nil { 42 | fmt.Fprintf(os.Stderr, "Error opening device: %s\n", err) 43 | os.Exit(1) 44 | } 45 | t := u2ftoken.NewToken(dev) 46 | 47 | fmt.Fprintf(os.Stderr, "Registering, press the button on your U2F device #%d [%s]", deviceNum, device.Product) 48 | 49 | var res []byte 50 | for { 51 | res, err = t.Register(u2ftoken.RegisterRequest{Challenge: challengeHash, Application: appIDHash}) 52 | if err == u2ftoken.ErrPresenceRequired { 53 | time.Sleep(200 * time.Millisecond) 54 | continue 55 | } else if err != nil { 56 | fmt.Fprintf(os.Stderr, "Error registering with device: %s\n", err) 57 | os.Exit(1) 58 | } 59 | break 60 | } 61 | 62 | // Parse the response 63 | pubKey := res[1:66] 64 | res = res[66:] 65 | khLen := int(res[0]) 66 | res = res[1:] 67 | keyHandle := res[:khLen] 68 | 69 | // output for easier consumption by another program 70 | output := map[string]interface{}{ 71 | "RegisteredData": base64.RawURLEncoding.EncodeToString(res), 72 | "PublicKey": base64.RawURLEncoding.EncodeToString(pubKey), 73 | "KeyHandle": base64.RawURLEncoding.EncodeToString(keyHandle), 74 | } 75 | jsonOut, _ := json.MarshalIndent(output, "", " ") 76 | 77 | fmt.Println(string(jsonOut)) 78 | }, 79 | } 80 | 81 | func init() { 82 | RootCmd.AddCommand(regCmd) 83 | } 84 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "os" 7 | "sort" 8 | 9 | "github.com/flynn/hid" 10 | "github.com/flynn/u2f/u2fhid" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var keyHandleFlag string 15 | var appIDFlag string 16 | var challengeFlag string 17 | var publicKeyFlag string 18 | var signatureFlag string 19 | var deviceNum int 20 | 21 | func sum256(s string) []byte { 22 | h := sha256.New() 23 | h.Write([]byte(s)) 24 | return h.Sum(nil) 25 | } 26 | 27 | // Ordered Devices by Path 28 | type Devices []*hid.DeviceInfo 29 | 30 | func (s Devices) Len() int { 31 | return len(s) 32 | } 33 | 34 | func (s Devices) Less(i, j int) bool { 35 | return s[i].Path < s[j].Path 36 | } 37 | 38 | func (s Devices) Swap(i, j int) { 39 | s[i], s[j] = s[j], s[i] 40 | } 41 | 42 | // Devices come back in a random order, which makes it diffucult to select 43 | // the device on a command line for each run 44 | func getOrderedDevices() []*hid.DeviceInfo { 45 | devices, err := u2fhid.Devices() 46 | if err != nil { 47 | fmt.Fprintf(os.Stderr, "Error opening hid usb: %+v", err) 48 | os.Exit(1) 49 | } 50 | 51 | if len(devices) == 0 { 52 | fmt.Fprintln(os.Stderr, "Error: No devices found") 53 | os.Exit(1) 54 | } 55 | 56 | sort.Sort(Devices(devices)) 57 | return devices 58 | } 59 | 60 | func getDevice() *hid.DeviceInfo { 61 | devices := getOrderedDevices() 62 | 63 | if deviceNum > len(devices) { 64 | fmt.Fprintf(os.Stderr, "Error: Device [%d] not found", deviceNum) 65 | os.Exit(1) 66 | } 67 | 68 | return devices[deviceNum-1] 69 | } 70 | 71 | // This represents the base command when called without any subcommands 72 | var RootCmd = &cobra.Command{ 73 | Use: "u2fcli", 74 | Short: "Interact with U2F tokens from the command line", 75 | Long: `u2fcli lets you interact with a hardware U2F token. 76 | Could be used for debugging, development and demonstrations purposes`, 77 | // Uncomment the following line if your bare application has an action associated with it 78 | // Run: func(cmd *cobra.Command, args []string) { }, 79 | } 80 | 81 | //Execute adds all child commands to the root command sets flags appropriately. 82 | // This is called by main.main(). It only needs to happen once to the rootCmd 83 | func Execute() { 84 | if err := RootCmd.Execute(); err != nil { 85 | fmt.Println(err) 86 | os.Exit(-1) 87 | } 88 | } 89 | 90 | func init() { 91 | RootCmd.PersistentFlags().StringVar(&challengeFlag, "challenge", "", "Challenge(string)") 92 | RootCmd.PersistentFlags().StringVar(&appIDFlag, "appid", "", "Applicaiton ID(string)") 93 | RootCmd.PersistentFlags().StringVar(&keyHandleFlag, "keyhandle", "", "Key Handle ID(base64 string)") 94 | RootCmd.PersistentFlags().StringVar(&publicKeyFlag, "publickey", "", "Public Key of the signer") 95 | RootCmd.PersistentFlags().StringVar(&signatureFlag, "signature", "", "Raw Signature from U2F 'sig'") 96 | RootCmd.PersistentFlags().IntVar(&deviceNum, "d", 1, "Device number if multiple devices available") 97 | } 98 | -------------------------------------------------------------------------------- /cmd/sig.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "os" 9 | "time" 10 | 11 | "github.com/flynn/u2f/u2fhid" 12 | "github.com/flynn/u2f/u2ftoken" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // sigCmd respresents the sig command 17 | var sigCmd = &cobra.Command{ 18 | Use: "sig", 19 | Short: "Sign a challenge with the provided Key Handle", 20 | Long: "Sign a challenge with the provided Key Handle", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | device := getDevice() 23 | 24 | if challengeFlag == "" { 25 | fmt.Fprintln(os.Stderr, "Please supply the challenge using -challenge option.") 26 | return 27 | } 28 | if appIDFlag == "" { 29 | fmt.Fprintln(os.Stderr, "Please supply the appID using -appid option.") 30 | return 31 | } 32 | if keyHandleFlag == "" { 33 | fmt.Fprintln(os.Stderr, "Please supply a valid Key Handle using -keyhandle option.") 34 | return 35 | } 36 | 37 | appIDHash := sum256(appIDFlag) 38 | challengeHash := sum256(challengeFlag) 39 | 40 | keyHandleBytes, err := base64.RawURLEncoding.DecodeString(keyHandleFlag) 41 | if err != nil { 42 | fmt.Fprintln(os.Stderr, "Keyhandle is not valid url encoded base64") 43 | } 44 | 45 | dev, err := u2fhid.Open(device) 46 | defer dev.Close() 47 | 48 | if err != nil { 49 | fmt.Fprintf(os.Stderr, "Error: %s\n", err) 50 | os.Exit(1) 51 | } 52 | 53 | t := u2ftoken.NewToken(dev) 54 | 55 | req := u2ftoken.AuthenticateRequest{ 56 | Challenge: challengeHash, 57 | Application: appIDHash, 58 | KeyHandle: keyHandleBytes, 59 | } 60 | 61 | if err := t.CheckAuthenticate(req); err != nil { 62 | fmt.Fprintf(os.Stderr, "Error: %s\n", err) 63 | os.Exit(1) 64 | } 65 | 66 | fmt.Fprintln(os.Stderr, "Authenticating, press the button on your U2F device") 67 | 68 | var resp *u2ftoken.AuthenticateResponse 69 | for { 70 | resp, err = t.Authenticate(req) 71 | if err == u2ftoken.ErrPresenceRequired { 72 | time.Sleep(200 * time.Millisecond) 73 | continue 74 | } else if err != nil { 75 | log.Fatal(err) 76 | } 77 | break 78 | } 79 | 80 | // output for easier consumption by another program 81 | output := map[string]interface{}{ 82 | "Counter": resp.Counter, 83 | "Signature": base64.RawURLEncoding.EncodeToString(resp.RawResponse), 84 | } 85 | jsonOut, _ := json.MarshalIndent(output, "", " ") 86 | 87 | fmt.Println(string(jsonOut)) 88 | 89 | }, 90 | } 91 | 92 | func init() { 93 | RootCmd.AddCommand(sigCmd) 94 | } 95 | -------------------------------------------------------------------------------- /cmd/ver.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/sha256" 7 | "encoding/asn1" 8 | "encoding/base64" 9 | "errors" 10 | "fmt" 11 | "math/big" 12 | "os" 13 | 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // Following code pulled from Timothy Stranex's U2F library 18 | // https://github.com/tstranex/u2f 19 | 20 | type ecdsaSig struct { 21 | R, S *big.Int 22 | } 23 | 24 | type authResp struct { 25 | UserPresenceVerified bool 26 | Counter uint32 27 | sig ecdsaSig 28 | raw []byte 29 | } 30 | 31 | func parseSignResponse(sd []byte) (*authResp, error) { 32 | if len(sd) < 5 { 33 | return nil, errors.New("u2f: data is too short") 34 | } 35 | 36 | var ar authResp 37 | 38 | userPresence := sd[0] 39 | if userPresence|1 != 1 { 40 | return nil, errors.New("u2f: invalid user presence byte") 41 | } 42 | ar.UserPresenceVerified = userPresence == 1 43 | 44 | ar.Counter = uint32(sd[1])<<24 | uint32(sd[2])<<16 | uint32(sd[3])<<8 | uint32(sd[4]) 45 | 46 | ar.raw = sd[:5] 47 | 48 | rest, err := asn1.Unmarshal(sd[5:], &ar.sig) 49 | if err != nil { 50 | return nil, err 51 | } 52 | if len(rest) != 0 { 53 | return nil, errors.New("u2f: trailing data") 54 | } 55 | 56 | return &ar, nil 57 | } 58 | 59 | func verifyAuthSignature(ar authResp, pubKey *ecdsa.PublicKey, appID string, clientData []byte) error { 60 | appParam := sha256.Sum256([]byte(appID)) 61 | challenge := sha256.Sum256(clientData) 62 | 63 | var buf []byte 64 | buf = append(buf, appParam[:]...) 65 | buf = append(buf, ar.raw...) 66 | buf = append(buf, challenge[:]...) 67 | hash := sha256.Sum256(buf) 68 | 69 | if !ecdsa.Verify(pubKey, hash[:], ar.sig.R, ar.sig.S) { 70 | return errors.New("u2f: invalid signature") 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func verify(appID, challenge string, signature, publicKey []byte) error { 77 | ar, err := parseSignResponse(signature) 78 | if err != nil { 79 | return fmt.Errorf("Error parsing signature: %s\n", err) 80 | } 81 | 82 | x, y := elliptic.Unmarshal(elliptic.P256(), publicKey) 83 | if x == nil { 84 | return fmt.Errorf("Error unmarshalling public key") 85 | } 86 | 87 | pubKey := &ecdsa.PublicKey{} 88 | pubKey.Curve = elliptic.P256() 89 | pubKey.X = x 90 | pubKey.Y = y 91 | 92 | if err := verifyAuthSignature(*ar, pubKey, appID, []byte(challenge)); err != nil { 93 | return fmt.Errorf("Signature did not verify") 94 | } 95 | 96 | return nil 97 | } 98 | 99 | // verCmd respresents the verify command 100 | var verCmd = &cobra.Command{ 101 | Use: "ver", 102 | Short: "Verify a signed response with the provided Public Key and Challenge", 103 | Long: "Verify a signed response with the provided Public Key and Challenge", 104 | Run: func(cmd *cobra.Command, args []string) { 105 | publicKeyBytes, err := base64.RawURLEncoding.DecodeString(publicKeyFlag) 106 | if err != nil { 107 | fmt.Fprintln(os.Stderr, "Public Key is not valid url encoded base64") 108 | os.Exit(1) 109 | } 110 | 111 | signatureBytes, err := base64.RawURLEncoding.DecodeString(signatureFlag) 112 | if err != nil { 113 | fmt.Println("Signature is not valid url encoded base64") 114 | os.Exit(1) 115 | } 116 | 117 | if err := verify(appIDFlag, challengeFlag, signatureBytes, publicKeyBytes); err != nil { 118 | fmt.Fprintf(os.Stderr, "Signature did not verify: %s\n", err) 119 | os.Exit(1) 120 | } 121 | 122 | fmt.Println("Signature verified") 123 | }, 124 | } 125 | 126 | func init() { 127 | RootCmd.AddCommand(verCmd) 128 | } 129 | -------------------------------------------------------------------------------- /cmd/ver_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/base64" 5 | "testing" 6 | ) 7 | 8 | var verifyAppID = "https://mdp.im" 9 | var verifyChallenge = "complexChallengeGoesHere" 10 | var verifyPubKey, _ = base64.RawURLEncoding.DecodeString("BCwSU4NEplH1-UYlohohwnm68YU9H54RPlCffNWa83xlOQWQ19WrqS8J17HWXk5vAFF2gcGMn__1hXxCgyYxw_k") 11 | var verifySignature, _ = base64.RawURLEncoding.DecodeString("AQAAAB8wRAIgJhy-8HvH-XOPakVnUggfzSSn0aUeObQ0TedWsjpli8ACIHiKVdcQhQ9EaHOAROL_CLcgKvJXp4-e46yMgmoIXWCt") 12 | 13 | func TestVerify(t *testing.T) { 14 | if err := verify(verifyAppID, verifyChallenge, verifySignature, verifyPubKey); err != nil { 15 | t.Error(err) 16 | } 17 | } 18 | 19 | func TestVerifyAppID(t *testing.T) { 20 | if err := verify("https://mdp.io", verifyChallenge, verifySignature, verifyPubKey); err == nil { 21 | t.Error("Should fail with the incorrect appID") 22 | } 23 | } 24 | func TestVerifyChallenge(t *testing.T) { 25 | if err := verify(verifyAppID, "notTheRightChallenge", verifySignature, verifyPubKey); err == nil { 26 | t.Error("Should fail with the incorrect challlenge") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/mdp/u2fcli/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | --------------------------------------------------------------------------------