Skip to content

Commit

Permalink
Merge branch 'morten/keyring'
Browse files Browse the repository at this point in the history
* morten/keyring:
  agent: add SIGINT to the signal handler
  testdata: Include a test for askpass during signing
  scripts_test: set agent simulator to an underscore env
  agent: Integrate ThreadKeyring into the ssh agent
  keyring/threadkeyring: Implement a keyring pinned to the os thread
  keyring: add a keyctl implementation for keyrings
  keyring/key: implement a boxed byte slice for memory sensitive things
  agent: pass staticcheck

Signed-off-by: Morten Linderud <[email protected]>
  • Loading branch information
Foxboron committed Jan 15, 2025
2 parents d579cf8 + 7275e88 commit 4aba3de
Show file tree
Hide file tree
Showing 18 changed files with 488 additions and 75 deletions.
7 changes: 5 additions & 2 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"log/slog"

keyfile "github.com/foxboron/go-tpm-keyfiles"
"github.com/foxboron/ssh-tpm-agent/internal/keyring"
"github.com/foxboron/ssh-tpm-agent/key"
"github.com/foxboron/ssh-tpm-agent/signer"
"github.com/google/go-tpm/tpm2/transport"
Expand All @@ -40,6 +41,7 @@ type Agent struct {
listener *net.UnixListener
quit chan interface{}
wg sync.WaitGroup
keyring func() *keyring.ThreadKeyring
keys []*key.SSHTPMKey
agents []agent.ExtendedAgent
}
Expand Down Expand Up @@ -106,7 +108,7 @@ func (a *Agent) signers() ([]ssh.Signer, error) {

for _, k := range a.keys {
s, err := ssh.NewSignerFromSigner(
signer.NewSSHKeySigner(k, a.op, a.tpm,
signer.NewSSHKeySigner(k, a.keyring(), a.op, a.tpm,
func(_ *keyfile.TPMKey) ([]byte, error) {
// Shimming the function to get the correct type
return a.pin(k)
Expand Down Expand Up @@ -432,7 +434,7 @@ func LoadKeys(keyDir string) ([]*key.SSHTPMKey, error) {
return keys, err
}

func NewAgent(listener *net.UnixListener, agents []agent.ExtendedAgent, tpmFetch func() transport.TPMCloser, ownerPassword func() ([]byte, error), pin func(*key.SSHTPMKey) ([]byte, error)) *Agent {
func NewAgent(listener *net.UnixListener, agents []agent.ExtendedAgent, keyring func() *keyring.ThreadKeyring, tpmFetch func() transport.TPMCloser, ownerPassword func() ([]byte, error), pin func(*key.SSHTPMKey) ([]byte, error)) *Agent {
a := &Agent{
agents: agents,
tpm: tpmFetch,
Expand All @@ -441,6 +443,7 @@ func NewAgent(listener *net.UnixListener, agents []agent.ExtendedAgent, tpmFetch
pin: pin,
quit: make(chan interface{}),
keys: []*key.SSHTPMKey{},
keyring: keyring,
}

a.wg.Add(1)
Expand Down
3 changes: 3 additions & 0 deletions agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"testing"

"github.com/foxboron/ssh-tpm-agent/agent"
"github.com/foxboron/ssh-tpm-agent/internal/keyring"
"github.com/foxboron/ssh-tpm-agent/internal/keytest"
"github.com/foxboron/ssh-tpm-agent/key"
"github.com/google/go-tpm/tpm2"
Expand All @@ -37,6 +38,8 @@ func TestAddKey(t *testing.T) {

ag := agent.NewAgent(unixList,
[]sshagent.ExtendedAgent{},
// Keyring callback
func() *keyring.ThreadKeyring { return &keyring.ThreadKeyring{} },
// TPM Callback
func() transport.TPMCloser { return tpm },
// Owner password
Expand Down
92 changes: 41 additions & 51 deletions agent/gocrypto.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
package agent

import (
"encoding/binary"
"fmt"

"golang.org/x/crypto/ssh"

sshagent "golang.org/x/crypto/ssh/agent"
)

// Code taken from crypto/x/ssh/agent

const (
Expand All @@ -18,59 +9,58 @@ const (
// Constraint extension identifier up to version 2 of the protocol. A
// backward incompatible change will be required if we want to add support
// for SSH_AGENT_CONSTRAIN_MAXSIGN which uses the same ID.
agentConstrainExtensionV00 = 3
// agentConstrainExtensionV00 = 3
// Constraint extension identifier in version 3 and later of the protocol.
agentConstrainExtension = 255
// agentConstrainExtension = 255
)

type constrainExtensionAgentMsg struct {
ExtensionName string `sshtype:"255|3"`
ExtensionDetails []byte
// type constrainExtensionAgentMsg struct {
// ExtensionName string `sshtype:"255|3"`
// ExtensionDetails []byte

// Rest is a field used for parsing, not part of message
Rest []byte `ssh:"rest"`
}
// // Rest is a field used for parsing, not part of message
// Rest []byte `ssh:"rest"`
// }

// 3.7 Key constraint identifiers
type constrainLifetimeAgentMsg struct {
LifetimeSecs uint32 `sshtype:"1"`
}

func parseConstraints(constraints []byte) (lifetimeSecs uint32, confirmBeforeUse bool, extensions []sshagent.ConstraintExtension, err error) {
for len(constraints) != 0 {
switch constraints[0] {
case agentConstrainLifetime:
lifetimeSecs = binary.BigEndian.Uint32(constraints[1:5])
constraints = constraints[5:]
case agentConstrainConfirm:
confirmBeforeUse = true
constraints = constraints[1:]
case agentConstrainExtension, agentConstrainExtensionV00:
var msg constrainExtensionAgentMsg
if err = ssh.Unmarshal(constraints, &msg); err != nil {
return 0, false, nil, err
}
extensions = append(extensions, sshagent.ConstraintExtension{
ExtensionName: msg.ExtensionName,
ExtensionDetails: msg.ExtensionDetails,
})
constraints = msg.Rest
default:
return 0, false, nil, fmt.Errorf("unknown constraint type: %d", constraints[0])
}
}
return
}
// func parseConstraints(constraints []byte) (lifetimeSecs uint32, confirmBeforeUse bool, extensions []sshagent.ConstraintExtension, err error) {
// for len(constraints) != 0 {
// switch constraints[0] {
// case agentConstrainLifetime:
// lifetimeSecs = binary.BigEndian.Uint32(constraints[1:5])
// constraints = constraints[5:]
// case agentConstrainConfirm:
// confirmBeforeUse = true
// constraints = constraints[1:]
// // case agentConstrainExtension, agentConstrainExtensionV00:
// // var msg constrainExtensionAgentMsg
// // if err = ssh.Unmarshal(constraints, &msg); err != nil {
// // return 0, false, nil, err
// // }
// // extensions = append(extensions, sshagent.ConstraintExtension{
// // ExtensionName: msg.ExtensionName,
// // ExtensionDetails: msg.ExtensionDetails,
// // })
// // constraints = msg.Rest
// default:
// return 0, false, nil, fmt.Errorf("unknown constraint type: %d", constraints[0])
// }
// }
// return
// }

// TODO: Add constraints to our keys
// func setConstraints(key *key.SSHTPMKey, constraintBytes []byte) error {
// lifetimeSecs, confirmBeforeUse, constraintExtensions, err := parseConstraints(constraintBytes)
// if err != nil {
// return err
// }
// lifetimeSecs, confirmBeforeUse, constraintExtensions, err := parseConstraints(constraintBytes)
// if err != nil {
// return err
// }

// key.LifetimeSecs = lifetimeSecs
// key.ConfirmBeforeUse = confirmBeforeUse
// key.ConstraintExtensions = constraintExtensions
// return nil
// key.LifetimeSecs = lifetimeSecs
// key.ConfirmBeforeUse = confirmBeforeUse
// key.ConstraintExtensions = constraintExtensions
// return nil
// }
2 changes: 1 addition & 1 deletion cmd/scripts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func ScriptsWithPath(t *testing.T, path string) {
Deadline: time.Now().Add(5 * time.Second),
Setup: func(e *testscript.Env) error {
e.Setenv("PATH", tmp+string(filepath.ListSeparator)+e.Getenv("PATH"))
e.Vars = append(e.Vars, "SSH_TPM_AGENT_SIMULATOR=1")
e.Vars = append(e.Vars, "_SSH_TPM_AGENT_SIMULATOR=1")
e.Vars = append(e.Vars, fmt.Sprintf("SSH_AUTH_SOCK=%s/agent.sock", e.WorkDir))
e.Vars = append(e.Vars, fmt.Sprintf("SSH_TPM_AUTH_SOCK=%s/agent.sock", e.WorkDir))
e.Vars = append(e.Vars, fmt.Sprintf("HOME=%s", e.WorkDir))
Expand Down
46 changes: 35 additions & 11 deletions cmd/ssh-tpm-agent/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"context"
"errors"
"flag"
"fmt"
"log"
Expand All @@ -17,6 +19,7 @@ import (

"github.com/foxboron/ssh-tpm-agent/agent"
"github.com/foxboron/ssh-tpm-agent/askpass"
"github.com/foxboron/ssh-tpm-agent/internal/keyring"
"github.com/foxboron/ssh-tpm-agent/key"
"github.com/foxboron/ssh-tpm-agent/utils"
"github.com/google/go-tpm/tpm2/transport"
Expand Down Expand Up @@ -198,8 +201,22 @@ func main() {
os.Exit(1)
}

// TODO: Ensure the agent also uses thix context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

agentkeyring, err := keyring.NewThreadKeyring(ctx, keyring.SessionKeyring)
if err != nil {
log.Fatal(err)
}

agent := agent.NewAgent(listener, agents,

// Keyring Callback
func() *keyring.ThreadKeyring {
return agentkeyring
},

// TPM Callback
func() (tpm transport.TPMCloser) {
// the agent will close the TPM after this is called
Expand All @@ -216,7 +233,6 @@ func main() {
return askpass.ReadPassphrase("Enter owner password for TPM", askpass.RP_USE_ASKPASS)
} else {
ownerPassword := os.Getenv("SSH_TPM_AGENT_OWNER_PASSWORD")

return []byte(ownerPassword), nil
}
},
Expand All @@ -225,23 +241,31 @@ func main() {
// SSHKeySigner in signer/signer.go resets this value if
// we get a TPMRCAuthFail
func(key *key.SSHTPMKey) ([]byte, error) {
if len(key.Userauth) != 0 {
slog.Debug("providing cached userauth for key", slog.String("desc", key.Description))
return key.Userauth, nil
}
keyInfo := fmt.Sprintf("Enter passphrase for (%s): ", key.Description)
userauth, err := askpass.ReadPassphrase(keyInfo, askpass.RP_USE_ASKPASS)
if !noCache && err == nil {
slog.Debug("caching userauth for key", slog.String("desc", key.Description))
key.Userauth = userauth
auth, err := agentkeyring.ReadKey(key.Fingerprint())
if err == nil {
slog.Debug("providing cached userauth for key", slog.String("fp", key.Fingerprint()))
// TODO: This is not great, but easier for now
return auth.Read(), nil
} else if errors.Is(err, syscall.ENOKEY) || errors.Is(err, syscall.EACCES) {
keyInfo := fmt.Sprintf("Enter passphrase for (%s): ", key.Description)
// TODOt kjk: askpass should box the byte slice
userauth, err := askpass.ReadPassphrase(keyInfo, askpass.RP_USE_ASKPASS)
if !noCache && err == nil {
slog.Debug("caching userauth for key in keyring", slog.String("fp", key.Fingerprint()))
if err := agentkeyring.AddKey(key.Fingerprint(), userauth); err != nil {
return nil, err
}
}
return userauth, err
}
return userauth, err
return nil, fmt.Errorf("failed getting pin for key: %w", err)
},
)

// Signal handling
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP)
signal.Notify(c, syscall.SIGINT)
go func() {
for range c {
agent.Stop()
Expand Down
3 changes: 3 additions & 0 deletions cmd/ssh-tpm-agent/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"github.com/foxboron/ssh-tpm-agent/agent"
"github.com/foxboron/ssh-tpm-agent/internal/keyring"
"github.com/foxboron/ssh-tpm-agent/internal/keytest"
"github.com/foxboron/ssh-tpm-agent/key"
"github.com/google/go-tpm/tpm2"
Expand Down Expand Up @@ -135,6 +136,8 @@ func runSSHAuth(t *testing.T, keytype tpm2.TPMAlgID, bits int, pin []byte, keyfn

ag := agent.NewAgent(unixList,
[]sshagent.ExtendedAgent{},
// Keyring Callback
func() *keyring.ThreadKeyring { return &keyring.ThreadKeyring{} },
// TPM Callback
func() transport.TPMCloser { return tpm },
// Owner password
Expand Down
44 changes: 44 additions & 0 deletions cmd/ssh-tpm-agent/testdata/script/agent_password.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Create an askpass binary
exec go build -o askpass-test askpass.go
exec ./askpass-test passphrase

# Env
env SSH_ASKPASS=./askpass-test
env SSH_ASKPASS_REQUIRE=force

# ssh sign file with password
env _ASKPASS_PASSWORD=12345
exec ssh-tpm-agent -d --no-load &agent&
exec ssh-tpm-keygen -N $_ASKPASS_PASSWORD
exec ssh-tpm-add
stdout id_ecdsa.tpm
exec ssh-add -l
stdout ECDSA
exec ssh-keygen -Y sign -n file -f .ssh/id_ecdsa.pub file_to_sign.txt
stdin file_to_sign.txt
exec ssh-keygen -Y check-novalidate -n file -f .ssh/id_ecdsa.pub -s file_to_sign.txt.sig
exists file_to_sign.txt.sig
exec ssh-add -D
rm file_to_sign.txt.sig
rm .ssh/id_ecdsa.tpm .ssh/id_ecdsa.pub

-- file_to_sign.txt --
Hello World

-- go.mod --
module example.com/askpass

-- askpass.go --
package main

import (
"fmt"
"os"
"strings"
)

func main() {
if strings.Contains(os.Args[1], "passphrase") {
fmt.Println(os.Getenv("_ASKPASS_PASSWORD"))
}
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ go 1.22.4
toolchain go1.22.5

require (
github.com/awnumar/memcall v0.4.0
github.com/foxboron/go-tpm-keyfiles v0.0.0-20240805214234-f870d6f1ff68
github.com/foxboron/ssh-tpm-ca-authority v0.0.0-20240806093457-88eeced81948
github.com/google/go-tpm v0.9.2-0.20240625170440-991b038b62b6
github.com/google/go-tpm-tools v0.4.4
github.com/rogpeppe/go-internal v1.13.1
golang.org/x/crypto v0.25.0
golang.org/x/sys v0.27.0
golang.org/x/sys v0.28.0
golang.org/x/term v0.26.0
)

Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/awnumar/memcall v0.4.0 h1:B7hgZYdfH6Ot1Goaz8jGne/7i8xD4taZie/PNSFZ29g=
github.com/awnumar/memcall v0.4.0/go.mod h1:8xOx1YbfyuCg3Fy6TO8DK0kZUua3V42/goA5Ru47E8w=
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -93,8 +95,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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=
Expand Down
Loading

0 comments on commit 4aba3de

Please sign in to comment.