Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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)
|
payload = base64.RawURLEncoding.EncodeToString(cs)
|
||||||
}
|
}
|
||||||
hash := sha.New()
|
sig, err := jwsSign(key, sha, []byte(phead+"."+payload))
|
||||||
hash.Write([]byte(phead + "." + payload))
|
|
||||||
sig, err := jwsSign(key, sha, hash.Sum(nil))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -195,14 +193,13 @@ func jwkEncode(pub crypto.PublicKey) (string, error) {
|
|||||||
return "", ErrUnsupportedKey
|
return "", ErrUnsupportedKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// jwsSign signs the digest using the given key.
|
// jwsSign signs the payload using the given key.
|
||||||
// The hash is unused for ECDSA keys.
|
func jwsSign(key crypto.Signer, hash crypto.Hash, payload []byte) ([]byte, error) {
|
||||||
func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {
|
|
||||||
switch pub := key.Public().(type) {
|
switch pub := key.Public().(type) {
|
||||||
case *rsa.PublicKey:
|
case *rsa.PublicKey:
|
||||||
return key.Sign(rand.Reader, digest, hash)
|
return crypto.SignMessage(key, rand.Reader, payload, hash)
|
||||||
case *ecdsa.PublicKey:
|
case *ecdsa.PublicKey:
|
||||||
sigASN1, err := key.Sign(rand.Reader, digest, hash)
|
sigASN1, err := crypto.SignMessage(key, rand.Reader, payload, hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-5
@@ -14,6 +14,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
@@ -353,6 +354,22 @@ func (s *customTestSigner) Sign(io.Reader, []byte, crypto.SignerOpts) ([]byte, e
|
|||||||
return s.sig, nil
|
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) {
|
func TestJWSEncodeJSONCustom(t *testing.T) {
|
||||||
claims := struct{ Msg string }{"hello"}
|
claims := struct{ Msg string }{"hello"}
|
||||||
const (
|
const (
|
||||||
@@ -391,24 +408,33 @@ func TestJWSEncodeJSONCustom(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
tt := []struct {
|
tt := []struct {
|
||||||
alg, phead string
|
name, phead string
|
||||||
pub crypto.PublicKey
|
pub crypto.PublicKey
|
||||||
stdsig, jwsig string
|
stdsig, jwsig string
|
||||||
|
msgSigner bool
|
||||||
}{
|
}{
|
||||||
{"ES256", es256phead, testKeyEC.Public(), es256stdsig, es256jwsig},
|
{"ES256", es256phead, testKeyEC.Public(), es256stdsig, es256jwsig, false},
|
||||||
{"RS256", rs256phead, testKey.Public(), testsig, testsig},
|
{"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 {
|
for _, tc := range tt {
|
||||||
tc := tc
|
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)
|
stdsig, err := base64.RawStdEncoding.DecodeString(tc.stdsig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("couldn't decode test vector: %v", err)
|
t.Errorf("couldn't decode test vector: %v", err)
|
||||||
}
|
}
|
||||||
signer := &customTestSigner{
|
cs := customTestSigner{
|
||||||
sig: stdsig,
|
sig: stdsig,
|
||||||
pub: tc.pub,
|
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")
|
b, err := jwsEncodeJSON(claims, signer, noKeyID, "nonce", "url")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -427,6 +453,14 @@ func TestJWSEncodeJSONCustom(t *testing.T) {
|
|||||||
if j.Sig != tc.jwsig {
|
if j.Sig != tc.jwsig {
|
||||||
t.Errorf("j.Sig = %q\nwant %q", 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,6 +1,6 @@
|
|||||||
module golang.org/x/crypto
|
module golang.org/x/crypto
|
||||||
|
|
||||||
go 1.25.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/net v0.57.0 // tagx:ignore
|
golang.org/x/net v0.57.0 // tagx:ignore
|
||||||
|
|||||||
+17
-21
@@ -10,6 +10,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -305,8 +306,11 @@ const sourceAddressCriticalOption = "source-address"
|
|||||||
// minimally, the IsAuthority callback should be set.
|
// minimally, the IsAuthority callback should be set.
|
||||||
type CertChecker struct {
|
type CertChecker struct {
|
||||||
// SupportedCriticalOptions lists the CriticalOptions that the
|
// SupportedCriticalOptions lists the CriticalOptions that the
|
||||||
// server application layer understands. These are only used
|
// application layer understands. A certificate carrying a critical
|
||||||
// for user certificates.
|
// 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
|
SupportedCriticalOptions []string
|
||||||
|
|
||||||
// IsUserAuthority should return true if the key is recognized as an
|
// 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)
|
return c.CheckCert(hostname, cert)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate checks a user certificate. Authenticate can be used as
|
// Authenticate checks a user certificate. Authenticate can be used as a value
|
||||||
// a value for ServerConfig.PublicKeyCallback.
|
// 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) {
|
func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permissions, error) {
|
||||||
cert, ok := pubKey.(*Certificate)
|
cert, ok := pubKey.(*Certificate)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -389,8 +394,11 @@ func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permis
|
|||||||
if !c.IsUserAuthority(cert.SignatureKey) {
|
if !c.IsUserAuthority(cert.SignatureKey) {
|
||||||
return nil, fmt.Errorf("ssh: certificate signed by unrecognized authority")
|
return nil, fmt.Errorf("ssh: certificate signed by unrecognized authority")
|
||||||
}
|
}
|
||||||
|
// The source-address critical option is enforced by serverAuthenticate,
|
||||||
if err := c.CheckCert(conn.User(), cert); err != nil {
|
// 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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,27 +406,15 @@ func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permis
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CheckCert checks CriticalOptions, ValidPrincipals, revocation, timestamp and
|
// 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 {
|
func (c *CertChecker) CheckCert(principal string, cert *Certificate) error {
|
||||||
if c.IsRevoked != nil && c.IsRevoked(cert) {
|
if c.IsRevoked != nil && c.IsRevoked(cert) {
|
||||||
return fmt.Errorf("ssh: certificate serial %d revoked", cert.Serial)
|
return fmt.Errorf("ssh: certificate serial %d revoked", cert.Serial)
|
||||||
}
|
}
|
||||||
|
|
||||||
for opt := range cert.CriticalOptions {
|
for opt := range cert.CriticalOptions {
|
||||||
// sourceAddressCriticalOption will be enforced by
|
if !slices.Contains(c.SupportedCriticalOptions, opt) {
|
||||||
// serverAuthenticate
|
|
||||||
if opt == sourceAddressCriticalOption {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, supp := range c.SupportedCriticalOptions {
|
|
||||||
if supp == opt {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("ssh: unsupported critical option %q in certificate", 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 {
|
type legacyRSASigner struct {
|
||||||
Signer
|
Signer
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-1
@@ -173,6 +173,12 @@ type channel struct {
|
|||||||
// (for outbound channels) or received (for inbound channels).
|
// (for outbound channels) or received (for inbound channels).
|
||||||
decided bool
|
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
|
// direction contains either channelOutbound, for channels created
|
||||||
// locally, or channelInbound, for channels created by the peer.
|
// locally, or channelInbound, for channels created by the peer.
|
||||||
direction channelDirection
|
direction channelDirection
|
||||||
@@ -434,10 +440,20 @@ func (ch *channel) responseMessageReceived() error {
|
|||||||
return errors.New("ssh: duplicate response received for channel")
|
return errors.New("ssh: duplicate response received for channel")
|
||||||
}
|
}
|
||||||
ch.decided = true
|
ch.decided = true
|
||||||
|
ch.established.Store(true)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *channel) handlePacket(packet []byte) error {
|
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] {
|
switch packet[0] {
|
||||||
case msgChannelData, msgChannelExtendedData:
|
case msgChannelData, msgChannelExtendedData:
|
||||||
return ch.handleData(packet)
|
return ch.handleData(packet)
|
||||||
@@ -503,7 +519,8 @@ func (ch *channel) handlePacket(packet []byte) error {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -554,6 +571,7 @@ func (ch *channel) Accept() (Channel, <-chan *Request, error) {
|
|||||||
MaxPacketSize: ch.maxIncomingPayload,
|
MaxPacketSize: ch.maxIncomingPayload,
|
||||||
}
|
}
|
||||||
ch.decided = true
|
ch.decided = true
|
||||||
|
ch.established.Store(true)
|
||||||
if err := ch.sendMessage(confirm); err != nil {
|
if err := ch.sendMessage(confirm); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,15 @@ func keyEq(a, b ssh.PublicKey) bool {
|
|||||||
return bytes.Equal(a.Marshal(), b.Marshal())
|
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
|
// IsHostAuthority can be used as a callback in ssh.CertChecker
|
||||||
func (db *hostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool {
|
func (db *hostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool {
|
||||||
h, p, err := net.SplitHostPort(address)
|
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
|
// IsRevoked can be used as a callback in ssh.CertChecker
|
||||||
func (db *hostKeyDB) IsRevoked(key *ssh.Certificate) bool {
|
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
|
return true
|
||||||
}
|
}
|
||||||
if _, ok := db.revoked[string(key.SignatureKey.Marshal())]; ok {
|
if _, ok := db.revoked[plainKeyBlob(key.SignatureKey)]; ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -228,7 +237,7 @@ func (db *hostKeyDB) parseLine(line []byte, filename string, linenum int) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
if marker == markerRevoked {
|
if marker == markerRevoked {
|
||||||
db.revoked[string(key.Marshal())] = &KnownKey{
|
db.revoked[plainKeyBlob(key)] = &KnownKey{
|
||||||
Key: key,
|
Key: key,
|
||||||
Filename: filename,
|
Filename: filename,
|
||||||
Line: linenum,
|
Line: linenum,
|
||||||
@@ -341,7 +350,7 @@ func (r *RevokedError) Error() string {
|
|||||||
// check checks a key against the host database. This should not be
|
// check checks a key against the host database. This should not be
|
||||||
// used for verifying certificates.
|
// used for verifying certificates.
|
||||||
func (db *hostKeyDB) check(address string, remote net.Addr, remoteKey ssh.PublicKey) error {
|
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}
|
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"
|
const testHostname = "hostname"
|
||||||
|
|
||||||
// generated with keygen -H -f
|
// generated with keygen -H -f
|
||||||
|
|||||||
+198
-1
@@ -5,9 +5,11 @@
|
|||||||
package ssh
|
package ssh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -1181,6 +1183,13 @@ func TestChannelUnexpectedResponsesDiscarded(t *testing.T) {
|
|||||||
return
|
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.
|
// Echo any legitimate request back.
|
||||||
for req := range serverCh.incomingRequests {
|
for req := range serverCh.incomingRequests {
|
||||||
if req.WantReply {
|
if req.WantReply {
|
||||||
@@ -1193,9 +1202,17 @@ func TestChannelUnexpectedResponsesDiscarded(t *testing.T) {
|
|||||||
done <- nil
|
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
|
// If the flood had wedged the mux loop, this SendRequest would never
|
||||||
// receive a reply.
|
// receive a reply.
|
||||||
ok, err := clientCh.SendRequest("ping", true, []byte("hello"))
|
ok, err = clientCh.SendRequest("ping", true, []byte("hello"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("SendRequest: %v", err)
|
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) {
|
func TestChannelCloseIdempotent(t *testing.T) {
|
||||||
a, b := memPipe()
|
a, b := memPipe()
|
||||||
defer a.Close()
|
defer a.Close()
|
||||||
|
|||||||
+9
-3
@@ -331,13 +331,19 @@ func exchangeVersions(rw io.ReadWriter, versionLine []byte) (them []byte, err er
|
|||||||
// chars
|
// chars
|
||||||
const maxVersionStringBytes = 255
|
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.
|
// Read version string as specified by RFC 4253, section 4.2.
|
||||||
func readVersion(r io.Reader) ([]byte, error) {
|
func readVersion(r io.Reader) ([]byte, error) {
|
||||||
versionString := make([]byte, 0, 64)
|
versionString := make([]byte, 0, 64)
|
||||||
var ok bool
|
var ok bool
|
||||||
var buf [1]byte
|
var buf [1]byte
|
||||||
|
|
||||||
for length := 0; length < maxVersionStringBytes; length++ {
|
for lines := 0; len(versionString) < maxVersionStringBytes && lines < maxPreVersionLines; {
|
||||||
_, err := io.ReadFull(r, buf[:])
|
_, err := io.ReadFull(r, buf[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -347,9 +353,9 @@ func readVersion(r io.Reader) ([]byte, error) {
|
|||||||
if buf[0] == '\n' {
|
if buf[0] == '\n' {
|
||||||
if !bytes.HasPrefix(versionString, []byte("SSH-")) {
|
if !bytes.HasPrefix(versionString, []byte("SSH-")) {
|
||||||
// RFC 4253 says we need to ignore all version string lines
|
// RFC 4253 says we need to ignore all version string lines
|
||||||
// except the one containing the SSH version (provided that
|
// except the one containing the SSH version.
|
||||||
// all the lines do not exceed 255 bytes in total).
|
|
||||||
versionString = versionString[:0]
|
versionString = versionString[:0]
|
||||||
|
lines++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ok = true
|
ok = true
|
||||||
|
|||||||
+16
-4
@@ -35,11 +35,15 @@ func TestReadVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReadVersionError(t *testing.T) {
|
func TestReadVersionError(t *testing.T) {
|
||||||
longVersion := strings.Repeat("SSH-2.0-bla", 50)[:253]
|
tooLongVersion := strings.Repeat("SSH-2.0-bla", 60) + "\r\n"
|
||||||
multiLineVersion := strings.Repeat("ignored\r\n", 50) + "SSH-2.0-bla\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{
|
cases := []string{
|
||||||
longVersion + "too-long\r\n",
|
tooLongVersion,
|
||||||
multiLineVersion,
|
multiLine + tooLongVersion,
|
||||||
|
tooManyLines + "SSH-2.0-bla\r\n",
|
||||||
|
tooLongLine + "SSH-2.0-bla\r\n",
|
||||||
}
|
}
|
||||||
for _, in := range cases {
|
for _, in := range cases {
|
||||||
if _, err := readVersion(bytes.NewBufferString(in)); err == nil {
|
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) {
|
func TestExchangeVersionsBasic(t *testing.T) {
|
||||||
v := "SSH-2.0-bla"
|
v := "SSH-2.0-bla"
|
||||||
buf := bytes.NewBufferString(v + "\r\n")
|
buf := bytes.NewBufferString(v + "\r\n")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ package bundle
|
|||||||
|
|
||||||
var unparsedCertificates = []unparsedCertificate{
|
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",
|
sha256Hash: "554153b13d2cf9ddb753bfbe1a4e0ae08d0aa4187058fe60a2b862b2e4b87bcb",
|
||||||
certStartOff: 0,
|
certStartOff: 0,
|
||||||
certLength: 626,
|
certLength: 626,
|
||||||
@@ -364,7 +364,7 @@ var unparsedCertificates = []unparsedCertificate{
|
|||||||
certLength: 1525,
|
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",
|
sha256Hash: "3c5f81fea5fab82c64bfa2eaecafcde8e077fc8620a7cae537163df36edbf378",
|
||||||
certStartOff: 64761,
|
certStartOff: 64761,
|
||||||
certLength: 1038,
|
certLength: 1038,
|
||||||
@@ -652,13 +652,13 @@ var unparsedCertificates = []unparsedCertificate{
|
|||||||
certLength: 1506,
|
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",
|
sha256Hash: "beb00b30839b9bc32c32e4447905950641f26421b15ed089198b518ae2ea1b99",
|
||||||
certStartOff: 116668,
|
certStartOff: 116668,
|
||||||
certLength: 580,
|
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",
|
sha256Hash: "b49141502d00663d740f2e7ec340c52800962666121a36d09cf7dd2b90384fb4",
|
||||||
certStartOff: 117248,
|
certStartOff: 117248,
|
||||||
certLength: 723,
|
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 (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/asn1"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"regexp"
|
|
||||||
"testing"
|
"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
|
module golang.org/x/crypto/x509roots/fallback
|
||||||
|
|
||||||
go 1.25.0
|
go 1.26.0
|
||||||
|
|||||||
Reference in New Issue
Block a user