Browse Source

Initial commit

Thomas Dy 8 months ago
commit
fb80ce846b
8 changed files with 599 additions and 0 deletions
  1. 1 0
      .gitignore
  2. 61 0
      flake.lock
  3. 19 0
      flake.nix
  4. 17 0
      go.mod
  5. 59 0
      go.sum
  6. 255 0
      keyring.go
  7. 81 0
      main.go
  8. 106 0
      proxy.go

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+agent-proxy

+ 61 - 0
flake.lock

@@ -0,0 +1,61 @@
+{
+  "nodes": {
+    "flake-utils": {
+      "inputs": {
+        "systems": "systems"
+      },
+      "locked": {
+        "lastModified": 1681202837,
+        "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "cfacdce06f30d2b68473a46042957675eebb3401",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1683535585,
+        "narHash": "sha256-ND6gCDEfuMNsaJlZQzEbl6hTGrAzURtLoZoGR5dJaCw=",
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "d9b8ae36f31046dbcf05b6cdc45e860bf19b0b7e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nixos",
+        "ref": "nixpkgs-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}

+ 19 - 0
flake.nix

@@ -0,0 +1,19 @@
+{
+  description = "Agent Proxy";
+
+  inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
+  inputs.flake-utils.url = "github:numtide/flake-utils";
+
+  outputs = { self, nixpkgs, flake-utils }:
+    flake-utils.lib.eachDefaultSystem (system:
+      let pkgs = nixpkgs.legacyPackages.${system}; in
+      {
+        devShells.default = pkgs.mkShell {
+          packages = with pkgs; [
+            go
+            gopls
+          ];
+        };
+      }
+    );
+}

+ 17 - 0
go.mod

@@ -0,0 +1,17 @@
+module github.com/thatsmydoing/agent-proxy
+
+go 1.20
+
+require (
+	github.com/ncruces/zenity v0.10.8
+	golang.org/x/crypto v0.8.0
+)
+
+require (
+	github.com/akavel/rsrc v0.10.2 // indirect
+	github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f // indirect
+	github.com/josephspurrier/goversioninfo v1.4.0 // indirect
+	github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 // indirect
+	golang.org/x/image v0.7.0 // indirect
+	golang.org/x/sys v0.7.0 // indirect
+)

+ 59 - 0
go.sum

@@ -0,0 +1,59 @@
+github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=
+github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f h1:OGqDDftRTwrvUoL6pOG7rYTmWsTCvyEWFsMjg+HcOaA=
+github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f/go.mod h1:Dv9D0NUlAsaQcGQZa5kc5mqR9ua72SmA8VXi4cd+cBw=
+github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8=
+github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY=
+github.com/ncruces/zenity v0.10.8 h1:vNwTJazgxj47VOBXqik14fN8ML5eLMhl/Nw646Ln/7o=
+github.com/ncruces/zenity v0.10.8/go.mod h1:gPHbGF/lkwfJS3wdVG9sXYgK8c0vLAXkA2xHacOiFZY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 h1:GranzK4hv1/pqTIhMTXt2X8MmMOuH3hMeUR0o9SP5yc=
+github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844/go.mod h1:T1TLSfyWVBRXVGzWd0o9BI4kfoO9InEgfQe4NV3mLz8=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
+golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
+golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
+golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 255 - 0
keyring.go

@@ -0,0 +1,255 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"bytes"
+	"crypto/rand"
+	"crypto/subtle"
+	"errors"
+	"fmt"
+	"sync"
+	"time"
+
+	"golang.org/x/crypto/ssh"
+	"golang.org/x/crypto/ssh/agent"
+)
+
+type privKey struct {
+	signer  ssh.Signer
+	comment string
+	confirm bool
+	expire  *time.Time
+}
+
+type ConfirmFunction func(comment string) bool
+
+type keyring struct {
+	confirmFunction ConfirmFunction
+
+	mu   sync.Mutex
+	keys []privKey
+
+	locked     bool
+	passphrase []byte
+}
+
+var errLocked = errors.New("agent: locked")
+
+// NewKeyring returns an Agent that holds keys in memory.  It is safe
+// for concurrent use by multiple goroutines.
+func NewKeyring(confirmFunction ConfirmFunction) agent.ExtendedAgent {
+	return &keyring{
+		confirmFunction: confirmFunction,
+	}
+}
+
+// RemoveAll removes all identities.
+func (r *keyring) RemoveAll() error {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	if r.locked {
+		return errLocked
+	}
+
+	r.keys = nil
+	return nil
+}
+
+// removeLocked does the actual key removal. The caller must already be holding the
+// keyring mutex.
+func (r *keyring) removeLocked(want []byte) error {
+	found := false
+	for i := 0; i < len(r.keys); {
+		if bytes.Equal(r.keys[i].signer.PublicKey().Marshal(), want) {
+			found = true
+			r.keys[i] = r.keys[len(r.keys)-1]
+			r.keys = r.keys[:len(r.keys)-1]
+			continue
+		} else {
+			i++
+		}
+	}
+
+	if !found {
+		return errors.New("agent: key not found")
+	}
+	return nil
+}
+
+// Remove removes all identities with the given public key.
+func (r *keyring) Remove(key ssh.PublicKey) error {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	if r.locked {
+		return errLocked
+	}
+
+	return r.removeLocked(key.Marshal())
+}
+
+// Lock locks the agent. Sign and Remove will fail, and List will return an empty list.
+func (r *keyring) Lock(passphrase []byte) error {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	if r.locked {
+		return errLocked
+	}
+
+	r.locked = true
+	r.passphrase = passphrase
+	return nil
+}
+
+// Unlock undoes the effect of Lock
+func (r *keyring) Unlock(passphrase []byte) error {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	if !r.locked {
+		return errors.New("agent: not locked")
+	}
+	if 1 != subtle.ConstantTimeCompare(passphrase, r.passphrase) {
+		return fmt.Errorf("agent: incorrect passphrase")
+	}
+
+	r.locked = false
+	r.passphrase = nil
+	return nil
+}
+
+// expireKeysLocked removes expired keys from the keyring. If a key was added
+// with a lifetimesecs contraint and seconds >= lifetimesecs seconds have
+// elapsed, it is removed. The caller *must* be holding the keyring mutex.
+func (r *keyring) expireKeysLocked() {
+	for _, k := range r.keys {
+		if k.expire != nil && time.Now().After(*k.expire) {
+			r.removeLocked(k.signer.PublicKey().Marshal())
+		}
+	}
+}
+
+// List returns the identities known to the agent.
+func (r *keyring) List() ([]*agent.Key, error) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	if r.locked {
+		// section 2.7: locked agents return empty.
+		return nil, nil
+	}
+
+	r.expireKeysLocked()
+	var ids []*agent.Key
+	for _, k := range r.keys {
+		pub := k.signer.PublicKey()
+		ids = append(ids, &agent.Key{
+			Format:  pub.Type(),
+			Blob:    pub.Marshal(),
+			Comment: k.comment})
+	}
+	return ids, nil
+}
+
+// Insert adds a private key to the keyring. If a certificate
+// is given, that certificate is added as public key. Note that
+// any constraints given are ignored.
+func (r *keyring) Add(key agent.AddedKey) error {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	if r.locked {
+		return errLocked
+	}
+	signer, err := ssh.NewSignerFromKey(key.PrivateKey)
+
+	if err != nil {
+		return err
+	}
+
+	if cert := key.Certificate; cert != nil {
+		signer, err = ssh.NewCertSigner(cert, signer)
+		if err != nil {
+			return err
+		}
+	}
+
+	p := privKey{
+		signer:  signer,
+		comment: key.Comment,
+		confirm: key.ConfirmBeforeUse,
+	}
+
+	if key.LifetimeSecs > 0 {
+		t := time.Now().Add(time.Duration(key.LifetimeSecs) * time.Second)
+		p.expire = &t
+	}
+
+	r.keys = append(r.keys, p)
+
+	return nil
+}
+
+// Sign returns a signature for the data.
+func (r *keyring) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) {
+	return r.SignWithFlags(key, data, 0)
+}
+
+func (r *keyring) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	if r.locked {
+		return nil, errLocked
+	}
+
+	r.expireKeysLocked()
+	wanted := key.Marshal()
+	for _, k := range r.keys {
+		if bytes.Equal(k.signer.PublicKey().Marshal(), wanted) {
+			if k.confirm {
+				if !r.confirmFunction(k.comment) {
+					return nil, fmt.Errorf("agent: confirmation failed")
+				}
+			}
+			if flags == 0 {
+				return k.signer.Sign(rand.Reader, data)
+			} else {
+				if algorithmSigner, ok := k.signer.(ssh.AlgorithmSigner); !ok {
+					return nil, fmt.Errorf("agent: signature does not support non-default signature algorithm: %T", k.signer)
+				} else {
+					var algorithm string
+					switch flags {
+					case agent.SignatureFlagRsaSha256:
+						algorithm = ssh.KeyAlgoRSASHA256
+					case agent.SignatureFlagRsaSha512:
+						algorithm = ssh.KeyAlgoRSASHA512
+					default:
+						return nil, fmt.Errorf("agent: unsupported signature flags: %d", flags)
+					}
+					return algorithmSigner.SignWithAlgorithm(rand.Reader, data, algorithm)
+				}
+			}
+		}
+	}
+	return nil, errors.New("not found")
+}
+
+// Signers returns signers for all the known keys.
+func (r *keyring) Signers() ([]ssh.Signer, error) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	if r.locked {
+		return nil, errLocked
+	}
+
+	r.expireKeysLocked()
+	s := make([]ssh.Signer, 0, len(r.keys))
+	for _, k := range r.keys {
+		s = append(s, k.signer)
+	}
+	return s, nil
+}
+
+// The keyring does not support any extensions
+func (r *keyring) Extension(extensionType string, contents []byte) ([]byte, error) {
+	return nil, agent.ErrExtensionUnsupported
+}

+ 81 - 0
main.go

@@ -0,0 +1,81 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"net"
+	"os"
+	"os/signal"
+	"path"
+	"syscall"
+
+	"github.com/ncruces/zenity"
+	"golang.org/x/crypto/ssh/agent"
+)
+
+func confirm(comment string) bool {
+	return zenity.Question(
+		fmt.Sprintf("Are you sure you want to allow using the SSH key '%s'?", comment),
+		zenity.Title("Allow SSH Key"),
+		zenity.QuestionIcon,
+	) == nil
+}
+
+func getTmpDir() string {
+	dir, ok := os.LookupEnv("XDG_RUNTIME_DIR")
+	if ok {
+		return dir
+	}
+	dir, ok = os.LookupEnv("TMPDIR")
+	if ok {
+		return dir
+	}
+	return "/tmp"
+}
+
+func main() {
+	sock := flag.String(
+		"sock",
+		path.Join(getTmpDir(), "agent.sock"),
+		"Path to socket",
+	)
+	secretiveSock := flag.String(
+		"secretive-sock",
+		path.Join(os.Getenv("HOME"), "Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh"),
+		"Path to secretive agent socket",
+	)
+
+	keyring := NewKeyring(confirm)
+
+	conn, err := net.Dial("unix", *secretiveSock)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	secretive := agent.NewClient(conn)
+
+	proxy := NewProxy(secretive, keyring)
+
+	socket, err := net.Listen("unix", *sock)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	c := make(chan os.Signal, 1)
+	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
+	go func() {
+		<-c
+		os.Remove(*sock)
+		os.Exit(1)
+	}()
+
+	for {
+		conn, err := socket.Accept()
+		if err != nil {
+			log.Fatal(err)
+		}
+
+		agent.ServeAgent(proxy, conn)
+	}
+}

+ 106 - 0
proxy.go

@@ -0,0 +1,106 @@
+package main
+
+import (
+	"bytes"
+	"log"
+
+	"golang.org/x/crypto/ssh"
+	"golang.org/x/crypto/ssh/agent"
+)
+
+type proxy struct {
+	secretiveAgent agent.ExtendedAgent
+	keyringAgent   agent.ExtendedAgent
+}
+
+func NewProxy(secretiveAgent agent.ExtendedAgent, keyringAgent agent.ExtendedAgent) agent.ExtendedAgent {
+	return &proxy{
+		secretiveAgent: secretiveAgent,
+		keyringAgent:   keyringAgent,
+	}
+}
+
+func (p *proxy) List() ([]*agent.Key, error) {
+	secretiveKeys, err := p.secretiveAgent.List()
+	if err != nil {
+		return nil, err
+	}
+
+	keyringKeys, err := p.keyringAgent.List()
+	if err != nil {
+		return nil, err
+	}
+
+	var allKeys []*agent.Key
+	allKeys = append(allKeys, secretiveKeys...)
+	allKeys = append(allKeys, keyringKeys...)
+
+	return allKeys, nil
+}
+
+func (p *proxy) Add(key agent.AddedKey) error {
+	log.Printf("added key %s\n", key.Comment)
+	return p.keyringAgent.Add(key)
+}
+
+func (p *proxy) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) {
+	return p.SignWithFlags(key, data, 0)
+}
+
+func (p *proxy) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error) {
+	keyringKeys, err := p.keyringAgent.List()
+	if err != nil {
+		return nil, err
+	}
+
+	for _, k := range keyringKeys {
+		if bytes.Equal(k.Blob, key.Marshal()) {
+			log.Printf("signing with keyring\n")
+			return p.keyringAgent.SignWithFlags(key, data, flags)
+		}
+	}
+
+	log.Printf("signing with secretive\n")
+	return p.secretiveAgent.SignWithFlags(key, data, flags)
+}
+
+func (p *proxy) Remove(key ssh.PublicKey) error {
+	log.Printf("removed key\n")
+	return p.keyringAgent.Remove(key)
+}
+
+func (p *proxy) RemoveAll() error {
+	log.Printf("cleared keys\n")
+	return p.keyringAgent.RemoveAll()
+}
+
+func (p *proxy) Lock(passphrase []byte) error {
+	return p.keyringAgent.Lock(passphrase)
+}
+
+func (p *proxy) Unlock(passphrase []byte) error {
+	return p.keyringAgent.Unlock(passphrase)
+}
+
+func (p *proxy) Signers() ([]ssh.Signer, error) {
+	secretiveSigners, err := p.secretiveAgent.Signers()
+	if err != nil {
+		return nil, err
+	}
+
+	keyringSigners, err := p.keyringAgent.Signers()
+	if err != nil {
+		return nil, err
+	}
+
+	var allSigners []ssh.Signer
+	allSigners = append(allSigners, secretiveSigners...)
+	allSigners = append(allSigners, keyringSigners...)
+
+	return allSigners, nil
+}
+
+// The keyring does not support any extensions
+func (p *proxy) Extension(extensionType string, contents []byte) ([]byte, error) {
+	return nil, agent.ErrExtensionUnsupported
+}