From f747108fe2b7d50289b3f49e763a3d4145713ab6 Mon Sep 17 00:00:00 2001 From: Bjorn Neergaard Date: Wed, 27 May 2020 02:01:39 -0600 Subject: [PATCH 1/9] feat: initial windows port --- agent.go | 251 +++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 59 +-------- impl_unix.go | 39 ++++++ impl_windows.go | 18 +++ main.go | 320 ++---------------------------------------------- 6 files changed, 324 insertions(+), 364 deletions(-) create mode 100644 agent.go create mode 100644 impl_unix.go create mode 100644 impl_windows.go diff --git a/agent.go b/agent.go new file mode 100644 index 0000000..a8d0242 --- /dev/null +++ b/agent.go @@ -0,0 +1,251 @@ +// 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 ( + "bytes" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "errors" + "fmt" + "io" + "log" + "net" + "sync" + "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" +) + +type Agent struct { + mu sync.Mutex + yk *piv.YubiKey + serial uint32 +} + +var _ agent.ExtendedAgent = &Agent{} + +func serveConns(l net.Listener, a *Agent) { + for { + c, err := l.Accept() + if err != nil { + type temporary interface { + Temporary() bool + } + if err, ok := err.(temporary); ok && err.Temporary() { + log.Println("Temporary Accept error, sleeping 1s:", err) + time.Sleep(1 * time.Second) + continue + } + log.Fatalln("Failed to accept connections:", err) + } + go a.serveConn(c) + } + +} + +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) + } +} + +func healthy(yk *piv.YubiKey) bool { + // We can't use Serial because it locks the session on older firmwares, and + // can't use Retries because it fails when the session is unlocked. + _, err := yk.AttestationCertificate() + return err == nil +} + +func (a *Agent) ensureYK() error { + if a.yk == nil || !healthy(a.yk) { + if a.yk != nil { + log.Println("Reconnecting to the YubiKey...") + a.yk.Close() + } else { + log.Println("Connecting to the YubiKey...") + } + yk, err := a.connectToYK() + if err != nil { + return err + } + a.yk = yk + } + return nil +} + +func (a *Agent) connectToYK() (*piv.YubiKey, error) { + cards, err := piv.Cards() + if err != nil { + return nil, err + } + if len(cards) == 0 { + return nil, errors.New("no YubiKey detected") + } + // TODO: support multiple YubiKeys. + yk, err := piv.Open(cards[0]) + if err != nil { + return nil, err + } + // Cache the serial number locally because requesting it on older firmwares + // requires switching application, which drops the PIN cache. + a.serial, _ = yk.Serial() + return yk, nil +} + +func (a *Agent) Close() error { + a.mu.Lock() + defer a.mu.Unlock() + if a.yk != nil { + log.Println("Received SIGHUP, dropping YubiKey transaction...") + err := a.yk.Close() + a.yk = nil + return err + } + return nil +} + +func (a *Agent) getPIN() (string, error) { + p, err := pinentry.New() + if err != nil { + return "", fmt.Errorf("failed to start %q: %w", pinentry.GetBinary(), err) + } + defer p.Close() + p.Set("title", "yubikey-agent PIN Prompt") + var retries string + if r, err := a.yk.Retries(); err == nil { + retries = fmt.Sprintf(" (%d tries remaining)", r) + } + p.Set("desc", fmt.Sprintf("YubiKey serial number: %d"+retries, a.serial)) + p.Set("prompt", "Please enter your PIN:") + pin, err := p.GetPin() + return string(pin), err +} + +func (a *Agent) List() ([]*agent.Key, error) { + a.mu.Lock() + defer a.mu.Unlock() + if err := a.ensureYK(); err != nil { + return nil, fmt.Errorf("could not reach YubiKey: %w", err) + } + + pk, err := getPublicKey(a.yk, piv.SlotAuthentication) + if err != nil { + return nil, err + } + return []*agent.Key{{ + Format: pk.Type(), + Blob: pk.Marshal(), + Comment: fmt.Sprintf("YubiKey #%d PIV Slot 9a", a.serial), + }}, nil +} + +func getPublicKey(yk *piv.YubiKey, slot piv.Slot) (ssh.PublicKey, error) { + cert, err := yk.Certificate(slot) + if err != nil { + return nil, fmt.Errorf("could not get public key: %w", err) + } + switch cert.PublicKey.(type) { + case *ecdsa.PublicKey: + case *rsa.PublicKey: + default: + return nil, fmt.Errorf("unexpected public key type: %T", cert.PublicKey) + } + pk, err := ssh.NewPublicKey(cert.PublicKey) + if err != nil { + return nil, fmt.Errorf("failed to process public key: %w", err) + } + return pk, nil +} + +func (a *Agent) Signers() ([]ssh.Signer, error) { + a.mu.Lock() + defer a.mu.Unlock() + if err := a.ensureYK(); err != nil { + return nil, fmt.Errorf("could not reach YubiKey: %w", err) + } + + return a.signers() +} + +func (a *Agent) signers() ([]ssh.Signer, error) { + pk, err := getPublicKey(a.yk, piv.SlotAuthentication) + if err != nil { + return nil, err + } + priv, err := a.yk.PrivateKey( + piv.SlotAuthentication, + pk.(ssh.CryptoPublicKey).CryptoPublicKey(), + piv.KeyAuth{PINPrompt: a.getPIN}, + ) + if err != nil { + return nil, fmt.Errorf("failed to prepare private key: %w", err) + } + s, err := ssh.NewSignerFromKey(priv) + if err != nil { + return nil, fmt.Errorf("failed to prepare signer: %w", err) + } + return []ssh.Signer{s}, nil +} + +func (a *Agent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { + return a.SignWithFlags(key, data, 0) +} + +func (a *Agent) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error) { + a.mu.Lock() + defer a.mu.Unlock() + if err := a.ensureYK(); err != nil { + return nil, fmt.Errorf("could not reach YubiKey: %w", err) + } + + signers, err := a.signers() + if err != nil { + return nil, err + } + for _, s := range signers { + if !bytes.Equal(s.PublicKey().Marshal(), key.Marshal()) { + continue + } + alg := key.Type() + switch { + case alg == ssh.KeyAlgoRSA && flags&agent.SignatureFlagRsaSha256 != 0: + alg = ssh.SigAlgoRSASHA2256 + case alg == ssh.KeyAlgoRSA && flags&agent.SignatureFlagRsaSha512 != 0: + alg = ssh.SigAlgoRSASHA2512 + } + // TODO: maybe retry if the PIN is not correct? + return s.(ssh.AlgorithmSigner).SignWithAlgorithm(rand.Reader, data, alg) + } + return nil, fmt.Errorf("no private keys match the requested public key") +} + +func (a *Agent) Extension(extensionType string, contents []byte) ([]byte, error) { + return nil, agent.ErrExtensionUnsupported +} + +var ErrOperationUnsupported = errors.New("operation unsupported") + +func (a *Agent) Add(key agent.AddedKey) error { + return ErrOperationUnsupported +} +func (a *Agent) Remove(key ssh.PublicKey) error { + return ErrOperationUnsupported +} +func (a *Agent) RemoveAll() error { + return ErrOperationUnsupported +} +func (a *Agent) Lock(passphrase []byte) error { + return ErrOperationUnsupported +} +func (a *Agent) Unlock(passphrase []byte) error { + return ErrOperationUnsupported +} diff --git a/go.mod b/go.mod index 345a314..fff5991 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 2025e35..9edd48a 100644 --- a/go.sum +++ b/go.sum @@ -1,41 +1,31 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -filippo.io/age v1.0.0-beta4 h1:czSjaSa0owsI5gw/cE9yI/mfTiuhgYjozHI96v0PVJo= filippo.io/age v1.0.0-beta4/go.mod h1:TOa3exZvzRCLfjmbJGsqwSQ0HtWjJfTTCQnQsNCC4E0= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/alecthomas/binary v0.0.0-20190922233330-fb1b1d9c299c h1:SnUAzBu0FguUHChHbuy2HInhc2YBBTmbDcZOOByAVt8= +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/alecthomas/binary v0.0.0-20190922233330-fb1b1d9c299c/go.mod h1:v4e05/vzE8ubOim1No9Xx5eIQ/WRq6AtcnQIy/Z/JPs= -github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/blang/semver v0.0.0-20190414182527-1a9109f8c4a1 h1:J50TZ8HB8AZI2nOQkRhoCprLJnjiWNhxonSWcrscUUw= github.com/blang/semver v0.0.0-20190414182527-1a9109f8c4a1/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/caspr-io/yamlpath v0.0.0-20200722075116-502e8d113a9b h1:2K3B6Xm7/lnhOugeGB3nIk50bZ9zhuJvXCEfUuL68ik= github.com/caspr-io/yamlpath v0.0.0-20200722075116-502e8d113a9b/go.mod h1:4rP9T6iHCuPAIDKdNaZfTuuqSIoQQvFctNWIAUI1rlg= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dominikschulz/github-releases v0.0.3 h1:nCfpouDbB5RxdUKRH3nhA4GAOf+8epmYWu60oNmyms8= github.com/dominikschulz/github-releases v0.0.3/go.mod h1:uByjb2frn7tRe9DiPHBk5TdhI1SmZEZKgMNrrIlIT04= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/go-piv/piv-go v1.5.1-0.20200523071327-a3e5767e8b72 h1:ks5VMs/eHR427mPrB3+v7DtN+VO2Ndp/csIc0ZADApE= github.com/go-piv/piv-go v1.5.1-0.20200523071327-a3e5767e8b72/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8HKy7Gk= -github.com/godbus/dbus v0.0.0-20190623212516-8a1682060722 h1:NNKZiuNXd6lpZRyoFM/uhssj5W9Ps1DbhGHxT49Pm9I= github.com/godbus/dbus v0.0.0-20190623212516-8a1682060722/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= -github.com/gokyle/twofactor v1.0.1 h1:uRhvx0S4Hb82RPIDALnf7QxbmPL49LyyaCkJDpWx+Ek= github.com/gokyle/twofactor v1.0.1/go.mod h1:4gxzH1eaE/F3Pct/sCDNOylP0ClofUO5j4XZN9tKtLE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -45,76 +35,52 @@ github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gopasspw/gopass v1.10.1 h1:rg3iNYymLLIiBe5okW2KaT8BV1lptTU4mHtAQojLs4I= github.com/gopasspw/gopass v1.10.1/go.mod h1:E1u/gfTb4t/Bx6kXNmIAjvtkBHhP1PvZGACP2bkKOlc= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/jsimonetti/pwscheme v0.0.0-20160922125227-76804708ecad h1:hye7cQTVxBLWi3dJBAcM4Qhfqnb+VeiZzaKj6sCpTCA= github.com/jsimonetti/pwscheme v0.0.0-20160922125227-76804708ecad/go.mod h1:alT8eQtqtVCsVweGnMnfJcjNkTcmWbuVn+lYaBtBl9E= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/martinhoefling/goxkcdpwgen v0.0.0-20190331205820-7dc3d102eca3 h1:fvQLuMSKU08pIM+I7I8pjbbPjW6Nx4sf7jOx/Pjc0qI= github.com/martinhoefling/goxkcdpwgen v0.0.0-20190331205820-7dc3d102eca3/go.mod h1:4HvZROUEazha3RDnoBcxQlwcIbQfwx035roFOMnICSE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4= github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= -github.com/minio/minio-go/v6 v6.0.57 h1:ixPkbKkyD7IhnluRgQpGSpHdpvNVaW6OD5R9IAO/9Tw= github.com/minio/minio-go/v6 v6.0.57/go.mod h1:5+R/nM9Pwrh0vqF+HbYYDQ84wdUFPyXHkrdT4AIkifM= -github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/muesli/crunchy v0.4.0 h1:qdiml8gywULHBsztiSAf6rrE6EyuNasNKZ104mAaahM= github.com/muesli/crunchy v0.4.0/go.mod h1:9k4x6xdSbb7WwtAVy0iDjaiDjIk6Wa5AgUIqp+HqOpU= -github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -122,16 +88,12 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/schollz/closestmatch v0.0.0-20190308193919-1fbe626be92e h1:HFUDYOpUVZ0oTXeZy2A59Lkf69SsOF03Lg1GsI3Xh9o= github.com/schollz/closestmatch v0.0.0-20190308193919-1fbe626be92e/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= @@ -139,15 +101,13 @@ github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:s github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= -github.com/xrash/smetrics v0.0.0-20170218160415-a3153f7040e9 h1:w8V9v0qVympSF6GjdjIyeqR7+EVhAF9CBQmkmW7Zw0w= github.com/xrash/smetrics v0.0.0-20170218160415-a3153f7040e9/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -github.com/xrash/smetrics v0.0.0-20200730060457-89a2a8a1fb0b h1:tnWgqoOBmInkt5pbLjagwNVjjT4RdJhFHzL1ebCSRh8= github.com/xrash/smetrics v0.0.0-20200730060457-89a2a8a1fb0b/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -165,18 +125,18 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -184,7 +144,6 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9 h1:yi1hN8dcqI9l8klZfy4B8mJvFmmAxJEePIQQFNSd7Cs= golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -207,30 +166,24 @@ google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLY google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/impl_unix.go b/impl_unix.go new file mode 100644 index 0000000..a0d2ff5 --- /dev/null +++ b/impl_unix.go @@ -0,0 +1,39 @@ +// +build darwin dragonfly freebsd linux netbsd openbsd + +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) +} diff --git a/impl_windows.go b/impl_windows.go new file mode 100644 index 0000000..c1b77ff --- /dev/null +++ b/impl_windows.go @@ -0,0 +1,18 @@ +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) +} diff --git a/main.go b/main.go index 9456923..d1a086f 100644 --- a/main.go +++ b/main.go @@ -7,35 +7,18 @@ package main import ( - "bytes" - "context" - "crypto/ecdsa" - "crypto/rand" - "crypto/rsa" - "errors" "flag" "fmt" - "io" "log" - "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() { + log.SetFlags(0) flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage of yubikey-agent:\n") fmt.Fprintf(os.Stderr, "\n") @@ -60,7 +43,6 @@ func main() { } if *setupFlag { - log.SetFlags(0) yk := connectForSetup() if *resetFlag { runReset(yk) @@ -68,302 +50,18 @@ func main() { runSetup(yk) } else { if *socketPath == "" { - flag.Usage() - os.Exit(1) - } - runAgent(*socketPath) - } -} - -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{} - - c := make(chan os.Signal) - signal.Notify(c, syscall.SIGHUP) - go func() { - for range c { - a.Close() - } - }() - - 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) - } - - for { - c, err := l.Accept() - if err != nil { - type temporary interface { - Temporary() bool - } - if err, ok := err.(temporary); ok && err.Temporary() { - log.Println("Temporary Accept error, sleeping 1s:", err) - time.Sleep(1 * time.Second) - continue + if runtime.GOOS == "windows" { + *socketPath = "\\\\.\\\\pipe\\\\openssh-ssh-agent" + } else { + flag.Usage() + os.Exit(1) } - log.Fatalln("Failed to accept connections:", err) } - 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) - } -} - -func healthy(yk *piv.YubiKey) bool { - // We can't use Serial because it locks the session on older firmwares, and - // can't use Retries because it fails when the session is unlocked. - _, err := yk.AttestationCertificate() - return err == nil -} - -func (a *Agent) ensureYK() error { - if a.yk == nil || !healthy(a.yk) { - if a.yk != nil { - log.Println("Reconnecting to the YubiKey...") - a.yk.Close() - } else { - log.Println("Connecting to the YubiKey...") - } - yk, err := a.connectToYK() - if err != nil { - return err + if _, err := exec.LookPath(pinentry.GetBinary()); err != nil { + log.Fatalf("PIN entry program %q not found!", pinentry.GetBinary()) } - a.yk = yk - } - return nil -} - -func (a *Agent) connectToYK() (*piv.YubiKey, error) { - cards, err := piv.Cards() - if err != nil { - return nil, err - } - if len(cards) == 0 { - return nil, errors.New("no YubiKey detected") - } - // TODO: support multiple YubiKeys. - yk, err := piv.Open(cards[0]) - if err != nil { - return nil, err - } - // Cache the serial number locally because requesting it on older firmwares - // requires switching application, which drops the PIN cache. - a.serial, _ = yk.Serial() - return yk, nil -} - -func (a *Agent) Close() error { - a.mu.Lock() - defer a.mu.Unlock() - if a.yk != nil { - log.Println("Received SIGHUP, dropping YubiKey transaction...") - err := a.yk.Close() - a.yk = nil - return err - } - return nil -} -func (a *Agent) getPIN() (string, error) { - if a.touchNotification != nil && a.touchNotification.Stop() { - defer a.touchNotification.Reset(5 * time.Second) - } - p, err := pinentry.New() - if err != nil { - return "", fmt.Errorf("failed to start %q: %w", pinentry.GetBinary(), err) - } - defer p.Close() - p.Set("title", "yubikey-agent PIN Prompt") - var retries string - if r, err := a.yk.Retries(); err == nil { - retries = fmt.Sprintf(" (%d tries remaining)", r) - } - p.Set("desc", fmt.Sprintf("YubiKey serial number: %d"+retries, a.serial)) - p.Set("prompt", "Please enter your PIN:") - - // Enable opt-in external PIN caching (in the OS keychain). - // https://gist.github.com/mdeguzis/05d1f284f931223624834788da045c65#file-info-pinentry-L324 - p.Option("allow-external-password-cache") - p.Set("KEYINFO", fmt.Sprintf("--yubikey-id-%d", a.serial)) - - pin, err := p.GetPin() - return string(pin), err -} - -func (a *Agent) List() ([]*agent.Key, error) { - a.mu.Lock() - defer a.mu.Unlock() - if err := a.ensureYK(); err != nil { - return nil, fmt.Errorf("could not reach YubiKey: %w", err) - } - - pk, err := getPublicKey(a.yk, piv.SlotAuthentication) - if err != nil { - return nil, err - } - return []*agent.Key{{ - Format: pk.Type(), - Blob: pk.Marshal(), - Comment: fmt.Sprintf("YubiKey #%d PIV Slot 9a", a.serial), - }}, nil -} - -func getPublicKey(yk *piv.YubiKey, slot piv.Slot) (ssh.PublicKey, error) { - cert, err := yk.Certificate(slot) - if err != nil { - return nil, fmt.Errorf("could not get public key: %w", err) - } - switch cert.PublicKey.(type) { - case *ecdsa.PublicKey: - case *rsa.PublicKey: - default: - return nil, fmt.Errorf("unexpected public key type: %T", cert.PublicKey) - } - pk, err := ssh.NewPublicKey(cert.PublicKey) - if err != nil { - return nil, fmt.Errorf("failed to process public key: %w", err) - } - return pk, nil -} - -func (a *Agent) Signers() ([]ssh.Signer, error) { - a.mu.Lock() - defer a.mu.Unlock() - if err := a.ensureYK(); err != nil { - return nil, fmt.Errorf("could not reach YubiKey: %w", err) - } - - return a.signers() -} - -func (a *Agent) signers() ([]ssh.Signer, error) { - pk, err := getPublicKey(a.yk, piv.SlotAuthentication) - if err != nil { - return nil, err - } - priv, err := a.yk.PrivateKey( - piv.SlotAuthentication, - pk.(ssh.CryptoPublicKey).CryptoPublicKey(), - piv.KeyAuth{PINPrompt: a.getPIN}, - ) - if err != nil { - return nil, fmt.Errorf("failed to prepare private key: %w", err) - } - s, err := ssh.NewSignerFromKey(priv) - if err != nil { - return nil, fmt.Errorf("failed to prepare signer: %w", err) - } - return []ssh.Signer{s}, nil -} - -func (a *Agent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { - return a.SignWithFlags(key, data, 0) -} - -func (a *Agent) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error) { - a.mu.Lock() - defer a.mu.Unlock() - if err := a.ensureYK(); err != nil { - return nil, fmt.Errorf("could not reach YubiKey: %w", err) - } - - signers, err := a.signers() - if err != nil { - return nil, err - } - for _, s := range signers { - if !bytes.Equal(s.PublicKey().Marshal(), key.Marshal()) { - continue - } - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - a.touchNotification = time.NewTimer(5 * time.Second) - go func() { - select { - case <-a.touchNotification.C: - case <-ctx.Done(): - a.touchNotification.Stop() - return - } - showNotification("Waiting for YubiKey touch...") - }() - - alg := key.Type() - switch { - case alg == ssh.KeyAlgoRSA && flags&agent.SignatureFlagRsaSha256 != 0: - alg = ssh.SigAlgoRSASHA2256 - case alg == ssh.KeyAlgoRSA && flags&agent.SignatureFlagRsaSha512 != 0: - alg = ssh.SigAlgoRSASHA2512 - } - // TODO: maybe retry if the PIN is not correct? - return s.(ssh.AlgorithmSigner).SignWithAlgorithm(rand.Reader, data, alg) - } - return nil, fmt.Errorf("no private keys match the requested public key") -} - -func showNotification(message string) { - switch runtime.GOOS { - case "darwin": - message = strings.ReplaceAll(message, `\`, `\\`) - message = strings.ReplaceAll(message, `"`, `\"`) - appleScript := `display notification "%s" with title "yubikey-agent"` - exec.Command("osascript", "-e", fmt.Sprintf(appleScript, message)).Run() - case "linux": - exec.Command("notify-send", "-i", "dialog-password", "yubikey-agent", message).Run() + runAgent(*socketPath) } } - -func (a *Agent) Extension(extensionType string, contents []byte) ([]byte, error) { - return nil, agent.ErrExtensionUnsupported -} - -var ErrOperationUnsupported = errors.New("operation unsupported") - -func (a *Agent) Add(key agent.AddedKey) error { - return ErrOperationUnsupported -} -func (a *Agent) Remove(key ssh.PublicKey) error { - return ErrOperationUnsupported -} -func (a *Agent) RemoveAll() error { - return ErrOperationUnsupported -} -func (a *Agent) Lock(passphrase []byte) error { - return ErrOperationUnsupported -} -func (a *Agent) Unlock(passphrase []byte) error { - return ErrOperationUnsupported -} From bb2f8f5062613f694f1126e9ea1718e4772b314d Mon Sep 17 00:00:00 2001 From: Axel Gluth Date: Mon, 21 Dec 2020 12:50:48 +0100 Subject: [PATCH 2/9] Move agent functions back to main.go --- agent.go | 251 ------------------------------------------------- main.go | 282 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 278 insertions(+), 255 deletions(-) delete mode 100644 agent.go diff --git a/agent.go b/agent.go deleted file mode 100644 index a8d0242..0000000 --- a/agent.go +++ /dev/null @@ -1,251 +0,0 @@ -// 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 ( - "bytes" - "crypto/ecdsa" - "crypto/rand" - "crypto/rsa" - "errors" - "fmt" - "io" - "log" - "net" - "sync" - "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" -) - -type Agent struct { - mu sync.Mutex - yk *piv.YubiKey - serial uint32 -} - -var _ agent.ExtendedAgent = &Agent{} - -func serveConns(l net.Listener, a *Agent) { - for { - c, err := l.Accept() - if err != nil { - type temporary interface { - Temporary() bool - } - if err, ok := err.(temporary); ok && err.Temporary() { - log.Println("Temporary Accept error, sleeping 1s:", err) - time.Sleep(1 * time.Second) - continue - } - log.Fatalln("Failed to accept connections:", err) - } - go a.serveConn(c) - } - -} - -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) - } -} - -func healthy(yk *piv.YubiKey) bool { - // We can't use Serial because it locks the session on older firmwares, and - // can't use Retries because it fails when the session is unlocked. - _, err := yk.AttestationCertificate() - return err == nil -} - -func (a *Agent) ensureYK() error { - if a.yk == nil || !healthy(a.yk) { - if a.yk != nil { - log.Println("Reconnecting to the YubiKey...") - a.yk.Close() - } else { - log.Println("Connecting to the YubiKey...") - } - yk, err := a.connectToYK() - if err != nil { - return err - } - a.yk = yk - } - return nil -} - -func (a *Agent) connectToYK() (*piv.YubiKey, error) { - cards, err := piv.Cards() - if err != nil { - return nil, err - } - if len(cards) == 0 { - return nil, errors.New("no YubiKey detected") - } - // TODO: support multiple YubiKeys. - yk, err := piv.Open(cards[0]) - if err != nil { - return nil, err - } - // Cache the serial number locally because requesting it on older firmwares - // requires switching application, which drops the PIN cache. - a.serial, _ = yk.Serial() - return yk, nil -} - -func (a *Agent) Close() error { - a.mu.Lock() - defer a.mu.Unlock() - if a.yk != nil { - log.Println("Received SIGHUP, dropping YubiKey transaction...") - err := a.yk.Close() - a.yk = nil - return err - } - return nil -} - -func (a *Agent) getPIN() (string, error) { - p, err := pinentry.New() - if err != nil { - return "", fmt.Errorf("failed to start %q: %w", pinentry.GetBinary(), err) - } - defer p.Close() - p.Set("title", "yubikey-agent PIN Prompt") - var retries string - if r, err := a.yk.Retries(); err == nil { - retries = fmt.Sprintf(" (%d tries remaining)", r) - } - p.Set("desc", fmt.Sprintf("YubiKey serial number: %d"+retries, a.serial)) - p.Set("prompt", "Please enter your PIN:") - pin, err := p.GetPin() - return string(pin), err -} - -func (a *Agent) List() ([]*agent.Key, error) { - a.mu.Lock() - defer a.mu.Unlock() - if err := a.ensureYK(); err != nil { - return nil, fmt.Errorf("could not reach YubiKey: %w", err) - } - - pk, err := getPublicKey(a.yk, piv.SlotAuthentication) - if err != nil { - return nil, err - } - return []*agent.Key{{ - Format: pk.Type(), - Blob: pk.Marshal(), - Comment: fmt.Sprintf("YubiKey #%d PIV Slot 9a", a.serial), - }}, nil -} - -func getPublicKey(yk *piv.YubiKey, slot piv.Slot) (ssh.PublicKey, error) { - cert, err := yk.Certificate(slot) - if err != nil { - return nil, fmt.Errorf("could not get public key: %w", err) - } - switch cert.PublicKey.(type) { - case *ecdsa.PublicKey: - case *rsa.PublicKey: - default: - return nil, fmt.Errorf("unexpected public key type: %T", cert.PublicKey) - } - pk, err := ssh.NewPublicKey(cert.PublicKey) - if err != nil { - return nil, fmt.Errorf("failed to process public key: %w", err) - } - return pk, nil -} - -func (a *Agent) Signers() ([]ssh.Signer, error) { - a.mu.Lock() - defer a.mu.Unlock() - if err := a.ensureYK(); err != nil { - return nil, fmt.Errorf("could not reach YubiKey: %w", err) - } - - return a.signers() -} - -func (a *Agent) signers() ([]ssh.Signer, error) { - pk, err := getPublicKey(a.yk, piv.SlotAuthentication) - if err != nil { - return nil, err - } - priv, err := a.yk.PrivateKey( - piv.SlotAuthentication, - pk.(ssh.CryptoPublicKey).CryptoPublicKey(), - piv.KeyAuth{PINPrompt: a.getPIN}, - ) - if err != nil { - return nil, fmt.Errorf("failed to prepare private key: %w", err) - } - s, err := ssh.NewSignerFromKey(priv) - if err != nil { - return nil, fmt.Errorf("failed to prepare signer: %w", err) - } - return []ssh.Signer{s}, nil -} - -func (a *Agent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { - return a.SignWithFlags(key, data, 0) -} - -func (a *Agent) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error) { - a.mu.Lock() - defer a.mu.Unlock() - if err := a.ensureYK(); err != nil { - return nil, fmt.Errorf("could not reach YubiKey: %w", err) - } - - signers, err := a.signers() - if err != nil { - return nil, err - } - for _, s := range signers { - if !bytes.Equal(s.PublicKey().Marshal(), key.Marshal()) { - continue - } - alg := key.Type() - switch { - case alg == ssh.KeyAlgoRSA && flags&agent.SignatureFlagRsaSha256 != 0: - alg = ssh.SigAlgoRSASHA2256 - case alg == ssh.KeyAlgoRSA && flags&agent.SignatureFlagRsaSha512 != 0: - alg = ssh.SigAlgoRSASHA2512 - } - // TODO: maybe retry if the PIN is not correct? - return s.(ssh.AlgorithmSigner).SignWithAlgorithm(rand.Reader, data, alg) - } - return nil, fmt.Errorf("no private keys match the requested public key") -} - -func (a *Agent) Extension(extensionType string, contents []byte) ([]byte, error) { - return nil, agent.ErrExtensionUnsupported -} - -var ErrOperationUnsupported = errors.New("operation unsupported") - -func (a *Agent) Add(key agent.AddedKey) error { - return ErrOperationUnsupported -} -func (a *Agent) Remove(key ssh.PublicKey) error { - return ErrOperationUnsupported -} -func (a *Agent) RemoveAll() error { - return ErrOperationUnsupported -} -func (a *Agent) Lock(passphrase []byte) error { - return ErrOperationUnsupported -} -func (a *Agent) Unlock(passphrase []byte) error { - return ErrOperationUnsupported -} diff --git a/main.go b/main.go index d1a086f..b13f35a 100644 --- a/main.go +++ b/main.go @@ -7,18 +7,31 @@ package main import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "errors" "flag" "fmt" + "io" "log" + "net" "os" "os/exec" "runtime" + "strings" + "sync" + "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" ) func main() { - log.SetFlags(0) flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage of yubikey-agent:\n") fmt.Fprintf(os.Stderr, "\n") @@ -43,6 +56,7 @@ func main() { } if *setupFlag { + log.SetFlags(0) yk := connectForSetup() if *resetFlag { runReset(yk) @@ -57,11 +71,271 @@ func main() { os.Exit(1) } } + runAgent(*socketPath) + } +} + +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{} - if _, err := exec.LookPath(pinentry.GetBinary()); err != nil { - log.Fatalf("PIN entry program %q not found!", pinentry.GetBinary()) +func serveConns(l net.Listener, a *Agent) { + for { + c, err := l.Accept() + if err != nil { + type temporary interface { + Temporary() bool + } + if err, ok := err.(temporary); ok && err.Temporary() { + log.Println("Temporary Accept error, sleeping 1s:", err) + time.Sleep(1 * time.Second) + continue + } + log.Fatalln("Failed to accept connections:", err) } + go a.serveConn(c) + } - runAgent(*socketPath) +} + +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) + } +} + +func healthy(yk *piv.YubiKey) bool { + // We can't use Serial because it locks the session on older firmwares, and + // can't use Retries because it fails when the session is unlocked. + _, err := yk.AttestationCertificate() + return err == nil +} + +func (a *Agent) ensureYK() error { + if a.yk == nil || !healthy(a.yk) { + if a.yk != nil { + log.Println("Reconnecting to the YubiKey...") + a.yk.Close() + } else { + log.Println("Connecting to the YubiKey...") + } + yk, err := a.connectToYK() + if err != nil { + return err + } + a.yk = yk + } + return nil +} + +func (a *Agent) connectToYK() (*piv.YubiKey, error) { + cards, err := piv.Cards() + if err != nil { + return nil, err + } + if len(cards) == 0 { + return nil, errors.New("no YubiKey detected") + } + // TODO: support multiple YubiKeys. + yk, err := piv.Open(cards[0]) + if err != nil { + return nil, err + } + // Cache the serial number locally because requesting it on older firmwares + // requires switching application, which drops the PIN cache. + a.serial, _ = yk.Serial() + return yk, nil +} + +func (a *Agent) Close() error { + a.mu.Lock() + defer a.mu.Unlock() + if a.yk != nil { + log.Println("Received SIGHUP, dropping YubiKey transaction...") + err := a.yk.Close() + a.yk = nil + return err + } + return nil +} + +func (a *Agent) getPIN() (string, error) { + if a.touchNotification != nil && a.touchNotification.Stop() { + defer a.touchNotification.Reset(5 * time.Second) + } + p, err := pinentry.New() + if err != nil { + return "", fmt.Errorf("failed to start %q: %w", pinentry.GetBinary(), err) + } + defer p.Close() + p.Set("title", "yubikey-agent PIN Prompt") + var retries string + if r, err := a.yk.Retries(); err == nil { + retries = fmt.Sprintf(" (%d tries remaining)", r) + } + p.Set("desc", fmt.Sprintf("YubiKey serial number: %d"+retries, a.serial)) + p.Set("prompt", "Please enter your PIN:") + + // Enable opt-in external PIN caching (in the OS keychain). + // https://gist.github.com/mdeguzis/05d1f284f931223624834788da045c65#file-info-pinentry-L324 + p.Option("allow-external-password-cache") + p.Set("KEYINFO", fmt.Sprintf("--yubikey-id-%d", a.serial)) + + pin, err := p.GetPin() + return string(pin), err +} + +func (a *Agent) List() ([]*agent.Key, error) { + a.mu.Lock() + defer a.mu.Unlock() + if err := a.ensureYK(); err != nil { + return nil, fmt.Errorf("could not reach YubiKey: %w", err) + } + + pk, err := getPublicKey(a.yk, piv.SlotAuthentication) + if err != nil { + return nil, err } + return []*agent.Key{{ + Format: pk.Type(), + Blob: pk.Marshal(), + Comment: fmt.Sprintf("YubiKey #%d PIV Slot 9a", a.serial), + }}, nil +} + +func getPublicKey(yk *piv.YubiKey, slot piv.Slot) (ssh.PublicKey, error) { + cert, err := yk.Certificate(slot) + if err != nil { + return nil, fmt.Errorf("could not get public key: %w", err) + } + switch cert.PublicKey.(type) { + case *ecdsa.PublicKey: + case *rsa.PublicKey: + default: + return nil, fmt.Errorf("unexpected public key type: %T", cert.PublicKey) + } + pk, err := ssh.NewPublicKey(cert.PublicKey) + if err != nil { + return nil, fmt.Errorf("failed to process public key: %w", err) + } + return pk, nil +} + +func (a *Agent) Signers() ([]ssh.Signer, error) { + a.mu.Lock() + defer a.mu.Unlock() + if err := a.ensureYK(); err != nil { + return nil, fmt.Errorf("could not reach YubiKey: %w", err) + } + + return a.signers() +} + +func (a *Agent) signers() ([]ssh.Signer, error) { + pk, err := getPublicKey(a.yk, piv.SlotAuthentication) + if err != nil { + return nil, err + } + priv, err := a.yk.PrivateKey( + piv.SlotAuthentication, + pk.(ssh.CryptoPublicKey).CryptoPublicKey(), + piv.KeyAuth{PINPrompt: a.getPIN}, + ) + if err != nil { + return nil, fmt.Errorf("failed to prepare private key: %w", err) + } + s, err := ssh.NewSignerFromKey(priv) + if err != nil { + return nil, fmt.Errorf("failed to prepare signer: %w", err) + } + return []ssh.Signer{s}, nil +} + +func (a *Agent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { + return a.SignWithFlags(key, data, 0) +} + +func (a *Agent) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error) { + a.mu.Lock() + defer a.mu.Unlock() + if err := a.ensureYK(); err != nil { + return nil, fmt.Errorf("could not reach YubiKey: %w", err) + } + + signers, err := a.signers() + if err != nil { + return nil, err + } + for _, s := range signers { + if !bytes.Equal(s.PublicKey().Marshal(), key.Marshal()) { + continue + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + a.touchNotification = time.NewTimer(5 * time.Second) + go func() { + select { + case <-a.touchNotification.C: + case <-ctx.Done(): + a.touchNotification.Stop() + return + } + showNotification("Waiting for YubiKey touch...") + }() + + alg := key.Type() + switch { + case alg == ssh.KeyAlgoRSA && flags&agent.SignatureFlagRsaSha256 != 0: + alg = ssh.SigAlgoRSASHA2256 + case alg == ssh.KeyAlgoRSA && flags&agent.SignatureFlagRsaSha512 != 0: + alg = ssh.SigAlgoRSASHA2512 + } + // TODO: maybe retry if the PIN is not correct? + return s.(ssh.AlgorithmSigner).SignWithAlgorithm(rand.Reader, data, alg) + } + return nil, fmt.Errorf("no private keys match the requested public key") +} + +func showNotification(message string) { + switch runtime.GOOS { + case "darwin": + message = strings.ReplaceAll(message, `\`, `\\`) + message = strings.ReplaceAll(message, `"`, `\"`) + appleScript := `display notification "%s" with title "yubikey-agent"` + exec.Command("osascript", "-e", fmt.Sprintf(appleScript, message)).Run() + case "linux": + exec.Command("notify-send", "-i", "dialog-password", "yubikey-agent", message).Run() + } +} + +func (a *Agent) Extension(extensionType string, contents []byte) ([]byte, error) { + return nil, agent.ErrExtensionUnsupported +} + +var ErrOperationUnsupported = errors.New("operation unsupported") + +func (a *Agent) Add(key agent.AddedKey) error { + return ErrOperationUnsupported +} +func (a *Agent) Remove(key ssh.PublicKey) error { + return ErrOperationUnsupported +} +func (a *Agent) RemoveAll() error { + return ErrOperationUnsupported +} +func (a *Agent) Lock(passphrase []byte) error { + return ErrOperationUnsupported +} +func (a *Agent) Unlock(passphrase []byte) error { + return ErrOperationUnsupported } From faee8482e896f845bbc299fecabb5ad6b71eb6fb Mon Sep 17 00:00:00 2001 From: Axel Gluth Date: Tue, 22 Dec 2020 07:01:44 +0100 Subject: [PATCH 3/9] Add missing license headers --- impl_unix.go | 6 ++++++ impl_windows.go | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/impl_unix.go b/impl_unix.go index a0d2ff5..697a936 100644 --- a/impl_unix.go +++ b/impl_unix.go @@ -1,5 +1,11 @@ // +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 ( diff --git a/impl_windows.go b/impl_windows.go index c1b77ff..4190eea 100644 --- a/impl_windows.go +++ b/impl_windows.go @@ -1,3 +1,9 @@ +// 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 ( From 0a8970da6b6192461e264e0843e9dcd4005a5713 Mon Sep 17 00:00:00 2001 From: Axel Gluth Date: Tue, 22 Dec 2020 07:03:37 +0100 Subject: [PATCH 4/9] Consistent naming for main files --- impl_unix.go => main_unix.go | 0 impl_windows.go => main_windows.go | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename impl_unix.go => main_unix.go (100%) rename impl_windows.go => main_windows.go (100%) diff --git a/impl_unix.go b/main_unix.go similarity index 100% rename from impl_unix.go rename to main_unix.go diff --git a/impl_windows.go b/main_windows.go similarity index 100% rename from impl_windows.go rename to main_windows.go From 5c0c633d25ce4421b58bc7fbbb057f1d719cc013 Mon Sep 17 00:00:00 2001 From: Axel Gluth Date: Tue, 22 Dec 2020 07:21:47 +0100 Subject: [PATCH 5/9] Move default named pipe to flag --- main.go | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/main.go b/main.go index b13f35a..6a35986 100644 --- a/main.go +++ b/main.go @@ -39,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() @@ -64,12 +78,8 @@ func main() { runSetup(yk) } else { if *socketPath == "" { - if runtime.GOOS == "windows" { - *socketPath = "\\\\.\\\\pipe\\\\openssh-ssh-agent" - } else { - flag.Usage() - os.Exit(1) - } + flag.Usage() + os.Exit(1) } runAgent(*socketPath) } From 89b383be17b309de31679dd9407663ff63ed9bbe Mon Sep 17 00:00:00 2001 From: Axel Gluth Date: Tue, 22 Dec 2020 06:47:21 +0100 Subject: [PATCH 6/9] Identify UbiKey with multiple cards in the system --- main.go | 22 ++++++++++++++++++---- setup.go | 9 +-------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/main.go b/main.go index 6a35986..bda2ecd 100644 --- a/main.go +++ b/main.go @@ -147,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 } diff --git a/setup.go b/setup.go index 7cba085..3cf64f6 100644 --- a/setup.go +++ b/setup.go @@ -43,15 +43,8 @@ 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) } From 4f046ef92cff147f06acd6f81541abe684079c85 Mon Sep 17 00:00:00 2001 From: Axel Gluth Date: Tue, 22 Dec 2020 10:02:22 +0100 Subject: [PATCH 7/9] Get user input before connecting to card The card might timeout or be reset by other tools accessing it while waiting for input. --- main.go | 5 ++--- setup.go | 24 +++++++++++++----------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/main.go b/main.go index bda2ecd..90d44f4 100644 --- a/main.go +++ b/main.go @@ -71,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() diff --git a/setup.go b/setup.go index 3cf64f6..a80d352 100644 --- a/setup.go +++ b/setup.go @@ -51,23 +51,15 @@ func connectForSetup() *piv.YubiKey { 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("") @@ -89,6 +81,16 @@ func runSetup(yk *piv.YubiKey) { 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...") From 5fb0a4d36ee582874474a37925488dc666693734 Mon Sep 17 00:00:00 2001 From: Axel Gluth Date: Tue, 22 Dec 2020 10:13:35 +0100 Subject: [PATCH 8/9] Remove unnecessary else --- setup.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.go b/setup.go index a80d352..ba15bf2 100644 --- a/setup.go +++ b/setup.go @@ -77,7 +77,8 @@ func runSetup() { 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!") } From 41efb9a38c38e002f65951ab3b875b334db04302 Mon Sep 17 00:00:00 2001 From: Axel Gluth Date: Wed, 23 Dec 2020 19:02:44 +0100 Subject: [PATCH 9/9] WIP: Cache PIN --- main.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/main.go b/main.go index 90d44f4..4e5c3ef 100644 --- a/main.go +++ b/main.go @@ -88,6 +88,7 @@ type Agent struct { mu sync.Mutex yk *piv.YubiKey serial uint32 + ykPIN string // 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 @@ -195,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) @@ -214,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 }