Skip to content

Commit

Permalink
feat: initial windows port
Browse files Browse the repository at this point in the history
  • Loading branch information
neersighted committed Jun 1, 2020
1 parent 667be2c commit d420066
Show file tree
Hide file tree
Showing 6 changed files with 332 additions and 328 deletions.
251 changes: 251 additions & 0 deletions agent.go
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ 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.0
github.com/gopasspw/gopass v1.9.1
github.com/gopasspw/gopass v1.9.3-0.20200526132452-b2f36ba7fb8a
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79
)
Loading

0 comments on commit d420066

Please sign in to comment.