Compare commits

..
12 Commits
Author SHA1 Message Date
Gopher Robot 3f62bf119e go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: I39f495ef42016b2ab0c91f54b792ac16b967006f
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/829324
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: Cherry Mui <cherryyz@google.com>
2026-09-08 11:05:01 -07:00
Nicola Murino 86efde54dc ssh: reject unexpected message types on established channels
ch.msg is only read while the channel open or a channel request with a
reply is pending, so anything the default arm of channel.handlePacket
delivered to it was never consumed. The blocking send there let a
misbehaving peer fill the buffer with well-formed but unexpected message
types carrying a valid channel id and stall the mux read loop,
deadlocking the whole connection.

No conforming peer sends such messages during the connection protocol.
Treat them as a protocol error and tear the connection down, as
handleUnknownChannelPacket already does for the same messages when the
channel id is not in use.

Fixes CVE-2026-56855
Fixes golang/go#81317

Change-Id: I87420dfe68fcb62a17df4b47dc5ffb6ccd72ba26
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/826524
Reviewed-by: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Neal Patel <nealpatel@google.com>
Reviewed-by: Nicholas Husin <husin@google.com>
2026-09-02 11:02:47 -07:00
Nicola Murino a6cdac6084 ssh: drop traffic on undecided channels
A channel in the mux's chanList is not usable until it is established:
an outbound channel has no confirmed remote id until the peer's open
confirmation, and an inbound channel is not serviced by the application
until it is accepted. handlePacket processed any channel message on it,
so a misbehaving peer could flood channel requests and block the mux
read loop on the send to incomingRequests, deadlocking the connection,
or close an outbound channel before confirming it, making the victim
tear down a half-initialized channel and emit a close for remote id 0,
an unrelated channel of the peer.

No such packet can be legitimate: the peer learns an inbound channel's
local id only from the confirmation we have not sent yet, and on an
outbound channel RFC 4254 lets it answer the open request only with a
confirmation or a failure.

Add an established flag, set when the channel becomes usable: for an
outbound channel when the open response is received, for an inbound
channel by Accept before the confirmation is sent. Until then
handlePacket drops every packet other than the open response. The flag
is separate from decided, which Reject also sets: a rejected channel is
decided but must never carry traffic.

Fixes CVE-2026-78662
Fixes golang/go#81316

Change-Id: Ib0983bb216a49808a2db1f4a4d92ee9fe38a3c51
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/826504
Auto-Submit: Gopher Robot <gobot@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Neal Patel <nealpatel@google.com>
Reviewed-by: Nicholas Husin <husin@google.com>
2026-09-02 10:48:31 -07:00
Nicola Murino 39dc44e69c ssh: don't skip the source-address critical option in CheckCert
CertChecker.CheckCert ignored source-address on the assumption that
serverAuthenticate would enforce it, but that only happens in the
server-side user authentication path. Nothing enforced it in the host key
path, so CheckHostKey accepted CA-signed host certificates carrying
source-address regardless of the server's address, and the same applied to
applications calling CheckCert directly.

Drop the special case: every critical option must be listed in
SupportedCriticalOptions, host and user certificates alike. Authenticate,
the only path where serverAuthenticate does enforce source-address, checks
against a copy of the CertChecker with that option appended.

Fixes golang/go#80872

Change-Id: I3c3554b71ea2a4ce4b17696a9596b973b7d74b00
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/816840
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Auto-Submit: Nicola Murino <nicola.murino@gmail.com>
2026-08-30 20:04:51 -07:00
Dmitri Shuralyov afebf4cb4e x509roots/fallback/bundle: make subjectsEqual stricter on Go 1.27+
This gradual step towards removing/inlining subjectsEqual can happen
after Go 1.27.0 is released and the bundle starts being generated
with it.

Change-Id: Ib315bae69c45922c3f0aac5d2c0155fb5d94229f
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/775782
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
2026-08-26 07:40:58 -07:00
Gopher Robot 89f4e9bb5b x509roots/fallback: update bundle
This is an automated CL which updates the NSS root bundle.

[git-generate]
go generate ./x509roots

Change-Id: I3be04cd4cb6e44856beaa074fdb62a58ff8ce35b
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/820300
Auto-Submit: Gopher Robot <gobot@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
2026-08-24 13:54:08 -07:00
Nicola Murino 71488c48c2 ssh/knownhosts: compare only public key portions for revocation
Revocation matching compared full marshaled key blobs, so a @revoked
line containing a plain host key did not revoke a certificate
certifying that key: when the server presented such a certificate,
IsRevoked only matched the whole certificate blob or the CA key, and
verification succeeded even though the underlying host key was revoked.

OpenSSH's check_key_not_revoked compares keys with sshkey_equal_public,
which considers only the public portions and explicitly allows
comparisons between certificates and plain keys. Match that behavior by
normalizing both @revoked entries and lookups to the underlying public
key: for a certificate, the certified key rather than the certificate
blob. This also makes a @revoked line containing a certificate revoke
the plain key it certifies

Change-Id: I3029d59e68fb01f2340763e2eae25cac23c6193d
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/816841
Reviewed-by: Filippo Valsorda <filippo@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
2026-08-23 06:53:19 -07:00
Daniel McCarney 82adefa711 ssh: synchronize unexpected response test
Wait for an ordered channel request after sending the unsolicited
response flood. This ensures that the client mux has discarded the
entire flood before SendRequest opens its response gate, avoiding
confusion between an in-flight unsolicited response and the expected
reply.

This fixes TestChannelUnexpectedResponsesDiscarded from flaking, where
otherwise running 1000000 iterations of the test with GOMAXPROCS=16
produced ~15 distinct failures of the form  "expected success reply".

Change-Id: Ic1125c1f44bd919a5cf32b889d331bbc94c06c25
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/817140
Reviewed-by: Neal Patel <neal@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Neal Patel <nealpatel@google.com>
Reviewed-by: Nicola Murino <nicola.murino@gmail.com>
Auto-Submit: Daniel McCarney <daniel@binaryparadox.net>
Reviewed-by: David Chase <drchase@google.com>
2026-08-19 13:42:46 -07:00
Gopher Robot c757c9851f all: upgrade go directive to at least 1.26.0 [generated]
By now Go 1.27.0 has been released, and Go 1.25 is no longer supported
per the Go Release Policy (see https://go.dev/doc/devel/release#policy).

See https://go.dev/doc/godebug#go-126 for GODEBUG setting changes
relevant to Go 1.26.

For golang/go#69095.

[git-generate]
(cd . && go get go@1.26.0 && go mod tidy)
(cd x509roots/fallback && go get go@1.26.0 && go mod tidy)

Change-Id: I54826e019edd4e12ad821915b1d102de719bfb31
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/817981
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Auto-Submit: Gopher Robot <gobot@golang.org>
2026-08-19 10:16:24 -07:00
Damien Miller 593c81af8a ssh: correctly ignore pre-banner lines
Reading the peer's benner in readVersion() was incorrectly counting
lines sent by the server before the actual `SSH-2.0` banner against
the line limit for the banner itself.

This is incorrect per RFC 4253 section 4.2, where the limit of 255
characters is written to apply only to the `SSH-` banner string
itself.

This instead limits pre-banner lines to 255 characters each but
instead allows a maximum of 1024 lines.

Change-Id: Ief014bb3413e26e8d2077d3eb204ceaea9dc9444
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/811320
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Nicola Murino <nicola.murino@gmail.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
2026-08-17 08:34:06 -07:00
Daniel McCarney 46efc8bc62 acme: add crypto.SignMessage test coverage
This commit updates the pre-existing TestJWSEncodeJSONCustom test to
also verify JWS signature production works with a crypto.MessageSigner.

Change-Id: I5156cb4da9a7feee98340ff4fb75848da5827055
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/815480
Auto-Submit: Daniel McCarney <daniel@binaryparadox.net>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
Reviewed-by: Carlos Amedee <carlos@golang.org>
2026-08-17 08:22:44 -07:00
Daniel Tang e0a54fdc4b acme: use crypto.SignMessage instead of calling (crypto.Signer).Sign
crypto.SignMessage allows using crypto.Signers that only usefully
implement SignMessage to be used for the ACME account key.

This would be useful for opening up integrations with opinionated
remote signing APIs that don't support pre-hashing.

Fixes golang/go#80877

Change-Id: I86bb76cf5865d3d2b8606a6dbd813cbb26447c86
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/815340
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Sean Liao <sean@liao.dev>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Carlos Amedee <carlos@golang.org>
2026-08-17 08:21:42 -07:00
17 changed files with 540 additions and 98 deletions
+5 -8
View File
@@ -103,9 +103,7 @@ func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid KeyID, nonce, ur
}
payload = base64.RawURLEncoding.EncodeToString(cs)
}
hash := sha.New()
hash.Write([]byte(phead + "." + payload))
sig, err := jwsSign(key, sha, hash.Sum(nil))
sig, err := jwsSign(key, sha, []byte(phead+"."+payload))
if err != nil {
return nil, err
}
@@ -195,14 +193,13 @@ func jwkEncode(pub crypto.PublicKey) (string, error) {
return "", ErrUnsupportedKey
}
// jwsSign signs the digest using the given key.
// The hash is unused for ECDSA keys.
func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {
// jwsSign signs the payload using the given key.
func jwsSign(key crypto.Signer, hash crypto.Hash, payload []byte) ([]byte, error) {
switch pub := key.Public().(type) {
case *rsa.PublicKey:
return key.Sign(rand.Reader, digest, hash)
return crypto.SignMessage(key, rand.Reader, payload, hash)
case *ecdsa.PublicKey:
sigASN1, err := key.Sign(rand.Reader, digest, hash)
sigASN1, err := crypto.SignMessage(key, rand.Reader, payload, hash)
if err != nil {
return nil, err
}
+39 -5
View File
@@ -14,6 +14,7 @@ import (
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"math/big"
@@ -353,6 +354,22 @@ func (s *customTestSigner) Sign(io.Reader, []byte, crypto.SignerOpts) ([]byte, e
return s.sig, nil
}
type customTestMessageSigner struct {
customTestSigner
msg []byte
opts crypto.SignerOpts
}
func (s *customTestMessageSigner) Sign(io.Reader, []byte, crypto.SignerOpts) ([]byte, error) {
return nil, errors.New("customTestSigner: Sign() called instead of SignMessage()")
}
func (s *customTestMessageSigner) SignMessage(_ io.Reader, msg []byte, opts crypto.SignerOpts) ([]byte, error) {
s.msg = msg
s.opts = opts
return s.sig, nil
}
func TestJWSEncodeJSONCustom(t *testing.T) {
claims := struct{ Msg string }{"hello"}
const (
@@ -391,24 +408,33 @@ func TestJWSEncodeJSONCustom(t *testing.T) {
)
tt := []struct {
alg, phead string
name, phead string
pub crypto.PublicKey
stdsig, jwsig string
msgSigner bool
}{
{"ES256", es256phead, testKeyEC.Public(), es256stdsig, es256jwsig},
{"RS256", rs256phead, testKey.Public(), testsig, testsig},
{"ES256", es256phead, testKeyEC.Public(), es256stdsig, es256jwsig, false},
{"RS256", rs256phead, testKey.Public(), testsig, testsig, false},
{"ES256-MessageSigner", es256phead, testKeyEC.Public(), es256stdsig, es256jwsig, true},
{"RS256-MessageSigner", rs256phead, testKey.Public(), testsig, testsig, true},
}
for _, tc := range tt {
tc := tc
t.Run(tc.alg, func(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
stdsig, err := base64.RawStdEncoding.DecodeString(tc.stdsig)
if err != nil {
t.Errorf("couldn't decode test vector: %v", err)
}
signer := &customTestSigner{
cs := customTestSigner{
sig: stdsig,
pub: tc.pub,
}
var signer crypto.Signer = &cs
var ms *customTestMessageSigner
if tc.msgSigner {
ms = &customTestMessageSigner{customTestSigner: cs}
signer = ms
}
b, err := jwsEncodeJSON(claims, signer, noKeyID, "nonce", "url")
if err != nil {
@@ -427,6 +453,14 @@ func TestJWSEncodeJSONCustom(t *testing.T) {
if j.Sig != tc.jwsig {
t.Errorf("j.Sig = %q\nwant %q", j.Sig, tc.jwsig)
}
if ms != nil {
if want := tc.phead + "." + payload; string(ms.msg) != want {
t.Errorf("msg = %q; want %q", ms.msg, want)
}
if ms.opts != crypto.SHA256 {
t.Errorf("opts = %v; want %v", ms.opts, crypto.SHA256)
}
}
})
}
}
+5 -5
View File
@@ -1,11 +1,11 @@
module golang.org/x/crypto
go 1.25.0
go 1.26.0
require (
golang.org/x/net v0.57.0 // tagx:ignore
golang.org/x/sys v0.47.0
golang.org/x/term v0.45.0
golang.org/x/net v0.58.0 // tagx:ignore
golang.org/x/sys v0.48.0
golang.org/x/term v0.46.0
)
require golang.org/x/text v0.41.0 // indirect
require golang.org/x/text v0.42.0 // indirect
+8 -8
View File
@@ -1,8 +1,8 @@
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
+17 -21
View File
@@ -10,6 +10,7 @@ import (
"fmt"
"io"
"net"
"slices"
"sort"
"time"
)
@@ -305,8 +306,11 @@ const sourceAddressCriticalOption = "source-address"
// minimally, the IsAuthority callback should be set.
type CertChecker struct {
// SupportedCriticalOptions lists the CriticalOptions that the
// server application layer understands. These are only used
// for user certificates.
// application layer understands. A certificate carrying a critical
// option that is not listed here is rejected.
// CertChecker.Authenticate additionally accepts the source-address
// option, which the server enforces on the Permissions that
// Authenticate returns.
SupportedCriticalOptions []string
// IsUserAuthority should return true if the key is recognized as an
@@ -369,8 +373,9 @@ func (c *CertChecker) CheckHostKey(addr string, remote net.Addr, key PublicKey)
return c.CheckCert(hostname, cert)
}
// Authenticate checks a user certificate. Authenticate can be used as
// a value for ServerConfig.PublicKeyCallback.
// Authenticate checks a user certificate. Authenticate can be used as a value
// for ServerConfig.PublicKeyCallback. The source-address critical option is
// allowed, as it will be enforced by the server.
func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permissions, error) {
cert, ok := pubKey.(*Certificate)
if !ok {
@@ -389,8 +394,11 @@ func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permis
if !c.IsUserAuthority(cert.SignatureKey) {
return nil, fmt.Errorf("ssh: certificate signed by unrecognized authority")
}
if err := c.CheckCert(conn.User(), cert); err != nil {
// The source-address critical option is enforced by serverAuthenticate,
// so it is supported regardless of SupportedCriticalOptions
cc := *c
cc.SupportedCriticalOptions = append(slices.Clip(cc.SupportedCriticalOptions), sourceAddressCriticalOption)
if err := cc.CheckCert(conn.User(), cert); err != nil {
return nil, err
}
@@ -398,27 +406,15 @@ func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permis
}
// CheckCert checks CriticalOptions, ValidPrincipals, revocation, timestamp and
// the signature of the certificate.
// the signature of the certificate. Critical options that are not listed in
// SupportedCriticalOptions are rejected.
func (c *CertChecker) CheckCert(principal string, cert *Certificate) error {
if c.IsRevoked != nil && c.IsRevoked(cert) {
return fmt.Errorf("ssh: certificate serial %d revoked", cert.Serial)
}
for opt := range cert.CriticalOptions {
// sourceAddressCriticalOption will be enforced by
// serverAuthenticate
if opt == sourceAddressCriticalOption {
continue
}
found := false
for _, supp := range c.SupportedCriticalOptions {
if supp == opt {
found = true
break
}
}
if !found {
if !slices.Contains(c.SupportedCriticalOptions, opt) {
return fmt.Errorf("ssh: unsupported critical option %q in certificate", opt)
}
}
+76
View File
@@ -385,6 +385,82 @@ func TestHostKeyCert(t *testing.T) {
}
}
type testConnMeta struct {
ConnMetadata
user string
}
func (c testConnMeta) User() string { return c.user }
func TestCertCriticalOptions(t *testing.T) {
appended := make([]string, 2)
appended[0], appended[1] = "supported-option", "appended-option"
supported := appended[:1]
checker := &CertChecker{
SupportedCriticalOptions: supported,
IsHostAuthority: func(p PublicKey, addr string) bool {
return bytes.Equal(testPublicKeys["ecdsa"].Marshal(), p.Marshal())
},
IsUserAuthority: func(p PublicKey) bool {
return bytes.Equal(testPublicKeys["ecdsa"].Marshal(), p.Marshal())
},
}
for _, test := range []struct {
name string
opts map[string]string
// succeed is the expected outcome of CheckHostKey, for a host
// certificate, and of CheckCert, for a user certificate.
succeed bool
// authSucceed is the expected outcome of Authenticate.
authSucceed bool
}{
{name: "no critical options", opts: nil, succeed: true, authSucceed: true},
{name: "source-address", opts: map[string]string{sourceAddressCriticalOption: "192.168.1.0/24"}, authSucceed: true},
{name: "unknown option", opts: map[string]string{"unknown-option": ""}},
{name: "supported option", opts: map[string]string{"supported-option": ""}, succeed: true, authSucceed: true},
} {
hostCert := &Certificate{
ValidPrincipals: []string{"hostname"},
Key: testPublicKeys["rsa"],
ValidBefore: CertTimeInfinity,
CertType: HostCert,
Permissions: Permissions{CriticalOptions: test.opts},
}
if err := hostCert.SignCert(rand.Reader, testSigners["ecdsa"]); err != nil {
t.Fatalf("SignCert: %v", err)
}
if err := checker.CheckHostKey("hostname:22", nil, hostCert); (err == nil) != test.succeed {
t.Errorf("CheckHostKey(%s): got %v, want success=%v", test.name, err, test.succeed)
}
userCert := &Certificate{
ValidPrincipals: []string{"user"},
Key: testPublicKeys["rsa"],
ValidBefore: CertTimeInfinity,
CertType: UserCert,
Permissions: Permissions{CriticalOptions: test.opts},
}
if err := userCert.SignCert(rand.Reader, testSigners["ecdsa"]); err != nil {
t.Fatalf("SignCert: %v", err)
}
if err := checker.CheckCert("user", userCert); (err == nil) != test.succeed {
t.Errorf("CheckCert(%s): got %v, want success=%v", test.name, err, test.succeed)
}
if _, err := checker.Authenticate(testConnMeta{user: "user"}, userCert); (err == nil) != test.authSucceed {
t.Errorf("Authenticate(%s): got %v, want success=%v", test.name, err, test.authSucceed)
}
}
if appended[1] != "appended-option" {
t.Errorf("Authenticate overwrote the caller's slice: got %q, want %q", appended[1], "appended-option")
}
}
type legacyRSASigner struct {
Signer
}
+19 -1
View File
@@ -173,6 +173,12 @@ type channel struct {
// (for outbound channels) or received (for inbound channels).
decided bool
// established is set to true once the channel is open and may carry normal
// channel traffic: for an outbound channel when the peer's open
// confirmation is received, for an inbound channel when the local side
// accepts it. It is set and read from different goroutines.
established atomic.Bool
// direction contains either channelOutbound, for channels created
// locally, or channelInbound, for channels created by the peer.
direction channelDirection
@@ -434,10 +440,20 @@ func (ch *channel) responseMessageReceived() error {
return errors.New("ssh: duplicate response received for channel")
}
ch.decided = true
ch.established.Store(true)
return nil
}
func (ch *channel) handlePacket(packet []byte) error {
// Only the open response is expected before the channel is established.
if !ch.established.Load() {
switch packet[0] {
case msgChannelOpenConfirm, msgChannelOpenFailure:
default:
return nil
}
}
switch packet[0] {
case msgChannelData, msgChannelExtendedData:
return ch.handleData(packet)
@@ -503,7 +519,8 @@ func (ch *channel) handlePacket(packet []byte) error {
default:
}
default:
ch.msg <- msg
// No other message type is expected on an established channel.
return fmt.Errorf("ssh: unexpected message type %d on channel %d", packet[0], ch.localId)
}
return nil
}
@@ -554,6 +571,7 @@ func (ch *channel) Accept() (Channel, <-chan *Request, error) {
MaxPacketSize: ch.maxIncomingPayload,
}
ch.decided = true
ch.established.Store(true)
if err := ch.sendMessage(confirm); err != nil {
return nil, nil, err
}
+13 -4
View File
@@ -142,6 +142,15 @@ func keyEq(a, b ssh.PublicKey) bool {
return bytes.Equal(a.Marshal(), b.Marshal())
}
// plainKeyBlob returns the serialized public portion of key: for a
// certificate this is the certified key, otherwise the key itself.
func plainKeyBlob(key ssh.PublicKey) string {
if cert, ok := key.(*ssh.Certificate); ok {
return string(cert.Key.Marshal())
}
return string(key.Marshal())
}
// IsHostAuthority can be used as a callback in ssh.CertChecker
func (db *hostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool {
h, p, err := net.SplitHostPort(address)
@@ -160,10 +169,10 @@ func (db *hostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool
// IsRevoked can be used as a callback in ssh.CertChecker
func (db *hostKeyDB) IsRevoked(key *ssh.Certificate) bool {
if _, ok := db.revoked[string(key.Marshal())]; ok {
if _, ok := db.revoked[plainKeyBlob(key)]; ok {
return true
}
if _, ok := db.revoked[string(key.SignatureKey.Marshal())]; ok {
if _, ok := db.revoked[plainKeyBlob(key.SignatureKey)]; ok {
return true
}
return false
@@ -228,7 +237,7 @@ func (db *hostKeyDB) parseLine(line []byte, filename string, linenum int) error
}
if marker == markerRevoked {
db.revoked[string(key.Marshal())] = &KnownKey{
db.revoked[plainKeyBlob(key)] = &KnownKey{
Key: key,
Filename: filename,
Line: linenum,
@@ -341,7 +350,7 @@ func (r *RevokedError) Error() string {
// check checks a key against the host database. This should not be
// used for verifying certificates.
func (db *hostKeyDB) check(address string, remote net.Addr, remoteKey ssh.PublicKey) error {
if revoked := db.revoked[string(remoteKey.Marshal())]; revoked != nil {
if revoked := db.revoked[plainKeyBlob(remoteKey)]; revoked != nil {
return &RevokedError{Revoked: *revoked}
}
+62
View File
@@ -316,6 +316,68 @@ func TestRevokedCA(t *testing.T) {
}
}
func TestRevokedCertifiedKey(t *testing.T) {
_, caPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
caSigner, err := ssh.NewSignerFromKey(caPriv)
if err != nil {
t.Fatal(err)
}
_, hostPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
hostKey, err := ssh.NewPublicKey(hostPriv.Public())
if err != nil {
t.Fatal(err)
}
signCert := func(serial uint64, principal string) *ssh.Certificate {
cert := &ssh.Certificate{
CertType: ssh.HostCert,
Key: hostKey,
Serial: serial,
ValidBefore: ssh.CertTimeInfinity,
ValidPrincipals: []string{principal},
}
if err := cert.SignCert(rand.Reader, caSigner); err != nil {
t.Fatal(err)
}
return cert
}
cert := signCert(1, "server.org")
// Revoking the host key revokes any certificate for it, even when
// the CA is trusted, matching OpenSSH's check_key_not_revoked.
db := testDB(t, "@revoked server.org "+serialize(hostKey)+"\n"+
"@cert-authority server.org "+serialize(caSigner.PublicKey())+"\n")
if !db.IsRevoked(cert) {
t.Error("IsRevoked returned false for certificate with revoked certified key")
}
// Conversely, a @revoked line containing a certificate revokes the
// plain key it certifies.
db = testDB(t, "@revoked server.org "+serialize(cert)+"\n")
if err := db.check("server.org:22", testAddr, hostKey); err == nil {
t.Error("no error for plain key revoked via certificate")
} else if _, ok := err.(*RevokedError); !ok {
t.Errorf("got %T (%v), want *RevokedError", err, err)
}
// Since only public portions are compared, revoking a certificate
// also revokes any sibling certificate certifying the same key,
// whatever its serial number and principals.
sibling := signCert(2, "other.org")
db = testDB(t, "@revoked * "+serialize(cert)+"\n"+
"@cert-authority * "+serialize(caSigner.PublicKey())+"\n")
if !db.IsRevoked(sibling) {
t.Error("IsRevoked returned false for sibling certificate of a revoked certificate")
}
}
const testHostname = "hostname"
// generated with keygen -H -f
+198 -1
View File
@@ -5,9 +5,11 @@
package ssh
import (
"encoding/binary"
"errors"
"fmt"
"io"
"strings"
"sync"
"testing"
"time"
@@ -1181,6 +1183,13 @@ func TestChannelUnexpectedResponsesDiscarded(t *testing.T) {
return
}
}
// Follow the flood with a request so the client can synchronize with
// its mux loop. Once the client receives this request, the mux loop has
// necessarily processed (and discarded) all the preceding responses.
if _, err := serverCh.SendRequest("sync", false, nil); err != nil {
done <- fmt.Errorf("send sync request: %w", err)
return
}
// Echo any legitimate request back.
for req := range serverCh.incomingRequests {
if req.WantReply {
@@ -1193,9 +1202,17 @@ func TestChannelUnexpectedResponsesDiscarded(t *testing.T) {
done <- nil
}()
req, ok := <-clientCh.incomingRequests
if !ok {
t.Fatal("channel closed before sync request")
}
if req.Type != "sync" {
t.Fatalf("unexpected sync request type %q", req.Type)
}
// If the flood had wedged the mux loop, this SendRequest would never
// receive a reply.
ok, err := clientCh.SendRequest("ping", true, []byte("hello"))
ok, err = clientCh.SendRequest("ping", true, []byte("hello"))
if err != nil {
t.Fatalf("SendRequest: %v", err)
}
@@ -1213,6 +1230,186 @@ func TestChannelUnexpectedResponsesDiscarded(t *testing.T) {
}
}
func TestChannelUndecidedOutboundDropsTraffic(t *testing.T) {
// Traffic on an outbound channel the peer has not confirmed yet is dropped;
// the open confirmation is still processed.
a, b := memPipe()
defer a.Close()
defer b.Close()
// Drain the peer end so that a write would not block.
go func() {
for {
if _, err := b.readPacket(); err != nil {
return
}
}
}()
m := newMux(a)
ch := m.newChannel("session", channelOutbound, nil)
if ch.decided {
t.Fatal("freshly opened outbound channel should not be decided")
}
// Flooding channel requests must not block.
reqPacket := Marshal(channelRequestMsg{PeersID: ch.localId, Request: "x"})
for range chanSize * 4 {
if err := ch.handlePacket(reqPacket); err != nil {
t.Fatalf("handlePacket(channelRequest): %v", err)
}
}
if n := len(ch.incomingRequests); n != 0 {
t.Fatalf("incomingRequests should be empty, has %d entries", n)
}
// A close before the confirmation must not remove or close the channel, and
// must not emit a close for the still-unknown remote id.
if err := ch.handlePacket(Marshal(channelCloseMsg{PeersID: ch.localId})); err != nil {
t.Fatalf("handlePacket(channelClose): %v", err)
}
if m.chanList.getChan(ch.localId) != ch {
t.Fatal("premature close removed the channel from the chanList")
}
// The open confirmation is still processed and decides the channel.
confirm := Marshal(channelOpenConfirmMsg{
PeersID: ch.localId,
MyID: 42,
MyWindow: 1 << 20,
MaxPacketSize: 1 << 15,
})
if err := ch.handlePacket(confirm); err != nil {
t.Fatalf("handlePacket(openConfirm): %v", err)
}
if !ch.decided {
t.Fatal("channel not decided after open confirmation")
}
if _, ok := (<-ch.msg).(*channelOpenConfirmMsg); !ok {
t.Fatal("open confirmation was not delivered on ch.msg")
}
}
func TestChannelUndecidedInboundDropsRequests(t *testing.T) {
// Channel requests on an inbound channel the local application has not
// accepted or rejected yet are dropped.
serverMux, clientMux := muxPair()
defer serverMux.Close()
defer clientMux.Close()
go DiscardRequests(serverMux.incomingRequests)
// The server assigns its first inbound channel id 0; open such a channel
// and flood it before it is decided. The channel is left undecided, as it
// would be in the window before the server accepts or rejects it: its
// NewChannel is never received from incomingChannels.
ch := clientMux.newChannel("chan", channelOutbound, nil)
if err := clientMux.sendMessage(channelOpenMsg{
ChanType: "chan",
PeersWindow: ch.myWindow,
MaxPacketSize: ch.maxIncomingPayload,
PeersID: ch.localId,
}); err != nil {
t.Fatalf("send open: %v", err)
}
for range chanSize * 4 {
if err := clientMux.sendMessage(channelRequestMsg{PeersID: 0, Request: "x"}); err != nil {
break // the server may have closed the connection
}
}
// The server loop keeps reading: closing the client makes it observe EOF
// and return from Wait.
clientMux.Close()
serverMux.Wait()
}
func TestChannelRejectedInboundDropsTraffic(t *testing.T) {
// A rejected channel is decided but is never established, and traffic on it
// is dropped.
a, b := memPipe()
defer a.Close()
defer b.Close()
go func() {
for {
if _, err := b.readPacket(); err != nil {
return
}
}
}()
m := newMux(a)
ch := m.newChannel("session", channelInbound, nil)
ch.remoteId = 42
if err := ch.Reject(ConnectionFailed, "no thanks"); err != nil {
t.Fatalf("Reject: %v", err)
}
if !ch.decided {
t.Fatal("a rejected channel should be decided")
}
if ch.established.Load() {
t.Fatal("a rejected channel should not be established")
}
// Flooding channel requests must not block.
reqPacket := Marshal(channelRequestMsg{PeersID: ch.localId, Request: "x"})
for range chanSize * 4 {
if err := ch.handlePacket(reqPacket); err != nil {
t.Fatalf("handlePacket(channelRequest): %v", err)
}
}
if n := len(ch.incomingRequests); n != 0 {
t.Fatalf("incomingRequests should be empty, has %d entries", n)
}
}
func TestChannelBogusMessageTearsDownMux(t *testing.T) {
// A message that is well-formed on the wire but is not a type expected on
// an established channel is a protocol error that tears the mux down.
clientMux, serverMux := muxPair()
defer serverMux.Close()
defer clientMux.Close()
serverRes := make(chan *channel, 1)
go func() {
newCh, ok := <-serverMux.incomingChannels
if !ok {
close(serverRes)
return
}
c, _, err := newCh.Accept()
if err != nil {
close(serverRes)
return
}
serverRes <- c.(*channel)
}()
if _, err := clientMux.openChannel("chan", nil); err != nil {
t.Fatalf("openChannel: %v", err)
}
serverCh := <-serverRes
if serverCh == nil {
t.Fatal("server did not accept channel")
}
// Craft a packet that the client mux routes to clientCh (bytes 1:5 hold the
// peer's channel id) and that decode accepts, but whose type is unexpected
// on a channel. The service-string length equals the channel id, so the
// same four bytes serve both as the routing id and as the string length.
id := serverCh.remoteId
packet := []byte{msgServiceRequest, 0, 0, 0, 0}
binary.BigEndian.PutUint32(packet[1:], id)
packet = append(packet, make([]byte, id)...)
if err := serverMux.conn.writePacket(packet); err != nil {
t.Fatalf("writePacket: %v", err)
}
// The mux loop exits with the protocol error.
if err := clientMux.Wait(); err == nil || !strings.Contains(err.Error(), "unexpected message type") {
t.Fatalf("mux error = %v, want unexpected message type", err)
}
}
func TestChannelCloseIdempotent(t *testing.T) {
a, b := memPipe()
defer a.Close()
+9 -3
View File
@@ -331,13 +331,19 @@ func exchangeVersions(rw io.ReadWriter, versionLine []byte) (them []byte, err er
// chars
const maxVersionStringBytes = 255
// maxPreVersionLines is the maximum number of lines sent by the peer
// before the version string. Each of these lines is limited to a maximum
// of maxVersionStringBytes chars. Lines sent before the version string
// are silently ignored.
const maxPreVersionLines = 1024
// Read version string as specified by RFC 4253, section 4.2.
func readVersion(r io.Reader) ([]byte, error) {
versionString := make([]byte, 0, 64)
var ok bool
var buf [1]byte
for length := 0; length < maxVersionStringBytes; length++ {
for lines := 0; len(versionString) < maxVersionStringBytes && lines < maxPreVersionLines; {
_, err := io.ReadFull(r, buf[:])
if err != nil {
return nil, err
@@ -347,9 +353,9 @@ func readVersion(r io.Reader) ([]byte, error) {
if buf[0] == '\n' {
if !bytes.HasPrefix(versionString, []byte("SSH-")) {
// RFC 4253 says we need to ignore all version string lines
// except the one containing the SSH version (provided that
// all the lines do not exceed 255 bytes in total).
// except the one containing the SSH version.
versionString = versionString[:0]
lines++
continue
}
ok = true
+16 -4
View File
@@ -35,11 +35,15 @@ func TestReadVersion(t *testing.T) {
}
func TestReadVersionError(t *testing.T) {
longVersion := strings.Repeat("SSH-2.0-bla", 50)[:253]
multiLineVersion := strings.Repeat("ignored\r\n", 50) + "SSH-2.0-bla\r\n"
tooLongVersion := strings.Repeat("SSH-2.0-bla", 60) + "\r\n"
multiLine := strings.Repeat("ignored\r\n", 50)
tooManyLines := strings.Repeat("ignored\r\n", 2000)
tooLongLine := strings.Repeat("blah ", 100) + "\r\n"
cases := []string{
longVersion + "too-long\r\n",
multiLineVersion,
tooLongVersion,
multiLine + tooLongVersion,
tooManyLines + "SSH-2.0-bla\r\n",
tooLongLine + "SSH-2.0-bla\r\n",
}
for _, in := range cases {
if _, err := readVersion(bytes.NewBufferString(in)); err == nil {
@@ -48,6 +52,14 @@ func TestReadVersionError(t *testing.T) {
}
}
func TestIgnorePreVersion(t *testing.T) {
longishLine := strings.Repeat("blah ", 30) + "\r\n"
multiLineVersion := strings.Repeat(longishLine, 100) + "SSH-2.0-bla\r\n"
if _, err := readVersion(bytes.NewBufferString(multiLineVersion)); err != nil {
t.Errorf("readVersion() got error %v, expected success", err)
}
}
func TestExchangeVersionsBasic(t *testing.T) {
v := "SSH-2.0-bla"
buf := bytes.NewBufferString(v + "\r\n")
+4 -4
View File
@@ -4,7 +4,7 @@ package bundle
var unparsedCertificates = []unparsedCertificate{
{
cn: "CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS,OU=Ceres,O=FNMT-RCM,C=ES,2.5.4.97=#130f56415445532d51323832363030344a",
cn: "CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS,OU=Ceres,O=FNMT-RCM,C=ES,2.5.4.97=VATES-Q2826004J",
sha256Hash: "554153b13d2cf9ddb753bfbe1a4e0ae08d0aa4187058fe60a2b862b2e4b87bcb",
certStartOff: 0,
certLength: 626,
@@ -364,7 +364,7 @@ var unparsedCertificates = []unparsedCertificate{
certLength: 1525,
},
{
cn: "CN=Microsec e-Szigno Root CA 2009,O=Microsec Ltd.,L=Budapest,C=HU,1.2.840.113549.1.9.1=#0c10696e666f40652d737a69676e6f2e6875",
cn: "CN=Microsec e-Szigno Root CA 2009,O=Microsec Ltd.,L=Budapest,C=HU,1.2.840.113549.1.9.1=info@e-szigno.hu",
sha256Hash: "3c5f81fea5fab82c64bfa2eaecafcde8e077fc8620a7cae537163df36edbf378",
certStartOff: 64761,
certLength: 1038,
@@ -652,13 +652,13 @@ var unparsedCertificates = []unparsedCertificate{
certLength: 1506,
},
{
cn: "CN=e-Szigno Root CA 2017,O=Microsec Ltd.,L=Budapest,C=HU,2.5.4.97=#130e56415448552d3233353834343937",
cn: "CN=e-Szigno Root CA 2017,O=Microsec Ltd.,L=Budapest,C=HU,2.5.4.97=VATHU-23584497",
sha256Hash: "beb00b30839b9bc32c32e4447905950641f26421b15ed089198b518ae2ea1b99",
certStartOff: 116668,
certLength: 580,
},
{
cn: "CN=e-Szigno TLS Root CA 2023,O=Microsec Ltd.,L=Budapest,C=HU,2.5.4.97=#130e56415448552d3233353834343937",
cn: "CN=e-Szigno TLS Root CA 2023,O=Microsec Ltd.,L=Budapest,C=HU,2.5.4.97=VATHU-23584497",
sha256Hash: "b49141502d00663d740f2e7ec340c52800962666121a36d09cf7dd2b90384fb4",
certStartOff: 117248,
certLength: 723,
@@ -0,0 +1,47 @@
// 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.
//go:build !go1.27
package bundle
import (
"encoding/asn1"
"encoding/hex"
"regexp"
)
// subjectsEqual reports whether two RFC 2253 DN strings match.
//
// When testing with Go 1.27 or newer, it requires subjects to match exactly.
// When testing with Go 1.26, it tolerates the rendering difference introduced in Go 1.27,
// where string-typed attribute values for OIDs outside attributeTypeNames are rendered
// as strings rather than hex-encoded DER (see Go CL 773800).
//
// When Go 1.26 is no longer supported,
// the Go 1.26 version can be removed and
// the Go 1.27+ version can be inlined.
func subjectsEqual(a, b string) bool {
return a == b || normalizeHexValues(a) == normalizeHexValues(b)
}
// normalizeHexValues rewrites any "oid=#hex" to the equivalent "oid=value"
// rendering produced by Go 1.27+.
func normalizeHexValues(s string) string {
return hexAttrRE.ReplaceAllStringFunc(s, func(match string) string {
m := hexAttrRE.FindStringSubmatch(match)
der, err := hex.DecodeString(m[2])
if err != nil {
return match
}
var v string
if rest, err := asn1.Unmarshal(der, &v); err != nil || len(rest) != 0 {
return match
}
return m[1] + "=" + v
})
}
// hexAttrRE matches a "oid=#hex" attribute value in an RFC 2253 DN string.
var hexAttrRE = regexp.MustCompile(`([\d.]+)=#([[:xdigit:]]+)`)
@@ -0,0 +1,21 @@
// 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.
//go:build go1.27
package bundle
// subjectsEqual reports whether two RFC 2253 DN strings match.
//
// When testing with Go 1.27 or newer, it requires subjects to match exactly.
// When testing with Go 1.26, it tolerates the rendering difference introduced in Go 1.27,
// where string-typed attribute values for OIDs outside attributeTypeNames are rendered
// as strings rather than hex-encoded DER (see Go CL 773800).
//
// When Go 1.26 is no longer supported,
// the Go 1.26 version can be removed and
// the Go 1.27+ version can be inlined.
func subjectsEqual(a, b string) bool {
return a == b
}
-33
View File
@@ -7,9 +7,7 @@ package bundle
import (
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"encoding/hex"
"regexp"
"testing"
)
@@ -32,34 +30,3 @@ func TestBundle(t *testing.T) {
}
}
}
// subjectsEqual reports whether two RFC 2253 DN strings match.
//
// It tolerates the rendering difference introduced in Go 1.27, where
// string-typed attribute values for OIDs outside attributeTypeNames are
// rendered as strings rather than hex-encoded DER (see Go CL 773800).
//
// This can be removed when Go 1.25/1.26 are no longer supported.
func subjectsEqual(a, b string) bool {
return a == b || normalizeHexValues(a) == normalizeHexValues(b)
}
// normalizeHexValues rewrites any "oid=#hex" to the equivalent "oid=value"
// rendering produced by Go 1.27+.
func normalizeHexValues(s string) string {
return hexAttrRE.ReplaceAllStringFunc(s, func(match string) string {
m := hexAttrRE.FindStringSubmatch(match)
der, err := hex.DecodeString(m[2])
if err != nil {
return match
}
var v string
if rest, err := asn1.Unmarshal(der, &v); err != nil || len(rest) != 0 {
return match
}
return m[1] + "=" + v
})
}
// hexAttrRE matches a "oid=#hex" attribute value in an RFC 2253 DN string.
var hexAttrRE = regexp.MustCompile(`([\d.]+)=#([[:xdigit:]]+)`)
+1 -1
View File
@@ -1,3 +1,3 @@
module golang.org/x/crypto/x509roots/fallback
go 1.25.0
go 1.26.0