Skip to content

x/crypto/ssh: agent-based ecdsa certs no longer work #52185

Closed
@jeffreytolar

Description

@jeffreytolar

What version of Go are you using (go version)?

# go version
go version go1.18 linux/amd64

(running in the go:1.18 docker image)

Does this issue reproduce with the latest release?

Yes

What operating system and processor architecture are you using (go env)?

go env Output
# go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/root/.cache/go-build"
GOENV="/root/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.18"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/work/go.mod"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build3649815783=/tmp/go-build -gno-record-gcc-switches"

What did you do?

Gist: https://gist.github.com/jeffreytolar/9fd9ed8c01802fb7d36d98ed268f3a18

This gist has three approaches for configuring SSH authentication using an ECDSA-backed certificate (ecdsa-sha2-nistp256-cert-v01@openssh.com):

  • Using ssh.NewCertSigner
  • Using an agent.Keyring
  • Using an agent.NewClient talking to an agent.Keyring served via agent.ServeAgent

Run using go run .; as written, the gist uses the last approach, with the other two commented out.

What did you expect to see?

Successful running of the gist:

2022/04/06 17:15:27 [server] Completed handshake
2022/04/06 17:15:27 [client] Completed handshake

What did you see instead?

An error during authentication:

panic: ssh: handshake failed: agent: unsupported algorithm "ecdsa-sha2-nistp256"

goroutine 1 [running]:
main.check(...)
	/tmp/9fd9ed8c01802fb7d36d98ed268f3a18/test.go:19
main.client({0x5ef2f8, 0xc000100800}, {0xc000100920, 0x1, 0x1})
	/tmp/9fd9ed8c01802fb7d36d98ed268f3a18/test.go:80 +0x271
main.main()
	/tmp/9fd9ed8c01802fb7d36d98ed268f3a18/test.go:124 +0x90
exit status 2

Using one of the other approaches (adjust the comments in main) works. go get golang.org/x/crypto@a5774263c1e06050d4c71b5794b2b5a321289a8f also works (with all three approaches).

I think this is a regression caused by golang/crypto@5d542ad; previously, (*agentKeyringSigner).SignWithOpts always fell back on s.agent.SignWithFlags with no flags, whereas the new SignWithAlgorithm errors out.

In doing some debugging, SignWithAlgorithm is getting called with algorithm="ecdsa-sha2-nistp256", whereas s.pub.Type() is "ecdsa-sha2-nistp256-cert-v01@openssh.com".

Currently, my workaround is calling ssh.underlyingAlgo(s.pub.Type()), which works, although might not be the correct fix:

type patchedAgent struct {
	agent.Agent
}

func (p patchedAgent) Signers() (signers []ssh.Signer, err error) {
	signers, err = p.Agent.Signers()
	for i := range signers {
		signers[i] = patchedSigner{signers[i]}
	}
	return
}

type patchedSigner struct {
	ssh.Signer
}

func (s patchedSigner) SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*ssh.Signature, error) {
	// missing check in crypto/ssh
	if algorithm == underlyingAlgo(s.Signer.PublicKey().Type()) {
		return s.Sign(rand, data)
	}

	if as, ok := s.Signer.(ssh.AlgorithmSigner); ok {
		return as.SignWithAlgorithm(rand, data, algorithm)
	}
	return s.Sign(rand, data)
}

//go:linkname underlyingAlgo golang.org/x/crypto/ssh.underlyingAlgo

func underlyingAlgo(algo string) string

Metadata

Metadata

Assignees

No one assigned

    Labels

    FrozenDueToAgeNeedsFixThe path to resolution is known, but the work has not been done.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions