pax_global_header00006660000000000000000000000064151115440130014505gustar00rootroot0000000000000052 comment=d67342f0dae3fd8c957753c17668e91c5712cafc ssh-tpm-agent-0.8.0/000077500000000000000000000000001511154401300142015ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/.editorconfig000066400000000000000000000001771511154401300166630ustar00rootroot00000000000000[*] charset = utf-8 [*.adoc] indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true ssh-tpm-agent-0.8.0/.github/000077500000000000000000000000001511154401300155415ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/.github/workflows/000077500000000000000000000000001511154401300175765ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/.github/workflows/build.yml000066400000000000000000000040521511154401300214210ustar00rootroot00000000000000name: Build and upload binaries on: release: types: [published] push: pull_request: permissions: contents: read jobs: build: name: Build binaries runs-on: ubuntu-latest strategy: matrix: include: - {GOOS: linux, GOARCH: amd64} - {GOOS: linux, GOARCH: arm, GOARM: 6} - {GOOS: linux, GOARCH: arm64} steps: - name: Install Go uses: actions/setup-go@v5 with: go-version: 1.x - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Build binary run: | cp LICENSE "$RUNNER_TEMP/LICENSE" echo -e "\n---\n" >> "$RUNNER_TEMP/LICENSE" curl -L "https://go.dev/LICENSE?m=text" >> "$RUNNER_TEMP/LICENSE" VERSION="$(git describe --tags 2> /dev/null || echo "WIP")" DIR="$(mktemp -d)" mkdir "$DIR/ssh-tpm-agent" cp "$RUNNER_TEMP/LICENSE" "$DIR/ssh-tpm-agent" go build -o "$DIR/ssh-tpm-agent" -ldflags "-X main.Version=$VERSION" -trimpath ./cmd/... tar -cvzf "ssh-tpm-agent-$VERSION-$GOOS-$GOARCH.tar.gz" -C "$DIR" ssh-tpm-agent env: CGO_ENABLED: 0 GOOS: ${{ matrix.GOOS }} GOARCH: ${{ matrix.GOARCH }} GOARM: ${{ matrix.GOARM }} - name: Upload workflow artifacts uses: actions/upload-artifact@v4 with: name: ssh-tpm-agent-binaries-${{ matrix.GOOS }}-${{ matrix.GOARCH }} path: ssh-tpm-agent-* upload: name: Upload release binaries if: github.event_name == 'release' needs: build permissions: contents: write runs-on: ubuntu-latest steps: - name: Download workflow artifacts uses: actions/download-artifact@v4 with: pattern: ssh-tpm-agent-binaries-* merge-multiple: true - name: Upload release artifacts run: gh release upload "$GITHUB_REF_NAME" ssh-tpm-agent-* env: GH_REPO: ${{ github.repository }} GH_TOKEN: ${{ github.token }} ssh-tpm-agent-0.8.0/.github/workflows/test.yml000066400000000000000000000012671511154401300213060ustar00rootroot00000000000000name: Go tests on: [push, pull_request] permissions: contents: read jobs: test: name: Test strategy: fail-fast: false matrix: go: [1.24.x] os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - name: Install Go ${{ matrix.go }} uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - name: Checkout repository uses: actions/checkout@v2 with: fetch-depth: 0 - name: Run tests run: go test ./... - name: Run go vet run: go vet ./... - name: staticcheck uses: dominikh/staticcheck-action@v1.3.1 with: install-go: false ssh-tpm-agent-0.8.0/.gitignore000066400000000000000000000000411511154401300161640ustar00rootroot00000000000000bin *.tar.gz* releases/* man/*.1 ssh-tpm-agent-0.8.0/LICENSE000066400000000000000000000021001511154401300151770ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2023 ssh-tpm-agent Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ssh-tpm-agent-0.8.0/Makefile000066400000000000000000000032751511154401300156500ustar00rootroot00000000000000PREFIX := /usr/local BINDIR := $(PREFIX)/bin LIBDIR := $(PREFIX)/lib SHRDIR := $(PREFIX)/share MANDIR := $(PREFIX)/share/man BINS = $(filter-out %_test.go,$(notdir $(wildcard cmd/*))) TAG = $(shell git describe --abbrev=0 --tags) VERSION = $(shell git describe --abbrev=7 | sed 's/-/./g;s/^v//;') MANPAGES = \ man/ssh-tpm-hostkeys.1 \ man/ssh-tpm-agent.1 \ man/ssh-tpm-keygen.1 \ man/ssh-tpm-add.1 all: man build build: $(BINS) man: $(MANPAGES) .PHONY: $(addprefix bin/,$(BINS)) $(addprefix bin/,$(BINS)): go build -buildmode=pie -trimpath -o $@ ./cmd/$(@F) # TODO: Needs to be better written $(BINS): $(addprefix bin/,$(BINS)) .PHONY: install install: $(BINS) @for bin in $(BINS); do \ install -Dm755 "bin/$$bin" -t '$(DESTDIR)$(BINDIR)'; \ done; for manfile in $(MANPAGES); do \ install -Dm644 "$$manfile" -t '$(DESTDIR)$(MANDIR)/man'"$${manfile##*.}"; \ done; @install -dm755 $(DESTDIR)$(LIBDIR)/systemd/system @install -dm755 $(DESTDIR)$(LIBDIR)/systemd/user @DESTDIR=$(DESTDIR) PREFIX=$(PREFIX) bin/ssh-tpm-hostkeys --install-system-units @TEMPLATE_BINARY=$(BINDIR)/ssh-tpm-agent DESTDIR=$(DESTDIR) PREFIX=$(PREFIX) bin/ssh-tpm-agent --install-user-units --install-system .PHONY: lint lint: go vet ./... staticcheck ./... .PHONY: test test: go test -v ./... .PHONY: clean clean: rm -rf bin/ rm -f $(MANPAGES) sign-release: gh release download $(TAG) gpg --sign ssh-tpm-agent-$(TAG)-linux-amd64.tar.gz gpg --sign ssh-tpm-agent-$(TAG)-linux-arm64.tar.gz gpg --sign ssh-tpm-agent-$(TAG)-linux-arm.tar.gz bash -c "gh release upload $(TAG) ssh-tpm-agent-$(TAG)*.gpg" man/%: man/%.adoc Makefile asciidoctor -b manpage -amansource="ssh-tpm-agent $(VERSION)" -amanversion="$(VERSION)" $< ssh-tpm-agent-0.8.0/README.md000066400000000000000000000223051511154401300154620ustar00rootroot00000000000000SSH agent for TPM ================= `ssh-tpm-agent` is a ssh-agent compatible agent that allows keys to be created by the Trusted Platform Module (TPM) for authentication towards ssh servers. TPM sealed keys are private keys created inside the Trusted Platform Module (TPM) and sealed in `.tpm` suffixed files. They are bound to the hardware they are produced on and can't be transferred to other machines. This allows you to utilize a native client instead of having to side load existing PKCS11 libraries into the ssh-agent and/or ssh client. The project uses [TPM 2.0 Key Files](https://www.hansenpartnership.com/draft-bottomley-tpm2-keys.html) implemented through the [`go-tpm-keyfiles`](https://github.com/Foxboron/go-tpm-keyfiles) project. # Features * A working `ssh-agent`. * Create shielded ssh keys on the TPM. * Creation of remotely wrapped SSH keys for import. * PIN support, dictionary attack protection from the TPM allows you to use low entropy PINs instead of passphrases. * TPM session encryption. * Proxy support towards other `ssh-agent` servers for fallbacks. # SWTPM support Instead of utilizing the TPM directly, you can use `--swtpm` or `export SSH_TPM_AGENT_SWTPM=1` to create an identity backed by [swtpm](https://github.com/stefanberger/swtpm) which will be stored under `/var/tmp/ssh-tpm-agent`. Note that `swtpm` provides no security properties and should only be used for testing. ## Installation The simplest way of installing this plugin is by running the following: ```bash go install github.com/foxboron/ssh-tpm-agent/cmd/...@latest ``` Alternatively download the [pre-built binaries](https://github.com/Foxboron/ssh-tpm-agent/releases). # Usage ```bash # Create key $ ssh-tpm-keygen Generating a sealed public/private ecdsa key pair. Enter file in which to save the key (/home/fox/.ssh/id_ecdsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/fox/.ssh/id_ecdsa.tpm Your public key has been saved in /home/fox/.ssh/id_ecdsa.pub The key fingerprint is: SHA256:NCMJJ2La+q5tGcngQUQvEOJP3gPH8bMP98wJOEMV564 The key's randomart image is the color of television, tuned to a dead channel. $ cat /home/fox/.ssh/id_ecdsa.pub ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOTOsMXyjTc1wiQSKhRiNhKFsHJNLzLk2r4foXPLQYKR0tuXIBMTQuMmc7OiTgNMvIjMrcb9adgGdT3s+GkNi1g= # Using the socket $ ssh-tpm-agent -l /var/tmp/tpm.sock $ export SSH_AUTH_SOCK="$(ssh-tpm-agent --print-socket)" $ ssh git@github.com ``` **Note:** For `ssh-tpm-agent` you can specify the TPM owner password using the command line flags `-o` or `--owner-password`, which are preferred. Alternatively, you can use the environment variable `SSH_TPM_AGENT_OWNER_PASSWORD`. ### Import existing key Useful if you want to back up the key to a remote secure storage while using the key day-to-day from the TPM. ```bash # Create a key, or use an existing one $ ssh-keygen -t ecdsa -f id_ecdsa Generating public/private ecdsa key pair. Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in id_ecdsa Your public key has been saved in id_ecdsa.pub The key fingerprint is: SHA256:bDn2EpX6XRX5ADXQSuTq+uUyia/eV3Z6MW+UtxjnXvU fox@framework The key's randomart image is: +---[ECDSA 256]---+ | .+=o..| | o. oo.| | o... .o| | . + .. ..| | S . . o| | o * . oo=*| | ..+.oo=+E| | .++o...o=| | .++++. .+ | +----[SHA256]-----+ # Import the key $ ssh-tpm-keygen --import id_ecdsa Sealing an existing public/private ecdsa key pair. Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in id_ecdsa.tpm The key fingerprint is: SHA256:bDn2EpX6XRX5ADXQSuTq+uUyia/eV3Z6MW+UtxjnXvU The key's randomart image is the color of television, tuned to a dead channel. ``` ### Install user service Socket activated services allow you to start `ssh-tpm-agent` when it's needed by your system. ```bash # Using the socket $ ssh-tpm-agent --install-user-units Installed /home/fox/.config/systemd/user/ssh-tpm-agent.socket Installed /home/fox/.config/systemd/user/ssh-tpm-agent.service Enable with: systemctl --user enable --now ssh-tpm-agent.socket $ systemctl --user enable --now ssh-tpm-agent.socket $ export SSH_AUTH_SOCK="$(ssh-tpm-agent --print-socket)" $ ssh git@github.com ``` ### Proxy support ```bash # Start the usual ssh-agent $ eval $(ssh-agent) # Create a strong RSA key $ ssh-keygen -t rsa -b 4096 -f id_rsa -C ssh-agent ... The key fingerprint is: SHA256:zLSeyU/6NKHGEvyZLA866S1jGqwdwdAxRFff8Z2N1i0 ssh-agent $ ssh-add id_rsa Identity added: id_rsa (ssh-agent) # Print looonnggg key $ ssh-add -L ssh-rsa AAAAB3NzaC1yc[...]8TWynQ== ssh-agent # Create key on the TPM $ ssh-tpm-keygen -C ssh-tpm-agent Generating a sealed public/private ecdsa key pair. Enter file in which to save the key (/home/fox/.ssh/id_ecdsa): Enter passphrase (empty for no passphrase): Confirm passphrase: Your identification has been saved in /home/fox/.ssh/id_ecdsa.tpm Your public key has been saved in /home/fox/.ssh/id_ecdsa.pub The key fingerprint is: SHA256:PoQyuzOpEBLqT+xtP0dnvyBVL6UQTiQeCWN/EXIxPOo The key's randomart image is the color of television, tuned to a dead channel. # Start ssh-tpm-agent with a proxy socket $ ssh-tpm-agent -A "${SSH_AUTH_SOCK}" & $ export SSH_AUTH_SOCK="$(ssh-tpm-agent --print-socket)" # ssh-tpm-agent is proxying the keys from ssh-agent $ ssh-add -L ssh-rsa AAAAB3NzaC1yc[...]8TWynQ== ssh-agent ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNo[...]q4whro= ssh-tpm-agent ``` ### ssh-tpm-add ```bash $ ssh-tpm-agent --no-load & 2023/08/12 13:40:50 Listening on /run/user/1000/ssh-tpm-agent.sock $ export SSH_AUTH_SOCK="$(ssh-tpm-agent --print-socket)" $ ssh-add -L The agent has no identities. $ ssh-tpm-add $HOME/.ssh/id_ecdsa.tpm Identity added: /home/user/.ssh/id_ecdsa.tpm $ ssh-add -L ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJCxqisGa9IUNh4Ik3kwihrDouxP7S5Oun2hnzTvFwktszaibJruKLJMxHqVYnNwKD9DegCNwUN1qXCI/UOwaSY= test ``` ### Create and Wrap private key for client machine on remote srver On the client side create one a primary key under an hierarchy. This example will use the owner hierarchy with an SRK. The output file `srk.pem` needs to be transferred to the remote end which creates the key. This could be done as part of client provisioning. ```bash $ tpm2_createprimary -C o -G ecc -g sha256 -c prim.ctx -a 'restricted|decrypt|fixedtpm|fixedparent|sensitivedataorigin|userwithauth|noda' -f pem -o srk.pem ``` On the remote end we create a p256 ssh key, with no password, and wrap it with `ssh-tpm-keygen` with the `srk.pem` from the client side. ```bash $ ssh-keygen -t ecdsa -b 256 -N "" -f ./ecdsa.key # OR with openssl $ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out ecdsa.key # Wrap with ssh-tpm-keygen $ ssh-tpm-keygen --wrap-with srk.pub --wrap ecdsa.key -f wrapped_id_ecdsa ``` On the client side we can unwrap `wrapped_id_ecdsa` to a loadable key. ```bash $ ssh-tpm-keygen --import ./wrapped_id_ecdsa.tpm -f id_ecdsa.tpm $ ssh-tpm-add id_ecdsa.tpm ``` ### ssh-tpm-hostkey `ssh-tpm-agent` also supports storing host keys inside the TPM. ```bash $ sudo ssh-tpm-keygen -A 2023/09/03 17:03:08 INFO Generating new ECDSA host key 2023/09/03 17:03:08 INFO Wrote /etc/ssh/ssh_tpm_host_ecdsa_key.tpm 2023/09/03 17:03:08 INFO Generating new RSA host key 2023/09/03 17:03:15 INFO Wrote /etc/ssh/ssh_tpm_host_rsa_key.tpm $ sudo ssh-tpm-hostkeys --install-system-units Installed /usr/lib/systemd/system/ssh-tpm-agent.service Installed /usr/lib/systemd/system/ssh-tpm-agent.socket Installed /usr/lib/systemd/system/ssh-tpm-genkeys.service Enable with: systemctl enable --now ssh-tpm-agent.socket $ sudo ssh-tpm-hostkeys --install-sshd-config Installed /etc/ssh/sshd_config.d/10-ssh-tpm-agent.conf Restart sshd: systemd restart sshd $ systemctl enable --now ssh-tpm-agent.socket $ systemd restart sshd $ sudo ssh-tpm-hostkeys ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCLDH2xMDIGb26Q3Fa/kZDuPvzLzfAH6CkNs0wlaY2AaiZT2qJkWI05lMDm+mf+wmDhhgQlkJAHmyqgzYNwqWY0= root@framework ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDAoMPsv5tEpTDFw34ltkF45dTHAPl4aLu6HigBkNnIzsuWqJxhjN6JK3vaV3eXBzy8/UJxo/R0Ml9/DRzFK8cccdIRT1KQtg8xIikRReZ0usdeqTC+wLpW/KQqgBLZ1PphRINxABWReqlnbtPVBfj6wKlCVNLEuTfzi1oAMj3KXOBDcTTB2UBLcwvTFg6YnbTjrpxY83Y+3QIZNPwYqd7r6k+e/ncUl4zgCvvxhoojGxEM3pjQIaZ0Him0yT6OGmCGFa7XIRKxwBSv9HtyHf5psgI+X5A2NV2JW2xeLhV2K1+UXmKW4aXjBWKSO08lPSWZ6/5jQTGN1Jg3fLQKSe7f root@framework $ ssh-keyscan -t ecdsa localhost # localhost:22 SSH-2.0-OpenSSH_9.4 localhost ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCLDH2xMDIGb26Q3Fa/kZDuPvzLzfAH6CkNs0wlaY2AaiZT2qJkWI05lMDm+mf+wmDhhgQlkJAHmyqgzYNwqWY0= ``` # ssh-config It is possible to use the public keys created by `ssh-tpm-keygen` inside ssh configurations. The below example uses `ssh-tpm-agent` and also passes the public key to ensure not all identities are leaked from the agent. ```sshconfig Host example.com IdentityAgent $SSH_AUTH_SOCK Host * IdentityAgent /run/user/1000/ssh-tpm-agent.sock IdentityFile ~/.ssh/id_ecdsa.pub ``` ## License Licensed under the MIT license. See [LICENSE](LICENSE) or https://opensource.org/licenses/MIT ssh-tpm-agent-0.8.0/agent/000077500000000000000000000000001511154401300152775ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/agent/agent.go000066400000000000000000000260221511154401300167260ustar00rootroot00000000000000package agent import ( "bytes" "crypto/rand" "errors" "fmt" "io" "io/fs" "log" "log/slog" "net" "os" "path/filepath" "slices" "strings" "sync" "time" 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/utils" "github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2/transport" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" "golang.org/x/text/cases" "golang.org/x/text/language" ) var ( ErrOperationUnsupported = errors.New("operation unsupported") ErrNoMatchPrivateKeys = errors.New("no private keys match the requested public key") ) var SSH_TPM_AGENT_ADD = "tpm-add-key" type Agent struct { mu sync.Mutex tpm func() transport.TPMCloser op func() ([]byte, error) pin func(key.SSHTPMKeys) ([]byte, error) listener *net.UnixListener quit chan interface{} wg sync.WaitGroup keyring func() *keyring.ThreadKeyring keys []key.SSHTPMKeys hierkeys []*key.HierSSHTPMKey agents []agent.ExtendedAgent } var _ agent.ExtendedAgent = &Agent{} func (a *Agent) Extension(extensionType string, contents []byte) ([]byte, error) { slog.Debug("called extensions") switch extensionType { case SSH_TPM_AGENT_ADD: slog.Debug("runnning extension", slog.String("type", extensionType)) return a.AddTPMKey(contents) } return nil, agent.ErrExtensionUnsupported } func (a *Agent) AddTPMKey(addedkey []byte) ([]byte, error) { slog.Debug("called addtpmkey") a.mu.Lock() defer a.mu.Unlock() k, err := ParseTPMKeyMsg(addedkey) if err != nil { return nil, err } // delete the key if it already exists in the list // it may have been loaded with no certificate or an old certificate a.keys = slices.DeleteFunc(a.keys, func(kk key.SSHTPMKeys) bool { return bytes.Equal(k.AgentKey().Marshal(), kk.AgentKey().Marshal()) }) a.keys = append(a.keys, k) return []byte(""), nil } func (a *Agent) AddProxyAgent(es agent.ExtendedAgent) error { // TODO: Write this up as an extension slog.Debug("called addproxyagent") a.mu.Lock() defer a.mu.Unlock() a.agents = append(a.agents, es) return nil } func (a *Agent) Close() error { slog.Debug("called close") // Flush hierarchy keys for _, k := range a.hierkeys { k.FlushHandle(a.tpm()) } a.Stop() return nil } func (a *Agent) signers() ([]ssh.Signer, error) { var signers []ssh.Signer for _, agent := range a.agents { l, err := agent.Signers() if err != nil { slog.Info("failed getting Signers from agent", slog.String("error", err.Error())) continue } signers = append(signers, l...) } for _, k := range a.keys { s, err := ssh.NewSignerFromSigner(k.Signer( a.keyring(), a.op, a.tpm, func(_ *keyfile.TPMKey) ([]byte, error) { // Shimming the function to get the correct type return a.pin(k) }), ) if err != nil { return nil, fmt.Errorf("failed to prepare signer: %w", err) } signers = append(signers, s) } return signers, nil } func (a *Agent) Signers() ([]ssh.Signer, error) { slog.Debug("called signers") a.mu.Lock() defer a.mu.Unlock() return a.signers() } func (a *Agent) List() ([]*agent.Key, error) { slog.Debug("called list") var agentKeys []*agent.Key a.mu.Lock() defer a.mu.Unlock() // Our keys first, then proxied agents for _, k := range a.keys { agentKeys = append(agentKeys, k.AgentKey()) } for _, agent := range a.agents { l, err := agent.List() if err != nil { slog.Info("failed getting list from agent", slog.String("error", err.Error())) continue } agentKeys = append(agentKeys, l...) } return agentKeys, nil } func (a *Agent) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error) { slog.Debug("called signwithflags") a.mu.Lock() defer a.mu.Unlock() signers, err := a.signers() if err != nil { return nil, err } var wantKey []byte wantKey = key.Marshal() alg := key.Type() // Unwrap the ssh.Certificate PublicKey if strings.Contains(alg, "cert") { parsedCert, err := ssh.ParsePublicKey(wantKey) if err != nil { return nil, err } cert, ok := parsedCert.(*ssh.Certificate) if ok { wantKey = cert.Key.Marshal() alg = cert.Key.Type() } } switch { case alg == ssh.KeyAlgoRSA && flags&agent.SignatureFlagRsaSha256 != 0: alg = ssh.KeyAlgoRSASHA256 case alg == ssh.KeyAlgoRSA && flags&agent.SignatureFlagRsaSha512 != 0: alg = ssh.KeyAlgoRSASHA512 } for _, s := range signers { if !bytes.Equal(s.PublicKey().Marshal(), wantKey) { continue } return s.(ssh.AlgorithmSigner).SignWithAlgorithm(rand.Reader, data, alg) } slog.Debug("trying to sign as proxy...") for _, agent := range a.agents { signers, err := agent.Signers() if err != nil { slog.Info("failed getting signers from agent", slog.String("error", err.Error())) continue } for _, s := range signers { if !bytes.Equal(s.PublicKey().Marshal(), wantKey) { continue } return s.(ssh.AlgorithmSigner).SignWithAlgorithm(rand.Reader, data, alg) } } return nil, ErrNoMatchPrivateKeys } func (a *Agent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { slog.Debug("called sign") return a.SignWithFlags(key, data, 0) } func (a *Agent) serveConn(c net.Conn) { if err := agent.ServeAgent(a, c); err != io.EOF { slog.Info("Agent client connection ended unsuccessfully", slog.String("error", err.Error())) } } func (a *Agent) Wait() { a.wg.Wait() } func (a *Agent) Stop() { close(a.quit) a.listener.Close() a.wg.Wait() } func (a *Agent) serve() { defer a.wg.Done() for { c, err := a.listener.AcceptUnix() if err != nil { type temporary interface { Temporary() bool Error() string } if err, ok := err.(temporary); ok && err.Temporary() { slog.Info("Temporary Accept failure, sleeping 1s", slog.String("error", err.Error())) time.Sleep(1 * time.Second) continue } select { case <-a.quit: return default: slog.Error("Failed to accept connections", slog.String("error", err.Error())) } } a.wg.Add(1) go func() { a.serveConn(c) a.wg.Done() }() } } func (a *Agent) AddKey(k *key.SSHTPMKey) error { slog.Debug("called addkey") a.keys = append(a.keys, k) return nil } func (a *Agent) LoadKeys(keys []key.SSHTPMKeys) { slog.Debug("called loadkeys") a.mu.Lock() defer a.mu.Unlock() a.keys = append(a.keys, keys...) } func (a *Agent) AddHierarchyKeys(hier string) error { tpm := a.tpm() h, err := utils.GetParentHandle(hier) if err != nil { log.Fatal(err) } for n, t := range map[string]struct { alg tpm2.TPMAlgID }{ "rsa": {alg: tpm2.TPMAlgRSA}, "ecdsa": {alg: tpm2.TPMAlgECC}, } { slog.Info("hierarchy key", slog.String("algorithm", strings.ToUpper(n)), slog.String("hierarchy", hier)) hkey, err := key.CreateHierarchyKey(tpm, t.alg, h, fmt.Sprintf("%s hierarchy key", cases.Title(language.Und, cases.NoLower).String(hier))) if err != nil { return err } a.mu.Lock() a.hierkeys = append(a.hierkeys, hkey) a.keys = append(a.keys, hkey) a.mu.Unlock() } return nil } func (a *Agent) Add(key agent.AddedKey) error { // This just proxies the Add call to all proxied agents // First to accept gets the key! slog.Debug("called add") for _, agent := range a.agents { if err := agent.Add(key); err == nil { return nil } } return nil } func (a *Agent) Remove(sshkey ssh.PublicKey) error { slog.Debug("called remove") a.mu.Lock() defer a.mu.Unlock() var found bool a.keys = slices.DeleteFunc(a.keys, func(k key.SSHTPMKeys) bool { if bytes.Equal(sshkey.Marshal(), k.AgentKey().Marshal()) { slog.Debug("deleting key from ssh-tpm-agent", slog.String("fingerprint", ssh.FingerprintSHA256(sshkey)), slog.String("type", sshkey.Type()), ) found = true return true } return false }) if found { return nil } for _, agent := range a.agents { lkeys, err := agent.List() if err != nil { slog.Debug("agent returned err on List()", slog.Any("err", err)) continue } for _, k := range lkeys { if !bytes.Equal(k.Marshal(), sshkey.Marshal()) { continue } if err := agent.Remove(sshkey); err != nil { slog.Debug("agent returned err on Remove()", slog.Any("err", err)) } slog.Debug("deleting key from an proxy agent", slog.String("fingerprint", ssh.FingerprintSHA256(sshkey)), slog.String("type", sshkey.Type()), ) return nil } } slog.Debug("could not find key in any proxied agent", slog.String("fingerprint", ssh.FingerprintSHA256(sshkey)), slog.String("type", sshkey.Type()), ) return fmt.Errorf("key not found") } func (a *Agent) RemoveAll() error { slog.Debug("called removeall") a.mu.Lock() defer a.mu.Unlock() a.keys = []key.SSHTPMKeys{} for _, agent := range a.agents { if err := agent.RemoveAll(); err == nil { return nil } } return nil } func (a *Agent) Lock(passphrase []byte) error { slog.Debug("called lock") return ErrOperationUnsupported } func (a *Agent) Unlock(passphrase []byte) error { slog.Debug("called unlock") return ErrOperationUnsupported } func LoadKeys(keyDir string) ([]key.SSHTPMKeys, error) { keyDir, err := filepath.EvalSymlinks(keyDir) if err != nil { return nil, err } var keys []key.SSHTPMKeys walkFunc := func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } if !strings.HasSuffix(path, ".tpm") { slog.Debug("skipping key: does not have .tpm suffix", slog.String("name", path)) return nil } f, err := os.ReadFile(path) if err != nil { return fmt.Errorf("failed reading %s", path) } k, err := key.Decode(f) if err != nil { if errors.Is(err, key.ErrOldKey) { slog.Info("TPM key is in an old format. Will not load it.", slog.String("key_path", path), slog.String("error", err.Error())) } else { slog.Debug("not a TPM sealed key", slog.String("key_path", path), slog.String("error", err.Error())) } return nil } keys = append(keys, k) slog.Debug("added TPM key", slog.String("name", path)) certStr := fmt.Sprintf("%s-cert.pub", strings.TrimSuffix(path, filepath.Ext(path))) if _, err := os.Stat(certStr); !errors.Is(err, os.ErrNotExist) { b, err := os.ReadFile(certStr) if err != nil { return err } pubKey, _, _, _, err := ssh.ParseAuthorizedKey(b) if err != nil { return err } cert, ok := pubKey.(*ssh.Certificate) if !ok { return err } c := *k c.Certificate = cert keys = append(keys, &c) slog.Debug("added certificate", slog.String("name", path)) } return nil } err = filepath.WalkDir(keyDir, walkFunc) return keys, err } func NewAgent(listener *net.UnixListener, agents []agent.ExtendedAgent, keyring func() *keyring.ThreadKeyring, tpmFetch func() transport.TPMCloser, ownerPassword func() ([]byte, error), pin func(key.SSHTPMKeys) ([]byte, error)) *Agent { a := &Agent{ agents: agents, tpm: tpmFetch, op: ownerPassword, listener: listener, pin: pin, quit: make(chan interface{}), keys: []key.SSHTPMKeys{}, hierkeys: []*key.HierSSHTPMKey{}, keyring: keyring, } a.wg.Add(1) go a.serve() return a } ssh-tpm-agent-0.8.0/agent/agent_test.go000066400000000000000000000116161511154401300177700ustar00rootroot00000000000000package agent_test import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "errors" "log" "net" "path" "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" "github.com/google/go-tpm/tpm2/transport" "github.com/google/go-tpm/tpm2/transport/simulator" "golang.org/x/crypto/ssh" sshagent "golang.org/x/crypto/ssh/agent" ) func TestAddKey(t *testing.T) { tpm, err := simulator.OpenSimulator() if err != nil { t.Fatal(err) } defer tpm.Close() socket := path.Join(t.TempDir(), "socket") unixList, err := net.ListenUnix("unix", &net.UnixAddr{Net: "unix", Name: socket}) if err != nil { log.Fatalln("Failed to listen on UNIX socket:", err) } defer unixList.Close() ag := agent.NewAgent(unixList, []sshagent.ExtendedAgent{}, // Keyring callback func() *keyring.ThreadKeyring { return &keyring.ThreadKeyring{} }, // TPM Callback func() transport.TPMCloser { return tpm }, // Owner password func() ([]byte, error) { return []byte(""), nil }, // PIN Callback func(_ key.SSHTPMKeys) ([]byte, error) { return []byte(""), nil }, ) defer ag.Stop() conn, err := net.Dial("unix", socket) if err != nil { log.Fatal(err) } defer conn.Close() client := sshagent.NewClient(conn) k, err := key.NewSSHTPMKey(tpm, tpm2.TPMAlgECC, 256, []byte("")) if err != nil { t.Fatal(err) } addedkey := sshagent.AddedKey{ PrivateKey: k, Certificate: nil, Comment: k.Description, } _, err = client.Extension(agent.SSH_TPM_AGENT_ADD, agent.MarshalTPMKeyMsg(&addedkey)) if err != nil { t.Fatal(err) } } func TestSigning(t *testing.T) { tpm, err := simulator.OpenSimulator() if err != nil { t.Fatal(err) } defer tpm.Close() ca := keytest.MkECDSA(t, elliptic.P256()) for _, c := range []struct { text string alg tpm2.TPMAlgID bits int f keytest.KeyFunc wanterr error }{ { text: "sign key", alg: tpm2.TPMAlgECC, bits: 256, f: keytest.MkKey, }, { text: "sign key cert", alg: tpm2.TPMAlgECC, bits: 256, f: keytest.MkCertificate(t, &ca), }, } { t.Run(c.text, func(t *testing.T) { k, err := c.f(t, tpm, c.alg, c.bits, []byte(""), "") if err != nil { t.Fatalf("failed key import: %v", err) } ag := keytest.NewTestAgent(t, tpm) defer ag.Stop() if err := ag.AddKey(k); err != nil { t.Fatalf("failed saving key: %v", err) } // Shim the certificate if there is one var sshkey ssh.PublicKey if k.Certificate != nil { sshkey = k.Certificate } else { sshkey = *k.PublicKey } _, err = ag.Sign(sshkey, []byte("test")) if !errors.Is(err, c.wanterr) { t.Fatalf("failed signing: %v", err) } }) } } func TestRemoveCertFromProxy(t *testing.T) { tpm, err := simulator.OpenSimulator() if err != nil { t.Fatal(err) } defer tpm.Close() caEcdsa, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("failed creating CA key") } for _, c := range []struct { text string alg tpm2.TPMAlgID bits int f keytest.KeyFunc wanterr error numkeys int }{ { text: "sign key", alg: tpm2.TPMAlgECC, bits: 256, f: keytest.MkKey, numkeys: 0, }, { text: "sign key cert", alg: tpm2.TPMAlgECC, bits: 256, f: keytest.MkCertificate(t, caEcdsa), numkeys: 1, }, } { t.Run(c.text, func(t *testing.T) { k, err := c.f(t, tpm, c.alg, c.bits, []byte(""), "") if err != nil { t.Fatalf("failed key import: %v", err) } proxyagent := keytest.NewTestAgent(t, tpm) defer proxyagent.Stop() testagent := keytest.NewTestAgent(t, tpm) defer testagent.Stop() if err := testagent.AddKey(k); err != nil { t.Fatalf("failed saving key: %v", err) } if k.Certificate != nil { // If we have a certificate, include // the key without the certificate c := *k c.Certificate = nil if err := testagent.AddKey(&c); err != nil { t.Fatalf("failed saving key: %v", err) } } // Add testagent to proxyagent // We'll try to remove the key from testagent. proxyagent.AddProxyAgent(testagent) // Shim the certificate if there is one var sshkey ssh.PublicKey if k.Certificate != nil { sshkey = k.Certificate } else { sshkey = *k.PublicKey } if err := proxyagent.Remove(sshkey); err != nil { t.Fatalf("failed to remove key: %v", err) } // Check the key doesn't exist in the proxy nor the agent proxysl, err := proxyagent.List() if err != nil { t.Fatalf("%v", err) } if len(proxysl) != c.numkeys { t.Fatalf("still keys in the agent. Should be 0") } sl, err := testagent.List() if err != nil { t.Fatalf("%v", err) } if len(sl) != c.numkeys { t.Fatalf("still keys in the agent. Should be 0") } }) } } ssh-tpm-agent-0.8.0/agent/client.go000066400000000000000000000037141511154401300171110ustar00rootroot00000000000000package agent import ( "errors" keyfile "github.com/foxboron/go-tpm-keyfiles" "github.com/foxboron/ssh-tpm-agent/key" "golang.org/x/crypto/ssh" sshagent "golang.org/x/crypto/ssh/agent" ) // type AddedKey struct { // PrivateKey *keyfile.TPMKey // Certificate *ssh.Certificate // Comment string // LifetimeSecs uint32 // ConfirmBeforeUse bool // ConstraintExtensions []sshagent.ConstraintExtension // } type TPMKeyMsg struct { Type string `sshtype:"17|25"` PrivateKey []byte CertBytes []byte Constraints []byte `ssh:"rest"` } func MarshalTPMKeyMsg(cert *sshagent.AddedKey) []byte { var req []byte var constraints []byte if secs := cert.LifetimeSecs; secs != 0 { constraints = append(constraints, ssh.Marshal(constrainLifetimeAgentMsg{secs})...) } if cert.ConfirmBeforeUse { constraints = append(constraints, agentConstrainConfirm) } var certBytes []byte if cert.Certificate != nil { certBytes = cert.Certificate.Marshal() } switch k := cert.PrivateKey.(type) { case *keyfile.TPMKey: req = ssh.Marshal(TPMKeyMsg{ Type: "TPMKEY", PrivateKey: k.Bytes(), CertBytes: certBytes, Constraints: constraints, }) case *key.SSHTPMKey: req = ssh.Marshal(TPMKeyMsg{ Type: "TPMKEY", PrivateKey: k.Bytes(), CertBytes: certBytes, Constraints: constraints, }) } return req } func ParseTPMKeyMsg(req []byte) (*key.SSHTPMKey, error) { var ( k TPMKeyMsg tpmkey *key.SSHTPMKey err error ) if err := ssh.Unmarshal(req, &k); err != nil { return nil, err } if len(k.PrivateKey) != 0 { tpmkey, err = key.Decode(k.PrivateKey) if err != nil { return nil, err } } if len(k.CertBytes) != 0 { pubKey, err := ssh.ParsePublicKey(k.CertBytes) if err != nil { return nil, err } cert, ok := pubKey.(*ssh.Certificate) if !ok { return nil, errors.New("agent: bad tpm thing") } tpmkey.Certificate = cert } return tpmkey, nil } ssh-tpm-agent-0.8.0/agent/gocrypto.go000066400000000000000000000042371511154401300175020ustar00rootroot00000000000000package agent // Code taken from crypto/x/ssh/agent const ( // 3.7 Key constraint identifiers agentConstrainLifetime = 1 agentConstrainConfirm = 2 // 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 // Constraint extension identifier in version 3 and later of the protocol. // agentConstrainExtension = 255 ) // 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"` // } // 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 setConstraints(key *key.SSHTPMKey, constraintBytes []byte) error { // lifetimeSecs, confirmBeforeUse, constraintExtensions, err := parseConstraints(constraintBytes) // if err != nil { // return err // } // key.LifetimeSecs = lifetimeSecs // key.ConfirmBeforeUse = confirmBeforeUse // key.ConstraintExtensions = constraintExtensions // return nil // } ssh-tpm-agent-0.8.0/askpass/000077500000000000000000000000001511154401300156465ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/askpass/askpass.go000066400000000000000000000106171511154401300176470ustar00rootroot00000000000000package askpass import ( "bufio" "bytes" "errors" "fmt" "log/slog" "os" "os/exec" "strings" "syscall" "golang.org/x/sys/unix" "golang.org/x/term" ) // Most of this is copied from OpenSSH readpassphrase. // State for the ReadPassphrase function type ReadPassFlags uint8 const ( RP_ECHO = 1 << iota /* echo stuff or something 8 */ RP_ALLOW_STDIN /* Allow stdin and not askpass */ RP_ALLOW_EOF /* not used */ RP_USE_ASKPASS /* Use SSH_ASKPASS */ RP_ASK_PERMISSION /* Ask for permission, yes/no prompt */ RP_NEWLINE /* Print newline after answer. */ RPP_ECHO_OFF /* Turn off echo (default). */ RPP_ECHO_ON /* Leave echo on. */ RPP_REQUIRE_TTY /* Fail if there is no tty. */ RPP_FORCELOWER /* Force input to lower case. */ RPP_FORCEUPPER /* Force input to upper case. */ RPP_SEVENBIT /* Strip the high bit from input. */ RPP_STDIN /* Read from stdin, not /dev/tty */ ) var ( ErrNoAskpass = errors.New("system does not have an askpass program") // Default ASKPASS programs SSH_ASKPASS_DEFAULTS = []string{ "/usr/lib/ssh/x11-ssh-askpass", "/usr/lib/ssh/gnome-ssh-askpass3", "/usr/lib/ssh/gnome-ssh-askpass", "/usr/libexec/openssh/gnome-ssh-askpass", "/usr/bin/ksshaskpass", "/usr/libexec/seahorse/ssh-askpass", "/usr/lib/seahorse/ssh-askpass", } ) func findAskPass() (string, error) { for _, s := range SSH_ASKPASS_DEFAULTS { if _, err := os.Stat(s); errors.Is(err, os.ErrNotExist) { continue } return s, nil } return "", ErrNoAskpass } func isTerminal(fd uintptr) bool { _, err := unix.IoctlGetTermios(int(fd), unix.TCGETS) return err == nil } func ReadPassphrase(prompt string, flags ReadPassFlags) ([]byte, error) { var allow_askpass bool var use_askpass bool var askpass_hint string if _, ok := os.LookupEnv("DISPLAY"); ok { allow_askpass = true } else if _, ok2 := os.LookupEnv("WAYLAND_DISPLAY"); ok2 { allow_askpass = true } if s, ok := os.LookupEnv("SSH_ASKPASS_REQUIRE"); ok { switch s { case "force": use_askpass = true allow_askpass = true case "prefer": use_askpass = allow_askpass case "never": allow_askpass = false } } if use_askpass { slog.Debug("requested to askpass") } else if (flags & RP_USE_ASKPASS) != 0 { use_askpass = true } else if (flags & RP_ALLOW_STDIN) != 0 { if !isTerminal(os.Stdout.Fd()) { slog.Debug("stdin is not a tty") use_askpass = true } } if use_askpass && allow_askpass { if (flags & RP_ASK_PERMISSION) != 0 { askpass_hint = "confirm" } return SshAskPass(prompt, askpass_hint) } // If we want to echo stuff, we read directly from stdin // using bufio.NewReader. if (flags & RPP_ECHO_ON) != 0 { fmt.Printf("%s", prompt) reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n') if err != nil { return []byte(""), nil } return []byte(strings.TrimSpace(input)), nil } // Then we are defaulting to TTY prompt fmt.Printf("%s", prompt) pin, err := term.ReadPassword(int(syscall.Stdin)) if err != nil { return []byte{}, nil } if (flags & RP_NEWLINE) != 0 { fmt.Println("") } return pin, nil } func SshAskPass(prompt, hint string) ([]byte, error) { var askpass string var err error if s, ok := os.LookupEnv("SSH_ASKPASS"); ok { askpass = s } else if s, _ := exec.LookPath("ssh-askpass"); s != "" { askpass = s } else { askpass, err = findAskPass() if err != nil { return nil, err } } if hint != "" { os.Setenv("SSH_ASKPASS_PROMPT", hint) } out, err := exec.Command(askpass, prompt).Output() switch hint { case "confirm": // TODO: Ugly and needs a rework var exerr *exec.ExitError if errors.As(err, &exerr) { if exerr.ExitCode() != 0 { return []byte("no"), nil } } else { return []byte("yes"), nil } } if err != nil { return []byte{}, err } return bytes.TrimSpace(out), nil } // AskPremission runs SSH_ASKPASS in with SSH_ASKPASS_PROMPT=confirm set as env // it will expect exit code 0 or !0 and return 'yes' and 'no' respectively. func AskPermission() (bool, error) { a, err := ReadPassphrase("Confirm touch", RP_USE_ASKPASS|RP_ASK_PERMISSION) if err != nil { return false, err } if bytes.Equal(a, []byte("yes")) { return true, nil } else if bytes.Equal(a, []byte("no")) { return false, nil } return false, nil } ssh-tpm-agent-0.8.0/cmd/000077500000000000000000000000001511154401300147445ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/cmd/scripts_test.go000066400000000000000000000052541511154401300200270ustar00rootroot00000000000000package script_tests import ( "encoding/pem" "fmt" "log" "os" "os/exec" "path/filepath" "testing" "time" keyfile "github.com/foxboron/go-tpm-keyfiles" "github.com/foxboron/go-tpm-keyfiles/pkix" "github.com/foxboron/ssh-tpm-agent/utils" "github.com/google/go-tpm/tpm2" "github.com/rogpeppe/go-internal/testscript" ) func ScriptsWithPath(t *testing.T, path string) { t.Parallel() tmp := t.TempDir() c := exec.Command("go", "build", "-buildmode=pie", "-o", tmp, "../cmd/...") out, err := c.CombinedOutput() if err != nil { t.Fatal(string(out)) } testscript.Run(t, testscript.Params{ 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, 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)) return nil }, Dir: path, Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ // Create an EK certificate from our fixed seed simulator "getekcert": func(ts *testscript.TestScript, neg bool, args []string) { tpm, err := utils.GetFixedSim() if err != nil { t.Fatal(err) } defer tpm.Close() rsp, err := tpm2.CreatePrimary{ PrimaryHandle: tpm2.AuthHandle{ Handle: tpm2.TPMRHOwner, Auth: tpm2.PasswordAuth([]byte(nil)), }, InSensitive: tpm2.TPM2BSensitiveCreate{ Sensitive: &tpm2.TPMSSensitiveCreate{ UserAuth: tpm2.TPM2BAuth{ Buffer: []byte(nil), }, }, }, InPublic: tpm2.New2B(keyfile.ECCSRK_H2_Template), }.Execute(tpm) if err != nil { log.Fatalf("failed creating primary key: %v", err) } keyfile.FlushHandle(tpm, rsp.ObjectHandle) srkPublic, err := rsp.OutPublic.Contents() if err != nil { log.Fatalf("failed getting srk public content: %v", err) } b, err := pkix.FromTPMPublic(srkPublic) if err != nil { log.Fatal(err) } if err := os.WriteFile(ts.MkAbs("srk.pem"), pem.EncodeToMemory(&pem.Block{ Type: "PUBLIC KEY", Bytes: b, }), 0o664); err != nil { log.Fatal(err) } }, }, }) } func TestAgent(t *testing.T) { ScriptsWithPath(t, "ssh-tpm-agent/testdata/script") } func TestKeygen(t *testing.T) { ScriptsWithPath(t, "ssh-tpm-keygen/testdata/script") } // func TestAdd(t *testing.T) { // ScriptsWithPath(t, "ssh-tpm-add/testdata/script") // } // func TestHostkeys(t *testing.T) { // ScriptsWithPath(t, "ssh-tpm-hostkeys/testdata/script") // } ssh-tpm-agent-0.8.0/cmd/ssh-tpm-add/000077500000000000000000000000001511154401300170655ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/cmd/ssh-tpm-add/main.go000066400000000000000000000074521511154401300203500ustar00rootroot00000000000000package main import ( "errors" "flag" "fmt" "log" "net" "os" "path/filepath" "strings" "github.com/foxboron/ssh-tpm-agent/agent" "github.com/foxboron/ssh-tpm-agent/internal/lsm" "github.com/foxboron/ssh-tpm-agent/key" "github.com/foxboron/ssh-tpm-agent/utils" "github.com/foxboron/ssh-tpm-ca-authority/client" "github.com/google/go-tpm/tpm2/transport/linuxtpm" "github.com/landlock-lsm/go-landlock/landlock" "golang.org/x/crypto/ssh" sshagent "golang.org/x/crypto/ssh/agent" ) var Version string const usage = `Usage: ssh-tpm-add [FILE ...] ssh-tpm-add --ca [URL] --user [USER] --host [HOSTNAME] Options for CA provisioning: --ca URL URL to the CA authority for CA key provisioning. --user USER Username of the ssh server user. --host HOSTNAME Hostname of the ssh server. Add a sealed TPM key to ssh-tpm-agent. Allows CA key provisioning with the --ca option. Example: $ ssh-tpm-add id_rsa.tpm` func main() { flag.Usage = func() { fmt.Println(usage) } var caURL, host, user string flag.StringVar(&caURL, "ca", "", "ca authority") flag.StringVar(&host, "host", "", "ssh hot") flag.StringVar(&user, "user", "", "remote ssh user") flag.Parse() socket := utils.EnvSocketPath("") if socket == "" { fmt.Println("Can't find any ssh-tpm-agent socket.") os.Exit(1) } lsm.RestrictAdditionalPaths(landlock.RWFiles(socket)) var ignorefile bool var paths []string if len(os.Args) == 1 { sshdir := utils.SSHDir() paths = []string{ fmt.Sprintf("%s/id_ecdsa.tpm", sshdir), fmt.Sprintf("%s/id_rsa.tpm", sshdir), } ignorefile = true } else if len(os.Args) != 1 { paths = os.Args[1:] } lsm.RestrictAdditionalPaths( // RW on socket landlock.RWFiles(socket), // RW on files we should encode/decode landlock.RWFiles(paths...), ) if err := lsm.Restrict(); err != nil { log.Fatal(err) } conn, err := net.Dial("unix", socket) if err != nil { log.Fatal(err) } defer conn.Close() if caURL != "" && host != "" { c := client.NewClient(caURL) rwc, err := linuxtpm.Open("/dev/tpmrm0") if err != nil { log.Fatal(err) } k, cert, err := c.GetKey(rwc, user, host) if err != nil { log.Fatal(err) } sshagentclient := sshagent.NewClient(conn) addedkey := sshagent.AddedKey{ PrivateKey: k, Comment: k.Description, Certificate: cert, } _, err = sshagentclient.Extension(agent.SSH_TPM_AGENT_ADD, agent.MarshalTPMKeyMsg(&addedkey)) if err != nil { log.Fatal(err) } fmt.Printf("Identity added from CA authority: %s\n", caURL) os.Exit(0) } for _, path := range paths { b, err := os.ReadFile(path) if err != nil { if ignorefile { continue } log.Fatal(err) } k, err := key.Decode(b) if err != nil { log.Fatal(err) } client := sshagent.NewClient(conn) if _, err = client.Extension(agent.SSH_TPM_AGENT_ADD, agent.MarshalTPMKeyMsg( &sshagent.AddedKey{ PrivateKey: k, Comment: k.Description, }, )); err != nil { log.Fatal(err) } fmt.Printf("Identity added: %s (%s)\n", path, k.Description) certStr := fmt.Sprintf("%s-cert.pub", strings.TrimSuffix(path, filepath.Ext(path))) if _, err := os.Stat(certStr); !errors.Is(err, os.ErrNotExist) { b, err := os.ReadFile(certStr) if err != nil { log.Fatal(err) } pubKey, _, _, _, err := ssh.ParseAuthorizedKey(b) if err != nil { log.Fatal("failed parsing ssh certificate") } cert, ok := pubKey.(*ssh.Certificate) if !ok { log.Fatal("failed parsing ssh certificate") } if _, err = client.Extension(agent.SSH_TPM_AGENT_ADD, agent.MarshalTPMKeyMsg( &sshagent.AddedKey{ PrivateKey: k, Certificate: cert, Comment: k.Description, }, )); err != nil { log.Fatal(err) } fmt.Printf("Identity added: %s (%s)\n", certStr, k.Description) } } } ssh-tpm-agent-0.8.0/cmd/ssh-tpm-agent/000077500000000000000000000000001511154401300174335ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/cmd/ssh-tpm-agent/main.go000066400000000000000000000215001511154401300207040ustar00rootroot00000000000000package main import ( "context" "errors" "flag" "fmt" "log" "log/slog" "net" "os" "os/signal" "path/filepath" "slices" "syscall" "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/internal/lsm" "github.com/foxboron/ssh-tpm-agent/key" "github.com/foxboron/ssh-tpm-agent/utils" "github.com/google/go-tpm/tpm2/transport" "github.com/landlock-lsm/go-landlock/landlock" sshagent "golang.org/x/crypto/ssh/agent" "golang.org/x/term" ) var Version string const usage = `Usage: ssh-tpm-agent [OPTIONS] ssh-tpm-agent -l [PATH] ssh-tpm-agent --install-user-units Options: -l PATH Path of the UNIX socket to open, defaults to $XDG_RUNTIME_DIR/ssh-tpm-agent.sock. -A PATH Fallback ssh-agent sockets for additional key lookup. --print-socket Prints the socket to STDIN. --key-dir PATH Path of the directory to look for TPM sealed keys in, defaults to $HOME/.ssh --no-load Do not load TPM sealed keys by default. -o, --owner-password Ask for the owner password. --no-cache The agent will not cache key passwords. --hierarchy HIERARCHY Preload the agent with a hierarchy key. owner, o (default) endorsement, e null, n platform, p -d Enable debug logging. --install-user-units Installs systemd user units for using ssh-tpm-agent as a service. ssh-tpm-agent is a program that loads TPM sealed keys for public key authentication. It is an ssh-agent(1) compatible program and can be used for ssh(1) authentication. TPM sealed keys are private keys created inside the Trusted Platform Module (TPM) and sealed in .tpm suffixed files. They are bound to the hardware they where produced on and can't be transferred to other machines. Use ssh-tpm-keygen to create new keys. The agent loads all TPM sealed keys from $HOME/.ssh, unless --key-dir is specified. Example: $ ssh-tpm-agent & $ export SSH_AUTH_SOCK=$(ssh-tpm-agent --print-socket) $ ssh git@github.com` type SocketSet struct { Value []string } func (s SocketSet) String() string { return "set" } func (s *SocketSet) Set(p string) error { if !slices.Contains(s.Value, p) { s.Value = append(s.Value, p) } return nil } func (s SocketSet) Type() string { return "[PATH]" } func NewSocketSet(allowed []string, d string) *SocketSet { return &SocketSet{ Value: []string{}, } } func main() { flag.Usage = func() { fmt.Println(usage) } var ( socketPath, keyDir string swtpmFlag, printSocketFlag bool installUserUnits, system, noLoad bool askOwnerPassword, debugMode bool noCache bool hierarchy string ) var sockets SocketSet flag.StringVar(&socketPath, "l", func(s string) string { return utils.EnvSocketPath(s) }(socketPath), "path of the UNIX socket to listen on") flag.Var(&sockets, "A", "fallback ssh-agent sockets") flag.BoolVar(&swtpmFlag, "swtpm", false, "use swtpm instead of actual tpm") flag.BoolVar(&printSocketFlag, "print-socket", false, "print path of UNIX socket to stdout") flag.StringVar(&keyDir, "key-dir", "", "path of the directory to look for keys in") flag.BoolVar(&installUserUnits, "install-user-units", false, "install systemd user units") flag.BoolVar(&system, "install-system", false, "install systemd user units") flag.BoolVar(&noLoad, "no-load", false, "don't load TPM sealed keys") flag.BoolVar(&askOwnerPassword, "o", false, "ask for the owner password") flag.BoolVar(&askOwnerPassword, "owner-password", false, "ask for the owner password") flag.BoolVar(&debugMode, "d", false, "debug mode") flag.BoolVar(&noCache, "no-cache", false, "do not cache key passwords") flag.StringVar(&hierarchy, "hierarchy", "", "hierarchy for the created key") flag.Parse() opts := &slog.HandlerOptions{ Level: slog.LevelInfo, } if debugMode { opts.Level = slog.LevelDebug } logger := slog.New(slog.NewTextHandler(os.Stdout, opts)) slog.SetDefault(logger) if installUserUnits { if err := utils.InstallUserUnits(system); err != nil { log.Fatal(err) fmt.Println(err.Error()) os.Exit(1) } fmt.Println("Enable with: systemctl --user enable --now ssh-tpm-agent.socket") os.Exit(0) } if socketPath == "" { flag.Usage() os.Exit(1) } if printSocketFlag { fmt.Println(socketPath) os.Exit(0) } if keyDir == "" { keyDir = utils.SSHDir() } if term.IsTerminal(int(os.Stdin.Fd())) { slog.Info("Warning: ssh-tpm-agent is meant to run as a background daemon.") slog.Info("Running multiple instances is likely to lead to conflicts.") slog.Info("Consider using a systemd service.") } var agents []sshagent.ExtendedAgent for _, s := range sockets.Value { lsm.RestrictAdditionalPaths(landlock.RWFiles(s)) conn, err := net.Dial("unix", s) if err != nil { slog.Error(err.Error()) os.Exit(1) } agents = append(agents, sshagent.NewClient(conn)) } // Ensure we can rw socket path lsm.RestrictAdditionalPaths(landlock.RWFiles(socketPath)) listener, err := createListener(socketPath) if err != nil { slog.Error("creating listener", slog.String("error", err.Error())) 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) } // We need to pre-read all the keys before we run landlock var keys []key.SSHTPMKeys if !noLoad { keys, err = agent.LoadKeys(keyDir) if err != nil { log.Fatalf("can't preload keys from ~/.ssh: %v", err) } } // Try to landlock everything before we run the agent lsm.RestrictAgentFiles() if err := lsm.Restrict(); 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 tpm, err := utils.TPM(swtpmFlag) if err != nil { log.Fatal(err) } return tpm }, // Owner password func() ([]byte, error) { if askOwnerPassword { 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 } }, // PIN Callback with caching // SSHKeySigner in signer/signer.go resets this value if // we get a TPMRCAuthFail func(key key.SSHTPMKeys) ([]byte, error) { 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.GetDescription()) // TODOt kjk: askpass should box the byte slice userauth, err := askpass.ReadPassphrase(keyInfo, askpass.RP_USE_ASKPASS) fmt.Println(err) 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 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() } }() if !noLoad { agent.LoadKeys(keys) } if hierarchy != "" { if err := agent.AddHierarchyKeys(hierarchy); err != nil { log.Fatal(err) } } agent.Wait() } func createListener(socketPath string) (*net.UnixListener, error) { if _, ok := os.LookupEnv("LISTEN_FDS"); ok { f := os.NewFile(uintptr(3), "ssh-tpm-agent.socket") fListener, err := net.FileListener(f) if err != nil { return nil, err } listener, ok := fListener.(*net.UnixListener) if !ok { return nil, fmt.Errorf("socket-activation file descriptor isn't an unix socket") } slog.Info("Activated agent by socket") return listener, nil } _ = os.Remove(socketPath) if err := os.MkdirAll(filepath.Dir(socketPath), 0o770); err != nil { return nil, fmt.Errorf("creating UNIX socket directory: %w", err) } listener, err := net.ListenUnix("unix", &net.UnixAddr{Net: "unix", Name: socketPath}) if err != nil { return nil, err } slog.Info("Listening on socket", slog.String("path", socketPath)) return listener, nil } ssh-tpm-agent-0.8.0/cmd/ssh-tpm-agent/main_test.go000066400000000000000000000114121511154401300217440ustar00rootroot00000000000000package main import ( "bytes" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "fmt" "log" "net" "path" "testing" "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" "github.com/google/go-tpm/tpm2/transport" "github.com/google/go-tpm/tpm2/transport/simulator" "golang.org/x/crypto/ssh" sshagent "golang.org/x/crypto/ssh/agent" ) func newSSHKey() ssh.Signer { key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { panic(err) } signer, err := ssh.NewSignerFromSigner(key) if err != nil { panic(err) } return signer } func setupServer(listener net.Listener, clientKey ssh.PublicKey) (hostkey ssh.PublicKey, msgSent chan bool) { hostSigner := newSSHKey() msgSent = make(chan bool) srvStart := make(chan bool) authorizedKeysMap := map[string]bool{} authorizedKeysMap[string(clientKey.Marshal())] = true config := &ssh.ServerConfig{ PublicKeyCallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { if authorizedKeysMap[string(pubKey.Marshal())] { return &ssh.Permissions{ Extensions: map[string]string{ "pubkey-fp": ssh.FingerprintSHA256(pubKey), }, }, nil } return nil, fmt.Errorf("unknown public key for %q", c.User()) }, } config.AddHostKey(hostSigner) go func() { close(srvStart) nConn, err := listener.Accept() if err != nil { log.Fatal("failed to accept incoming connection: ", err) } _, chans, reqs, err := ssh.NewServerConn(nConn, config) if err != nil { log.Fatal("failed to handshake: ", err) } go ssh.DiscardRequests(reqs) for newChannel := range chans { if newChannel.ChannelType() != "session" { newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") continue } channel, requests, err := newChannel.Accept() if err != nil { log.Fatalf("Could not accept channel: %v", err) } go func(in <-chan *ssh.Request) { for req := range in { req.Reply(req.Type == "shell", nil) } }(requests) channel.Write([]byte("connected")) // Need to figure out something better time.Sleep(time.Millisecond * 100) close(msgSent) channel.Close() } }() // Waiting until the server has started <-srvStart return hostSigner.PublicKey(), msgSent } func runSSHAuth(t *testing.T, keytype tpm2.TPMAlgID, bits int, pin []byte, keyfn keytest.KeyFunc) { t.Parallel() tpm, err := simulator.OpenSimulator() if err != nil { t.Fatal(err) } defer tpm.Close() k, err := keyfn(t, tpm, keytype, bits, pin, "") if err != nil { t.Fatalf("failed creating key: %v", err) } listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { log.Fatal("failed to listen for connection: ", err) } hostkey, msgSent := setupServer(listener, *k.PublicKey) defer listener.Close() socket := path.Join(t.TempDir(), "socket") unixList, err := net.ListenUnix("unix", &net.UnixAddr{Net: "unix", Name: socket}) if err != nil { log.Fatalln("Failed to listen on UNIX socket:", err) } defer unixList.Close() ag := agent.NewAgent(unixList, []sshagent.ExtendedAgent{}, // Keyring Callback func() *keyring.ThreadKeyring { return &keyring.ThreadKeyring{} }, // TPM Callback func() transport.TPMCloser { return tpm }, // Owner password func() ([]byte, error) { return []byte(""), nil }, // PIN Callback func(_ key.SSHTPMKeys) ([]byte, error) { return pin, nil }, ) defer ag.Stop() if err := ag.AddKey(k); err != nil { t.Fatalf("failed saving key: %v", err) } sshClient := &ssh.ClientConfig{ User: "username", Auth: []ssh.AuthMethod{ ssh.PublicKeysCallback(ag.Signers), }, HostKeyCallback: ssh.FixedHostKey(hostkey), } client, err := ssh.Dial("tcp", listener.Addr().String(), sshClient) if err != nil { t.Fatal("Failed to dial: ", err) } defer client.Close() session, err := client.NewSession() if err != nil { t.Fatal("Failed to create session: ", err) } defer session.Close() session.Shell() var b bytes.Buffer session.Stdout = &b <-msgSent if b.String() != "connected" { t.Fatalf("failed to connect") } } func TestSSHAuth(t *testing.T) { for _, c := range []struct { name string alg tpm2.TPMAlgID bits int }{ { "ecdsa p256 - agent", tpm2.TPMAlgECC, 256, }, { "ecdsa p384 - agent", tpm2.TPMAlgECC, 384, }, { "ecdsa p521 - agent", tpm2.TPMAlgECC, 521, }, { "rsa - agent", tpm2.TPMAlgRSA, 2048, }, } { t.Run(c.name+" - tpm key", func(t *testing.T) { runSSHAuth(t, c.alg, c.bits, []byte(""), keytest.MkKey) }) t.Run(c.name+" - imported key", func(t *testing.T) { runSSHAuth(t, c.alg, c.bits, []byte(""), keytest.MkImportableKey) }) } } ssh-tpm-agent-0.8.0/cmd/ssh-tpm-agent/testdata/000077500000000000000000000000001511154401300212445ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/cmd/ssh-tpm-agent/testdata/script/000077500000000000000000000000001511154401300225505ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/cmd/ssh-tpm-agent/testdata/script/agent.txt000066400000000000000000000040251511154401300244100ustar00rootroot00000000000000# Ensure we can run the agent exec ssh-tpm-agent -d --no-load &agent& exec sleep .2s exec ssh-tpm-keygen exec ssh-tpm-keygen -t rsa exec ssh-tpm-add stdout id_ecdsa.tpm stdout id_rsa.tpm exec ssh-add -l stdout ECDSA stdout RSA exec ssh-add -D # ssh sign file - ecdsa exec ssh-tpm-add .ssh/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 # ssh sign file - rsa exec ssh-tpm-add .ssh/id_rsa.tpm exec ssh-add -l stdout RSA exec ssh-keygen -Y sign -n file -f .ssh/id_rsa.pub file_to_sign.txt stdin file_to_sign.txt exec ssh-keygen -Y check-novalidate -n file -f .ssh/id_rsa.pub -s file_to_sign.txt.sig exists file_to_sign.txt.sig rm file_to_sign.txt.sig exec ssh-add -D # ssh create a certificate - ecdsa exec ssh-keygen -t ecdsa -f id_ca -N '' exec ssh-keygen -s id_ca -n fox -I 'cert' -z '0001' .ssh/id_ecdsa.pub exists .ssh/id_ecdsa-cert.pub exec ssh-tpm-add .ssh/id_ecdsa.tpm stdout id_ecdsa.tpm stdout id_ecdsa-cert.pub exec ssh-add -l stdout \(ECDSA\) stdout \(ECDSA-CERT\) exec ssh-keygen -Y sign -n file -f .ssh/id_ecdsa-cert.pub file_to_sign.txt stdin file_to_sign.txt exec ssh-keygen -Y check-novalidate -n file -f .ssh/id_ecdsa-cert.pub -s file_to_sign.txt.sig exists file_to_sign.txt.sig rm file_to_sign.txt.sig exec ssh-add -D rm id_ca id_ca.pub # ssh create a certificate - rsa exec ssh-keygen -t rsa -f id_ca -N '' exec ssh-keygen -s id_ca -n fox -I 'cert' -z '0001' .ssh/id_rsa.pub exists .ssh/id_rsa-cert.pub exec ssh-tpm-add .ssh/id_rsa.tpm exec ssh-add -l stdout \(RSA\) stdout \(RSA-CERT\) exec ssh-keygen -Y sign -n file -f .ssh/id_rsa-cert.pub file_to_sign.txt stdin file_to_sign.txt exec ssh-keygen -Y check-novalidate -n file -f .ssh/id_rsa-cert.pub -s file_to_sign.txt.sig exists file_to_sign.txt.sig rm file_to_sign.txt.sig exec ssh-add -D rm id_ca id_ca.pub -- file_to_sign.txt -- Hello World ssh-tpm-agent-0.8.0/cmd/ssh-tpm-agent/testdata/script/agent_hierarchy_keys.txt000066400000000000000000000004151511154401300275000ustar00rootroot00000000000000# Create hierarchy keys exec ssh-tpm-agent -d --no-load --hierarchy owner &agent& # Wait for key generation. 2 seconds'ish exec sleep 2s exec ssh-add -l stdout 'EqZ4 Owner hierarchy key \(RSA\)' stdout 'lCPg Owner hierarchy key \(ECDSA\)' exec ls # TODO: Signing test ssh-tpm-agent-0.8.0/cmd/ssh-tpm-agent/testdata/script/agent_password.txt000066400000000000000000000016401511154401300263320ustar00rootroot00000000000000# 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")) } } ssh-tpm-agent-0.8.0/cmd/ssh-tpm-hostkeys/000077500000000000000000000000001511154401300202065ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/cmd/ssh-tpm-hostkeys/main.go000066400000000000000000000027671511154401300214750ustar00rootroot00000000000000package main import ( "flag" "fmt" "log" "net" "os" "github.com/foxboron/ssh-tpm-agent/utils" sshagent "golang.org/x/crypto/ssh/agent" ) var Version string const usage = `Usage: ssh-tpm-hostkeys ssh-tpm-hostkeys --install-system-units Options: --install-system-units Installs systemd system units for using ssh-tpm-agent as a hostkey agent. --install-sshd-config Installs sshd configuration for the ssh-tpm-agent socket. Display host keys.` func main() { flag.Usage = func() { fmt.Println(usage) } var ( installSystemUnits bool installSshdConfig bool ) flag.BoolVar(&installSystemUnits, "install-system-units", false, "install systemd system units") flag.BoolVar(&installSshdConfig, "install-sshd-config", false, "install sshd config") flag.Parse() if installSystemUnits { if err := utils.InstallHostkeyUnits(); err != nil { log.Fatal(err) } fmt.Println("Enable with: systemctl enable --now ssh-tpm-agent.socket") os.Exit(0) } if installSshdConfig { if err := utils.InstallSshdConf(); err != nil { log.Fatal(err) } os.Exit(0) } socket := "/var/tmp/ssh-tpm-agent.sock" if socket == "" { fmt.Println("Can't find any ssh-tpm-agent socket.") os.Exit(1) } conn, err := net.Dial("unix", socket) if err != nil { log.Fatal(err) } defer conn.Close() client := sshagent.NewClient(conn) keys, err := client.List() if err != nil { log.Fatal(err) } for _, k := range keys { fmt.Println(k.String()) } } ssh-tpm-agent-0.8.0/cmd/ssh-tpm-keygen/000077500000000000000000000000001511154401300176175ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/cmd/ssh-tpm-keygen/main.go000066400000000000000000000463311511154401300211010ustar00rootroot00000000000000package main import ( "bytes" "crypto" "crypto/ecdsa" "crypto/rsa" "crypto/x509" "errors" "flag" "fmt" "log" "log/slog" "os" "os/user" "path" "slices" "strings" "sync" keyfile "github.com/foxboron/go-tpm-keyfiles" tpmpkix "github.com/foxboron/go-tpm-keyfiles/pkix" "github.com/foxboron/ssh-tpm-agent/askpass" "github.com/foxboron/ssh-tpm-agent/key" "github.com/foxboron/ssh-tpm-agent/utils" "github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2/transport" "golang.org/x/crypto/ssh" ) var Version string const usage = `Usage: ssh-tpm-keygen ssh-tpm-keygen --wrap keyfile --wrap-with keyfile ssh-tpm-keygen --import keyfile ssh-tpm-keygen --print-pubkey keyfile ssh-tpm-keygen --supported ssh-tpm-keygen -p [-f keyfile] [-N new_passphrase] [-P old_passphrase] ssh-tpm-keygen -A [-f path] [--hierarchy hierarchy] Options: -o, --owner-password Ask for the owner password. -C Provide a comment with the key. -f Output keyfile. -N passphrase for the key. -t ecdsa | rsa Specify the type of key to create. Defaults to ecdsa -b bits Number of bits in the key to create. rsa: 2048 (default) ecdsa: 256 (default) | 384 | 521 -p Change keyfile passphrase -P Old passphrase for keyfile -I, --import PATH Import existing key into ssh-tpm-agent. -A Generate host keys for all key types (rsa and ecdsa). --hierarchy HIERARCHY Hierarchy to create the persistent public key under. Only useable with -A. owner, o (default) endorsement, e null, n platform, p --parent-handle Parent for the TPM key. Can be a hierarchy or a persistent handle. owner, o (default) endorsement, e null, n platform, p --print-pubkey Print the public key given a TPM private key. --supported List the supported keys of the TPM. --wrap PATH A SSH key to wrap for import on remote machine. --wrap-with PATH Parent key to wrap the SSH key with. Generate new TPM sealed keys for ssh-tpm-agent. TPM sealed keys are private keys created inside the Trusted Platform Module (TPM) and sealed in .tpm suffixed files. They are bound to the hardware they where produced on and can't be transferred to other machines. Example: $ ssh-tpm-keygen Generating a sealed public/private ecdsa key pair. Enter file in which to save the key (/home/user/.ssh/id_ecdsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/user/.ssh/id_ecdsa.tpm Your public key has been saved in /home/user/.ssh/id_ecdsa.pub The key fingerprint is: SHA256:NCMJJ2La+q5tGcngQUQvEOJP3gPH8bMP98wJOEMV564 The key's randomart image is the color of television, tuned to a dead channel.` func getPin() ([]byte, error) { for { pin1, err := askpass.ReadPassphrase("Enter passphrase (empty for no passphrase): ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE) if err != nil { return nil, err } pin2, err := askpass.ReadPassphrase("Enter same passphrase again: ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE) if err != nil { return nil, err } if !bytes.Equal(pin1, pin2) { fmt.Println("Passphrases do not match. Try again.") continue } return pin1, nil } } func getOwnerPassword() ([]byte, error) { return askpass.ReadPassphrase("Enter owner password: ", askpass.RP_ALLOW_STDIN) } func doHostKeys(tpm transport.TPMCloser, outputFile string, ownerPassword []byte, hierarchy string) { // Mimics the `ssh-keygen -A -f ./something` behaviour outputPath := "/etc/ssh" if outputFile != "" { outputPath = path.Join(outputFile, outputPath) } for n, t := range map[string]struct { alg tpm2.TPMAlgID bits int }{ "rsa": {alg: tpm2.TPMAlgRSA, bits: 2048}, "ecdsa": {alg: tpm2.TPMAlgECC, bits: 256}, } { filename := fmt.Sprintf("ssh_tpm_host_%s_key", n) privatekeyFilename := path.Join(outputPath, filename+".tpm") pubkeyFilename := path.Join(outputPath, filename+".pub") if utils.FileExists(privatekeyFilename) || utils.FileExists(pubkeyFilename) { continue } if hierarchy != "" { slog.Info("Generating new hierarcy host key", slog.String("algorithm", strings.ToUpper(n)), slog.String("hierarchy", hierarchy)) h, err := utils.GetParentHandle(hierarchy) if err != nil { log.Fatal(err) } hkey, err := key.CreateHierarchyKey(tpm, t.alg, h, defaultComment()) if err != nil { log.Fatal(err) } if err := os.WriteFile(pubkeyFilename, hkey.AuthorizedKey(), 0o600); err != nil { log.Fatal(err) } slog.Info("Wrote public key", slog.String("filename", pubkeyFilename)) } else { slog.Info("Generating new host key", slog.String("algorithm", strings.ToUpper(n))) k, err := keyfile.NewLoadableKey(tpm, t.alg, t.bits, ownerPassword, keyfile.WithDescription(defaultComment()), ) if err != nil { log.Fatal(err) } sshkey, err := key.WrapTPMKey(k) if err != nil { log.Fatal(err) } if err := os.WriteFile(pubkeyFilename, sshkey.AuthorizedKey(), 0o600); err != nil { log.Fatal(err) } if err := os.WriteFile(privatekeyFilename, sshkey.Bytes(), 0o600); err != nil { log.Fatal(err) } slog.Info("Wrote private key", slog.String("filename", privatekeyFilename)) } } } func doChangePin(tpm transport.TPMCloser, passphrase, keyPin, ownerPassword []byte, filename, outputFile string) error { filename = filename + ".tpm" if outputFile != "" { filename = outputFile } b, err := os.ReadFile(filename) if err != nil { return err } k, err := key.Decode(b) if err != nil { return err } if k.Description != "" { fmt.Printf("Key has comment '%s'\n", k.Description) } if outputFile == "" { f, err := askpass.ReadPassphrase(fmt.Sprintf("Enter file in which the key is (%s): ", filename), askpass.RP_ALLOW_STDIN|askpass.RPP_ECHO_ON) if err != nil { return err } filename = string(f) } if len(passphrase) == 0 { passphrase, err = askpass.ReadPassphrase("Enter old passphrase: ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE) if err != nil { return err } } if len(keyPin) == 0 { keyPin, err = askpass.ReadPassphrase("Enter new passphrase (empty for no passphrase): ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE) if err != nil { return err } newPin, err := askpass.ReadPassphrase("Enter same passphrase: ", askpass.RP_ALLOW_STDIN) if err != nil { return err } if !bytes.Equal(keyPin, newPin) { log.Fatal("Passphrases do not match. Try again.") } fmt.Println() } if err := keyfile.ChangeAuth(tpm, ownerPassword, k.TPMKey, keyPin, passphrase); err != nil { log.Fatal("Failed changing passphrase on the key.") } if err := os.WriteFile(filename, k.Bytes(), 0o600); err != nil { return err } fmt.Println("Your identification has been saved with the new passphrase.") return nil } func doWrapWith(supportedECCBitsizes []int, wrap, wrapWith string, keyParentHandle tpm2.TPMHandle, comment, outputFile string) { pem, err := os.ReadFile(wrap) if err != nil { log.Fatal(err) } wrapperFile, err := os.ReadFile(wrapWith) if err != nil { log.Fatal(err) } parentPublic, err := tpmpkix.ToTPMPublic(wrapperFile) if err != nil { log.Fatalf("wrapper-with does not contain a valid parent TPMTPublic: %v", err) } fmt.Println("Wrapping an existing public/private ecdsa key pair for import.") var kerr *ssh.PassphraseMissingError var rawKey any rawKey, err = ssh.ParseRawPrivateKey(pem) if errors.As(err, &kerr) { for { pin, err := askpass.ReadPassphrase("Enter existing passphrase (empty for no passphrase): ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE) if err != nil { log.Fatal(err) } rawKey, err = ssh.ParseRawPrivateKeyWithPassphrase(pem, pin) if err == nil { break } else if errors.Is(err, x509.IncorrectPasswordError) { fmt.Println("Wrong passphrase, try again.") continue } else { log.Fatal(err) } } } // Because go-tpm-keyfiles expects a pointer at some point we deserialize the pointer var pk crypto.PrivateKey switch key := rawKey.(type) { case *ecdsa.PrivateKey: if !slices.Contains(supportedECCBitsizes, key.Params().BitSize) { log.Fatalf("invalid ecdsa key length: TPM does not support %v bits", key.Params().BitSize) } pk = *key case *rsa.PrivateKey: if key.N.BitLen() != 2048 { log.Fatal("can only support 2048 bit RSA") } pk = *key default: log.Fatal("unsupported key type") } k, err := keyfile.NewImportablekey(parentPublic, pk, keyfile.WithDescription(comment), keyfile.WithParent(keyParentHandle), ) if err != nil { log.Fatal(err) } privatekeyFilename := outputFile + ".tpm" pubkeyFilename := outputFile + ".pub" if err := os.WriteFile(privatekeyFilename, k.Bytes(), 0o600); err != nil { log.Fatal(err) } // Write out the public key sshkey, err := key.WrapTPMKey(k) if err != nil { log.Fatal(err) } if err := os.WriteFile(pubkeyFilename, sshkey.AuthorizedKey(), 0o600); err != nil { log.Fatal(err) } } func defaultComment() string { cache.Do(func() { cache.u, cache.err = user.Current() if cache.err != nil { return } cache.host, cache.err = os.Hostname() }) if cache.err != nil { slog.Error(cache.err.Error()) os.Exit(1) return "" } return cache.u.Username + "@" + cache.host } var cache struct { sync.Once u *user.User host string err error } var eccBitCache struct { sync.Once bits []int } func supportedECCBitsizes(tpm transport.TPMCloser) []int { eccBitCache.Do(func() { eccBitCache.bits = keyfile.SupportedECCAlgorithms(tpm) }) return eccBitCache.bits } func checkFile(f string) error { if utils.FileExists(f) { fmt.Printf("%s already exists.\n", f) s, err := askpass.ReadPassphrase("Overwrite (y/n)? ", askpass.RP_ALLOW_STDIN|askpass.RPP_ECHO_ON) if err != nil { return err } if !bytes.Equal(s, []byte("y")) { return nil } } return nil } func doImportKey(tpm transport.TPMCloser, keyParentHandle tpm2.TPMHandle, ownerPassword []byte, keyPin string, pem []byte, filename string) (*key.SSHTPMKey, error) { var toImportKey any var kerr *ssh.PassphraseMissingError var rawKey any var err error rawKey, err = ssh.ParseRawPrivateKey(pem) if errors.As(err, &kerr) { for { pin, err := askpass.ReadPassphrase("Enter existing passphrase (empty for no passphrase): ", askpass.RP_ALLOW_STDIN|askpass.RP_NEWLINE) if err != nil { return nil, err } rawKey, err = ssh.ParseRawPrivateKeyWithPassphrase(pem, pin) if err == nil { break } else if errors.Is(err, x509.IncorrectPasswordError) { fmt.Println("Wrong passphrase, try again.") continue } else { return nil, err } } } switch key := rawKey.(type) { case *ecdsa.PrivateKey: toImportKey = *key if !slices.Contains(supportedECCBitsizes(tpm), key.Params().BitSize) { log.Fatalf("invalid ecdsa key length: TPM does not support %v bits", key.Params().BitSize) } case *rsa.PrivateKey: if key.N.BitLen() != 2048 { log.Fatal("can only support 2048 bit RSA") } toImportKey = *key default: log.Fatal("unsupported key type") } pubPem, err := os.ReadFile(filename + ".pub") if err != nil { log.Fatalf("can't find corresponding public key: %v", err) } _, comment, _, _, err := ssh.ParseAuthorizedKey(pubPem) if err != nil { log.Fatal("can't parse public key", err) } var pin []byte if keyPin != "" { pin = []byte(keyPin) } else { pinInput, err := getPin() if err != nil { log.Fatal(err) } pin = []byte(pinInput) } k, err := key.NewImportedSSHTPMKey(tpm, toImportKey, ownerPassword, keyfile.WithParent(keyParentHandle), keyfile.WithUserAuth(pin), keyfile.WithDescription(comment)) if err != nil { return nil, err } return k, nil } func doImportWrappedKey(tpm transport.TPMCloser, ownerPassword, pem []byte) (*key.SSHTPMKey, error) { tpmkey, err := keyfile.Decode(pem) if errors.Is(err, keyfile.ErrNotTPMKey) { log.Fatal("This shouldnt happen") } tkey, err := keyfile.ImportTPMKey(tpm, tpmkey, ownerPassword) if err != nil { return nil, err } k, err := key.WrapTPMKey(tkey) if err != nil { return nil, err } return k, nil } func doCreateSSHKey(tpm transport.TPMCloser, ownerPassword []byte, keyPin string, keyParentHandle tpm2.TPMHandle, keyType string, comment string, bits int) (*key.SSHTPMKey, error) { var tpmkeyType tpm2.TPMAlgID switch keyType { case "ecdsa": tpmkeyType = tpm2.TPMAlgECC case "rsa": tpmkeyType = tpm2.TPMAlgRSA } var pin []byte if keyPin != "" { pin = []byte(keyPin) } else { pinInput, err := getPin() if err != nil { log.Fatal(err) } pin = []byte(pinInput) } k, err := key.NewSSHTPMKey(tpm, tpmkeyType, bits, ownerPassword, keyfile.WithParent(keyParentHandle), keyfile.WithUserAuth(pin), keyfile.WithDescription(comment), ) if err != nil { return nil, err } return k, nil } func main() { flag.Usage = func() { fmt.Println(usage) } var ( askOwnerPassword bool comment, outputFile, keyPin, passphrase string keyType, importKey string bits int swtpmFlag, hostKeys, changePin bool listsupported bool printPubkey string parentHandle, wrap, wrapWith string hierarchy string ) flag.BoolVar(&askOwnerPassword, "o", false, "ask for the owner password") flag.BoolVar(&askOwnerPassword, "owner-password", false, "ask for the owner password") flag.StringVar(&comment, "C", defaultComment(), "provide a comment, default to user@host") flag.StringVar(&outputFile, "f", "", "output keyfile") flag.StringVar(&keyPin, "N", "", "new passphrase for the key") flag.StringVar(&keyType, "t", "ecdsa", "key to create") flag.IntVar(&bits, "b", 0, "number of bits") flag.StringVar(&importKey, "I", "", "import key") flag.StringVar(&importKey, "import", "", "import key") flag.BoolVar(&changePin, "p", false, "change passphrase") flag.StringVar(&passphrase, "P", "", "old passphrase") flag.BoolVar(&swtpmFlag, "swtpm", false, "use swtpm instead of actual tpm") flag.BoolVar(&hostKeys, "A", false, "generate host keys") flag.BoolVar(&listsupported, "supported", false, "list tpm caps") flag.StringVar(&printPubkey, "print-pubkey", "", "print tpm pubkey") flag.StringVar(&wrap, "wrap", "", "wrap key") flag.StringVar(&wrapWith, "wrap-with", "", "wrap with key") flag.StringVar(&parentHandle, "parent-handle", "owner", "parent handle for the key") flag.StringVar(&hierarchy, "hierarchy", "", "hierarchy for the created key") flag.Parse() tpm, err := utils.TPM(swtpmFlag) if err != nil { log.Fatal(err) } defer tpm.Close() if bits == 0 { if keyType == "ecdsa" { bits = 256 } if keyType == "rsa" { bits = 2048 } } supportedECCBitsizes := keyfile.SupportedECCAlgorithms(tpm) if printPubkey != "" { f, err := os.ReadFile(printPubkey) if err != nil { log.Fatalf("failed reading TPM key %s: %v", printPubkey, err) } k, err := key.Decode(f) if err != nil { log.Fatal(err) } fmt.Print(string(k.AuthorizedKey())) os.Exit(0) } if listsupported { fmt.Printf("ecdsa bit lengths:") for _, alg := range supportedECCBitsizes { fmt.Printf(" %d", alg) } fmt.Println("\nrsa bit lengths: 2048") os.Exit(0) } // Ask for owner password var ownerPassword []byte if askOwnerPassword { ownerPassword, err = getOwnerPassword() if err != nil { log.Fatal(err) } } else { ownerPassword = []byte("") } // Generate host keys if hostKeys { doHostKeys(tpm, outputFile, ownerPassword, hierarchy) os.Exit(0) } var filename string var privatekeyFilename string var pubkeyFilename string // TODO: Support custom handles var keyParentHandle tpm2.TPMHandle if parentHandle != "" { keyParentHandle, err = utils.GetParentHandle(parentHandle) if err != nil { log.Fatal(err) } } // Create ~/.ssh if it doesn't exist if !utils.FileExists(utils.SSHDir()) { if err := os.Mkdir(utils.SSHDir(), 0o700); err != nil { log.Fatalf("Could not create directory %s", utils.SSHDir()) os.Exit(1) } } // Wrapping of keyfile for import if wrap != "" { if wrapWith == "" { log.Fatal("--wrap needs --wrap-with") } if outputFile == "" { log.Fatal("Specify output filename with --output/-o") } doWrapWith(supportedECCBitsizes, wrap, wrapWith, keyParentHandle, comment, outputFile) os.Exit(0) } switch keyType { case "ecdsa": filename = "id_ecdsa" if !slices.Contains(supportedECCBitsizes, bits) { log.Fatalf("invalid ecdsa key length: TPM does not support %v bits", bits) } case "rsa": filename = "id_rsa" } if outputFile != "" { filename = outputFile } else { filename = path.Join(utils.SSHDir(), filename) } if changePin { if err := doChangePin(tpm, []byte(passphrase), []byte(keyPin), ownerPassword, filename, outputFile); err != nil { log.Fatal(err) } os.Exit(0) } // If we don't need to write a public key var writePubKey bool var k *key.SSHTPMKey if importKey != "" { pem, err := os.ReadFile(importKey) if err != nil { log.Fatal(err) } if _, err := keyfile.Decode(pem); !errors.Is(err, keyfile.ErrNotTPMKey) { fmt.Println("Importing a wrapped public/private key pair.") k, err = doImportWrappedKey(tpm, ownerPassword, pem) if err != nil { log.Fatal(err) } writePubKey = true } else { // Import a ssh key if it's not a TPM key fmt.Println("Sealing an existing public/private key pair.") k, err = doImportKey(tpm, keyParentHandle, ownerPassword, keyPin, pem, importKey) if err != nil { log.Fatal(err) } writePubKey = false } } else { // Else create a normal key fmt.Printf("Generating a sealed public/private %s key pair.\n", keyType) if outputFile == "" { f, err := askpass.ReadPassphrase(fmt.Sprintf("Enter file in which to save the key (%s): ", filename), askpass.RP_ALLOW_STDIN|askpass.RPP_ECHO_ON) if err != nil { log.Fatal(err) } filenameInput := string(f) if filenameInput != "" { filename = strings.TrimSuffix(filenameInput, ".tpm") } } else { filename = outputFile } k, err = doCreateSSHKey(tpm, ownerPassword, keyPin, keyParentHandle, keyType, comment, bits) if err != nil { log.Fatal(err) } writePubKey = true } privatekeyFilename = filename + ".tpm" if err := checkFile(privatekeyFilename); err != nil { log.Fatal(err) } pubkeyFilename = filename + ".pub" if err := checkFile(pubkeyFilename); err != nil { log.Fatal(err) } if err := os.WriteFile(privatekeyFilename, k.Bytes(), 0o600); err != nil { log.Fatal(err) } fmt.Printf("Your identification has been saved in %s\n", privatekeyFilename) if writePubKey { if err := os.WriteFile(pubkeyFilename, k.AuthorizedKey(), 0o600); err != nil { log.Fatal(err) } fmt.Printf("Your public key has been saved in %s\n", pubkeyFilename) } fmt.Printf("The key fingerprint is:\n") fmt.Println(k.Fingerprint()) fmt.Println("The key's randomart image is the color of television, tuned to a dead channel.") } ssh-tpm-agent-0.8.0/cmd/ssh-tpm-keygen/testdata/000077500000000000000000000000001511154401300214305ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/cmd/ssh-tpm-keygen/testdata/script/000077500000000000000000000000001511154401300227345ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/cmd/ssh-tpm-keygen/testdata/script/keygen.txt000066400000000000000000000026211511154401300247600ustar00rootroot00000000000000# Check we can create ecdsa keys exec ssh-tpm-keygen exists .ssh/id_ecdsa.pub exists .ssh/id_ecdsa.tpm rm .ssh # Check that we can create RSA keys exec ssh-tpm-keygen -t rsa exists .ssh/id_rsa.pub exists .ssh/id_rsa.tpm rm .ssh # Check if we can give it a new name stdin save_name.txt exec ssh-tpm-keygen exists .ssh/new_name.tpm exists .ssh/new_name.pub rm .ssh # Change passphrase exec ssh-tpm-keygen -N 1234 exec ssh-tpm-keygen -p -N 1234 -P 12345 -f .ssh/id_ecdsa.tpm stdout 'new passphrase' rm .ssh # Create ssh key and import as TSS keys exec ssh-keygen -t ecdsa -f id_ecdsa -N '' exec ssh-tpm-keygen --import id_ecdsa -f id_ecdsa_tpm exists id_ecdsa exists id_ecdsa.pub exists id_ecdsa_tpm.tpm # Wrap a key with an EK and import the key getekcert exists srk.pem exec ssh-keygen -t ecdsa -b 256 -N '' -f ./ecdsa.key exec ssh-tpm-keygen --wrap-with srk.pem --wrap ecdsa.key -f wrapped_id_ecdsa exec ssh-tpm-keygen --import ./wrapped_id_ecdsa.tpm -f unwrapped_id_ecdsa exists unwrapped_id_ecdsa.tpm # Create hostkeys exec mkdir -p test/etc/ssh exec ssh-tpm-keygen -A -f test exists test/etc/ssh/ssh_tpm_host_rsa_key.tpm exists test/etc/ssh/ssh_tpm_host_ecdsa_key.tpm rm test # Create hierarchy hostkeys exec mkdir -p test/etc/ssh exec ssh-tpm-keygen -A -f test --hierarchy owner exists test/etc/ssh/ssh_tpm_host_rsa_key.pub exists test/etc/ssh/ssh_tpm_host_ecdsa_key.pub rm test -- save_name.txt -- .ssh/new_name ssh-tpm-agent-0.8.0/contrib/000077500000000000000000000000001511154401300156415ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/contrib/contrib.go000066400000000000000000000011661511154401300176340ustar00rootroot00000000000000package contrib import ( "embed" "path" ) //go:embed services/* var services embed.FS //go:embed sshd/* var sshd embed.FS func readPath(f embed.FS, s string) map[string][]byte { ret := map[string][]byte{} files, _ := f.ReadDir(s) for _, file := range files { b, _ := f.ReadFile(path.Join(s, file.Name())) ret[file.Name()] = b } return ret } func EmbeddedUserServices() map[string][]byte { return readPath(services, "services/user") } func EmbeddedSystemServices() map[string][]byte { return readPath(services, "services/system") } func EmbeddedSshdConfig() map[string][]byte { return readPath(sshd, "sshd") } ssh-tpm-agent-0.8.0/contrib/contrib_test.go000066400000000000000000000006601511154401300206710ustar00rootroot00000000000000package contrib import ( "testing" ) func TestUserServices(t *testing.T) { m := EmbeddedUserServices() if len(m) != 2 { t.Fatalf("invalid number of entries") } } func TestSystemServices(t *testing.T) { m := EmbeddedSystemServices() if len(m) != 3 { t.Fatalf("invalid number of entries") } } func TestSshdConfig(t *testing.T) { m := EmbeddedSshdConfig() if len(m) != 1 { t.Fatalf("invalid number of entries") } } ssh-tpm-agent-0.8.0/contrib/services/000077500000000000000000000000001511154401300174645ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/contrib/services/hierarchy_keys/000077500000000000000000000000001511154401300224755ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/contrib/services/hierarchy_keys/ssh-tpm-agent@.service000066400000000000000000000007361511154401300266540ustar00rootroot00000000000000[Unit] ConditionEnvironment=!SSH_AGENT_PID Description=ssh-tpm-agent service Documentation=man:ssh-agent(1) man:ssh-add(1) man:ssh(1) Wants=ssh-tpm-genkeys@%i.service After=ssh-tpm-genkeys@%i.service After=network.target After=sshd.target Requires=ssh-tpm-agent@%i.socket [Service] ExecStart=/usr/bin/ssh-tpm-agent --key-dir /etc/ssh --hierarchy %i PassEnvironment=SSH_AGENT_PID KillMode=process Restart=always [Install] WantedBy=multi-user.target DefaultInstance=endorsement ssh-tpm-agent-0.8.0/contrib/services/hierarchy_keys/ssh-tpm-agent@.socket000066400000000000000000000004041511154401300264740ustar00rootroot00000000000000[Unit] Description=SSH TPM agent socket Documentation=man:ssh-agent(1) man:ssh-add(1) man:ssh(1) [Socket] ListenStream=/var/tmp/ssh-tpm-agent.sock SocketMode=0600 Service=ssh-tpm-agent@%i.service [Install] WantedBy=sockets.target DefaultInstance=endorsement ssh-tpm-agent-0.8.0/contrib/services/hierarchy_keys/ssh-tpm-hierarchy-genkeys.service000066400000000000000000000003741511154401300310750ustar00rootroot00000000000000[Unit] Description=SSH TPM Key Generation ConditionPathExists=|!/etc/ssh/ssh_tpm_host_ecdsa_key.pub ConditionPathExists=|!/etc/ssh/ssh_tpm_host_rsa_key.pub [Service] ExecStart=/usr/bin/ssh-tpm-keygen -A --hierarchy %i Type=oneshot RemainAfterExit=yes ssh-tpm-agent-0.8.0/contrib/services/system/000077500000000000000000000000001511154401300210105ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/contrib/services/system/ssh-tpm-agent.service000066400000000000000000000006521511154401300250640ustar00rootroot00000000000000[Unit] ConditionEnvironment=!SSH_AGENT_PID Description=ssh-tpm-agent service Documentation=man:ssh-agent(1) man:ssh-add(1) man:ssh(1) Wants=ssh-tpm-genkeys.service After=ssh-tpm-genkeys.service After=network.target After=sshd.target Requires=ssh-tpm-agent.socket [Service] ExecStart=/usr/bin/ssh-tpm-agent --key-dir /etc/ssh PassEnvironment=SSH_AGENT_PID KillMode=process Restart=always [Install] WantedBy=multi-user.target ssh-tpm-agent-0.8.0/contrib/services/system/ssh-tpm-agent.socket000066400000000000000000000003451511154401300247130ustar00rootroot00000000000000[Unit] Description=SSH TPM agent socket Documentation=man:ssh-agent(1) man:ssh-add(1) man:ssh(1) [Socket] ListenStream=/var/tmp/ssh-tpm-agent.sock SocketMode=0600 Service=ssh-tpm-agent.service [Install] WantedBy=sockets.target ssh-tpm-agent-0.8.0/contrib/services/system/ssh-tpm-genkeys.service000066400000000000000000000005371511154401300254350ustar00rootroot00000000000000[Unit] Description=SSH TPM Key Generation ConditionPathExists=|!/etc/ssh/ssh_tpm_host_ecdsa_key.tpm ConditionPathExists=|!/etc/ssh/ssh_tpm_host_ecdsa_key.pub ConditionPathExists=|!/etc/ssh/ssh_tpm_host_rsa_key.tpm ConditionPathExists=|!/etc/ssh/ssh_tpm_host_rsa_key.pub [Service] ExecStart=/usr/bin/ssh-tpm-keygen -A Type=oneshot RemainAfterExit=yes ssh-tpm-agent-0.8.0/contrib/services/user/000077500000000000000000000000001511154401300204425ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/contrib/services/user/ssh-tpm-agent.service000066400000000000000000000005321511154401300245130ustar00rootroot00000000000000[Unit] ConditionEnvironment=!SSH_AGENT_PID Description=ssh-tpm-agent service Documentation=man:ssh-agent(1) man:ssh-add(1) man:ssh(1) Requires=ssh-tpm-agent.socket [Service] Environment=SSH_TPM_AUTH_SOCK=%t/ssh-tpm-agent.sock ExecStart={{.GoBinary}} PassEnvironment=SSH_AGENT_PID SuccessExitStatus=2 Type=simple [Install] Also=ssh-agent.socket ssh-tpm-agent-0.8.0/contrib/services/user/ssh-tpm-agent.socket000066400000000000000000000003371511154401300243460ustar00rootroot00000000000000[Unit] Description=SSH TPM agent socket Documentation=man:ssh-agent(1) man:ssh-add(1) man:ssh(1) [Socket] ListenStream=%t/ssh-tpm-agent.sock SocketMode=0600 Service=ssh-tpm-agent.service [Install] WantedBy=sockets.target ssh-tpm-agent-0.8.0/contrib/sshd/000077500000000000000000000000001511154401300166025ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/contrib/sshd/10-ssh-tpm-agent.conf000066400000000000000000000002451511154401300223570ustar00rootroot00000000000000# This enables TPM sealed host keys HostKeyAgent /var/tmp/ssh-tpm-agent.sock HostKey /etc/ssh/ssh_tpm_host_ecdsa_key.pub HostKey /etc/ssh/ssh_tpm_host_rsa_key.pub ssh-tpm-agent-0.8.0/go.mod000066400000000000000000000021441511154401300153100ustar00rootroot00000000000000module github.com/foxboron/ssh-tpm-agent go 1.23.0 toolchain go1.24.0 require ( github.com/awnumar/memcall v0.4.0 github.com/foxboron/go-tpm-keyfiles v0.0.0-20250318194951-cba49fbf70fa github.com/foxboron/ssh-tpm-ca-authority v0.0.0-20240831163633-e92b30331d2d github.com/google/go-tpm v0.9.3 github.com/google/go-tpm-tools v0.4.4 github.com/landlock-lsm/go-landlock v0.0.0-20241014143150-479ddab4c04c github.com/rogpeppe/go-internal v1.13.1 golang.org/x/crypto v0.36.0 golang.org/x/sys v0.31.0 golang.org/x/term v0.30.0 golang.org/x/text v0.23.0 ) require ( github.com/coreos/go-oidc/v3 v3.12.0 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/segmentio/ksuid v1.0.4 // indirect github.com/sigstore/sigstore v1.8.15 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect golang.org/x/oauth2 v0.26.0 // indirect golang.org/x/tools v0.22.0 // indirect kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 // indirect ) ssh-tpm-agent-0.8.0/go.sum000066400000000000000000000272501511154401300153420ustar00rootroot00000000000000github.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.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo= github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/foxboron/go-tpm-keyfiles v0.0.0-20250318194951-cba49fbf70fa h1:2wXSGCPVpdFEi+SjcY1+SY0A0a1nbFzz/3HYuirLpX0= github.com/foxboron/go-tpm-keyfiles v0.0.0-20250318194951-cba49fbf70fa/go.mod h1:uAyTlAUxchYuiFjTHmuIEJ4nGSm7iOPaGcAyA81fJ80= github.com/foxboron/ssh-tpm-ca-authority v0.0.0-20240831163633-e92b30331d2d h1:hz0L1k0eZgHkJIgFj3Uyd0LSn7UXIwWJq9Xjj/8iGJM= github.com/foxboron/ssh-tpm-ca-authority v0.0.0-20240831163633-e92b30331d2d/go.mod h1:7BQDgpVVyISJ9W4O52KCbdBgQuujnZG2Ytuep1ya5NE= github.com/foxboron/swtpm_test v0.0.0-20230726224112-46aaafdf7006 h1:50sW4r0PcvlpG4PV8tYh2RVCapszJgaOLRCS2subvV4= github.com/foxboron/swtpm_test v0.0.0-20230726224112-46aaafdf7006/go.mod h1:eIXCMsMYCaqq9m1KSSxXwQG11krpuNPGP3k0uaWrbas= github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-configfs-tsm v0.2.2 h1:YnJ9rXIOj5BYD7/0DNnzs8AOp7UcvjfTvt215EWcs98= github.com/google/go-configfs-tsm v0.2.2/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo= github.com/google/go-sev-guest v0.9.3 h1:GOJ+EipURdeWFl/YYdgcCxyPeMgQUWlI056iFkBD8UU= github.com/google/go-sev-guest v0.9.3/go.mod h1:hc1R4R6f8+NcJwITs0L90fYWTsBpd1Ix+Gur15sqHDs= github.com/google/go-tdx-guest v0.3.1 h1:gl0KvjdsD4RrJzyLefDOvFOUH3NAJri/3qvaL5m83Iw= github.com/google/go-tdx-guest v0.3.1/go.mod h1:/rc3d7rnPykOPuY8U9saMyEps0PZDThLk/RygXm04nE= github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc= github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98= github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY= github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ= github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/landlock-lsm/go-landlock v0.0.0-20241014143150-479ddab4c04c h1:nwPp7v5drD5P9tUDUGF5P6Mrg7qb/oCEJBmmjlMIwG0= github.com/landlock-lsm/go-landlock v0.0.0-20241014143150-479ddab4c04c/go.mod h1:RSub3ourNF8Hf+swvw49Catm3s7HVf4hzdFxDUnEzdA= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/sigstore/sigstore v1.8.15 h1:9HHnZmxjPQSTPXTCZc25HDxxSTWwsGMh/ZhWZZ39maU= github.com/sigstore/sigstore v1.8.15/go.mod h1:+Wa5mrG6A+Gss516YC9owy10q3IazqIRe0y1EoQRHHM= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 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/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 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-20210616045830-e2b7044e8c71/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.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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 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/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 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/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI= kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= ssh-tpm-agent-0.8.0/internal/000077500000000000000000000000001511154401300160155ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/internal/keyring/000077500000000000000000000000001511154401300174655ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/internal/keyring/key.go000066400000000000000000000013711511154401300206060ustar00rootroot00000000000000package keyring import ( "github.com/awnumar/memcall" "golang.org/x/sys/unix" ) // Key is a boxed byte slice where we allocate the underlying memory with memcall type Key struct { b []byte } func (k *Key) Read() []byte { if k == nil { return []byte{} } return k.b } func (k *Key) Free() error { return memcall.Free(k.b) } func ReadKeyIntoMemory(id int) (*Key, error) { sz, err := unix.KeyctlBuffer(unix.KEYCTL_READ, int(id), nil, 0) if err != nil { return nil, err } buffer, err := memcall.Alloc(sz) if err != nil { return nil, err } if _, err = unix.KeyctlBuffer(unix.KEYCTL_READ, int(id), buffer, 0); err != nil { return nil, err } if err := memcall.Lock(buffer); err != nil { return nil, err } return &Key{buffer}, nil } ssh-tpm-agent-0.8.0/internal/keyring/keyring.go000066400000000000000000000023021511154401300214610ustar00rootroot00000000000000package keyring import ( "fmt" "log/slog" "golang.org/x/sys/unix" ) var ( SessionKeyring *Keyring = &Keyring{ringid: unix.KEY_SPEC_SESSION_KEYRING} ) type Keyring struct { ringid int } func (ring *Keyring) CreateKeyring() (*Keyring, error) { id, err := unix.KeyctlJoinSessionKeyring("ssh-tpm-agent") if err != nil { return nil, err } return &Keyring{ringid: id}, nil } func (k *Keyring) AddKey(name string, b []byte) error { slog.Debug("addkey", slog.String("name", name)) _, err := unix.AddKey("user", name, b, k.ringid) if err != nil { return fmt.Errorf("failed add-key: %v", err) } return nil } func (k *Keyring) ReadKey(name string) (*Key, error) { slog.Debug("readkey", slog.String("name", name)) id, err := unix.RequestKey("user", name, "", k.ringid) if err != nil { return nil, err } b, err := ReadKeyIntoMemory(id) if err != nil { return nil, err } return b, err } func (k *Keyring) RemoveKey(name string) error { slog.Debug("removekey", slog.String("name", name)) id, err := unix.RequestKey("user", name, "", k.ringid) if err != nil { return fmt.Errorf("failed remove-key: %v", err) } _, err = unix.KeyctlInt(unix.KEYCTL_UNLINK, id, k.ringid, 0, 0) return err } ssh-tpm-agent-0.8.0/internal/keyring/keyring_test.go000066400000000000000000000025561511154401300225330ustar00rootroot00000000000000package keyring import ( "bytes" "errors" "syscall" "testing" ) func TestSaveandGetData(t *testing.T) { keyring, err := SessionKeyring.CreateKeyring() if err != nil { t.Fatalf("failed getting keyring: %v", err) } b := []byte("test string") if err := keyring.AddKey("test", b); err != nil { t.Fatalf("err: %v", err) } bb, err := keyring.ReadKey("test") if err != nil { t.Fatalf("err: %v", err) } if !bytes.Equal(b, bb.Read()) { t.Fatalf("strings not equal") } } func TestNokey(t *testing.T) { keyring, err := SessionKeyring.CreateKeyring() if err != nil { t.Fatalf("failed getting keyring: %v", err) } _, err = keyring.ReadKey("this.key.does.not.exist") if !errors.Is(err, syscall.ENOKEY) { t.Fatalf("err: %v", err) } } func TestRemoveKey(t *testing.T) { keyring, err := SessionKeyring.CreateKeyring() if err != nil { t.Fatalf("failed getting keyring: %v", err) } b := []byte("test string") if err := keyring.AddKey("test-2", b); err != nil { t.Fatalf("err: %v", err) } bb, err := keyring.ReadKey("test-2") if err != nil { t.Fatalf("err: %v", err) } if !bytes.Equal(b, bb.Read()) { t.Fatalf("strings not equal") } if err = keyring.RemoveKey("test-2"); err != nil { t.Fatalf("failed removing key: %v", err) } _, err = keyring.ReadKey("test-2") if !errors.Is(err, syscall.ENOKEY) { t.Fatalf("we can still read the key") } } ssh-tpm-agent-0.8.0/internal/keyring/threadkeyring.go000066400000000000000000000033241511154401300226560ustar00rootroot00000000000000package keyring import ( "context" "runtime" "sync" ) // ThreadKeyring runs Keyring in a dedicated OS Thread type ThreadKeyring struct { wg sync.WaitGroup addkey chan *addkeyMsg removekey chan *removekeyMsg readkey chan *readkeyMsg } type addkeyMsg struct { name string key []byte cb chan error } type removekeyMsg struct { name string cb chan error } type readkeyRet struct { key *Key err error } type readkeyMsg struct { name string cb chan *readkeyRet } func (tk *ThreadKeyring) Wait() { tk.wg.Wait() } func (tk *ThreadKeyring) AddKey(name string, key []byte) error { cb := make(chan error) tk.addkey <- &addkeyMsg{name, key, cb} return <-cb } func (tk *ThreadKeyring) RemoveKey(name string) error { cb := make(chan error) tk.removekey <- &removekeyMsg{name, cb} return <-cb } func (tk *ThreadKeyring) ReadKey(name string) (*Key, error) { cb := make(chan *readkeyRet) tk.readkey <- &readkeyMsg{name, cb} ret := <-cb if ret.err != nil { return nil, ret.err } return ret.key, nil } func NewThreadKeyring(ctx context.Context, keyring *Keyring) (*ThreadKeyring, error) { var err error var tk ThreadKeyring tk.addkey = make(chan *addkeyMsg) tk.removekey = make(chan *removekeyMsg) tk.readkey = make(chan *readkeyMsg) tk.wg.Add(1) go func() { var ak *Keyring runtime.LockOSThread() ak, err = keyring.CreateKeyring() if err != nil { return } for { select { case msg := <-tk.addkey: msg.cb <- ak.AddKey(msg.name, msg.key) case msg := <-tk.readkey: key, err := ak.ReadKey(msg.name) msg.cb <- &readkeyRet{key, err} case msg := <-tk.removekey: msg.cb <- ak.RemoveKey(msg.name) case <-ctx.Done(): return } } }() return &tk, err } ssh-tpm-agent-0.8.0/internal/keyring/threadkeyring_test.go000066400000000000000000000031171511154401300237150ustar00rootroot00000000000000package keyring import ( "bytes" "context" "errors" "syscall" "testing" ) var ( ctx = context.Background() ) func TestSaveandGetDataThreaded(t *testing.T) { keyring, err := NewThreadKeyring(ctx, SessionKeyring) if err != nil { t.Fatalf("failed getting keyring: %v", err) } b := []byte("test string") if err := keyring.AddKey("test", b); err != nil { t.Fatalf("err: %v", err) } bb, err := keyring.ReadKey("test") if err != nil { t.Fatalf("err: %v", err) } if !bytes.Equal(b, bb.Read()) { t.Fatalf("strings not equal") } } func TestNokeyThreaded(t *testing.T) { keyring, err := NewThreadKeyring(ctx, SessionKeyring) if err != nil { t.Fatalf("failed getting keyring: %v", err) } if err != nil { t.Fatalf("failed getting keyring: %v", err) } _, err = keyring.ReadKey("this.key.does.not.exist") if !errors.Is(err, syscall.ENOKEY) { t.Fatalf("err: %v", err) } } func TestRemoveKeyThreaded(t *testing.T) { keyring, err := NewThreadKeyring(ctx, SessionKeyring) if err != nil { t.Fatalf("failed getting keyring: %v", err) } if err != nil { t.Fatalf("failed getting keyring: %v", err) } b := []byte("test string") if err := keyring.AddKey("test-2", b); err != nil { t.Fatalf("err: %v", err) } bb, err := keyring.ReadKey("test-2") if err != nil { t.Fatalf("err: %v", err) } if !bytes.Equal(b, bb.Read()) { t.Fatalf("strings not equal") } if err = keyring.RemoveKey("test-2"); err != nil { t.Fatalf("failed removing key: %v", err) } _, err = keyring.ReadKey("test-2") if !errors.Is(err, syscall.ENOKEY) { t.Fatalf("we can still read the key") } } ssh-tpm-agent-0.8.0/internal/keytest/000077500000000000000000000000001511154401300175055ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/internal/keytest/keytest.go000066400000000000000000000066411511154401300215330ustar00rootroot00000000000000package keytest import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "net" "path" "testing" keyfile "github.com/foxboron/go-tpm-keyfiles" "github.com/foxboron/ssh-tpm-agent/agent" "github.com/foxboron/ssh-tpm-agent/internal/keyring" "github.com/foxboron/ssh-tpm-agent/key" "github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2/transport" "golang.org/x/crypto/ssh" sshagent "golang.org/x/crypto/ssh/agent" ) // Represents the type for MkKey and mkImportableKey type KeyFunc func(*testing.T, transport.TPMCloser, tpm2.TPMAlgID, int, []byte, string) (*key.SSHTPMKey, error) func MkRSA(t *testing.T, bits int) rsa.PrivateKey { t.Helper() pk, err := rsa.GenerateKey(rand.Reader, bits) if err != nil { t.Fatalf("failed to generate rsa key: %v", err) } return *pk } func MkECDSA(t *testing.T, a elliptic.Curve) ecdsa.PrivateKey { t.Helper() pk, err := ecdsa.GenerateKey(a, rand.Reader) if err != nil { t.Fatalf("failed to generate ecdsa key: %v", err) } return *pk } // Test helper for CreateKey func MkKey(t *testing.T, tpm transport.TPMCloser, keytype tpm2.TPMAlgID, bits int, pin []byte, comment string) (*key.SSHTPMKey, error) { t.Helper() return key.NewSSHTPMKey(tpm, keytype, bits, []byte(""), keyfile.WithUserAuth(pin), keyfile.WithDescription(comment), ) } func MkCertificate(t *testing.T, ca crypto.Signer) KeyFunc { t.Helper() return func(t *testing.T, tpm transport.TPMCloser, keytype tpm2.TPMAlgID, bits int, pin []byte, comment string) (*key.SSHTPMKey, error) { k, err := MkKey(t, tpm, keytype, bits, pin, comment) if err != nil { t.Fatalf("message") } signer, err := ssh.NewSignerFromKey(ca) if err != nil { t.Fatalf("unable to generate signer from key: %v", err) } mas, err := ssh.NewSignerWithAlgorithms(signer.(ssh.AlgorithmSigner), []string{ssh.KeyAlgoECDSA256}) if err != nil { t.Fatalf("unable to create signer with algorithms: %v", err) } k.Certificate = &ssh.Certificate{ Key: *k.PublicKey, CertType: ssh.UserCert, } if err := k.Certificate.SignCert(rand.Reader, mas); err != nil { t.Fatalf("unable to sign certificate: %v", err) } return k, nil } } // Helper to make an importable key func MkImportableKey(t *testing.T, tpm transport.TPMCloser, keytype tpm2.TPMAlgID, bits int, pin []byte, comment string) (*key.SSHTPMKey, error) { t.Helper() var pk any switch keytype { case tpm2.TPMAlgECC: switch bits { case 256: pk = MkECDSA(t, elliptic.P256()) case 384: pk = MkECDSA(t, elliptic.P384()) case 521: pk = MkECDSA(t, elliptic.P521()) } case tpm2.TPMAlgRSA: pk = MkRSA(t, bits) } return key.NewImportedSSHTPMKey(tpm, pk, []byte(""), keyfile.WithUserAuth(pin), keyfile.WithDescription(comment)) } // Give us some random bytes func MustRand(size int) []byte { b := make([]byte, size) if _, err := rand.Read(b); err != nil { panic(err) } return b } func NewTestAgent(t *testing.T, tpm transport.TPMCloser) *agent.Agent { unixList, err := net.ListenUnix("unix", &net.UnixAddr{Net: "unix", Name: path.Join(t.TempDir(), "socket")}) if err != nil { t.Fatalf("failed listening: %v", err) } return agent.NewAgent(unixList, []sshagent.ExtendedAgent{}, func() *keyring.ThreadKeyring { return &keyring.ThreadKeyring{} }, func() transport.TPMCloser { return tpm }, func() ([]byte, error) { return []byte(""), nil }, func(_ key.SSHTPMKeys) ([]byte, error) { return []byte(""), nil }, ) } ssh-tpm-agent-0.8.0/internal/lsm/000077500000000000000000000000001511154401300166105ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/internal/lsm/lsm.go000066400000000000000000000021501511154401300177300ustar00rootroot00000000000000package lsm import ( "log/slog" "os" "github.com/foxboron/ssh-tpm-agent/askpass" "github.com/landlock-lsm/go-landlock/landlock" ) var rules []landlock.Rule func HasLandlock() bool { _, ok := os.LookupEnv("SSH_TPM_LANDLOCK") return ok } func RestrictAdditionalPaths(r ...landlock.Rule) { rules = append(rules, r...) } func RestrictAgentFiles() { RestrictAdditionalPaths( // Probably what we need to do for most askpass binaries landlock.RWDirs( "/usr/lib", ).IgnoreIfMissing(), // Default Go paths landlock.ROFiles( "/proc/sys/net/core/somaxconn", "/etc/localtime", "/dev/null", ), // We almost always want to read the TPM landlock.RWFiles( "/dev/tpm0", "/dev/tpmrm0", ), // Ensure we can read+exec askpass binaries landlock.ROFiles( askpass.SSH_ASKPASS_DEFAULTS..., ).IgnoreIfMissing(), ) } func Restrict() error { if !HasLandlock() { return nil } slog.Debug("sandboxing with landlock") for _, r := range rules { slog.Debug("landlock", slog.Any("rule", r)) } landlock.V5.BestEffort().RestrictNet() return landlock.V5.BestEffort().RestrictPaths(rules...) } ssh-tpm-agent-0.8.0/key/000077500000000000000000000000001511154401300147715ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/key/hierarchy_keys.go000066400000000000000000000120111511154401300203240ustar00rootroot00000000000000package key import ( "errors" "fmt" keyfile "github.com/foxboron/go-tpm-keyfiles" "github.com/foxboron/ssh-tpm-agent/internal/keyring" "github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2/transport" "golang.org/x/crypto/cryptobyte" "golang.org/x/crypto/cryptobyte/asn1" ) var ( ECCSRK_H10_Template = tpm2.TPMTPublic{ Type: tpm2.TPMAlgECC, NameAlg: tpm2.TPMAlgSHA256, ObjectAttributes: tpm2.TPMAObject{ FixedTPM: true, FixedParent: true, SensitiveDataOrigin: true, UserWithAuth: true, AdminWithPolicy: false, SignEncrypt: true, Decrypt: true, }, AuthPolicy: tpm2.TPM2BDigest{ Buffer: []byte{ 0xCA, 0x3D, 0x0A, 0x99, 0xA2, 0xB9, 0x39, 0x06, 0xF7, 0xA3, 0x34, 0x24, 0x14, 0xEF, 0xCF, 0xB3, 0xA3, 0x85, 0xD4, 0x4C, 0xD1, 0xFD, 0x45, 0x90, 0x89, 0xD1, 0x9B, 0x50, 0x71, 0xC0, 0xB7, 0xA0, }, }, Parameters: tpm2.NewTPMUPublicParms( tpm2.TPMAlgECC, &tpm2.TPMSECCParms{ CurveID: tpm2.TPMECCNistP256, Scheme: tpm2.TPMTECCScheme{ Scheme: tpm2.TPMAlgNull, }, }, ), Unique: tpm2.NewTPMUPublicID( tpm2.TPMAlgECC, &tpm2.TPMSECCPoint{ X: tpm2.TPM2BECCParameter{ Buffer: make([]byte, 0), }, Y: tpm2.TPM2BECCParameter{ Buffer: make([]byte, 0), }, }, ), } RSASRK_H9_Template = tpm2.TPMTPublic{ Type: tpm2.TPMAlgRSA, NameAlg: tpm2.TPMAlgSHA256, ObjectAttributes: tpm2.TPMAObject{ FixedTPM: true, FixedParent: true, SensitiveDataOrigin: true, UserWithAuth: true, AdminWithPolicy: false, SignEncrypt: true, Decrypt: true, }, AuthPolicy: tpm2.TPM2BDigest{ Buffer: []byte{ 0xCA, 0x3D, 0x0A, 0x99, 0xA2, 0xB9, 0x39, 0x06, 0xF7, 0xA3, 0x34, 0x24, 0x14, 0xEF, 0xCF, 0xB3, 0xA3, 0x85, 0xD4, 0x4C, 0xD1, 0xFD, 0x45, 0x90, 0x89, 0xD1, 0x9B, 0x50, 0x71, 0xC0, 0xB7, 0xA0, }, }, Parameters: tpm2.NewTPMUPublicParms( tpm2.TPMAlgRSA, &tpm2.TPMSRSAParms{ Scheme: tpm2.TPMTRSAScheme{ Scheme: tpm2.TPMAlgNull, }, KeyBits: 2048, }, ), Unique: tpm2.NewTPMUPublicID( tpm2.TPMAlgRSA, &tpm2.TPM2BPublicKeyRSA{Buffer: make([]byte, 0)}, ), } ) type HierSSHTPMKey struct { *SSHTPMKey handle *tpm2.AuthHandle name tpm2.TPM2BName } // from crypto/ecdsa func addASN1IntBytes(b *cryptobyte.Builder, bytes []byte) { for len(bytes) > 0 && bytes[0] == 0 { bytes = bytes[1:] } if len(bytes) == 0 { b.SetError(errors.New("invalid integer")) return } b.AddASN1(asn1.INTEGER, func(c *cryptobyte.Builder) { if bytes[0]&0x80 != 0 { c.AddUint8(0) } c.AddBytes(bytes) }) } // from crypto/ecdsa func encodeSignature(r, s []byte) ([]byte, error) { var b cryptobyte.Builder b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) { addASN1IntBytes(b, r) addASN1IntBytes(b, s) }) return b.Bytes() } func (h *HierSSHTPMKey) Sign(tpm transport.TPMCloser, _, auth, digest []byte, digestalgo tpm2.TPMAlgID) ([]byte, error) { rsp, err := keyfile.TPMSign(tpm, h.handle, digest, digestalgo, h.KeySize(), h.KeyAlgo()) if err != nil { return nil, err } switch h.KeyAlgo() { case tpm2.TPMAlgECC: eccsig, err := rsp.Signature.ECDSA() if err != nil { return nil, fmt.Errorf("failed getting signature: %v", err) } return encodeSignature(eccsig.SignatureR.Buffer, eccsig.SignatureS.Buffer) case tpm2.TPMAlgRSA: rsassa, err := rsp.Signature.RSASSA() if err != nil { return nil, fmt.Errorf("failed getting rsassa signature") } return rsassa.Sig.Buffer, nil } return nil, fmt.Errorf("failed returning signature") } func (h *HierSSHTPMKey) FlushHandle(tpm transport.TPMCloser) { if h.handle != nil { keyfile.FlushHandle(tpm, *h.handle) } } func (h *HierSSHTPMKey) Signer(keyring *keyring.ThreadKeyring, ownerAuth func() ([]byte, error), tpm func() transport.TPMCloser, auth func(*keyfile.TPMKey) ([]byte, error)) *SSHKeySigner { return NewSSHKeySigner(h, keyring, ownerAuth, tpm, auth) } func CreateHierarchyKey(tpm transport.TPMCloser, keytype tpm2.TPMAlgID, hier tpm2.TPMHandle, desc string) (*HierSSHTPMKey, error) { var tmpl tpm2.TPMTPublic switch keytype { case tpm2.TPMAlgECC: tmpl = ECCSRK_H10_Template case tpm2.TPMAlgRSA: tmpl = RSASRK_H9_Template } srk := tpm2.CreatePrimary{ PrimaryHandle: tpm2.AuthHandle{ Handle: hier, Auth: tpm2.PasswordAuth(nil), }, InSensitive: tpm2.TPM2BSensitiveCreate{ Sensitive: &tpm2.TPMSSensitiveCreate{ UserAuth: tpm2.TPM2BAuth{ Buffer: []byte(nil), }, }, }, InPublic: tpm2.New2B(tmpl), } rsp, err := srk.Execute(tpm) if err != nil { return nil, err } var tpmkey keyfile.TPMKey tpmkey.AddOptions( keyfile.WithUserAuth([]byte(nil)), keyfile.WithPubkey(rsp.OutPublic), keyfile.WithDescription(desc), ) wkey, err := WrapTPMKey(&tpmkey) if err != nil { return nil, err } return &HierSSHTPMKey{ SSHTPMKey: wkey, handle: &tpm2.AuthHandle{ Handle: rsp.ObjectHandle, Name: rsp.Name, Auth: tpm2.PasswordAuth(nil), }, name: rsp.Name, }, nil } ssh-tpm-agent-0.8.0/key/hierarchy_keys_test.go000066400000000000000000000036501511154401300213740ustar00rootroot00000000000000package key_test import ( "crypto" "io" "testing" 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/utils" "github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2/transport" "github.com/google/go-tpm/tpm2/transport/simulator" ) func TestHierKey(t *testing.T) { tpm, err := utils.GetFixedSim() if err != nil { t.Fatalf("%v", err) } defer tpm.Close() hkey, err := key.CreateHierarchyKey(tpm, tpm2.TPMAlgECC, tpm2.TPMRHOwner, "") if err != nil { t.Fatalf("%v", err) } defer hkey.FlushHandle(tpm) if hkey.Fingerprint() != "SHA256:8kry+y93GpsJYho0GoIUpC6Ja7KFHajgqqXPTadlCPg" { t.Fatalf("ssh key fingerprint does not match") } } func TestHierKeySigning(t *testing.T) { tpm, err := simulator.OpenSimulator() if err != nil { t.Fatalf("%v", err) } defer tpm.Close() hkey, err := key.CreateHierarchyKey(tpm, tpm2.TPMAlgECC, tpm2.TPMRHOwner, "") if err != nil { t.Fatalf("%v", err) } defer hkey.FlushHandle(tpm) h := crypto.SHA256.New() h.Write([]byte("message")) b := h.Sum(nil) _, err = hkey.Sign(tpm, []byte(nil), []byte(nil), b[:], tpm2.TPMAlgSHA256) if err != nil { t.Fatalf("%v", err) } } func TestHierKeySigner(t *testing.T) { tpm, err := simulator.OpenSimulator() if err != nil { t.Fatalf("%v", err) } defer tpm.Close() hkey, err := key.CreateHierarchyKey(tpm, tpm2.TPMAlgECC, tpm2.TPMRHOwner, "") if err != nil { t.Fatalf("%v", err) } defer hkey.FlushHandle(tpm) signer := hkey.Signer(&keyring.ThreadKeyring{}, func() ([]byte, error) { return []byte(nil), nil }, func() transport.TPMCloser { return tpm }, func(_ *keyfile.TPMKey) ([]byte, error) { return []byte(nil), nil }, ) h := crypto.SHA256.New() h.Write([]byte("message")) b := h.Sum(nil) _, err = signer.Sign((io.Reader)(nil), b[:], crypto.SHA256) if err != nil { t.Fatalf("message") } } ssh-tpm-agent-0.8.0/key/key.go000066400000000000000000000064651511154401300161230ustar00rootroot00000000000000package key import ( "errors" "fmt" "strings" keyfile "github.com/foxboron/go-tpm-keyfiles" "github.com/foxboron/ssh-tpm-agent/internal/keyring" "github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2/transport" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" ) var ( ErrOldKey = errors.New("old format on key") ) type SSHTPMKeys interface { Signer(*keyring.ThreadKeyring, func() ([]byte, error), func() transport.TPMCloser, func(*keyfile.TPMKey) ([]byte, error)) *SSHKeySigner GetDescription() string Fingerprint() string AuthorizedKey() []byte AgentKey() *agent.Key GetTPMKey() *keyfile.TPMKey } // SSHTPMKey is a wrapper for TPMKey implementing the ssh.PublicKey specific parts type SSHTPMKey struct { *keyfile.TPMKey PublicKey *ssh.PublicKey Certificate *ssh.Certificate } func WrapTPMKey(k *keyfile.TPMKey) (*SSHTPMKey, error) { pubkey, err := k.PublicKey() if err != nil { return nil, err } sshkey, err := ssh.NewPublicKey(pubkey) if err != nil { return nil, err } return &SSHTPMKey{k, &sshkey, nil}, nil } func NewSSHTPMKey(tpm transport.TPMCloser, alg tpm2.TPMAlgID, bits int, ownerauth []byte, fn ...keyfile.TPMKeyOption) (*SSHTPMKey, error) { k, err := keyfile.NewLoadableKey( tpm, alg, bits, ownerauth, fn..., ) if err != nil { return nil, err } return WrapTPMKey(k) } // This assumes we are just getting a local PK. func NewImportedSSHTPMKey(tpm transport.TPMCloser, pk any, ownerauth []byte, fn ...keyfile.TPMKeyOption) (*SSHTPMKey, error) { sess := keyfile.NewTPMSession(tpm) srkHandle, srkPub, err := keyfile.CreateSRK(sess, tpm2.TPMRHOwner, ownerauth) if err != nil { return nil, fmt.Errorf("failed creating SRK: %v", err) } sess.SetSalted(srkHandle.Handle, *srkPub) defer sess.FlushHandle() k, err := keyfile.NewImportablekey( srkPub, pk, fn...) if err != nil { return nil, fmt.Errorf("failed failed creating importable key: %v", err) } k, err = keyfile.ImportTPMKey(tpm, k, ownerauth) if err != nil { return nil, fmt.Errorf("failed turning imported key to loadable key: %v", err) } pubkey, err := k.PublicKey() if err != nil { return nil, err } sshkey, err := ssh.NewPublicKey(pubkey) if err != nil { return nil, err } return &SSHTPMKey{k, &sshkey, nil}, nil } func (k *SSHTPMKey) Fingerprint() string { return ssh.FingerprintSHA256(*k.PublicKey) } func (k *SSHTPMKey) AuthorizedKey() []byte { return []byte(fmt.Sprintf("%s %s\n", strings.TrimSpace(string(ssh.MarshalAuthorizedKey(*k.PublicKey))), k.Description)) } func (k *SSHTPMKey) GetDescription() string { return k.Description } func (k *SSHTPMKey) AgentKey() *agent.Key { if k.Certificate != nil { return &agent.Key{ Format: k.Certificate.Type(), Blob: k.Certificate.Marshal(), Comment: k.Description, } } return &agent.Key{ Format: (*k.PublicKey).Type(), Blob: (*k.PublicKey).Marshal(), Comment: k.Description, } } func (k *SSHTPMKey) GetTPMKey() *keyfile.TPMKey { return k.TPMKey } func (k *SSHTPMKey) Signer(keyring *keyring.ThreadKeyring, ownerAuth func() ([]byte, error), tpm func() transport.TPMCloser, auth func(*keyfile.TPMKey) ([]byte, error)) *SSHKeySigner { return NewSSHKeySigner(k, keyring, ownerAuth, tpm, auth) } func Decode(b []byte) (*SSHTPMKey, error) { k, err := keyfile.Decode(b) if err != nil { return nil, err } return WrapTPMKey(k) } ssh-tpm-agent-0.8.0/key/signer.go000066400000000000000000000035421511154401300166130ustar00rootroot00000000000000package key import ( "crypto" "errors" "fmt" "io" "log/slog" "github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2/transport" keyfile "github.com/foxboron/go-tpm-keyfiles" "github.com/foxboron/ssh-tpm-agent/internal/keyring" ) // Shim for keyfile.TPMKeySigner // We need access to the SSHTPMKey to change the userauth for caching type SSHKeySigner struct { *keyfile.TPMKeySigner key SSHTPMKeys keyring *keyring.ThreadKeyring tpm func() transport.TPMCloser ownerauth func() ([]byte, error) } var _ crypto.Signer = &SSHKeySigner{} func (t *SSHKeySigner) Sign(r io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { var b []byte var err error switch key := t.key.(type) { case *HierSSHTPMKey: var digestalg tpm2.TPMAlgID switch opts.HashFunc() { case crypto.SHA256: digestalg = tpm2.TPMAlgSHA256 case crypto.SHA384: digestalg = tpm2.TPMAlgSHA384 case crypto.SHA512: digestalg = tpm2.TPMAlgSHA512 default: return nil, fmt.Errorf("%s is not a supported hashing algorithm", opts.HashFunc()) } b, err = key.Sign(t.tpm(), []byte(nil), []byte(nil), digest, digestalg) case *SSHTPMKey: b, err = t.TPMKeySigner.Sign(r, digest, opts) default: return nil, fmt.Errorf("this should not happen") } if errors.Is(err, tpm2.TPMRCAuthFail) { slog.Debug("removed cached userauth for key", slog.Any("err", err), slog.String("desc", t.key.GetDescription())) t.keyring.RemoveKey(t.key.Fingerprint()) } return b, err } func NewSSHKeySigner(k SSHTPMKeys, keyring *keyring.ThreadKeyring, ownerAuth func() ([]byte, error), tpm func() transport.TPMCloser, auth func(*keyfile.TPMKey) ([]byte, error)) *SSHKeySigner { return &SSHKeySigner{ TPMKeySigner: keyfile.NewTPMKeySigner(k.GetTPMKey(), ownerAuth, tpm, auth), keyring: keyring, tpm: tpm, ownerauth: ownerAuth, key: k, } } ssh-tpm-agent-0.8.0/man/000077500000000000000000000000001511154401300147545ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/man/ssh-tpm-add.1.adoc000066400000000000000000000026341511154401300200710ustar00rootroot00000000000000= ssh-tpm-add(1) :doctype: manpage :manmanual: ssh-tpm-add manual == Name ssh-tpm-add - adds private keys to the *ssh-tpm-agent* == Synopsis *ssh-tpm-add* *ssh-tpm-add* [__PATH__ ...] == Description *ssh-tpm-add* adds TPM wrapped private keys to *ssh-tpm-agent*(1). Any specified keys as arguments are added to the running agent. It requires the environment variable *SSH_TPM_AUTH_SOCK* to point at an active UNIX domain socket with an agent listening. If no files are given it will try to load the default keys *~/.ssh/id_ecdsa.tpm* and *~/.ssh/id_rsa.tpm*. == Environment *SSH_TPM_AUTH_SOCK*:: Identifies the path of a unix-domain socket for communication with the agent. + Default to _/var/tmp/ssh-tpm-agent.sock_. == Files _~/ssh/id_rsa.tpm_:: _~/ssh/id_ecdsa.tpm_:: Contains the ssh private keys used by *ssh-tpm-agent*. They are TPM 2.0 TSS key files and securely wrapped by the TPM. They can be shared publicly as they can only be used by the TPM they where created on. However it is probably better to not do that. _~/ssh/id_rsa.pub_:: _~/ssh/id_ecdsa.pub_:: Contains the ssh public keys. These can be shared publicly, and is the same format as the ones created by *ssh-keygen*(1). == See Also *ssh-add*(1), *ssh-agent*(1), *ssh*(1), *ssh-tpm-keygen*(1), *ssh-keygen*(1) == Notes, standards and other https://www.hansenpartnership.com/draft-bottomley-tpm2-keys.html[ASN.1 Specification for TPM 2.0 Key Files] ssh-tpm-agent-0.8.0/man/ssh-tpm-agent.1.adoc000066400000000000000000000220311511154401300204300ustar00rootroot00000000000000= ssh-tpm-agent(1) :doctype: manpage :manmanual: ssh-tpm-agent manual == Name ssh-tpm-agent - ssh-agent for TPM 2.0 keys == Synopsis *ssh-tpm-agent* [_OPTIONS_] *ssh-tpm-agent* *--print-socket* *ssh-tpm-agent* *--install-user-units* == Description *ssh-tpm-agent* is a program that created keys utilizing a Trusted Platform Module (TPM) to enable wrapped private keys for public key authentication. == Options *-l* _PATH_:: Path of the UNIX socket to open + Defaults to _$XDG_RUNTIME_DIR/ssh-tpm-agent.sock_. *-A* _PATH_:: Fallback ssh-agent sockets for additional key lookup. *--print-socket*:: Prints the socket to STDIN. *--key-dir* _PATH_:: Path of the directory to look for TPM sealed keys in. + Defaults to _~/.ssh_. *--no-load*:: Do not load TPM sealed keys from _~/.ssh_ by default. *-o, --owner-password*:: Ask for the owner password. *--no-cache*:: The agent will not cache key passwords. *--hierarchy* __HIERARCHY__:: Preload hierarchy keys into the agent. + See *Hierarchy Keys* for more information. + Available hierarchies: + - owner, o - endorsement, e - null, n - platform, p *-d*:: Enable debug logging. *--install-user-units*:: Installs systemd system units and sshd configs for using ssh-tpm-agent as a hostkey agent. *--swtpm*:: Stores keys inside a swtpm instance instead of the actual TPM. This is not a security feature and your keys are not stored securely. + Can also be enabled with the environment variable *SSH_TPM_AGENT_SWTPM*. == Examples === Normal agent usage *ssh-tpm-agent* can be used as a dropin replacement to ssh-agent and works the same way. $ ssh-tpm-keygen # Add ~/.ssh/id_ecdsa.pub to your Github accounts $ ssh-tpm-agent & $ export SSH_AUTH_SOCK=$(ssh-tpm-agent --print-socket) $ ssh git@github.com See *ssh-tpm-keygen*(1) for keygen usage. === Agent fallback support *ssh-tpm-agent* supports fallback to different ssh-agent. Agents can be added with the _-A_ switch. This will cause *ssh-tpm-agent* to fan-out to all available agents for keys. This is practical if you have multiple keys from different agent implementations but want to rely on one socket. # Start the usual ssh-agent $ eval $(ssh-agent) # Create a strong RSA key $ ssh-keygen -t rsa -b 4096 -f id_rsa -C ssh-agent ... The key fingerprint is: SHA256:zLSeyU/6NKHGEvyZLA866S1jGqwdwdAxRFff8Z2N1i0 ssh-agent $ ssh-add id_rsa Identity added: id_rsa (ssh-agent) # Print looonnggg key $ ssh-add -L ssh-rsa AAAAB3NzaC1yc[...]8TWynQ== ssh-agent # Create key on the TPM $ ssh-tpm-keygen -C ssh-tpm-agent Generating a sealed public/private ecdsa key pair. Enter file in which to save the key (/home/user/.ssh/id_ecdsa): Enter passphrase (empty for no passphrase): Confirm passphrase: Your identification has been saved in /home/user/.ssh/id_ecdsa.tpm Your public key has been saved in /home/user/.ssh/id_ecdsa.pub The key fingerprint is: SHA256:PoQyuzOpEBLqT+xtP0dnvyBVL6UQTiQeCWN/EXIxPOo The key's randomart image is the color of television, tuned to a dead channel. # Start ssh-tpm-agent with a proxy socket $ ssh-tpm-agent -A "${SSH_AUTH_SOCK}" & $ export SSH_AUTH_SOCK="$(ssh-tpm-agent --print-socket)" # ssh-tpm-agent is proxying the keys from ssh-agent $ ssh-add -L ssh-rsa AAAAB3NzaC1yc[...]8TWynQ== ssh-agent ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNo[...]q4whro= ssh-tpm-agent === Hostkeys usage *ssh-tpm-agent* can also be used to serve host keys for an ssh server. *ssh-tpm-hostkeys* has convenient flags to help install systemd configurations and services to the system. This will create a system socket for ssh-tpm-agent under _/var/tmp/ssh-tpm-agent.sock_. $ sudo ssh-tpm-keygen -A 2023/09/03 17:03:08 INFO Generating new ECDSA host key 2023/09/03 17:03:08 INFO Wrote /etc/ssh/ssh_tpm_host_ecdsa_key.tpm 2023/09/03 17:03:08 INFO Generating new RSA host key 2023/09/03 17:03:15 INFO Wrote /etc/ssh/ssh_tpm_host_rsa_key.tpm $ sudo ssh-tpm-hostkeys --install-system-units Installed /usr/lib/systemd/system/ssh-tpm-agent.service Installed /usr/lib/systemd/system/ssh-tpm-agent.socket Installed /usr/lib/systemd/system/ssh-tpm-genkeys.service Enable with: systemctl enable --now ssh-tpm-agent.socket $ sudo ssh-tpm-hostkeys --install-sshd-config Installed /etc/ssh/sshd_config.d/10-ssh-tpm-agent.conf Restart sshd: systemd restart sshd $ systemctl enable --now ssh-tpm-agent.socket $ systemd restart sshd $ sudo ssh-tpm-hostkeys ecdsa-sha2-nistp256 AAAAE2V[...]YNwqWY0= root@localhost ssh-rsa AAAAB3NzaC1ycA[...]N1Jg3fLQKSe7f root@localhost $ ssh-keyscan -t ecdsa localhost # localhost:22 SSH-2.0-OpenSSH_9.4 localhost ecdsa-sha2-nistp256 AAAAE2V[...]YNwqWY0= Alternatively one can omit the embedded install flags and just include a drop-in configuration for sshd under /etc/ssh/sshd_config.d with the following content. HostKeyAgent /var/tmp/ssh-tpm-agent.sock HostKey /etc/ssh/ssh_tpm_host_ecdsa_key.pub HostKey /etc/ssh/ssh_tpm_host_rsa_key.pub === Hierarchy keys TPMs are capable of creating static keys utilizing the top-level hierarchies. This enables the user to create keys that are available for the lifetime of the device, for the current owner of the device, or the current session of the device. These keys do not leave the TPM, like other keys created by *ssh-tpm-keygen*, and can always be recreated. These keys can be preloaded into *ssh-tpm-agent*. $ ssh-tpm-agent --hierarchy owner & $ export SSH_AUTH_SOCK="$(ssh-tpm-agent --print-socket)" $ ssh-add -l 2048 SHA256:yt7A20tcRnzgaD2ATgAXSNWy9sP6wznysp3SkoK3Gj8 Owner hierarchy key (RSA) 256 SHA256:PmEsMeh/DwFP04iUaWLNeX4maMR6r1vfqw1BbbdFjIg Owner hierarchy key (ECDSA) For usage with `sshd` the public part of these keys can be created by combining _-A_ with _--hierarchy_. $ ssh-tpm-keygen -A --hierarchy owner 2025/03/10 21:57:08 INFO Generating new hierarcy host key algorithm=RSA hierarchy=owner 2025/03/10 21:57:10 INFO Wrote public key filename=/etc/ssh/ssh_tpm_host_rsa_key.pub 2025/03/10 21:57:10 INFO Generating new hierarcy host key algorithm=ECDSA hierarchy=owner 2025/03/10 21:57:10 INFO Wrote public key filename=/etc/ssh/ssh_tpm_host_ecdsa_key.pub These files can be used with _HostKey_ as normal in _ssh_config_. The different key hierarchies have different properties and lifetimes. _endorsement_ hierarchy stores keys created for the lifetime of the device. This hierarchy should not change during the lifetime of the device. _owner_ hierarchy stores keys created for the device owner. These keys will be rotated when *tpm2_clear*(1) is issued on the platform, which should be done when the device gets a new owner. _null_ hierarchy stores keys created for the current session. The session should be a power cycle of the devices. *Note:* This feature is _experimental_. *ssh-tpm-agent* keeps the TPM objects loaded while running. Some TPM devices run out of memory if you attempt to use the hierarchy keys with the usual keys created by *ssh-tpm-keygen*. == Environment *SSH_TPM_AUTH_SOCK*:: Identifies the path of a unix-domain socket for communication with the agent. + Default to _/var/tmp/ssh-tpm-agent.sock_. *SSH_ASKPASS*:: If *ssh-tpm-agent*, and other binaries, needs to read a password it will default to using the terminal if it can. If there is no terminal available it will fall back to calling the binary *SSH_ASKPASS* point at. + See *ssh*(1) under *ENVIRONMENT* for more information. *SSH_ASKPASS_REQUIRE*:: Allows control of the use of the askpass program. Valid values are: * *never* ensures *ssh* will never try to use the askpass program. * *prefer* will prefer to use the askpass program. * *force* will ensure all passphrase inputs will be using the askpass program. + See *ssh*(1) under *ENVIRONMENT* for more information. *SSH_TPM_AGENT_SWTPM*:: Specify if *ssh-tpm-agent* should use the swtpm backend or not. Accepts any non-empty value as true. *SSH_TPM_LANDLOCK*:: If set then *ssh-tpm-agent*, and the other binaries, will enforce the landlock sandbox where applicable. + Disabled by default. + See *landlock*(7) for more information. == Files _~/ssh/id_rsa.tpm_:: _~/ssh/id_ecdsa.tpm_:: Contains the ssh private keys used by *ssh-tpm-agent*. They are TPM 2.0 TSS key files and securely wrapped by the TPM. They can be shared publicly as they can only be used by the TPM they where created on. However it is probably better to not do that. _~/ssh/id_rsa.pub_:: _~/ssh/id_ecdsa.pub_:: Contains the ssh public keys. These can be shared publicly, and is the same format as the ones created by *ssh-keygen*(1). _/run/user/$UID/ssh-tpm-agent.sock_:: The default user *ssh-tpm-agent* UNIX socket path. Used by induvidual users. _/var/tmp/ssh-tpm-agent.sock_:: The default system *ssh-tpm-agent* UNIX socket path. Used for host keys and the system. == See Also *ssh-agent*(1), *ssh*(1), *ssh-tpm-keygen*(1), *ssh-keygen*(1) == Notes, standards and other https://www.hansenpartnership.com/draft-bottomley-tpm2-keys.html[ASN.1 Specification for TPM 2.0 Key Files] https://linderud.dev/blog/store-ssh-keys-inside-the-tpm-ssh-tpm-agent/[Store ssh keys inside the TPM: ssh-tpm-agent] ssh-tpm-agent-0.8.0/man/ssh-tpm-hostkeys.1.adoc000066400000000000000000000012341511154401300212050ustar00rootroot00000000000000= ssh-tpm-hostkeys (1) :doctype: manpage :manmanual: ssh-tpm-hostkeys manual == Name ssh-tpm-hostkeys - ssh-tpm-agent hostkey utility == Synopsis *ssh-tpm-hostkeys* *ssh-tpm-hostkeys* *--install-system-units* *ssh-tpm-hostkeys* *--install-sshd-config* == Description *ssh-tpm-hostkeys* displays the system host keys, and can install relevant systemd units and sshd configuration to use TPM backed host keys. == Options *--install-system-units*:: Installs systemd system units for using ssh-tpm-agent as a hostkey agent. *--install-sshd-config*:: Installs sshd configuration for the ssh-tpm-agent socket. == See Also *ssh-tpm-agent*(1), *ssh-agent*(1) ssh-tpm-agent-0.8.0/man/ssh-tpm-keygen.1.adoc000066400000000000000000000130301511154401300206130ustar00rootroot00000000000000= ssh-tpm-keygen(1) :doctype: manpage :manmanual: ssh-tpm-keygen manual == Name ssh-tpm-keygen - ssh-tpm-agent key creation utility == Synopsis *ssh-tpm-keygen* [_OPTIONS_]... *ssh-tpm-keygen* *--wrap* __PATH__ *--wrap-with* __PATH__ *ssh-tpm-keygen* *--import* __PATH__ *ssh-tpm-keygen* *--print-pubkey* __PATH__ *ssh-tpm-keygen* *--supported* *ssh-tpm-keygen* *-p* [*-f* __keyfile__] [*-P* __old passphrase__] [*-N* __new passphrase__] *ssh-tpm-keygen* *-A* [*-f* __path prefix__] [*--hierarchy* __hierarchy__] == Description *ssh-tpm-keygen* is a program that allows the creation of TPM wrapped keys for *ssh-tpm-agent*. == Options *-A*:: Generate host keys for all key types (rsa and ecdsa). *-b* __BITS__:: Number of bits in the key to create. - rsa: 2048 (default) - ecdsa: 256 (default) | 384 | 521 *-C* __COMMENT__ :: Provide a comment with the key. *-f* __PATH__:: Output keyfile path. *-N* __PASSPHRASE__ :: Passphrase for the key. *-o*, *--owner-password* __PASSPHRASE__ :: Ask for the owner password. *-t* [__ecdsa__ | __rsa__]:: Specify the type of key to create. Defaults to ecdsa *-I*, *--import* __PATH__:: Import existing key into ssh-tpm-agent. *--parent-handle* __HIERARCHY__:: Parent for the TPM key. Can be a hierarchy or a persistent handle. + Available hierarchies: - owner, o (default) - endorsement, e - null, n - platform, p *--print-pubkey* __PATH__:: Print the public key given a TPM private key. *--supported*:: List the supported key types of the TPM. *--hierarchy* __HIERARCHY__:: Create a public key. Can only be used with *-A*. + See *Hierarchy Keys* in *ssh-tpm-agent*(1) for usage. + Available hierarchies: + - owner, o - endorsement, e - null, n - platform, p *--wrap* __PATH__:: A SSH key to wrap for import on remote machine. *--wrap-with* __PATH__:: Parent key to wrap the SSH key with. == Examples === Key creation Create a key with *ssh-tpm-keygen*. $ ssh-tpm-keygen Generating a sealed public/private ecdsa key pair. Enter file in which to save the key (/home/user/.ssh/id_ecdsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/user/.ssh/id_ecdsa.tpm Your public key has been saved in /home/user/.ssh/id_ecdsa.pub The key fingerprint is: SHA256:NCMJJ2La+q5tGcngQUQvEOJP3gPH8bMP98wJOEMV564 The key's randomart image is the color of television, tuned to a dead channel. $ cat /home/user/.ssh/id_ecdsa.pub ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOTOsMXyjTc1wiQSKhRiNhKFsHJNLzLk2r4foXPLQYKR0tuXIBMTQuMmc7OiTgNMvIjMrcb9adgGdT3s+GkNi1g= === Import existing key Useful if you want to back up the key to a remote secure storage while using the key day-to-day from the TPM. Create a key, or use an existing one. $ ssh-keygen -t ecdsa -f id_ecdsa Generating public/private ecdsa key pair. Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in id_ecdsa Your public key has been saved in id_ecdsa.pub The key fingerprint is: SHA256:bDn2EpX6XRX5ADXQSuTq+uUyia/eV3Z6MW+UtxjnXvU user@localhost The key's randomart image is: +---[ECDSA 256]---+ | .+=o..| | o. oo.| | o... .o| | . + .. ..| | S . . o| | o * . oo=*| | ..+.oo=+E| | .++o...o=| | .++++. .+ | +----[SHA256]-----+ Import the key using the `--import` switch. $ ssh-tpm-keygen --import id_ecdsa Sealing an existing public/private ecdsa key pair. Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in id_ecdsa.tpm The key fingerprint is: SHA256:bDn2EpX6XRX5ADXQSuTq+uUyia/eV3Z6MW+UtxjnXvU The key's randomart image is the color of television, tuned to a dead channel. === Create and Wrap private key for client machine on remote srver On the client side create one a primary key under an hierarchy. This example will use the owner hierarchy with an SRK. The output file `srk.pem` needs to be transferred to the remote end which creates the key. This could be done as part of client provisioning. $ tpm2_createprimary -C o -G ecc -g sha256 -c prim.ctx -a 'restricted|decrypt|fixedtpm|fixedparent|sensitivedataorigin|userwithauth|noda' -f pem -o srk.pem On the remote end we create a p256 ssh key, with no password, and wrap it with `ssh-tpm-keygen` with the `srk.pem` from the client side. $ ssh-keygen -t ecdsa -b 256 -N "" -f ./ecdsa.key OR with openssl $ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out ecdsa.key Wrap with ssh-tpm-keygen $ ssh-tpm-keygen --wrap-with srk.pub --wrap ecdsa.key -f wrapped_id_ecdsa On the client side we can unwrap `wrapped_id_ecdsa` to a loadable key. $ ssh-tpm-keygen --import ./wrapped_id_ecdsa.tpm --output id_ecdsa.tpm $ ssh-tpm-add id_ecdsa.tpm == Files _~/ssh/id_rsa.tpm_:: _~/ssh/id_ecdsa.tpm_:: Contains the ssh private keys used by *ssh-tpm-agent*. They are TPM 2.0 TSS key files and securely wrapped by the TPM. They can be shared publicly as they can only be used by the TPM they where created on. However it is probably better to not do that. _~/ssh/id_rsa.pub_:: _~/ssh/id_ecdsa.pub_:: Contains the ssh public keys. These can be shared publicly, and is the same format as the ones created by *ssh-keygen*(1). == See Also *ssh-agent*(1), *ssh*(1), *ssh-tpm-keygen*(1), *ssh-keygen*(1) == Notes, standards and other https://www.hansenpartnership.com/draft-bottomley-tpm2-keys.html[ASN.1 Specification for TPM 2.0 Key Files] ssh-tpm-agent-0.8.0/utils/000077500000000000000000000000001511154401300153415ustar00rootroot00000000000000ssh-tpm-agent-0.8.0/utils/tpm.go000066400000000000000000000043201511154401300164670ustar00rootroot00000000000000package utils import ( "errors" "io" "os" "path" "sync" "github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2/transport" "github.com/google/go-tpm/tpm2/transport/linuxtpm" "github.com/google/go-tpm/tpm2/transport/simulator" "github.com/google/go-tpm/tpmutil" sim "github.com/google/go-tpm-tools/simulator" ) // shadow the unexported interface from go-tpm type handle interface { HandleValue() uint32 KnownName() *tpm2.TPM2BName } // Helper to flush handles func FlushHandle(tpm transport.TPM, h handle) { flushSrk := tpm2.FlushContext{FlushHandle: h} flushSrk.Execute(tpm) } var swtpmPath = "/var/tmp/ssh-tpm-agent" // TPM represents a connection to a TPM simulator. type TPMCloser struct { transport io.ReadWriteCloser } // Send implements the TPM interface. func (t *TPMCloser) Send(input []byte) ([]byte, error) { return tpmutil.RunCommandRaw(t.transport, input) } // Close implements the TPM interface. func (t *TPMCloser) Close() error { return t.transport.Close() } var ( once sync.Once s transport.TPMCloser ) func GetFixedSim() (transport.TPMCloser, error) { var ss *sim.Simulator var err error once.Do(func() { ss, err = sim.GetWithFixedSeedInsecure(123456) s = &TPMCloser{ss} }) return s, err } var cache struct { sync.Once tpm transport.TPMCloser err error } // Smaller wrapper for getting the correct TPM instance func TPM(f bool) (transport.TPMCloser, error) { cache.Do(func() { if f || os.Getenv("SSH_TPM_AGENT_SWTPM") != "" { if _, err := os.Stat(swtpmPath); errors.Is(err, os.ErrNotExist) { os.MkdirTemp(path.Dir(swtpmPath), path.Base(swtpmPath)) } cache.tpm, cache.err = simulator.OpenSimulator() } else if f || os.Getenv("_SSH_TPM_AGENT_SIMULATOR") != "" { // Implements an insecure fixed thing cache.tpm, cache.err = GetFixedSim() } else { cache.tpm, cache.err = linuxtpm.Open("/dev/tpmrm0") } }) return cache.tpm, cache.err } func EnvSocketPath(socketPath string) string { // Find a default socket name from ssh-tpm-agent.service if val, ok := os.LookupEnv("SSH_TPM_AUTH_SOCK"); ok && socketPath == "" { return val } dir := os.Getenv("XDG_RUNTIME_DIR") if dir == "" { dir = "/var/tmp" } return path.Join(dir, "ssh-tpm-agent.sock") } ssh-tpm-agent-0.8.0/utils/utils.go000066400000000000000000000071621511154401300170360ustar00rootroot00000000000000package utils import ( "errors" "fmt" "html/template" "io/fs" "os" "path" "github.com/foxboron/ssh-tpm-agent/contrib" "github.com/google/go-tpm/tpm2" ) func SSHDir() string { dirname, err := os.UserHomeDir() if err != nil { panic("$HOME is not defined") } return path.Join(dirname, ".ssh") } func FileExists(s string) bool { _, err := os.Stat(s) return !errors.Is(err, fs.ErrNotExist) } // This is the sort of things I swore I'd never write. // but here we are. func fmtSystemdInstallPath() string { DESTDIR := "" if val, ok := os.LookupEnv("DESTDIR"); ok { DESTDIR = val } PREFIX := "/usr/" if val, ok := os.LookupEnv("PREFIX"); ok { PREFIX = val } return path.Join(DESTDIR, PREFIX, "lib/systemd") } // Installs user units to the target system. // It will either place the files under $HOME/.config/systemd/user or if global // is supplied (through --install-system) into system user directories. // // Passing the env TEMPLATE_BINARY will use /usr/bin/ssh-tpm-agent for the // binary in the service func InstallUserUnits(global bool) error { if global || os.Getuid() == 0 { // If ran as root, install global system units return installUnits(path.Join(fmtSystemdInstallPath(), "/user/"), contrib.EmbeddedUserServices()) } dirname, err := os.UserHomeDir() if err != nil { return err } return installUnits(path.Join(dirname, ".config/systemd/user"), contrib.EmbeddedUserServices()) } func InstallHostkeyUnits() error { return installUnits(path.Join(fmtSystemdInstallPath(), "/system/"), contrib.EmbeddedSystemServices()) } func installUnits(installPath string, files map[string][]byte) (err error) { execPath := os.Getenv("TEMPLATE_BINARY") if execPath == "" { execPath, err = os.Executable() if err != nil { return err } } if !FileExists(installPath) { if err := os.MkdirAll(installPath, 0o750); err != nil { return fmt.Errorf("creating service installation directory: %w", err) } } for name := range files { servicePath := path.Join(installPath, name) if FileExists(servicePath) { fmt.Printf("%s exists. Not installing units.\n", servicePath) return nil } } for name, data := range files { servicePath := path.Join(installPath, name) f, err := os.OpenFile(servicePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { return err } defer f.Close() t := template.Must(template.New("service").Parse(string(data))) if err = t.Execute(f, &map[string]string{ "GoBinary": execPath, }); err != nil { return err } fmt.Printf("Installed %s\n", servicePath) } return nil } func InstallSshdConf() error { // If ran as root, install sshd config if uid := os.Getuid(); uid != 0 { return fmt.Errorf("needs to be run as root") } sshdConfInstallPath := "/etc/ssh/sshd_config.d/" if !FileExists(sshdConfInstallPath) { return nil } files := contrib.EmbeddedSshdConfig() for name := range files { ff := path.Join(sshdConfInstallPath, name) if FileExists(ff) { fmt.Printf("%s exists. Not installing sshd config.\n", ff) return nil } } for name, data := range files { ff := path.Join(sshdConfInstallPath, name) if err := os.WriteFile(ff, data, 0o644); err != nil { return fmt.Errorf("failed writing sshd conf: %v", err) } fmt.Printf("Installed %s\n", ff) } fmt.Println("Restart sshd: systemd restart sshd") return nil } func GetParentHandle(ph string) (tpm2.TPMHandle, error) { switch ph { case "endoresement", "e": return tpm2.TPMRHEndorsement, nil case "null", "n": return tpm2.TPMRHNull, nil case "plattform", "p": return tpm2.TPMRHPlatform, nil case "owner", "o": fallthrough default: return tpm2.TPMRHOwner, nil } }