vendor: golang.org/x/crypto v0.53.0

full diff: https://github.com/golang/crypto/compare/v0.52.0...v0.53.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2026-06-19 17:12:50 +02:00
parent 7609bf8845
commit a271da13ea
15 changed files with 703 additions and 45 deletions
+226 -3
View File
@@ -26,6 +26,7 @@ import (
"io"
"math/big"
"sync"
"sync/atomic"
"golang.org/x/crypto/ssh"
)
@@ -307,17 +308,50 @@ func parseKey(in []byte) (out *Key, rest []byte, err error) {
}, record.Rest, nil
}
// pipelineMaxInFlight is the maximum number of outstanding requests the
// client will pipeline to the agent before applying backpressure.
const pipelineMaxInFlight = 32
// client is a client for an ssh-agent process.
//
// Exactly one of pipeline / (mu, conn) is set, chosen by NewClient
// based on whether the underlying transport implements io.Closer.
type client struct {
// conn is typically a *net.UnixConn
// pipeline, if non-nil, dispatches requests over a pipelined
// connection: requests are written as soon as the wire is
// available and responses are routed back to per-call reply
// channels in FIFO order by a background reader goroutine.
pipeline *pipeline
// mu and conn are used in fully-serialized mode, when the
// transport does not implement io.Closer. Each call takes mu,
// writes its request, reads the matching response, and releases
// mu before returning. There is no background goroutine.
mu sync.Mutex
conn io.ReadWriter
// mu is used to prevent concurrent access to the agent
mu sync.Mutex
}
// NewClient returns an Agent that talks to an ssh-agent process over
// the given connection.
//
// If rw also implements io.Closer (like *net.UnixConn and ssh.Channel
// do), the returned client pipelines concurrent requests over the
// connection: callers can issue Sign and other operations from
// multiple goroutines and they will be written to the agent as soon
// as the wire is available, rather than waiting for the previous
// responses. The ssh-agent protocol still requires responses to be
// returned in request order, so a slow request delays subsequent
// responses on the same connection (head-of-line blocking).
//
// Pipelining requires io.Closer because, on a Write error, the
// background reader goroutine must be unblocked by closing the
// underlying connection. When rw does not implement io.Closer
// this is not possible, so NewClient falls back to fully
// serializing each request: a single in-flight call at a time.
func NewClient(rw io.ReadWriter) ExtendedAgent {
if rwc, ok := rw.(io.ReadWriteCloser); ok {
return &client{pipeline: newPipeline(rwc)}
}
return &client{conn: rw}
}
@@ -340,6 +374,16 @@ func (c *client) call(req []byte) (reply interface{}, err error) {
// bytes of the response are returned; no unmarshalling is
// performed on the response.
func (c *client) callRaw(req []byte) (reply []byte, err error) {
if c.pipeline != nil {
return c.pipeline.call(req)
}
return c.serialCall(req)
}
// serialCall implements the fully-serialized request/response path
// used when the transport is not an io.Closer. It writes req under mu
// and reads the matching response before returning.
func (c *client) serialCall(req []byte) (reply []byte, err error) {
c.mu.Lock()
defer c.mu.Unlock()
@@ -577,6 +621,9 @@ func (c *client) insertKey(s interface{}, comment string, constraints []byte) er
Constraints: constraints,
})
case ed25519.PrivateKey:
if len(k) != ed25519.PrivateKeySize {
return fmt.Errorf("agent: bad ED25519 key size: %d", len(k))
}
req = ssh.Marshal(ed25519KeyMsg{
Type: ssh.KeyAlgoED25519,
Pub: []byte(k)[32:],
@@ -588,6 +635,9 @@ func (c *client) insertKey(s interface{}, comment string, constraints []byte) er
// general idiom is to pass ed25519.PrivateKey by value, not by pointer.
// We still support the pointer variant for backwards compatibility.
case *ed25519.PrivateKey:
if len(*k) != ed25519.PrivateKeySize {
return fmt.Errorf("agent: bad ED25519 key size: %d", len(*k))
}
req = ssh.Marshal(ed25519KeyMsg{
Type: ssh.KeyAlgoED25519,
Pub: []byte(*k)[32:],
@@ -712,6 +762,9 @@ func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string
Constraints: constraints,
})
case ed25519.PrivateKey:
if len(k) != ed25519.PrivateKeySize {
return fmt.Errorf("agent: bad ED25519 key size: %d", len(k))
}
req = ssh.Marshal(ed25519CertMsg{
Type: cert.Type(),
CertBytes: cert.Marshal(),
@@ -724,6 +777,9 @@ func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string
// general idiom is to pass ed25519.PrivateKey by value, not by pointer.
// We still support the pointer variant for backwards compatibility.
case *ed25519.PrivateKey:
if len(*k) != ed25519.PrivateKeySize {
return fmt.Errorf("agent: bad ED25519 key size: %d", len(*k))
}
req = ssh.Marshal(ed25519CertMsg{
Type: cert.Type(),
CertBytes: cert.Marshal(),
@@ -861,3 +917,170 @@ func (c *client) Extension(extensionType string, contents []byte) ([]byte, error
return buf, nil
}
// pipelineResult carries either a raw agent reply or an error back to a
// caller waiting on the response channel.
type pipelineResult struct {
reply []byte
err error
}
// pipeline implements request pipelining over a single agent connection.
//
// Writers serialize on writeMu to both register a reply channel in the
// pending FIFO queue and write the request bytes on the wire; the two
// must be atomic so the queue order matches the wire order. A single
// reader goroutine decodes responses from the connection and dispatches
// each one to the channel at the head of the queue.
//
// pending is a chan-of-chan acting as a FIFO queue with a fixed
// capacity of pipelineMaxInFlight. The outer channel provides ordering
// (reads happen in send order) and natural backpressure (a full queue
// blocks new writers). Each inner channel is buffered with capacity
// one and is sent to exactly once: either by the reader goroutine
// with the agent reply, or by shutdown with the terminal error during
// drain. The cap-one buffer makes the producer's send non-blocking,
// so the reader and shutdown never have to wait for the caller to be
// scheduled on the receive.
//
// When the reader goroutine exits (on read error or protocol
// violation), it closes exitCh to wake any writer blocked on the
// pending queue, then serializes with any in-flight writer to close
// the pending channel, and finally drains the remaining entries
// delivering the terminal error to each waiting caller. The
// pipeline relies on conn implementing io.Closer so a writer that
// hits a Write error can close the connection to unblock the reader
// goroutine; NewClient is responsible for only constructing a
// pipeline when this guarantee holds.
type pipeline struct {
conn io.ReadWriteCloser
writeMu sync.Mutex
// pending is the FIFO queue of reply channels with capacity
// pipelineMaxInFlight. See type-level documentation.
pending chan chan pipelineResult
exitCh chan struct{}
// err carries the terminal error to callers blocked on a closed
// pipeline. It is stored exactly once by the reader goroutine
// before exitCh is closed; every read happens after observing
// exitCh closed, so the load synchronises through the close and
// is guaranteed to return the stored value (never nil).
err atomic.Pointer[error]
}
func newPipeline(conn io.ReadWriteCloser) *pipeline {
p := &pipeline{
conn: conn,
pending: make(chan chan pipelineResult, pipelineMaxInFlight),
exitCh: make(chan struct{}),
}
go p.readLoop()
return p
}
// readLoop decodes responses from conn and dispatches them in FIFO order
// to reply channels in pending. On any failure it invokes shutdown.
func (p *pipeline) readLoop() {
var finalErr error
for {
var sizeBuf [4]byte
if _, err := io.ReadFull(p.conn, sizeBuf[:]); err != nil {
finalErr = err
break
}
respSize := binary.BigEndian.Uint32(sizeBuf[:])
if respSize > maxAgentResponseBytes {
finalErr = errors.New("response too large")
break
}
buf := make([]byte, respSize)
if _, err := io.ReadFull(p.conn, buf); err != nil {
finalErr = err
break
}
// Successful writes always enqueue before sending bytes, so
// pending has a waiting channel for this response.
ch := <-p.pending
// The reply channel is buffered with capacity 1 and is only
// ever written to once, so this send cannot block.
ch <- pipelineResult{reply: buf}
}
p.shutdown(clientErr(finalErr))
}
// shutdown is called exactly once, from readLoop, when the reader is
// terminating. It unblocks pending writers and fails all in-flight
// requests with finalErr.
func (p *pipeline) shutdown(finalErr error) {
// Publish the terminal error before closing exitCh so any
// writer that subsequently observes exitCh closed sees err.
p.err.Store(&finalErr)
// Wake any writer blocked waiting for a slot in the pending queue.
close(p.exitCh)
// Wait for any writer currently inside its critical section to
// complete. After this lock, no new writer can reach the send on
// pending: they will observe exitCh closed in the select and bail
// out before attempting the send.
p.writeMu.Lock()
close(p.pending)
p.writeMu.Unlock()
// Drain entries that were enqueued but never answered, delivering
// the terminal error to their waiting callers. The reply channels
// are buffered (cap 1) and written to exactly once, so these sends
// cannot block.
for ch := range p.pending {
ch <- pipelineResult{err: finalErr}
}
}
// call sends req to the agent and returns the matching raw response.
func (p *pipeline) call(req []byte) ([]byte, error) {
replyCh := make(chan pipelineResult, 1)
p.writeMu.Lock()
// Priority check: if the reader has already finished shutdown,
// pending is closed and sending to it would panic. Bail out now.
// Once we pass this check while holding writeMu, shutdown cannot
// complete close(pending) until we release writeMu, so the send
// below is safe against concurrent closure.
select {
case <-p.exitCh:
p.writeMu.Unlock()
return nil, *p.err.Load()
default:
}
// Enqueue the reply channel before writing the request, so FIFO
// order on the wire matches FIFO order in the pending queue. The
// exitCh arm handles the case where the reader errors while we
// block on a full queue.
select {
case p.pending <- replyCh:
case <-p.exitCh:
p.writeMu.Unlock()
return nil, *p.err.Load()
}
msg := make([]byte, 4+len(req))
binary.BigEndian.PutUint32(msg, uint32(len(req)))
copy(msg[4:], req)
_, werr := p.conn.Write(msg)
p.writeMu.Unlock()
if werr != nil {
// The connection is in an undefined state. Close it so the
// reader unblocks promptly and triggers shutdown for every
// other in-flight caller. NewClient guarantees conn is a
// real io.Closer when the pipeline is in use.
p.conn.Close()
return nil, clientErr(werr)
}
res := <-replyCh
return res.reply, res.err
}
+26 -4
View File
@@ -240,13 +240,35 @@ func setConstraints(key *AddedKey, constraintBytes []byte) error {
return nil
}
// checkRSAKeyParams enforces the same bounds as parseRSA in the ssh
// package, and additionally caps the prime factors. Without this,
// the rsa.PrivateKey built from an Add request would call Precompute()
// on arbitrary inputs; the CRT coefficient recomputation is cubic in
// |p| and can consume excessive CPU on oversized keys.
func checkRSAKeyParams(N, E, P, Q *big.Int) error {
if N.BitLen() > 8192 {
return errors.New("agent: RSA modulus too large")
}
if P.BitLen() > 4096 || Q.BitLen() > 4096 {
return errors.New("agent: RSA prime too large")
}
if E.BitLen() > 24 {
return errors.New("agent: RSA public exponent too large")
}
e := E.Int64()
if e < 3 || e&1 == 0 {
return errors.New("agent: incorrect RSA public exponent")
}
return nil
}
func parseRSAKey(req []byte) (*AddedKey, error) {
var k rsaKeyMsg
if err := ssh.Unmarshal(req, &k); err != nil {
return nil, err
}
if k.E.BitLen() > 30 {
return nil, errors.New("agent: RSA public exponent too large")
if err := checkRSAKeyParams(k.N, k.E, k.P, k.Q); err != nil {
return nil, err
}
priv := &rsa.PrivateKey{
PublicKey: rsa.PublicKey{
@@ -399,8 +421,8 @@ func parseRSACert(req []byte) (*AddedKey, error) {
return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err)
}
if rsaPub.E.BitLen() > 30 {
return nil, errors.New("agent: RSA public exponent too large")
if err := checkRSAKeyParams(rsaPub.N, rsaPub.E, k.P, k.Q); err != nil {
return nil, err
}
priv := rsa.PrivateKey{
+4 -1
View File
@@ -634,7 +634,10 @@ func (ch *channel) SendRequest(name string, wantReply bool, payload []byte) (boo
drain:
for {
select {
case <-ch.msg:
case _, ok := <-ch.msg:
if !ok {
break drain
}
default:
break drain
}
+85
View File
@@ -88,6 +88,32 @@ func NewClientConn(c net.Conn, addr string, config *ClientConfig) (Conn, <-chan
return conn, conn.mux.incomingChannels, conn.mux.incomingRequests, nil
}
// NewControlClientConn establishes an SSH connection over an OpenSSH
// ControlMaster socket c in proxy mode.
//
// Note that this package only implements the client side of the multiplexing
// protocol. The provided net.Conn must be a local, secure connection (such as a
// Unix domain socket) connected to an already-running OpenSSH process acting as
// the ControlMaster.
//
// WARNING: Because proxy mode bypasses the standard cryptographic handshake
// passing a standard network connection (e.g., TCP) will result in plaintext
// data leakage.
//
// The Request and NewChannel channels must be serviced or the connection
// will hang.
func NewControlClientConn(c net.Conn) (Conn, <-chan NewChannel, <-chan *Request, error) {
conn := &connection{
sshConn: sshConn{conn: c},
}
var err error
if conn.transport, err = handshakeControlProxy(c); err != nil {
return nil, nil, nil, fmt.Errorf("ssh: control proxy handshake failed: %w", err)
}
conn.mux = newMux(conn.transport)
return conn, conn.mux.incomingChannels, conn.mux.incomingRequests, nil
}
// clientHandshake performs the client side key exchange. See RFC 4253 Section
// 7.
func (c *connection) clientHandshake(dialAddress string, config *ClientConfig) error {
@@ -197,6 +223,59 @@ type HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
// the server. A BannerCallback receives the message sent by the remote server.
type BannerCallback func(message string) error
// ClientAuthContext contains information about the current state of the
// authentication process, passed to [ClientAuthCallback].
type ClientAuthContext struct {
// Metadata contains the connection metadata.
Metadata ConnMetadata
// Algorithms contains the negotiated algorithms.
Algorithms NegotiatedAlgorithms
// AllowedMethods lists the authentication methods currently accepted
// by the server. These are the protocol-level names defined in RFC 4252
// such as "publickey", "password".
AllowedMethods []string
// PartialSuccessMethods lists the authentication methods that have already
// succeeded, indicating a multi-step authentication flow. This list
// represents the exact sequence of partial successes and may contain
// duplicates if the same method succeeded multiple times.
PartialSuccessMethods []string
// TriedMethods lists the methods that have already been attempted and
// failed during this session. This list represents the exact sequence of
// failures and may contain duplicates. This allows the callback to also
// track the number of failed attempts for a specific method.
TriedMethods []string
}
// ClientAuthCallback is a hook invoked before each authentication attempt. It
// allows the client to dynamically select an authentication method based on the
// current context, server capabilities, or previous failures.
//
// The callback is invoked after the initial "none" authentication method, once
// the server's supported authentication methods are known.
//
// Return values:
// - (AuthMethod, nil): The client will attempt this specific method next.
// The returned method does NOT need to be present in [ClientConfig.Auth].
// This allows for dynamic authentication strategies (e.g., prompting
// for a password only if public key auth fails). Callers should inspect
// [ClientAuthContext.TriedMethods] to avoid repeatedly returning the
// same failing method.
// - (nil, nil): The client selects from [ClientConfig.Auth] the first
// instance of a method that has not been tried yet, or aborts if none
// are left. If authentication is not successful, the callback is invoked
// again before the following attempt.
// - (nil, error): The authentication process is aborted immediately,
// causing the ongoing SSH handshake to fail with the provided error.
//
// To bound resource use, the client caps the total number of authentication
// attempts (failures and partial successes combined) at 64. If the cap is
// exceeded the handshake aborts with an error.
type ClientAuthCallback func(ctx *ClientAuthContext) (AuthMethod, error)
// A ClientConfig structure is used to configure a Client. It must not be
// modified after having been passed to an SSH function.
type ClientConfig struct {
@@ -210,6 +289,9 @@ type ClientConfig struct {
// Auth contains possible authentication methods to use with the
// server. Only the first instance of a particular RFC 4252 method will
// be used during authentication.
//
// If AuthCallback is set, these AuthMethod are only used if the
// callback returns nil.
Auth []AuthMethod
// HostKeyCallback is called during the cryptographic
@@ -240,6 +322,9 @@ type ClientConfig struct {
//
// A Timeout of zero means no timeout.
Timeout time.Duration
// AuthCallback, if non-nil, is invoked before each authentication attempt.
AuthCallback ClientAuthCallback
}
// InsecureIgnoreHostKey returns a function that can be used for
+50 -14
View File
@@ -21,6 +21,12 @@ const (
authSuccess
)
// maxAuthClientTried bounds the total number of authentication attempts
// (failures and partial successes combined) the client makes before
// aborting the loop, to prevent unbounded growth when an AuthCallback
// keeps supplying methods.
const maxAuthClientTried = 64
// clientAuthenticate authenticates with the remote server. See RFC 4252.
func (c *connection) clientAuthenticate(config *ClientConfig) error {
// initiate user auth session
@@ -67,32 +73,62 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
// then any untried methods suggested by the server.
var tried []string
var lastMethods []string
var partialSuccess []string
sessionID := c.transport.getSessionID()
for auth := AuthMethod(new(noneAuth)); auth != nil; {
ok, methods, err := auth.auth(sessionID, config.User, c.transport, config.Rand, extensions)
if err != nil {
// On disconnect, return error immediately
if _, ok := err.(*disconnectMsg); ok {
if _, isDisconnect := err.(*disconnectMsg); isDisconnect {
return err
}
// We return the error later if there is no other method left to
// try.
// We return the error later if there is no other method
// left to try.
ok = authFailure
}
if ok == authSuccess {
// success
switch ok {
case authSuccess:
return nil
} else if ok == authFailure {
if m := auth.method(); !slices.Contains(tried, m) {
tried = append(tried, m)
}
case authPartialSuccess:
partialSuccess = append(partialSuccess, auth.method())
case authFailure:
tried = append(tried, auth.method())
}
if len(partialSuccess)+len(tried) > maxAuthClientTried {
return fmt.Errorf("ssh: too many authentication attempts (%d), aborting",
len(partialSuccess)+len(tried))
}
if methods == nil {
methods = lastMethods
}
lastMethods = methods
// If AuthCallback is set it takes precedence: it picks the next
// AuthMethod dynamically. The returned method need not be in
// config.Auth. If the callback returns (nil, nil) we fall back to
// selecting the next untried method from config.Auth below; on
// (nil, error) the handshake aborts.
if config.AuthCallback != nil {
ctx := &ClientAuthContext{
Metadata: c,
Algorithms: c.Algorithms(),
AllowedMethods: slices.Clone(methods),
PartialSuccessMethods: slices.Clone(partialSuccess),
TriedMethods: slices.Clone(tried),
}
altAuth, cbErr := config.AuthCallback(ctx)
if cbErr != nil {
return cbErr
}
if altAuth != nil {
auth = altAuth
continue
}
}
auth = nil
findNext:
@@ -377,11 +413,11 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
return authFailure, nil, err
}
// If authentication succeeds or the list of available methods does not
// contain the "publickey" method, do not attempt to authenticate with any
// other keys. According to RFC 4252 Section 7, the latter can occur when
// additional authentication methods are required.
if success == authSuccess || !slices.Contains(methods, cb.method()) {
// If authentication succeeds or partially succeeds, return immediately
// so the caller can select the next auth method. According to RFC 4252
// Section 7, if the server no longer lists "publickey" among its
// allowed methods, do not attempt to authenticate with any other keys.
if success == authSuccess || success == authPartialSuccess || !slices.Contains(methods, cb.method()) {
return success, methods, err
}
}
+9 -1
View File
@@ -91,9 +91,17 @@ func DiscardRequests(in <-chan *Request) {
}
}
// A connTransport represents the transport for a connection.
type connTransport interface {
packetConn
getAlgorithms() NegotiatedAlgorithms
getSessionID() []byte
waitSession() error
}
// A connection represents an incoming connection.
type connection struct {
transport *handshakeTransport
transport connTransport
sshConn
// The connection protocol.
+155
View File
@@ -0,0 +1,155 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"encoding/binary"
"errors"
"fmt"
"io"
"golang.org/x/crypto/cryptobyte"
)
const (
muxProtocolVersion = 4
muxMsgHello = 0x00000001
muxCProxy = 0x1000000f
muxSProxy = 0x8000000f
)
const controlProxyRequestID = 0
// handshakeControlProxy attempts to establish a transport connection with an
// OpenSSH ControlMaster socket in proxy mode. For details see:
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.mux
func handshakeControlProxy(rw io.ReadWriteCloser) (connTransport, error) {
if err := controlProxyWritePacket(rw, func(b *cryptobyte.Builder) {
b.AddUint32(muxMsgHello)
b.AddUint32(muxProtocolVersion)
}); err != nil {
return nil, fmt.Errorf("mux hello write failed: %w", err)
}
if err := controlProxyWritePacket(rw, func(b *cryptobyte.Builder) {
b.AddUint32(muxCProxy)
b.AddUint32(controlProxyRequestID)
}); err != nil {
return nil, fmt.Errorf("mux client proxy write failed: %w", err)
}
messageType, body, err := controlProxyReadMessage(rw)
if err != nil {
return nil, fmt.Errorf("mux hello read failed: %w", err)
}
if messageType != muxMsgHello {
return nil, fmt.Errorf("expected hello response, got %v", messageType)
}
var v uint32
if !body.ReadUint32(&v) {
return nil, errors.New("EOF reading mux protocol version")
}
if v != muxProtocolVersion {
return nil, fmt.Errorf("mux server has unsupported version %v", v)
}
messageType, body, err = controlProxyReadMessage(rw)
if err != nil {
return nil, fmt.Errorf("mux server proxy read failed: %w", err)
}
if messageType != muxSProxy {
return nil, fmt.Errorf("expected server proxy response, got %v", messageType)
}
var reqID uint32
if !body.ReadUint32(&reqID) {
return nil, errors.New("EOF reading request id")
}
if reqID != controlProxyRequestID {
return nil, fmt.Errorf("expected request id %v, got %v", controlProxyRequestID, reqID)
}
return &controlProxyTransport{rw}, nil
}
// controlProxyTransport implements the connTransport interface for
// ControlMaster connections. Each controlMessage has zero length padding and
// no MAC.
type controlProxyTransport struct {
rw io.ReadWriteCloser
}
func (p *controlProxyTransport) Close() error {
return p.rw.Close()
}
func (p *controlProxyTransport) writePacket(controlMessage []byte) error {
return controlProxyWritePacket(p.rw, func(b *cryptobyte.Builder) {
b.AddUint8(0) // Padding length.
b.AddBytes(controlMessage)
})
}
func (p *controlProxyTransport) readPacket() ([]byte, error) {
buf, err := controlProxyReadPacket(p.rw)
if err != nil {
return nil, fmt.Errorf("ssh: error reading control message: %w", err)
}
// Discard the padding length.
if len(buf) < 1 {
return nil, errors.New("ssh: EOF reading padding length")
}
if buf[0] != 0 {
return nil, errors.New("ssh: unexpected non-zero padding in control message")
}
return buf[1:], nil
}
func (p *controlProxyTransport) getAlgorithms() NegotiatedAlgorithms {
return NegotiatedAlgorithms{}
}
func (p *controlProxyTransport) getSessionID() []byte {
return nil
}
func (p *controlProxyTransport) waitSession() error {
return nil
}
func controlProxyWritePacket(w io.Writer, f cryptobyte.BuilderContinuation) error {
var buf []byte
b := cryptobyte.NewBuilder(buf)
b.AddUint32LengthPrefixed(f)
out, err := b.Bytes()
if err != nil {
return err
}
_, err = w.Write(out)
return err
}
func controlProxyReadPacket(r io.Reader) (cryptobyte.String, error) {
var l uint32
if err := binary.Read(r, binary.BigEndian, &l); err != nil {
return nil, err
}
if l > maxPacket {
return nil, fmt.Errorf("message length %v exceeds maximum %v", l, maxPacket)
}
buf := make([]byte, l)
if _, err := io.ReadFull(r, buf); err != nil {
return nil, err
}
return buf, nil
}
func controlProxyReadMessage(r io.Reader) (messageType uint32, body cryptobyte.String, err error) {
body, err = controlProxyReadPacket(r)
if err != nil {
return 0, nil, fmt.Errorf("error reading message body: %w", err)
}
if !body.ReadUint32(&messageType) {
return 0, nil, errors.New("EOF reading message type")
}
return messageType, body, nil
}
+66 -9
View File
@@ -16,6 +16,7 @@ import (
"io"
"math/big"
"slices"
"sync"
"golang.org/x/crypto/curve25519"
)
@@ -718,15 +719,9 @@ func (gex *dhGEXSHA) Server(c packetConn, randSource io.Reader, magics *handshak
kexDHGexRequest.MaxBits, kexDHGexRequest.PreferredBits)
}
var p *big.Int
// We hardcode sending Oakley Group 14 (2048 bits), Oakley Group 15 (3072
// bits) or Oakley Group 16 (4096 bits), based on the requested max size.
if kexDHGexRequest.MaxBits < 3072 {
p, _ = new(big.Int).SetString(oakleyGroup14, 16)
} else if kexDHGexRequest.MaxBits < 4096 {
p, _ = new(big.Int).SetString(oakleyGroup15, 16)
} else {
p, _ = new(big.Int).SetString(oakleyGroup16, 16)
p, err := chooseDH(kexDHGexRequest)
if err != nil {
return nil, err
}
g := big.NewInt(2)
@@ -805,3 +800,65 @@ func (gex *dhGEXSHA) Server(c packetConn, randSource io.Reader, magics *handshak
Hash: gex.hashFunc,
}, err
}
type dhKEXGroup struct {
size int
p *big.Int
}
// supportedDHKEXGroups returns the DH groups the server is willing to offer
// for diffie-hellman-group-exchange-* key exchanges. The list is built lazily
// on first use to keep the hex-to-big.Int parse out of package initialization.
var supportedDHKEXGroups = sync.OnceValue(func() []dhKEXGroup {
specs := []struct {
size int
hex string
}{
{2048, oakleyGroup14},
{3072, oakleyGroup15},
{4096, oakleyGroup16},
}
out := make([]dhKEXGroup, 0, len(specs))
for _, s := range specs {
p, _ := new(big.Int).SetString(s.hex, 16)
out = append(out, dhKEXGroup{size: s.size, p: p})
}
return out
})
// chooseDH picks a DH group for the given client request, mirroring the
// algorithm used by OpenSSH's choose_dh in dh.c: prefer the smallest known
// group larger than or equal to the client's PreferredBits, and otherwise pick
// the largest group within the accepted [MinBits, MaxBits] range.
func chooseDH(req kexDHGexRequestMsg) (*big.Int, error) {
var best *big.Int
bestSize := 0
wantBits := int(req.PreferredBits)
for _, group := range supportedDHKEXGroups() {
if uint32(group.size) < req.MinBits || uint32(group.size) > req.MaxBits {
continue
}
if bestSize == 0 {
best = group.p
bestSize = group.size
continue
}
closerFromAbove := group.size >= wantBits && group.size < bestSize
closerFromBelow := group.size > bestSize && bestSize < wantBits
if closerFromAbove || closerFromBelow {
best = group.p
bestSize = group.size
}
}
if bestSize == 0 {
return nil, fmt.Errorf("ssh: no suitable DH group found for request min: %d, preferred: %d, max: %d",
req.MinBits, req.PreferredBits, req.MaxBits)
}
return best, nil
}
+38 -3
View File
@@ -76,7 +76,7 @@ func parsePubKey(in []byte, algo string) (pubKey PublicKey, rest []byte, err err
case InsecureKeyAlgoDSA:
return parseDSA(in)
case KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521:
return parseECDSA(in)
return parseECDSA(in, algo)
case KeyAlgoSKECDSA256:
return parseSKECDSA(in)
case KeyAlgoED25519:
@@ -806,7 +806,7 @@ func supportedEllipticCurve(curve elliptic.Curve) bool {
}
// parseECDSA parses an ECDSA key according to RFC 5656, section 3.1.
func parseECDSA(in []byte) (out PublicKey, rest []byte, err error) {
func parseECDSA(in []byte, expectedType string) (out PublicKey, rest []byte, err error) {
var w struct {
Curve string
KeyBytes []byte
@@ -817,6 +817,12 @@ func parseECDSA(in []byte) (out PublicKey, rest []byte, err error) {
return nil, nil, err
}
actualType := "ecdsa-sha2-" + w.Curve
if expectedType != actualType {
return nil, nil, fmt.Errorf("ssh: algorithm type mismatch: expected %q, found curve %q (type %q)",
expectedType, w.Curve, actualType)
}
key := new(ecdsa.PublicKey)
switch w.Curve {
@@ -1466,6 +1472,17 @@ func passphraseProtectedOpenSSHKey(passphrase []byte) openSSHDecryptFunc {
return nil, err
}
// OpenSSH does not impose an upper bound on the bcrypt round count
// stored in the key file, but bcrypt_pbkdf cost is linear in rounds:
// the default is 16, ssh-keygen lets users pick anything up to
// INT_MAX. Cap at 2048 (128x the default, a few seconds of CPU) so
// that an oversized value in the file cannot tie up the caller for
// months.
const maxRounds = 1 << 11
if opts.Rounds > maxRounds {
return nil, fmt.Errorf("ssh: bcrypt KDF rounds %d exceed maximum %d", opts.Rounds, maxRounds)
}
k, err := bcrypt_pbkdf.Key(passphrase, []byte(opts.Salt), int(opts.Rounds), 32+16)
if err != nil {
return nil, err
@@ -1635,10 +1652,28 @@ func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.Priv
return nil, err
}
// Mirror the validation done in parseRSA for public keys: cap the
// modulus at the same limit enforced by crypto/tls, reject oversized
// or invalid exponents, and additionally bound the prime factors to
// avoid the expensive CRT coefficient recomputation in pk.Precompute.
if key.N.BitLen() > 8192 {
return nil, errors.New("ssh: rsa modulus too large")
}
if key.P.BitLen() > 4096 || key.Q.BitLen() > 4096 {
return nil, errors.New("ssh: rsa prime too large")
}
if key.E.BitLen() > 24 {
return nil, errors.New("ssh: exponent too large")
}
e := key.E.Int64()
if e < 3 || e&1 == 0 {
return nil, errors.New("ssh: incorrect exponent")
}
pk := &rsa.PrivateKey{
PublicKey: rsa.PublicKey{
N: key.N,
E: int(key.E.Int64()),
E: int(e),
},
D: key.D,
Primes: []*big.Int{key.P, key.Q},
+4 -1
View File
@@ -155,7 +155,10 @@ func (m *mux) SendRequest(name string, wantReply bool, payload []byte) (bool, []
drain:
for {
select {
case <-m.globalResponses:
case _, ok := <-m.globalResponses:
if !ok {
break drain
}
default:
break drain
}
+33 -5
View File
@@ -54,6 +54,9 @@ type Permissions struct {
ExtraData map[any]any
}
// GSSAPIWithMICConfig includes the server callbacks for gssapi-with-mic
// authentication. If either field is nil, gssapi-with-mic is considered not
// configured.
type GSSAPIWithMICConfig struct {
// AllowLogin, must be set, is called when gssapi-with-mic
// authentication is selected (RFC 4462 section 3). The srcName is from the
@@ -68,6 +71,10 @@ type GSSAPIWithMICConfig struct {
Server GSSAPIServer
}
func gssapiWithMICConfigured(config *GSSAPIWithMICConfig) bool {
return config != nil && config.AllowLogin != nil && config.Server != nil
}
// SendAuthBanner implements [ServerPreAuthConn].
func (s *connection) SendAuthBanner(msg string) error {
return s.transport.writePacket(Marshal(&userAuthBannerMsg{
@@ -382,8 +389,7 @@ func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error)
}
if !config.NoClientAuth && config.PasswordCallback == nil && config.PublicKeyCallback == nil &&
config.KeyboardInteractiveCallback == nil && (config.GSSAPIWithMICConfig == nil ||
config.GSSAPIWithMICConfig.AllowLogin == nil || config.GSSAPIWithMICConfig.Server == nil) {
config.KeyboardInteractiveCallback == nil && !gssapiWithMICConfigured(config.GSSAPIWithMICConfig) {
return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false")
}
@@ -607,6 +613,15 @@ func (b *BannerError) Error() string {
return b.Err.Error()
}
// maxAuthServerAttempts caps the total number of SSH_MSG_USERAUTH_REQUEST
// messages the server will process on a single connection, regardless of
// outcome (failure, partial success, public key query, or none). It is a
// backstop against clients that drive the authentication loop indefinitely
// without ever incurring a real failure — for example by repeatedly
// triggering PartialSuccessError or by spamming public key offer queries —
// neither of which increment the MaxAuthTries failure counter.
const maxAuthServerAttempts = 128
func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, error) {
if config.PreAuthConnCallback != nil {
config.PreAuthConnCallback(s)
@@ -617,6 +632,7 @@ func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, err
var perms *Permissions
authFailures := 0
authAttempts := 0
noneAuthCount := 0
var authErrs []error
var calledBannerCallback bool
@@ -645,6 +661,19 @@ userAuthLoop:
return nil, &ServerAuthError{Errors: authErrs}
}
if authAttempts >= maxAuthServerAttempts {
discMsg := &disconnectMsg{
Reason: 2,
Message: "too many authentication attempts",
}
if err := s.transport.writePacket(Marshal(discMsg)); err != nil {
return nil, err
}
authErrs = append(authErrs, discMsg)
return nil, &ServerAuthError{Errors: authErrs}
}
authAttempts++
var userAuthReq userAuthRequestMsg
if packet, err := s.transport.readPacket(); err != nil {
if err == io.EOF {
@@ -846,7 +875,7 @@ userAuthLoop:
}
}
case "gssapi-with-mic":
if authConfig.GSSAPIWithMICConfig == nil {
if !gssapiWithMICConfigured(authConfig.GSSAPIWithMICConfig) {
authErr = errors.New("ssh: gssapi-with-mic auth not configured")
break
}
@@ -979,8 +1008,7 @@ userAuthLoop:
if authConfig.KeyboardInteractiveCallback != nil {
failureMsg.Methods = append(failureMsg.Methods, "keyboard-interactive")
}
if authConfig.GSSAPIWithMICConfig != nil && authConfig.GSSAPIWithMICConfig.Server != nil &&
authConfig.GSSAPIWithMICConfig.AllowLogin != nil {
if gssapiWithMICConfigured(authConfig.GSSAPIWithMICConfig) {
failureMsg.Methods = append(failureMsg.Methods, "gssapi-with-mic")
}
+3
View File
@@ -423,6 +423,9 @@ func (s *Session) wait(reqs <-chan *Request) error {
for msg := range reqs {
switch msg.Type {
case "exit-status":
if len(msg.Payload) < 4 {
return errors.New("ssh: malformed exit-status request")
}
wm.status = int(binary.BigEndian.Uint32(msg.Payload))
case "exit-signal":
var sigval struct {
+1 -1
View File
@@ -1310,7 +1310,7 @@ go.yaml.in/yaml/v3
## explicit; go 1.18
go.yaml.in/yaml/v4
go.yaml.in/yaml/v4/internal/libyaml
# golang.org/x/crypto v0.52.0
# golang.org/x/crypto v0.53.0
## explicit; go 1.25.0
golang.org/x/crypto/argon2
golang.org/x/crypto/bcrypt