pax_global_header00006660000000000000000000000064140430264230014510gustar00rootroot0000000000000052 comment=6a3f4fc873150831b7b0e5de2efeb93c55605c72 yubikey-agent-0.1.4/000077500000000000000000000000001404302642300142675ustar00rootroot00000000000000yubikey-agent-0.1.4/LICENSE000066400000000000000000000026501404302642300152770ustar00rootroot00000000000000Copyright 2020 Google LLC Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. yubikey-agent-0.1.4/README.md000066400000000000000000000157621404302642300155610ustar00rootroot00000000000000# yubikey-agent yubikey-agent is a seamless ssh-agent for YubiKeys. * **Easy to use.** A one-command setup, one environment variable, and it just runs in the background. * **Indestructible.** Tolerates unplugging, sleep, and suspend. Never needs restarting. * **Compatible.** Provides a public key that works with all services and servers. * **Secure.** The key is generated on the YubiKey and can't be extracted. Every session requires the PIN, every login requires a touch. Setup takes care of PUK and management key. Written in pure Go, it's based on [github.com/go-piv/piv-go](https://github.com/go-piv/piv-go) and [golang.org/x/crypto/ssh](https://golang.org/x/crypto/ssh). ![](https://user-images.githubusercontent.com/1225294/81489747-63a03b00-9247-11ea-923a-b7434bcf7fd1.png) ## Installation ### macOS ``` brew install yubikey-agent brew services start yubikey-agent yubikey-agent -setup # generate a new key on the YubiKey ``` Then add the following line to your `~/.zshrc` and restart the shell. ``` export SSH_AUTH_SOCK="/usr/local/var/run/yubikey-agent.sock" ``` ### Linux #### Arch On Arch, use [the `yubikey-agent` package](https://aur.archlinux.org/packages/yubikey-agent/) from the AUR. ``` git clone https://aur.archlinux.org/yubikey-agent.git cd yubikey-agent && makepkg -si systemctl daemon-reload --user sudo systemctl enable --now pcscd.socket systemctl --user enable --now yubikey-agent export SSH_AUTH_SOCK="${XDG_RUNTIME_DIR}/yubikey-agent/yubikey-agent.sock" ``` #### NixOS / nixpkgs On NixOS unstable and 20.09 (unreleased at time of writing), you can add this to your `/etc/nixos/configuration.nix`: ``` services.yubikey-agent.enable = true; ``` This installs `yubikey-agent` and sets up a systemd unit to start yubikey-agent for you. On other systems using nix, you can also install from nixpkgs: ``` nix-env -iA nixpkgs.yubikey-agent ``` This installs the software but does *not* install a systemd unit. You will have to set up service management manually (see below). #### Other systemd-based Linux systems On other systemd-based Linux systems, follow [the manual installation instructions](systemd.md). Packaging contributions are very welcome. ### FreeBSD Install the [`yubikey-agent` port](https://svnweb.freebsd.org/ports/head/security/yubikey-agent/). ### Windows Windows support is currently WIP. ## Advanced topics ### Coexisting with other `ssh-agent`s It's possible to configure `ssh-agent`s on a per-host basis. For example to only use `yubikey-agent` when connecting to `example.com`, you'd add the following lines to `~/.ssh/config` instead of setting `SSH_AUTH_SOCK`. ``` Host example.com IdentityAgent /usr/local/var/run/yubikey-agent.sock ``` To use `yubikey-agent` for all hosts but one, you'd add the following lines instead. In both cases, you can keep using `ssh-add` to interact with the main `ssh-agent`. ``` Host example.com IdentityAgent $SSH_AUTH_SOCK Host * IdentityAgent /usr/local/var/run/yubikey-agent.sock ``` ### Conflicts with `gpg-agent` and Yubikey Manager `yubikey-agent` takes a persistent transaction so the YubiKey will cache the PIN after first use. Unfortunately, this makes the YubiKey PIV and PGP applets unavailable to any other applications, like `gpg-agent` and Yubikey Manager. Our upstream [is investigating solutions to this annoyance](https://github.com/go-piv/piv-go/issues/47). If you need `yubikey-agent` to release its lock on the YubiKey, send it a hangup signal. Likewise, you might have to kill `gpg-agent` after use for it to release its own lock. ``` killall -HUP yubikey-agent ``` This does not affect the FIDO2 functionality. ### Unblocking the PIN with the PUK If the wrong PIN is entered incorrectly three times in a row, YubiKey Manager can be used to unlock it. `yubikey-agent -setup` sets the PUK to the same value as the PIN. ``` ykman piv unblock-pin ``` If the PUK is also entered incorrectly three times, the key is permanently irrecoverable. The YubiKey PIV applet can be reset with `yubikey-agent -setup --really-delete-all-piv-keys`. ### Manual setup and technical details `yubikey-agent` only officially supports YubiKeys set up with `yubikey-agent -setup`. In practice, any PIV token with an RSA or ECDSA P-256 key and certificate in the Authentication slot should work, with any PIN and touch policy. Simply skip the setup step and use `ssh-add -L` to view the public key. `yubikey-agent -setup` generates a random Management Key and [stores it in PIN-protected metadata](https://pkg.go.dev/github.com/go-piv/piv-go/piv?tab=doc#YubiKey.SetMetadata). ### Alternatives #### Native FIDO2 Recent versions of OpenSSH [support using FIDO2 tokens as keys](https://buttondown.email/cryptography-dispatches/archive/cryptography-dispatches-openssh-82-just-works/). Since those are their own key type, they require server-side support, which is currently not available in Debian stable or on GitHub. FIDO2 keys also usually don't require a PIN, but depending on the token can require a private key file. `yubikey-agent` keys can be ported to a different machine simply by plugging in the YubiKey. #### `gpg-agent` `gpg-agent` can act as an `ssh-agent`, and it can use keys stored on the PGP applet of a YubiKey. This requires a finicky setup process dealing with PGP keys and the `gpg` UX, and seems to lose track of the YubiKey and require restarting all the time. Frankly, I had enough of PGP and GnuPG. #### `ssh-agent` and PKCS#11 `ssh-agent` can load PKCS#11 applets to interact with PIV tokens directly. There are two third-party PKCS#11 providers for YubiKeys (OpenSC and ykcs11) and one that ships with macOS (`man 8 ssh-keychain`). The UX of this solution is poor: it requires calling `ssh-add` to load the PKCS#11 module and to unlock it with the PIN (as the agent has no way of requesting input from the client during use, a limitation that `yubikey-agent` handles with `pinentry`), and needs manual reloading every time the YubiKey is unplugged or the machine goes to sleep. The ssh-agent that ships with macOS (which is pretty cool, as it starts on demand and is preconfigured in the environment) also has restrictions on where the `.so` modules can be loaded from. It can see through symlinks, so a Homebrew-installed `/usr/local/lib/libykcs11.dylib` won't work, while a hard copy at `/usr/local/lib/libykcs11.copy.dylib` will. `/usr/lib/ssh-keychain.dylib` works out of the box, but only with RSA keys. Key generation is undocumented. #### SeKey [SeKey](https://github.com/sekey/sekey) is a similar project that uses the Secure Enclave to store the private key and Touch ID for authorization. #### `pivy-agent` [`pivy-agent`](https://github.com/joyent/pivy#using-pivy-agent) is part of a suite of tools to work with PIV tokens. It's similar to `yubikey-agent`, and inspired its design. The main difference is that it requires unlocking via `ssh-add -X` rather than using a graphical pinentry, and it caches the PIN in memory rather than relying on the device PIN policy. It's also written in C. `yubikey-agent` also aims to provide an even smoother setup process. yubikey-agent-0.1.4/contrib/000077500000000000000000000000001404302642300157275ustar00rootroot00000000000000yubikey-agent-0.1.4/contrib/systemd/000077500000000000000000000000001404302642300174175ustar00rootroot00000000000000yubikey-agent-0.1.4/contrib/systemd/user/000077500000000000000000000000001404302642300203755ustar00rootroot00000000000000yubikey-agent-0.1.4/contrib/systemd/user/yubikey-agent.service000066400000000000000000000011201404302642300245260ustar00rootroot00000000000000[Unit] Description=Seamless ssh-agent for YubiKeys Documentation=https://filippo.io/yubikey-agent [Service] ExecStart=yubikey-agent -l %t/yubikey-agent/yubikey-agent.sock ExecReload=/bin/kill -HUP $MAINPID IPAddressDeny=any RestrictAddressFamilies=AF_UNIX RestrictNamespaces=yes RestrictRealtime=yes RestrictSUIDSGID=yes LockPersonality=yes SystemCallFilter=@system-service SystemCallFilter=~@privileged @resources SystemCallErrorNumber=EPERM SystemCallArchitectures=native NoNewPrivileges=yes KeyringMode=private UMask=0177 RuntimeDirectory=yubikey-agent [Install] WantedBy=default.target yubikey-agent-0.1.4/go.mod000066400000000000000000000004531404302642300153770ustar00rootroot00000000000000module filippo.io/yubikey-agent go 1.14 require ( github.com/go-piv/piv-go v1.7.0 github.com/gopasspw/pinentry v0.0.2 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf ) yubikey-agent-0.1.4/go.sum000066400000000000000000000027121404302642300154240ustar00rootroot00000000000000github.com/go-piv/piv-go v1.7.0 h1:rfjdFdASfGV5KLJhSjgpGJ5lzVZVtRWn8ovy/H9HQ/U= github.com/go-piv/piv-go v1.7.0/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8HKy7Gk= github.com/gopasspw/pinentry v0.0.2 h1:6OmkoTYMU05PmAJSIZSRjjhiQX15AstdgNa2KimH5XA= github.com/gopasspw/pinentry v0.0.2/go.mod h1:lR1WuNI96rXXBCgM601Ima3acnX3ZSPthIAuG6lHa68= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 h1:SgQ6LNaYJU0JIuEHv9+s6EbhSCwYeAf5Yvj6lpYlqAE= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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= yubikey-agent-0.1.4/main.go000066400000000000000000000224661404302642300155540ustar00rootroot00000000000000// 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" "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/pinentry" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" "golang.org/x/crypto/ssh/terminal" ) func main() { flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage of yubikey-agent:\n") fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, "\tyubikey-agent -setup\n") 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") } 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() if flag.NArg() > 0 { flag.Usage() os.Exit(1) } if *setupFlag { log.SetFlags(0) yk := connectForSetup() if *resetFlag { runReset(yk) } 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 } 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 } 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 } yubikey-agent-0.1.4/setup.go000066400000000000000000000142071404302642300157620ustar00rootroot00000000000000// 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/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "errors" "fmt" "log" "math/big" "os" "runtime/debug" "time" "github.com/go-piv/piv-go/piv" "golang.org/x/crypto/ssh" "golang.org/x/term" ) // Version can be set at link time to override debug.BuildInfo.Main.Version, // which is "(devel)" when building from within the module. See // golang.org/issue/29814 and golang.org/issue/29228. var Version string func init() { if Version != "" { return } if buildInfo, ok := debug.ReadBuildInfo(); ok { Version = buildInfo.Main.Version return } Version = "(unknown version)" } 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]) if err != nil { log.Fatalln("Failed to connect to the YubiKey:", err) } return yk } func runReset(yk *piv.YubiKey) { fmt.Println("Resetting YubiKey PIV applet...") 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) } 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("") fmt.Print("Choose a new PIN/PUK: ") pin, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Print("\n") if err != nil { log.Fatalln("Failed to read PIN:", err) } if len(pin) == 0 || len(pin) > 8 { log.Fatalln("The PIN needs to be 1-8 characters.") } fmt.Print("Repeat PIN/PUK: ") repeat, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Print("\n") if err != nil { log.Fatalln("Failed to read PIN:", err) } else if !bytes.Equal(repeat, pin) { log.Fatalln("PINs don't match!") } fmt.Println("") fmt.Println("🧪 Reticulating splines...") var key [24]byte if _, err := rand.Read(key[:]); err != nil { log.Fatal(err) } if err := yk.SetManagementKey(piv.DefaultManagementKey, key); err != nil { log.Println("‼️ The default Management Key did not work") log.Println("") log.Println("If you know what you're doing, reset PIN, PUK, and") log.Println("Management Key to the defaults before retrying.") log.Println("") log.Println("If you want to wipe all PIV keys and start fresh,") log.Fatalln("use --really-delete-all-piv-keys ⚠️") } if err := yk.SetMetadata(key, &piv.Metadata{ ManagementKey: &key, }); err != nil { log.Fatalln("Failed to store the Management Key on the device:", err) } if err := yk.SetPIN(piv.DefaultPIN, string(pin)); err != nil { log.Println("‼️ The default PIN did not work") log.Println("") log.Println("If you know what you're doing, reset PIN, PUK, and") log.Println("Management Key to the defaults before retrying.") log.Println("") log.Println("If you want to wipe all PIV keys and start fresh,") log.Fatalln("use --really-delete-all-piv-keys ⚠️") } if err := yk.SetPUK(piv.DefaultPUK, string(pin)); err != nil { log.Println("‼️ The default PUK did not work") log.Println("") log.Println("If you know what you're doing, reset PIN, PUK, and") log.Println("Management Key to the defaults before retrying.") log.Println("") log.Println("If you want to wipe all PIV keys and start fresh,") log.Fatalln("use --really-delete-all-piv-keys ⚠️") } pub, err := yk.GenerateKey(key, piv.SlotAuthentication, piv.Key{ Algorithm: piv.AlgorithmEC256, PINPolicy: piv.PINPolicyOnce, TouchPolicy: piv.TouchPolicyAlways, }) if err != nil { log.Fatalln("Failed to generate key:", err) } priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { log.Fatalln("Failed to generate parent key:", err) } parent := &x509.Certificate{ Subject: pkix.Name{ Organization: []string{"yubikey-agent"}, OrganizationalUnit: []string{Version}, }, PublicKey: priv.Public(), } template := &x509.Certificate{ Subject: pkix.Name{ CommonName: "SSH key", }, NotAfter: time.Now().AddDate(42, 0, 0), NotBefore: time.Now(), SerialNumber: randomSerialNumber(), KeyUsage: x509.KeyUsageKeyAgreement | x509.KeyUsageDigitalSignature, } certBytes, err := x509.CreateCertificate(rand.Reader, template, parent, pub, priv) if err != nil { log.Fatalln("Failed to generate certificate:", err) } cert, err := x509.ParseCertificate(certBytes) if err != nil { log.Fatalln("Failed to parse certificate:", err) } if err := yk.SetCertificate(key, piv.SlotAuthentication, cert); err != nil { log.Fatalln("Failed to store certificate:", err) } sshKey, err := ssh.NewPublicKey(pub) if err != nil { log.Fatalln("Failed to generate public key:", err) } fmt.Println("") fmt.Println("✅ Done! This YubiKey is secured and ready to go.") fmt.Println("🤏 When the YubiKey blinks, touch it to authorize the login.") fmt.Println("") fmt.Println("🔑 Here's your new shiny SSH public key:") os.Stdout.Write(ssh.MarshalAuthorizedKey(sshKey)) fmt.Println("") fmt.Println("Next steps: ensure yubikey-agent is running via launchd/systemd/...,") fmt.Println(`set the SSH_AUTH_SOCK environment variable, and test with "ssh-add -L"`) fmt.Println("") fmt.Println("💭 Remember: everything breaks, have a backup plan for when this YubiKey does.") } func randomSerialNumber() *big.Int { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { log.Fatalln("Failed to generate serial number:", err) } return serialNumber } yubikey-agent-0.1.4/systemd.md000066400000000000000000000026261404302642300163070ustar00rootroot00000000000000# Manual Linux setup with systemd Note: this is usually only necessary in case your distribution doesn't already provide a yubikey-agent as a package. Refer to [the README](README) for a list of distributions providing packages. First, install Go and the [`piv-go` dependencies](https://github.com/go-piv/piv-go#installation), build `yubikey-agent` and place it in `$PATH`. ```text $ git clone https://filippo.io/yubikey-agent && cd yubikey-agent $ go build && sudo cp yubikey-agent /usr/local/bin/ ``` Make sure you have a `pinentry` program that works for you (terminal-based or graphical) in `$PATH`. Use `yubikey-agent -setup` to create a new key on the YubiKey. ```text $ yubikey-agent -setup ``` Then, create a systemd user service at `~/.config/systemd/user/yubikey-agent.service` with the contents of [yubikey-agent.service](contrib/systemd/user/yubikey-agent.service). Depending on your distribution (`systemd <=239` or no user namespace support), you might need to edit the `ExecStart=` line and some of the sandboxing options. Refresh systemd, make sure that the PC/SC daemon is available, and start the yubikey-agent. ```text $ systemctl daemon-reload --user $ sudo systemctl enable --now pcscd.socket $ systemctl --user enable --now yubikey-agent ``` Finally, add the following line to your shell profile and restart it. ``` export SSH_AUTH_SOCK="${XDG_RUNTIME_DIR}/yubikey-agent/yubikey-agent.sock" ```