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

Windows support 2 #69

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module filippo.io/yubikey-agent
go 1.14

require (
github.com/Microsoft/go-winio v0.4.14
github.com/go-piv/piv-go v1.5.1-0.20200523071327-a3e5767e8b72
github.com/gopasspw/gopass v1.10.1
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
Expand Down
59 changes: 6 additions & 53 deletions go.sum

Large diffs are not rendered by default.

114 changes: 59 additions & 55 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,15 @@ import (
"net"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"

"github.com/go-piv/piv-go/piv"
"github.com/gopasspw/gopass/pkg/pinentry"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"golang.org/x/crypto/ssh/terminal"
)

func main() {
Expand All @@ -43,13 +39,27 @@ func main() {
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "\t\tGenerate a new SSH key on the attached YubiKey.\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "\tyubikey-agent -l PATH\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "\t\tRun the agent, listening on the UNIX socket at PATH.\n")
fmt.Fprintf(os.Stderr, "\n")
if runtime.GOOS == "windows" {
fmt.Fprintf(os.Stderr, "\tyubikey-agent [-l PATH]\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "\t\tRun the agent,\n")
fmt.Fprintf(os.Stderr, "\t\tlistening on the named pipe at PATH.\n")
fmt.Fprintf(os.Stderr, "\t\tdefaults to \\\\.\\\\pipe\\\\openssh-ssh-agent\n")
fmt.Fprintf(os.Stderr, "\n")
} else {
fmt.Fprintf(os.Stderr, "\tyubikey-agent -l PATH\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "\t\tRun the agent, listening on the UNIX socket at PATH.\n")
fmt.Fprintf(os.Stderr, "\n")
}
}

socketPath := flag.String("l", "", "agent: path of the UNIX socket to listen on")
var socketPath *string
if runtime.GOOS == "windows" {
socketPath = flag.String("l", "\\\\.\\\\pipe\\\\openssh-ssh-agent", "agent: named pipe to listen on")
} else {
socketPath = flag.String("l", "", "agent: path of the UNIX socket to listen on")
}
resetFlag := flag.Bool("really-delete-all-piv-keys", false, "setup: reset the PIV applet")
setupFlag := flag.Bool("setup", false, "setup: configure a new YubiKey")
flag.Parse()
Expand All @@ -61,11 +71,10 @@ func main() {

if *setupFlag {
log.SetFlags(0)
yk := connectForSetup()
if *resetFlag {
runReset(yk)
runReset()
}
runSetup(yk)
runSetup()
} else {
if *socketPath == "" {
flag.Usage()
Expand All @@ -75,36 +84,21 @@ func main() {
}
}

func runAgent(socketPath string) {
if _, err := exec.LookPath(pinentry.GetBinary()); err != nil {
log.Fatalf("PIN entry program %q not found!", pinentry.GetBinary())
}

if terminal.IsTerminal(int(os.Stdin.Fd())) {
log.Println("Warning: yubikey-agent is meant to run as a background daemon.")
log.Println("Running multiple instances is likely to lead to conflicts.")
log.Println("Consider using the launchd or systemd services.")
}

a := &Agent{}
type Agent struct {
mu sync.Mutex
yk *piv.YubiKey
serial uint32
ykPIN string

c := make(chan os.Signal)
signal.Notify(c, syscall.SIGHUP)
go func() {
for range c {
a.Close()
}
}()
// touchNotification is armed by Sign to show a notification if waiting for
// more than a few seconds for the touch operation. It is paused and reset
// by getPIN so it won't fire while waiting for the PIN.
touchNotification *time.Timer
}

os.Remove(socketPath)
if err := os.MkdirAll(filepath.Dir(socketPath), 0777); err != nil {
log.Fatalln("Failed to create UNIX socket folder:", err)
}
l, err := net.Listen("unix", socketPath)
if err != nil {
log.Fatalln("Failed to listen on UNIX socket:", err)
}
var _ agent.ExtendedAgent = &Agent{}

func serveConns(l net.Listener, a *Agent) {
for {
c, err := l.Accept()
if err != nil {
Expand All @@ -120,21 +114,9 @@ func runAgent(socketPath string) {
}
go a.serveConn(c)
}
}

type Agent struct {
mu sync.Mutex
yk *piv.YubiKey
serial uint32

// touchNotification is armed by Sign to show a notification if waiting for
// more than a few seconds for the touch operation. It is paused and reset
// by getPIN so it won't fire while waiting for the PIN.
touchNotification *time.Timer
}

var _ agent.ExtendedAgent = &Agent{}

func (a *Agent) serveConn(c net.Conn) {
if err := agent.ServeAgent(a, c); err != io.EOF {
log.Println("Agent client connection ended with error:", err)
Expand Down Expand Up @@ -165,16 +147,30 @@ func (a *Agent) ensureYK() error {
return nil
}

func (a *Agent) connectToYK() (*piv.YubiKey, error) {
// findYubikey returns the first card that contains "ubikey" in it's name
// If no name matches this pattern, returns the first card in the system
func findYubikey() string {
cards, err := piv.Cards()
if err != nil {
return nil, err
log.Fatalln("Failed to enumerate tokens:", err)
}
if len(cards) == 0 {
return nil, errors.New("no YubiKey detected")
log.Fatalln("No YubiKeys detected!")
}
for _, card := range cards {
if strings.Contains(strings.ToLower(card), "ubikey") {
// Return first UbiKey found
// YubiKey identifiers: https://support.yubico.com/hc/en-us/articles/360016614920-YubiKey-USB-ID-Values
return card
}
}
// Fallback to first card in system
return cards[0]
}

func (a *Agent) connectToYK() (*piv.YubiKey, error) {
// TODO: support multiple YubiKeys.
yk, err := piv.Open(cards[0])
yk, err := piv.Open(findYubikey())
if err != nil {
return nil, err
}
Expand All @@ -200,6 +196,9 @@ func (a *Agent) getPIN() (string, error) {
if a.touchNotification != nil && a.touchNotification.Stop() {
defer a.touchNotification.Reset(5 * time.Second)
}
if a.ykPIN != "" {
return a.ykPIN, nil
}
p, err := pinentry.New()
if err != nil {
return "", fmt.Errorf("failed to start %q: %w", pinentry.GetBinary(), err)
Expand All @@ -219,6 +218,11 @@ func (a *Agent) getPIN() (string, error) {
p.Set("KEYINFO", fmt.Sprintf("--yubikey-id-%d", a.serial))

pin, err := p.GetPin()
if err != nil {
return "", err
}
a.ykPIN = string(pin)

return string(pin), err
}

Expand Down
45 changes: 45 additions & 0 deletions main_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// +build darwin dragonfly freebsd linux netbsd openbsd

// Copyright 2020 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd

package main

import (
"log"
"net"
"os"
"os/signal"
"syscall"

"golang.org/x/crypto/ssh/terminal"
)

func runAgent(socketPath string) {
if terminal.IsTerminal(int(os.Stdin.Fd())) {
log.Println("Warning: yubikey-agent is meant to run as a background daemon.")
log.Println("Running multiple instances is likely to lead to conflicts.")
log.Println("Consider using the launchd or systemd services.")
}

a := &Agent{}

c := make(chan os.Signal)
signal.Notify(c, syscall.SIGHUP)
go func() {
for range c {
a.Close()
}
}()

os.Remove(socketPath)
l, err := net.Listen("unix", socketPath)
if err != nil {
log.Fatalln("Failed to listen on UNIX socket:", err)
}

serveConns(l, a)
}
24 changes: 24 additions & 0 deletions main_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2020 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd

package main

import (
"log"

"github.com/Microsoft/go-winio"
)

func runAgent(pipeAddress string) {
a := &Agent{}

l, err := winio.ListenPipe(pipeAddress, nil)
if err != nil {
log.Fatalln("Failed to listen on Windows pipe:", err)
}

serveConns(l, a)
}
36 changes: 16 additions & 20 deletions setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,38 +43,23 @@ func init() {
}

func connectForSetup() *piv.YubiKey {
cards, err := piv.Cards()
if err != nil {
log.Fatalln("Failed to enumerate tokens:", err)
}
if len(cards) == 0 {
log.Fatalln("No YubiKeys detected!")
}
// TODO: support multiple YubiKeys.
yk, err := piv.Open(cards[0])
yk, err := piv.Open(findYubikey())
if err != nil {
log.Fatalln("Failed to connect to the YubiKey:", err)
}
return yk
}

func runReset(yk *piv.YubiKey) {
func runReset() {
fmt.Println("Resetting YubiKey PIV applet...")
yk := connectForSetup()
if err := yk.Reset(); err != nil {
log.Fatalln("Failed to reset YubiKey:", err)
}
}

func runSetup(yk *piv.YubiKey) {
if _, err := yk.Certificate(piv.SlotAuthentication); err == nil {
log.Println("‼️ This YubiKey looks already setup")
log.Println("")
log.Println("If you want to wipe all PIV keys and start fresh,")
log.Fatalln("use --really-delete-all-piv-keys ⚠️")
} else if !errors.Is(err, piv.ErrNotFound) {
log.Fatalln("Failed to access authentication slot:", err)
}

func runSetup() {
fmt.Println("🔐 The PIN is up to 8 numbers, letters, or symbols. Not just numbers!")
fmt.Println("❌ The key will be lost if the PIN and PUK are locked after 3 incorrect tries.")
fmt.Println("")
Expand All @@ -92,10 +77,21 @@ func runSetup(yk *piv.YubiKey) {
fmt.Print("\n")
if err != nil {
log.Fatalln("Failed to read PIN:", err)
} else if !bytes.Equal(repeat, pin) {
}
if !bytes.Equal(repeat, pin) {
log.Fatalln("PINs don't match!")
}

yk := connectForSetup()
if _, err := yk.Certificate(piv.SlotAuthentication); err == nil {
log.Println("‼️ This YubiKey looks already setup")
log.Println("")
log.Println("If you want to wipe all PIV keys and start fresh,")
log.Fatalln("use --really-delete-all-piv-keys ⚠️")
} else if !errors.Is(err, piv.ErrNotFound) {
log.Fatalln("Failed to access authentication slot:", err)
}

fmt.Println("")
fmt.Println("🧪 Reticulating splines...")

Expand Down