Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f62bf119e | ||
|
|
86efde54dc | ||
|
|
a6cdac6084 | ||
|
|
39dc44e69c | ||
|
|
afebf4cb4e | ||
|
|
89f4e9bb5b | ||
|
|
71488c48c2 | ||
|
|
82adefa711 | ||
|
|
c757c9851f | ||
|
|
593c81af8a | ||
|
|
46efc8bc62 | ||
|
|
e0a54fdc4b |
+5
-8
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,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
|
||||
}
|
||||
@@ -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,3 +1,3 @@
|
||||
module golang.org/x/crypto/x509roots/fallback
|
||||
|
||||
go 1.25.0
|
||||
go 1.26.0
|
||||
|
||||
Reference in New Issue
Block a user