Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1c0d99298 | ||
|
|
3c7c86938f | ||
|
|
533fb3f7e4 | ||
|
|
abbc44d451 | ||
|
|
e052873987 | ||
|
|
b61cf853a8 | ||
|
|
9c2cd33e8d | ||
|
|
890731877d | ||
|
|
ffd87b4878 | ||
|
|
4e7a7384ec | ||
|
|
b25012b37b | ||
|
|
6c195c8a97 | ||
|
|
f717e29698 | ||
|
|
e7c36ccb47 | ||
|
|
0fb843a472 | ||
|
|
e3d1254f1e | ||
|
|
a1ce0fee12 | ||
|
|
a749d17980 | ||
|
|
7ee59707ff | ||
|
|
44decbfe70 |
@@ -14,6 +14,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ hash.XOF = (*xof)(nil)
|
||||||
|
|
||||||
func fromHex(s string) []byte {
|
func fromHex(s string) []byte {
|
||||||
b, err := hex.DecodeString(s)
|
b, err := hex.DecodeString(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
// Copyright 2025 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.25
|
|
||||||
|
|
||||||
package blake2b
|
|
||||||
|
|
||||||
import "hash"
|
|
||||||
|
|
||||||
var _ hash.XOF = (*xof)(nil)
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ func chacha20Poly1305Open(dst []byte, key []uint32, src, ad []byte) bool
|
|||||||
func chacha20Poly1305Seal(dst []byte, key []uint32, src, ad []byte)
|
func chacha20Poly1305Seal(dst []byte, key []uint32, src, ad []byte)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
useAVX2 = cpu.X86.HasAVX2 && cpu.X86.HasBMI2
|
useAVX2 = cpu.X86.HasSSSE3 && cpu.X86.HasAVX2 && cpu.X86.HasBMI2
|
||||||
)
|
)
|
||||||
|
|
||||||
// setupState writes a ChaCha20 input matrix to state. See
|
// setupState writes a ChaCha20 input matrix to state. See
|
||||||
@@ -47,7 +47,7 @@ func setupState(state *[16]uint32, key *[32]byte, nonce []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *chacha20poly1305) seal(dst, nonce, plaintext, additionalData []byte) []byte {
|
func (c *chacha20poly1305) seal(dst, nonce, plaintext, additionalData []byte) []byte {
|
||||||
if !cpu.X86.HasSSSE3 {
|
if !useAVX2 {
|
||||||
return c.sealGeneric(dst, nonce, plaintext, additionalData)
|
return c.sealGeneric(dst, nonce, plaintext, additionalData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ func (c *chacha20poly1305) seal(dst, nonce, plaintext, additionalData []byte) []
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *chacha20poly1305) open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
|
func (c *chacha20poly1305) open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
|
||||||
if !cpu.X86.HasSSSE3 {
|
if !useAVX2 {
|
||||||
return c.openGeneric(dst, nonce, ciphertext, additionalData)
|
return c.openGeneric(dst, nonce, ciphertext, additionalData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,8 @@ module golang.org/x/crypto
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/net v0.53.0 // tagx:ignore
|
golang.org/x/net v0.54.0 // tagx:ignore
|
||||||
golang.org/x/sys v0.44.0
|
golang.org/x/sys v0.45.0
|
||||||
golang.org/x/term v0.43.0
|
golang.org/x/term v0.43.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
|
|||||||
@@ -663,6 +663,13 @@ func (c *client) Add(key AddedKey) error {
|
|||||||
constraints = append(constraints, agentConstrainConfirm)
|
constraints = append(constraints, agentConstrainConfirm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, ext := range key.ConstraintExtensions {
|
||||||
|
constraints = append(constraints, ssh.Marshal(constrainExtensionAgentMsg{
|
||||||
|
ExtensionName: ext.ExtensionName,
|
||||||
|
ExtensionDetails: ext.ExtensionDetails,
|
||||||
|
})...)
|
||||||
|
}
|
||||||
|
|
||||||
cert := key.Certificate
|
cert := key.Certificate
|
||||||
if cert == nil {
|
if cert == nil {
|
||||||
return c.insertKey(key.PrivateKey, key.Comment, constraints)
|
return c.insertKey(key.PrivateKey, key.Comment, constraints)
|
||||||
|
|||||||
@@ -594,3 +594,63 @@ func TestAgentExtensions(t *testing.T) {
|
|||||||
t.Fatal("should have gotten agent extension failure")
|
t.Fatal("should have gotten agent extension failure")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// capturingAgent records the AddedKey observed on the server side of the
|
||||||
|
// agent protocol. It forwards to a keyring with constraint fields stripped,
|
||||||
|
// so the keyring's own rejection of unsupported constraints does not
|
||||||
|
// interfere with what this test is measuring: the client-side wire
|
||||||
|
// serialization of ConstraintExtensions.
|
||||||
|
type capturingAgent struct {
|
||||||
|
Agent
|
||||||
|
lastAdd AddedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCapturingAgent() *capturingAgent {
|
||||||
|
return &capturingAgent{Agent: NewKeyring()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *capturingAgent) Add(key AddedKey) error {
|
||||||
|
a.lastAdd = key
|
||||||
|
stripped := key
|
||||||
|
stripped.ConstraintExtensions = nil
|
||||||
|
stripped.ConfirmBeforeUse = false
|
||||||
|
return a.Agent.Add(stripped)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAddConstraintExtensionsWireFormat verifies that client.Add serializes
|
||||||
|
// ConstraintExtensions into the SSH_AGENTC_ADD_IDENTITY payload and the
|
||||||
|
// server deserializes them back into the AddedKey delivered to the backend.
|
||||||
|
// Regressions in the client marshal loop (missing, swapped fields, wrong
|
||||||
|
// framing) would be invisible to a keyring-based rejection test, which
|
||||||
|
// signals only "extensions were present", not "the right ones arrived".
|
||||||
|
func TestAddConstraintExtensionsWireFormat(t *testing.T) {
|
||||||
|
capturing := newCapturingAgent()
|
||||||
|
client, cleanup := startAgent(t, capturing)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
constraints := []ConstraintExtension{
|
||||||
|
{ExtensionName: "ext-one@example.com", ExtensionDetails: []byte("details-one")},
|
||||||
|
{ExtensionName: "ext-two@example.com", ExtensionDetails: []byte("\x00\x01\x02\xff")},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Add(AddedKey{
|
||||||
|
PrivateKey: testPrivateKeys["rsa"],
|
||||||
|
Comment: "wire-format-test",
|
||||||
|
ConstraintExtensions: constraints,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("client.Add: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := capturing.lastAdd.ConstraintExtensions
|
||||||
|
if len(got) != len(constraints) {
|
||||||
|
t.Fatalf("server received %d extensions, want %d", len(got), len(constraints))
|
||||||
|
}
|
||||||
|
for i, want := range constraints {
|
||||||
|
if got[i].ExtensionName != want.ExtensionName {
|
||||||
|
t.Errorf("extension[%d] name: got %q, want %q", i, got[i].ExtensionName, want.ExtensionName)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got[i].ExtensionDetails, want.ExtensionDetails) {
|
||||||
|
t.Errorf("extension[%d] details: got %x, want %x", i, got[i].ExtensionDetails, want.ExtensionDetails)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+18
-5
@@ -32,8 +32,10 @@ type keyring struct {
|
|||||||
|
|
||||||
var errLocked = errors.New("agent: locked")
|
var errLocked = errors.New("agent: locked")
|
||||||
|
|
||||||
// NewKeyring returns an Agent that holds keys in memory. It is safe
|
// NewKeyring returns an Agent that holds keys in memory. It is safe for
|
||||||
// for concurrent use by multiple goroutines.
|
// concurrent use by multiple goroutines.
|
||||||
|
//
|
||||||
|
// The returned Agent only supports the "lifetime" constraint.
|
||||||
func NewKeyring() Agent {
|
func NewKeyring() Agent {
|
||||||
return &keyring{}
|
return &keyring{}
|
||||||
}
|
}
|
||||||
@@ -143,15 +145,26 @@ func (r *keyring) List() ([]*Key, error) {
|
|||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert adds a private key to the keyring. If a certificate
|
// Add adds a private key to the keyring. If a certificate is given, that
|
||||||
// is given, that certificate is added as public key. Note that
|
// certificate is added as public key.
|
||||||
// any constraints given are ignored.
|
//
|
||||||
|
// Add returns an error if key contains ConstraintExtensions or
|
||||||
|
// ConfirmBeforeUse.
|
||||||
func (r *keyring) Add(key AddedKey) error {
|
func (r *keyring) Add(key AddedKey) error {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
if r.locked {
|
if r.locked {
|
||||||
return errLocked
|
return errLocked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if key.ConfirmBeforeUse {
|
||||||
|
return errors.New("agent: confirm before use constraint is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(key.ConstraintExtensions) > 0 {
|
||||||
|
return errors.New("agent: constraint extensions are present but not supported")
|
||||||
|
}
|
||||||
|
|
||||||
signer, err := ssh.NewSignerFromKey(key.PrivateKey)
|
signer, err := ssh.NewSignerFromKey(key.PrivateKey)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -120,3 +120,42 @@ func TestAddDuplicateKey(t *testing.T) {
|
|||||||
t.Fatal("key with the updated comment not found")
|
t.Fatal("key with the updated comment not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddKeyWithConstraints(t *testing.T) {
|
||||||
|
// Verifies the keyring refuses keys carrying constraint extensions it
|
||||||
|
// cannot enforce.
|
||||||
|
agent, cleanup := startKeyringAgent(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
constraints := []ConstraintExtension{
|
||||||
|
{
|
||||||
|
ExtensionName: "extension1",
|
||||||
|
ExtensionDetails: []byte("details1"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
key := testPrivateKeys["rsa"]
|
||||||
|
|
||||||
|
err := agent.Add(AddedKey{
|
||||||
|
PrivateKey: key,
|
||||||
|
ConstraintExtensions: constraints,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("adding a key with unsupported constraints succeeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddKeyWithConfirmBeforeUse(t *testing.T) {
|
||||||
|
agent, cleanup := startKeyringAgent(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
key := testPrivateKeys["rsa"]
|
||||||
|
|
||||||
|
err := agent.Add(AddedKey{
|
||||||
|
PrivateKey: key,
|
||||||
|
ConfirmBeforeUse: true,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("adding a key with confirm before use constraint succeeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -270,6 +270,9 @@ func parseEd25519Key(req []byte) (*AddedKey, error) {
|
|||||||
if err := ssh.Unmarshal(req, &k); err != nil {
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(k.Priv) != ed25519.PrivateKeySize {
|
||||||
|
return nil, fmt.Errorf("agent: bad ED25519 key size: %d", len(k.Priv))
|
||||||
|
}
|
||||||
priv := ed25519.PrivateKey(k.Priv)
|
priv := ed25519.PrivateKey(k.Priv)
|
||||||
|
|
||||||
addedKey := &AddedKey{PrivateKey: &priv, Comment: k.Comments}
|
addedKey := &AddedKey{PrivateKey: &priv, Comment: k.Comments}
|
||||||
@@ -336,6 +339,9 @@ func parseEd25519Cert(req []byte) (*AddedKey, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(k.Priv) != ed25519.PrivateKeySize {
|
||||||
|
return nil, fmt.Errorf("agent: bad ED25519 key size: %d", len(k.Priv))
|
||||||
|
}
|
||||||
priv := ed25519.PrivateKey(k.Priv)
|
priv := ed25519.PrivateKey(k.Priv)
|
||||||
cert, ok := pubKey.(*ssh.Certificate)
|
cert, ok := pubKey.(*ssh.Certificate)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -271,3 +271,35 @@ func TestParseConstraints(t *testing.T) {
|
|||||||
t.Errorf("unexpected error: %v", err)
|
t.Errorf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseEd25519KeyShortPanic(t *testing.T) {
|
||||||
|
msg := ssh.Marshal(ed25519KeyMsg{
|
||||||
|
Type: ssh.KeyAlgoED25519,
|
||||||
|
Pub: []byte{1, 2, 3},
|
||||||
|
Priv: []byte{1, 2, 3, 4, 5},
|
||||||
|
})
|
||||||
|
|
||||||
|
a, b, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer a.Close()
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { done <- ServeAgent(NewKeyring(), a) }()
|
||||||
|
|
||||||
|
c := NewClient(b)
|
||||||
|
_, err = c.(*client).call(msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys, err := c.List()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("agent died: %v", err)
|
||||||
|
}
|
||||||
|
if len(keys) != 0 {
|
||||||
|
t.Error("short ed25519 key was accepted into keyring")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+17
-1
@@ -348,6 +348,9 @@ func (c *CertChecker) CheckHostKey(addr string, remote net.Addr, key PublicKey)
|
|||||||
if cert.CertType != HostCert {
|
if cert.CertType != HostCert {
|
||||||
return fmt.Errorf("ssh: certificate presented as a host key has type %d", cert.CertType)
|
return fmt.Errorf("ssh: certificate presented as a host key has type %d", cert.CertType)
|
||||||
}
|
}
|
||||||
|
if c.IsHostAuthority == nil {
|
||||||
|
return errors.New("ssh: cannot verify certificate, IsHostAuthority not set")
|
||||||
|
}
|
||||||
if !c.IsHostAuthority(cert.SignatureKey, addr) {
|
if !c.IsHostAuthority(cert.SignatureKey, addr) {
|
||||||
return fmt.Errorf("ssh: no authorities for hostname: %v", addr)
|
return fmt.Errorf("ssh: no authorities for hostname: %v", addr)
|
||||||
}
|
}
|
||||||
@@ -375,6 +378,9 @@ func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permis
|
|||||||
if cert.CertType != UserCert {
|
if cert.CertType != UserCert {
|
||||||
return nil, fmt.Errorf("ssh: cert has type %d", cert.CertType)
|
return nil, fmt.Errorf("ssh: cert has type %d", cert.CertType)
|
||||||
}
|
}
|
||||||
|
if c.IsUserAuthority == nil {
|
||||||
|
return nil, errors.New("ssh: cannot verify certificate, IsUserAuthority not set")
|
||||||
|
}
|
||||||
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")
|
||||||
}
|
}
|
||||||
@@ -438,7 +444,17 @@ func (c *CertChecker) CheckCert(principal string, cert *Certificate) error {
|
|||||||
if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(CertTimeInfinity) && (unixNow >= before || before < 0) {
|
if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(CertTimeInfinity) && (unixNow >= before || before < 0) {
|
||||||
return fmt.Errorf("ssh: cert has expired")
|
return fmt.Errorf("ssh: cert has expired")
|
||||||
}
|
}
|
||||||
if err := cert.SignatureKey.Verify(cert.bytesForSigning(), cert.Signature); err != nil {
|
// Match OpenSSH: the SK user-presence flag is never enforced on a
|
||||||
|
// certificate's CA signature. OpenSSH calls sshkey_verify with
|
||||||
|
// detailsp==NULL in sshkey.c:cert_parse, so the UP/UV flags are
|
||||||
|
// not even extracted. The UP bit on a CA signature reflects the
|
||||||
|
// CA operator's presence at signing time, which has no bearing on
|
||||||
|
// whether the user being authenticated is present now; enforcing
|
||||||
|
// it here would only break interop with certificates issued by
|
||||||
|
// non-interactive SK CAs. skKeyWithoutUP is a no-op for non-SK
|
||||||
|
// keys (the common case).
|
||||||
|
caKey := skKeyWithoutUP(cert.SignatureKey)
|
||||||
|
if err := caKey.Verify(cert.bytesForSigning(), cert.Signature); err != nil {
|
||||||
return fmt.Errorf("ssh: certificate signature does not verify")
|
return fmt.Errorf("ssh: certificate signature does not verify")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import (
|
|||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -128,6 +130,103 @@ func TestValidateCert(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// signSKCert builds an SK-ECDSA signature over cert.bytesForSigning()
|
||||||
|
// using caKey as the simulated hardware token. The SK signing flags
|
||||||
|
// byte (UP bit and others) is caller-controlled so tests can exercise
|
||||||
|
// the user-presence enforcement paths. caApp is the SK application
|
||||||
|
// string (typically "ssh:").
|
||||||
|
func signSKCert(t *testing.T, cert *Certificate, caKey *ecdsa.PrivateKey, caApp string, flags byte) {
|
||||||
|
t.Helper()
|
||||||
|
cert.SignatureKey = &skECDSAPublicKey{
|
||||||
|
application: caApp,
|
||||||
|
PublicKey: caKey.PublicKey,
|
||||||
|
}
|
||||||
|
cert.Nonce = make([]byte, 32)
|
||||||
|
if _, err := rand.Read(cert.Nonce); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write([]byte(caApp))
|
||||||
|
appDigest := h.Sum(nil)
|
||||||
|
h.Reset()
|
||||||
|
h.Write(cert.bytesForSigning())
|
||||||
|
dataDigest := h.Sum(nil)
|
||||||
|
|
||||||
|
var counter uint32 = 1
|
||||||
|
blob := struct {
|
||||||
|
ApplicationDigest []byte `ssh:"rest"`
|
||||||
|
Flags byte
|
||||||
|
Counter uint32
|
||||||
|
MessageDigest []byte `ssh:"rest"`
|
||||||
|
}{appDigest, flags, counter, dataDigest}
|
||||||
|
h.Reset()
|
||||||
|
h.Write(Marshal(blob))
|
||||||
|
digest := h.Sum(nil)
|
||||||
|
|
||||||
|
r, s, err := ecdsa.Sign(rand.Reader, caKey, digest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cert.Signature = &Signature{
|
||||||
|
Format: KeyAlgoSKECDSA256,
|
||||||
|
Blob: Marshal(struct{ R, S *big.Int }{r, s}),
|
||||||
|
Rest: Marshal(struct {
|
||||||
|
Flags byte
|
||||||
|
Counter uint32
|
||||||
|
}{flags, counter}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckCertSKAuthorityNoUPRequired pins the OpenSSH-parity
|
||||||
|
// behavior of CertChecker.CheckCert for SK CA signatures: a
|
||||||
|
// certificate signed by an SK CA that produced a UP=0 signature is
|
||||||
|
// accepted, because the UP bit on a CA signature has no bearing on
|
||||||
|
// the current user authentication (see sshkey.c:cert_parse, which
|
||||||
|
// passes detailsp==NULL to sshkey_verify). Without this, certs issued
|
||||||
|
// by non-interactive SK CAs would fail to verify.
|
||||||
|
func TestCheckCertSKAuthorityNoUPRequired(t *testing.T) {
|
||||||
|
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
userKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
userPub, err := NewPublicKey(&userKey.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mkCert := func(flags byte) *Certificate {
|
||||||
|
c := &Certificate{
|
||||||
|
CertType: UserCert,
|
||||||
|
Key: userPub,
|
||||||
|
ValidBefore: CertTimeInfinity,
|
||||||
|
ValidPrincipals: []string{"user"},
|
||||||
|
}
|
||||||
|
signSKCert(t, c, caKey, "ssh:", flags)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
checker := CertChecker{
|
||||||
|
IsUserAuthority: func(k PublicKey) bool {
|
||||||
|
caSK := &skECDSAPublicKey{application: "ssh:", PublicKey: caKey.PublicKey}
|
||||||
|
return bytes.Equal(k.Marshal(), caSK.Marshal())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both UP=0 and UP=1 CA signatures verify: UP is not enforced
|
||||||
|
// here, matching OpenSSH.
|
||||||
|
if err := checker.CheckCert("user", mkCert(0)); err != nil {
|
||||||
|
t.Errorf("UP=0 CA signature must verify (OpenSSH parity): %v", err)
|
||||||
|
}
|
||||||
|
if err := checker.CheckCert("user", mkCert(flagUserPresence)); err != nil {
|
||||||
|
t.Errorf("UP=1 CA signature must verify: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateCertTime(t *testing.T) {
|
func TestValidateCertTime(t *testing.T) {
|
||||||
cert := Certificate{
|
cert := Certificate{
|
||||||
ValidPrincipals: []string{"user"},
|
ValidPrincipals: []string{"user"},
|
||||||
|
|||||||
+59
-6
@@ -11,6 +11,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -131,11 +132,17 @@ func (r RejectionReason) String() string {
|
|||||||
return fmt.Sprintf("unknown reason %d", int(r))
|
return fmt.Sprintf("unknown reason %d", int(r))
|
||||||
}
|
}
|
||||||
|
|
||||||
func min(a uint32, b int) uint32 {
|
// minPayloadSize returns min(limit, length) clamped to a uint32. It is used
|
||||||
if a < uint32(b) {
|
// to compute the size of the next channel data packet from the remaining
|
||||||
return a
|
// payload. The comparison is done in int64 because length is an int — on
|
||||||
|
// 64-bit systems len(data) can exceed 2^32, and a direct uint32(length)
|
||||||
|
// cast would silently truncate to 0 at every multiple of 2^32, causing
|
||||||
|
// WriteExtended's loop to spin without making progress.
|
||||||
|
func minPayloadSize(limit uint32, length int) uint32 {
|
||||||
|
if int64(length) > int64(limit) {
|
||||||
|
return limit
|
||||||
}
|
}
|
||||||
return uint32(b)
|
return uint32(length)
|
||||||
}
|
}
|
||||||
|
|
||||||
type channelDirection uint8
|
type channelDirection uint8
|
||||||
@@ -177,6 +184,12 @@ type channel struct {
|
|||||||
// with WantReply=true outstanding. This lock is held by a
|
// with WantReply=true outstanding. This lock is held by a
|
||||||
// goroutine that has such an outgoing request pending.
|
// goroutine that has such an outgoing request pending.
|
||||||
sentRequestMu sync.Mutex
|
sentRequestMu sync.Mutex
|
||||||
|
// sentRequestPending is set to true while a SendRequest call with
|
||||||
|
// WantReply=true is in flight. handlePacket uses it as a gate: responses
|
||||||
|
// arriving while no request is pending are dropped to prevent a
|
||||||
|
// misbehaving peer from stalling the mux read loop by filling ch.msg
|
||||||
|
// with unsolicited channelRequestSuccess/Failure messages.
|
||||||
|
sentRequestPending atomic.Bool
|
||||||
|
|
||||||
incomingRequests chan *Request
|
incomingRequests chan *Request
|
||||||
|
|
||||||
@@ -251,7 +264,7 @@ func (ch *channel) WriteExtended(data []byte, extendedCode uint32) (n int, err e
|
|||||||
ch.writeMu.Unlock()
|
ch.writeMu.Unlock()
|
||||||
|
|
||||||
for len(data) > 0 {
|
for len(data) > 0 {
|
||||||
space := min(ch.maxRemotePayload, len(data))
|
space := minPayloadSize(ch.maxRemotePayload, len(data))
|
||||||
if space, err = ch.remoteWin.reserve(space); err != nil {
|
if space, err = ch.remoteWin.reserve(space); err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
@@ -460,6 +473,18 @@ func (ch *channel) handlePacket(packet []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ch.incomingRequests <- &req
|
ch.incomingRequests <- &req
|
||||||
|
case *channelRequestSuccessMsg, *channelRequestFailureMsg:
|
||||||
|
// Drop responses that arrive when no SendRequest is waiting, to
|
||||||
|
// prevent a malicious peer from filling ch.msg and stalling the
|
||||||
|
// mux read loop. The non-blocking send additionally protects the
|
||||||
|
// loop if a well-behaved caller is slow to read.
|
||||||
|
if !ch.sentRequestPending.Load() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case ch.msg <- msg:
|
||||||
|
default:
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
ch.msg <- msg
|
ch.msg <- msg
|
||||||
}
|
}
|
||||||
@@ -530,7 +555,17 @@ func (ch *channel) Reject(reason RejectionReason, message string) error {
|
|||||||
Language: "en",
|
Language: "en",
|
||||||
}
|
}
|
||||||
ch.decided = true
|
ch.decided = true
|
||||||
return ch.sendMessage(reject)
|
err := ch.sendMessage(reject)
|
||||||
|
|
||||||
|
// Remove the channel from the mux to prevent memory leaks.
|
||||||
|
// Do not call ch.close() here: no goroutine holds a reference to a
|
||||||
|
// rejected channel's internal channels (msg, incomingRequests), so
|
||||||
|
// removing it from chanList is sufficient for GC. Calling close()
|
||||||
|
// would race with the mux loop goroutine (handlePacket or dropAll),
|
||||||
|
// causing a panic from closing an already-closed channel.
|
||||||
|
ch.mux.chanList.remove(ch.localId)
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *channel) Read(data []byte) (int, error) {
|
func (ch *channel) Read(data []byte) (int, error) {
|
||||||
@@ -586,6 +621,24 @@ func (ch *channel) SendRequest(name string, wantReply bool, payload []byte) (boo
|
|||||||
if wantReply {
|
if wantReply {
|
||||||
ch.sentRequestMu.Lock()
|
ch.sentRequestMu.Lock()
|
||||||
defer ch.sentRequestMu.Unlock()
|
defer ch.sentRequestMu.Unlock()
|
||||||
|
|
||||||
|
// Open the gate so that responses arriving while this request is in
|
||||||
|
// flight are allowed to reach ch.msg. Responses arriving while no
|
||||||
|
// request is pending are dropped by handlePacket.
|
||||||
|
ch.sentRequestPending.Store(true)
|
||||||
|
defer ch.sentRequestPending.Store(false)
|
||||||
|
|
||||||
|
// Drain any spurious responses that may have been buffered. This
|
||||||
|
// prevents a previously buffered unexpected response from being
|
||||||
|
// consumed instead of the actual response for this request.
|
||||||
|
drain:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ch.msg:
|
||||||
|
default:
|
||||||
|
break drain
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := channelRequestMsg{
|
msg := channelRequestMsg{
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
// Copyright 2025 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/bits"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMinPayloadSize(t *testing.T) {
|
||||||
|
// 4 GiB (2^32). Declared as a var (not a const) so that int(bigPayload)
|
||||||
|
// is a runtime conversion: a constant conversion would fail to compile
|
||||||
|
// on 32-bit platforms with "constant 4294967296 overflows int". On
|
||||||
|
// 32-bit the value truncates to 0 at runtime, but the is64Bit cases
|
||||||
|
// that reference it are skipped by the runtime check below.
|
||||||
|
var bigPayload int64 = 1 << 32
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
maxPayload uint32
|
||||||
|
dataLen int
|
||||||
|
want uint32
|
||||||
|
is64Bit bool // Flag to run only on 64-bit architectures
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Normal Case - Data fits in payload",
|
||||||
|
maxPayload: 32768,
|
||||||
|
dataLen: 1000,
|
||||||
|
want: 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Normal Case - Data larger than payload",
|
||||||
|
maxPayload: 32768,
|
||||||
|
dataLen: 50000,
|
||||||
|
want: 32768,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Boundary Case - Data zero",
|
||||||
|
maxPayload: 32768,
|
||||||
|
dataLen: 0,
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Overflow Case - Data is exactly 4GB (1<<32)",
|
||||||
|
maxPayload: 32768,
|
||||||
|
dataLen: int(bigPayload),
|
||||||
|
want: 32768,
|
||||||
|
is64Bit: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Overflow Case - Data is 4GB + small amount",
|
||||||
|
maxPayload: 32768,
|
||||||
|
dataLen: int(bigPayload + 100),
|
||||||
|
want: 32768,
|
||||||
|
is64Bit: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
is64Bit := bits.UintSize == 64
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.is64Bit && !is64Bit {
|
||||||
|
t.Skip("Skipping test requiring 64-bit int")
|
||||||
|
}
|
||||||
|
got := minPayloadSize(tt.maxPayload, tt.dataLen)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("minPayloadSize(%d, %d) = %d; want %d", tt.maxPayload, tt.dataLen, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWriteExtendedNoInfiniteLoopOnLargeWrite is an end-to-end regression
|
||||||
|
// test for the integer-overflow bug in WriteExtended. Before the fix, a
|
||||||
|
// write whose len(data) was a multiple of 2^32 caused minPayloadSize to
|
||||||
|
// return 0; WriteExtended then spun forever, reserving 0 bytes per
|
||||||
|
// iteration and never advancing the data slice.
|
||||||
|
//
|
||||||
|
// We exercise the real WriteExtended path with a slice whose declared
|
||||||
|
// length is exactly 2^32. Allocating 4 GiB is unnecessary: each iteration
|
||||||
|
// only reads up to maxRemotePayload bytes from the head of the slice, and
|
||||||
|
// the loop blocks in remoteWin.reserve() once the channel window is
|
||||||
|
// exhausted — before the slice base advances past the underlying buffer.
|
||||||
|
//
|
||||||
|
// With the fix, the loop blocks in reserve(); we detect that via
|
||||||
|
// waitWriterBlocked(), then close the window to let WriteExtended return.
|
||||||
|
// With the bug, the loop never blocks and the test times out.
|
||||||
|
//
|
||||||
|
//go:nocheckptr
|
||||||
|
func TestWriteExtendedNoInfiniteLoopOnLargeWrite(t *testing.T) {
|
||||||
|
if bits.UintSize < 64 {
|
||||||
|
t.Skip("test requires 64-bit int to construct a slice with len >= 2^32")
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, writer, mux := channelPair(t)
|
||||||
|
defer reader.Close()
|
||||||
|
defer writer.Close()
|
||||||
|
defer mux.Close()
|
||||||
|
|
||||||
|
// Sized to hold the full pre-update remote window so that no iteration
|
||||||
|
// reads past the backing buffer before reserve() blocks.
|
||||||
|
backing := make([]byte, channelWindowSize)
|
||||||
|
var bigLen int64 = 1 << 32
|
||||||
|
bigSlice := unsafe.Slice(&backing[0], int(bigLen))
|
||||||
|
|
||||||
|
done := make(chan int, 1)
|
||||||
|
go func() {
|
||||||
|
n, _ := writer.Write(bigSlice)
|
||||||
|
done <- n
|
||||||
|
}()
|
||||||
|
|
||||||
|
blocked := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
writer.remoteWin.waitWriterBlocked()
|
||||||
|
close(blocked)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-blocked:
|
||||||
|
// Good — the loop made progress and is now blocked in reserve().
|
||||||
|
// Close the window to let WriteExtended return.
|
||||||
|
writer.remoteWin.close()
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("WriteExtended did not block in reserve within 2s — minPayloadSize likely returned 0 (integer overflow regression)")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case n := <-done:
|
||||||
|
if n == 0 {
|
||||||
|
t.Fatalf("WriteExtended returned n=0; expected progress")
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("WriteExtended did not return after closing the window")
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -407,7 +407,7 @@ func (c *gcmCipher) readCipherPacket(seqNum uint32, r io.Reader) ([]byte, error)
|
|||||||
return nil, fmt.Errorf("ssh: illegal padding %d", padding)
|
return nil, fmt.Errorf("ssh: illegal padding %d", padding)
|
||||||
}
|
}
|
||||||
|
|
||||||
if int(padding+1) >= len(plain) {
|
if int(padding)+1 >= len(plain) {
|
||||||
return nil, fmt.Errorf("ssh: padding %d too large", padding)
|
return nil, fmt.Errorf("ssh: padding %d too large", padding)
|
||||||
}
|
}
|
||||||
plain = plain[1 : length-uint32(padding)]
|
plain = plain[1 : length-uint32(padding)]
|
||||||
|
|||||||
+58
@@ -469,6 +469,12 @@ func parseRSA(in []byte) (out PublicKey, rest []byte, err error) {
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 8192 bits is also the maximum RSA key size accepted by crypto/tls for
|
||||||
|
// signature verification:
|
||||||
|
// https://github.com/golang/go/blob/69801b25/src/crypto/tls/handshake_client.go#L1096
|
||||||
|
if w.N.BitLen() > 8192 {
|
||||||
|
return nil, nil, errors.New("ssh: rsa modulus too large")
|
||||||
|
}
|
||||||
if w.E.BitLen() > 24 {
|
if w.E.BitLen() > 24 {
|
||||||
return nil, nil, errors.New("ssh: exponent too large")
|
return nil, nil, errors.New("ssh: exponent too large")
|
||||||
}
|
}
|
||||||
@@ -574,6 +580,24 @@ func checkDSAParams(param *dsa.Parameters) error {
|
|||||||
return fmt.Errorf("ssh: unsupported DSA key size %d", l)
|
return fmt.Errorf("ssh: unsupported DSA key size %d", l)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIPS 186-2 specifies that Q must be exactly 160 bits. We must enforce
|
||||||
|
// this to prevent DoS attacks where an attacker sends a huge Q which makes
|
||||||
|
// verification slow.
|
||||||
|
if l := param.Q.BitLen(); l != 160 {
|
||||||
|
return fmt.Errorf("ssh: unsupported DSA sub-prime size %d", l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The generator G is an element of the group, so it must be strictly less
|
||||||
|
// than the modulus P.
|
||||||
|
if param.G.Cmp(param.P) >= 0 {
|
||||||
|
return errors.New("ssh: DSA generator larger than modulus")
|
||||||
|
}
|
||||||
|
|
||||||
|
// G must be positive.
|
||||||
|
if param.G.Sign() <= 0 {
|
||||||
|
return errors.New("ssh: DSA generator must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,6 +620,14 @@ func parseDSA(in []byte) (out PublicKey, rest []byte, err error) {
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The public value Y must be a non-zero element of the group, i.e.
|
||||||
|
// strictly between 0 and P. crypto/dsa.Verify does not range-check Y,
|
||||||
|
// so we reject out-of-range values here to prevent a maliciously
|
||||||
|
// oversized Y from slowing verification.
|
||||||
|
if w.Y.Sign() <= 0 || w.Y.Cmp(w.P) >= 0 {
|
||||||
|
return nil, nil, errors.New("ssh: DSA public value Y out of range")
|
||||||
|
}
|
||||||
|
|
||||||
key := &dsaPublicKey{
|
key := &dsaPublicKey{
|
||||||
Parameters: param,
|
Parameters: param,
|
||||||
Y: w.Y,
|
Y: w.Y,
|
||||||
@@ -869,11 +901,25 @@ type skFields struct {
|
|||||||
Counter uint32
|
Counter uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// flagUserPresence is the "user present" bit (UP) in the SK signature
|
||||||
|
// flags, matching the FIDO CTAP2 authenticatorData UP flag. See
|
||||||
|
// openssh/PROTOCOL.u2f.
|
||||||
|
const flagUserPresence = 0x01
|
||||||
|
|
||||||
|
// errSKMissingUserPresence is returned by SK key Verify methods when
|
||||||
|
// the signature does not assert user presence and the key was not
|
||||||
|
// marked as no-touch-required.
|
||||||
|
var errSKMissingUserPresence = errors.New("ssh: signature missing required user presence flag")
|
||||||
|
|
||||||
type skECDSAPublicKey struct {
|
type skECDSAPublicKey struct {
|
||||||
// application is a URL-like string, typically "ssh:" for SSH.
|
// application is a URL-like string, typically "ssh:" for SSH.
|
||||||
// see openssh/PROTOCOL.u2f for details.
|
// see openssh/PROTOCOL.u2f for details.
|
||||||
application string
|
application string
|
||||||
ecdsa.PublicKey
|
ecdsa.PublicKey
|
||||||
|
// noTouchRequired, when true, disables the default user-presence
|
||||||
|
// check in Verify. It is set by skKeyWithoutUP on a clone of the
|
||||||
|
// key, never on an instance shared across authentication attempts.
|
||||||
|
noTouchRequired bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *skECDSAPublicKey) Type() string {
|
func (k *skECDSAPublicKey) Type() string {
|
||||||
@@ -959,6 +1005,10 @@ func (k *skECDSAPublicKey) Verify(data []byte, sig *Signature) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if skf.Flags&flagUserPresence == 0 && !k.noTouchRequired {
|
||||||
|
return errSKMissingUserPresence
|
||||||
|
}
|
||||||
|
|
||||||
blob := struct {
|
blob := struct {
|
||||||
ApplicationDigest []byte `ssh:"rest"`
|
ApplicationDigest []byte `ssh:"rest"`
|
||||||
Flags byte
|
Flags byte
|
||||||
@@ -992,6 +1042,10 @@ type skEd25519PublicKey struct {
|
|||||||
// see openssh/PROTOCOL.u2f for details.
|
// see openssh/PROTOCOL.u2f for details.
|
||||||
application string
|
application string
|
||||||
ed25519.PublicKey
|
ed25519.PublicKey
|
||||||
|
// noTouchRequired, when true, disables the default user-presence
|
||||||
|
// check in Verify. It is set by skKeyWithoutUP on a clone of the
|
||||||
|
// key, never on an instance shared across authentication attempts.
|
||||||
|
noTouchRequired bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *skEd25519PublicKey) Type() string {
|
func (k *skEd25519PublicKey) Type() string {
|
||||||
@@ -1066,6 +1120,10 @@ func (k *skEd25519PublicKey) Verify(data []byte, sig *Signature) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if skf.Flags&flagUserPresence == 0 && !k.noTouchRequired {
|
||||||
|
return errSKMissingUserPresence
|
||||||
|
}
|
||||||
|
|
||||||
blob := struct {
|
blob := struct {
|
||||||
ApplicationDigest []byte `ssh:"rest"`
|
ApplicationDigest []byte `ssh:"rest"`
|
||||||
Flags byte
|
Flags byte
|
||||||
|
|||||||
+384
-12
@@ -12,6 +12,7 @@ import (
|
|||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -19,6 +20,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/big"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -216,6 +218,20 @@ func TestParseRSAPrivateKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseRSAModulusTooLarge(t *testing.T) {
|
||||||
|
rsa16384 := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAIAQC0vyqtnpzMZ8Th6gv1t3+7dDVd2X+7MwjqKe08/wrKZaIuCAHX7jglC8FdHfiOcPhLHAreSJLZGXiSzCTUExbp9Zdt7tHluKMQxZCnbk02V89ggc4KpptQqfcmjMNgrEPF4PGJBe3eSYu9m7A1ptm0buWxaKtA17O1c2q2CVNYmuajSUAfi8+AZcdyRAX8eqga3u77DEj4O+CCuNA908NYv9Pc7SbaCwCgX3FXh7MvlaYcruTk529psNk1SK6uwBNak3CLRrXUBo5yTO7cCO0gmlvd8SwtY5mPaDLLCJa+Ed2t9OCt8HCPrdkH3aiQBTP3k/Iofy3NRv+/1lenJP1qWB2rnyagoj+osrLc6m8PRd7GBje7KdO6nyY1D+q1MvX4zV1qNQfD+38N6PixVX1MGKw72sD+oLCDjHKS24BHfnfaE7f8n+Alp2mxnSz9tYqsN3JXrbbwblzH8YFOz9MMKkTY5lQNQFoKdTHoPVXBqcMlGQGgC/kkH9TH110MDE3L+Z0i9kG53QLcHNVC0cJzo6X77PQF1WoS33Ssf1s21kUB6fe/5G2L2Tuy635ingtnQMUazwJMli+y/3U7QqqcuJzITbLw/zozRzbqFRE4HHOyGkJSptraH4/3cjmYro8JL20A7EUDiXxmn+z+eoxaBWaEP2Rs5LH0ocrwYmBmSUIrPy4e5ESlRRVuv+prNUB1pYGnFZUI9HRRFxRFg+SxXZZp4mq8jzroRLZFAqKY1/eDgsCGoV1HG3QW1t0jGNkv8BL/Zpz7Hhmt0lRr7rVN+PJaTQx8NNUJZ/l7nMXp/+7aTbkyI8PiNyemko0+joDrkBWdcK0vi8Du6k3oQRBUsJ0aM0iPxMWQq/oYyHqpf6revycYTUdOGeckGWXuXLNRIZQ/PsKuw/teqDaBkkMKaQH2oXWMsacL6WDu5VYUQidOtaTgHX8AKVHF/vobTQfw7HgPKlyRN41dAX+YIjRABYoqU7M2MYPuoPYbqzTz7iLZyFP8FCJ3dRQBTWSeer/s+Ih6Ev1EsznVmAw9SKb5q8QCSYk5mMugUTDVETOQo10CGAF6094/PcJRXgag/uWs4k+13c+/HaeNzrHQyqzWnf9bFFHIvs7Wqov9k+nWv/TwSGplkyBk4bHfhEZNbl+b6T8sZXsv8mTT+INzKT4DCVZ5a/G+NQ6Xo26Aa6Se5DOdzdXOLZ7yIhOi+olip8AOXIIFLb7Vee1+q7Ri6ScUJEKxdEvec5EceXEtWs0U4AnuHgiH0ewJSX9yUp8T4ImCzu00JhTMOH2nXU3PJuGgZgrDifNWXGpFruIBjHWRKpESpy+9HPGoNoxSO/hzSfuM5LKvwX+I96cMK5OC+fKsBh59cMpU4YnqFNh59ZP/kaMDxAXgrVku5s9oB3Rgc7t1rfm6dqycBiwn4QnYJpk7M+gzAbrF/nty8y84ajNdhINXgMgvv3JLC5YTqznBPm3koHSOEF8f4xtCZtzjNN6jNjX8gEvxoRpC9Z+X15c8XBSFyDMBoi0FMDor619XJKgTxNiOYmeH/DLhKEb8MIaug8IFu34FKbG81IsWD+zwA1A5xqXEVlgsfEOcML+Oe2rulkpwQBWnsmY+f/Z80004Mytreo/ME2TDuPJPOi15D0j0CbwG7xzE8qrn0EomBE9mrwH3uuH7df3lzx1HiLAmh2s4MO5R64AoWeAPQW9lWZPzp+2dNgk+0qWpoEsMN+r91yWxUZHgeoC+GJKs13LItH32sGInyUrquMYQjEh44fAEFrMREcS4A7l+875GUmWC6i2MSLyvtAzuPS2IV0t9GU0ooH8cfG5JUeDnsJcVanJTR+XuXPetM+cDyGFTGri3lsCndE9maLgs9iDHtJxzKUBUUakaIPLsXtZD51cS4jhSbbHuzgC21BRtnBhCE2phVYbz4Tj0t90wBmemBaP2eVwv9p4s/JJAHEELvV7k+Gro4FozOoC2WBdqdTPDFB00la08O6ADBdjd2el2pRA7HCTG6v3qwA6RQJfm4UHOXaFYhhiNngxHB4VHZMvpd+YMEqNmtYOb4lzWwI13iHU0Rh0Sj4IaY9EwEy/KjR5dc2DjWGj11SFFCP1uG24fccF+LhtdMNDItI1mBxfaeRelYlsCZwtbbVLnuVQ5izuTzXa48x7CBaHnD3i09BZCuQITLX4d7KkD6CLWWTHrc5onLFJKxRF/p5AEFoqtN3vP9CsLXSYNKxFV5UbxAY5TnAoLFCCo0WbgvtAV77jWSbru9Oq/7ORN9smog4zNuYBZE1uXjxM2xszDduOym7+CxwpJMc7XPknt2XwiANaf2QANMMMH6lmnjTH1RVR7oH4+ts4xVcsdiO/QOlMp+Th9/KIMYyUevR18vHbs+88uxzIG28/58xLZTs6rZc71g9mGw9Q21ugL2sfTc7e0VDMnAmdg+92s2RXQMvkmx+oRc+IsFqgOZzjYcTmnJxZaXsoIsydDVcTalgmhK6/dD0grPLaGgaeXMEw2hN8p8seAHrGRW8+6WBD63NBYaAG0/zwuGOCHUo/BeG9bz39Hsz9Yvs5KvJiLhmM5K+8RlnxIY329REqdFZVyuXyy0NpDueFQelnd0j47Quc7GJGX3QycJiKpLolMtDnpnjgYOvdfycM+JEMZGwpLsBBE8R6vJ3RVczT6DdMtpVQ4l7kOzsPSYlp4qAv5fiqUboyv5eP7G7MOD/qSUwBnMS1p9Vm4Wr8B9w=="
|
||||||
|
_, _, _, _, err := ParseAuthorizedKey([]byte(rsa16384))
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("ParseAuthorizedKey accepted a 16384-bit modulus; expected it to be rejected")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedError := "rsa modulus too large"
|
||||||
|
if !strings.Contains(err.Error(), expectedError) {
|
||||||
|
t.Errorf("unexpected error message: got %q, want substring %q", err.Error(), expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseECPrivateKey(t *testing.T) {
|
func TestParseECPrivateKey(t *testing.T) {
|
||||||
key := testPrivateKeys["ecdsa"]
|
key := testPrivateKeys["ecdsa"]
|
||||||
|
|
||||||
@@ -272,18 +288,18 @@ func TestParseEncryptedPrivateKeysWithPassphrase(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseEncryptedPrivateKeysWithUnsupportedCiphers(t *testing.T) {
|
func TestParseEncryptedPrivateKeysWithUnsupportedCiphers(t *testing.T) {
|
||||||
for _, tt := range testdata.UnsupportedCipherData {
|
for _, tt := range testdata.UnsupportedCipherData {
|
||||||
t.Run(tt.Name, func(t *testing.T){
|
t.Run(tt.Name, func(t *testing.T) {
|
||||||
_, err := ParsePrivateKeyWithPassphrase(tt.PEMBytes, []byte(tt.EncryptionKey))
|
_, err := ParsePrivateKeyWithPassphrase(tt.PEMBytes, []byte(tt.EncryptionKey))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("expected 'unknown cipher' error for %q, got nil", tt.Name)
|
t.Fatalf("expected 'unknown cipher' error for %q, got nil", tt.Name)
|
||||||
// If this cipher is now supported, remove it from testdata.UnsupportedCipherData
|
// If this cipher is now supported, remove it from testdata.UnsupportedCipherData
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "unknown cipher") {
|
if !strings.Contains(err.Error(), "unknown cipher") {
|
||||||
t.Errorf("wanted 'unknown cipher' error, got %v", err.Error())
|
t.Errorf("wanted 'unknown cipher' error, got %v", err.Error())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseEncryptedPrivateKeysWithIncorrectPassphrase(t *testing.T) {
|
func TestParseEncryptedPrivateKeysWithIncorrectPassphrase(t *testing.T) {
|
||||||
@@ -356,6 +372,72 @@ func TestMarshalParsePublicKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseDSAHugeQ(t *testing.T) {
|
||||||
|
P := new(big.Int).Lsh(big.NewInt(1), 1023)
|
||||||
|
Q := new(big.Int).Lsh(big.NewInt(1), 20000) // very large
|
||||||
|
// G and Y: Dummy values, just needs to be < P to pass that specific check
|
||||||
|
G := big.NewInt(2)
|
||||||
|
Y := big.NewInt(5)
|
||||||
|
|
||||||
|
rawKey := struct {
|
||||||
|
P, Q, G, Y *big.Int
|
||||||
|
}{
|
||||||
|
P: P,
|
||||||
|
Q: Q,
|
||||||
|
G: G,
|
||||||
|
Y: Y,
|
||||||
|
}
|
||||||
|
|
||||||
|
inputBytes := Marshal(&rawKey)
|
||||||
|
|
||||||
|
_, _, err := parseDSA(inputBytes)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("parseDSA accepted a DSA key with large Q")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedError := "ssh: unsupported DSA sub-prime size"
|
||||||
|
if !strings.Contains(err.Error(), expectedError) {
|
||||||
|
t.Errorf("unexpected error message: got %q, want substring %q", err.Error(), expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseDSAYOutOfRange(t *testing.T) {
|
||||||
|
// Valid 1024/160 parameters (values don't need to be a real DSA group,
|
||||||
|
// they only need to pass the checkDSAParams bit-length checks and the
|
||||||
|
// G < P / G > 0 checks).
|
||||||
|
P := new(big.Int).Lsh(big.NewInt(1), 1023)
|
||||||
|
P.SetBit(P, 0, 1) // make P odd so it can pass as a prime candidate shape
|
||||||
|
Q := new(big.Int).Lsh(big.NewInt(1), 159)
|
||||||
|
Q.SetBit(Q, 0, 1)
|
||||||
|
G := big.NewInt(2)
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
Y *big.Int
|
||||||
|
}{
|
||||||
|
{"Y_zero", big.NewInt(0)},
|
||||||
|
{"Y_negative", big.NewInt(-1)},
|
||||||
|
{"Y_equals_P", new(big.Int).Set(P)},
|
||||||
|
{"Y_greater_than_P", new(big.Int).Add(P, big.NewInt(1))},
|
||||||
|
{"Y_much_greater_than_P", new(big.Int).Lsh(big.NewInt(1), 20000)},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
rawKey := struct {
|
||||||
|
P, Q, G, Y *big.Int
|
||||||
|
}{P: P, Q: Q, G: G, Y: tc.Y}
|
||||||
|
|
||||||
|
_, _, err := parseDSA(Marshal(&rawKey))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("parseDSA accepted a DSA key with Y=%s (P=%s)", tc.Y, P)
|
||||||
|
}
|
||||||
|
expectedError := "DSA public value Y out of range"
|
||||||
|
if !strings.Contains(err.Error(), expectedError) {
|
||||||
|
t.Errorf("unexpected error message: got %q, want substring %q", err.Error(), expectedError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMarshalPrivateKey(t *testing.T) {
|
func TestMarshalPrivateKey(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -760,6 +842,296 @@ func TestSKKeys(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// skTestHarness builds SK-formatted signatures over a fixed payload
|
||||||
|
// using a caller-supplied signing function, letting tests vary the UP
|
||||||
|
// flag byte without duplicating the wire-format scaffolding.
|
||||||
|
type skTestHarness struct {
|
||||||
|
format string
|
||||||
|
application string
|
||||||
|
data []byte
|
||||||
|
// sign takes the SHA-256 digest of the marshalled SK blob and
|
||||||
|
// returns the value to embed in Signature.Blob. For ECDSA keys the
|
||||||
|
// helper feeds the digest to ecdsa.Sign; for ed25519 the helper
|
||||||
|
// feeds the raw marshalled blob to ed25519.Sign.
|
||||||
|
signDigest func(digest []byte) []byte
|
||||||
|
signBlob func(blob []byte) []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h skTestHarness) sign(t *testing.T, flags byte) *Signature {
|
||||||
|
t.Helper()
|
||||||
|
hsh := sha256.New()
|
||||||
|
hsh.Write([]byte(h.application))
|
||||||
|
appDigest := hsh.Sum(nil)
|
||||||
|
|
||||||
|
hsh.Reset()
|
||||||
|
hsh.Write(h.data)
|
||||||
|
dataDigest := hsh.Sum(nil)
|
||||||
|
|
||||||
|
var counter uint32 = 1
|
||||||
|
blob := struct {
|
||||||
|
ApplicationDigest []byte `ssh:"rest"`
|
||||||
|
Flags byte
|
||||||
|
Counter uint32
|
||||||
|
MessageDigest []byte `ssh:"rest"`
|
||||||
|
}{appDigest, flags, counter, dataDigest}
|
||||||
|
marshalled := Marshal(blob)
|
||||||
|
|
||||||
|
var sigBlob []byte
|
||||||
|
if h.signDigest != nil {
|
||||||
|
hsh.Reset()
|
||||||
|
hsh.Write(marshalled)
|
||||||
|
sigBlob = h.signDigest(hsh.Sum(nil))
|
||||||
|
} else {
|
||||||
|
sigBlob = h.signBlob(marshalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Signature{
|
||||||
|
Format: h.format,
|
||||||
|
Blob: sigBlob,
|
||||||
|
Rest: Marshal(struct {
|
||||||
|
Flags byte
|
||||||
|
Counter uint32
|
||||||
|
}{flags, counter}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSKUserPresence(t *testing.T) {
|
||||||
|
ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := skTestHarness{
|
||||||
|
format: "sk-ecdsa-sha2-nistp256@openssh.com",
|
||||||
|
application: "ssh:",
|
||||||
|
data: []byte("test data"),
|
||||||
|
signDigest: func(digest []byte) []byte {
|
||||||
|
r, s, err := ecdsa.Sign(rand.Reader, ecKey, digest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return Marshal(struct{ R, S *big.Int }{r, s})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pk := &skECDSAPublicKey{
|
||||||
|
application: h.application,
|
||||||
|
PublicKey: ecKey.PublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid signature with UP=1 should pass.
|
||||||
|
if err := pk.Verify(h.data, h.sign(t, flagUserPresence)); err != nil {
|
||||||
|
t.Errorf("Verify failed with UP=1: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid signature with UP=0 should fail with the user-presence sentinel.
|
||||||
|
sigNoUP := h.sign(t, 0)
|
||||||
|
if err := pk.Verify(h.data, sigNoUP); !errors.Is(err, errSKMissingUserPresence) {
|
||||||
|
t.Errorf("expected errSKMissingUserPresence, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UV set but UP clear must still fail: we only waive UP, never UV-only.
|
||||||
|
if err := pk.Verify(h.data, h.sign(t, 0x04)); !errors.Is(err, errSKMissingUserPresence) {
|
||||||
|
t.Errorf("UV-only (flags=0x04): expected errSKMissingUserPresence, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With noTouchRequired, UP=0 passes; UP=1+UV=1 also passes.
|
||||||
|
pk.noTouchRequired = true
|
||||||
|
if err := pk.Verify(h.data, sigNoUP); err != nil {
|
||||||
|
t.Errorf("Verify with noTouchRequired failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := pk.Verify(h.data, h.sign(t, flagUserPresence|0x04)); err != nil {
|
||||||
|
t.Errorf("Verify UP|UV with noTouchRequired failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSKKeyWithoutUP(t *testing.T) {
|
||||||
|
ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
edPub, _, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
skEC := &skECDSAPublicKey{PublicKey: ecKey.PublicKey}
|
||||||
|
skED := &skEd25519PublicKey{PublicKey: edPub}
|
||||||
|
certEC := &Certificate{Key: &skECDSAPublicKey{PublicKey: ecKey.PublicKey}}
|
||||||
|
certED := &Certificate{Key: &skEd25519PublicKey{PublicKey: edPub}}
|
||||||
|
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rsaPub, err := NewPublicKey(&rsaKey.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
certRSA := &Certificate{Key: rsaPub}
|
||||||
|
|
||||||
|
// SK raw keys: return a clone with the flag set, original untouched.
|
||||||
|
gotEC := skKeyWithoutUP(skEC)
|
||||||
|
if gotEC == skEC {
|
||||||
|
t.Error("skECDSAPublicKey: expected a clone, got the original pointer")
|
||||||
|
}
|
||||||
|
if !gotEC.(*skECDSAPublicKey).noTouchRequired {
|
||||||
|
t.Error("skECDSAPublicKey clone: noTouchRequired not set")
|
||||||
|
}
|
||||||
|
if skEC.noTouchRequired {
|
||||||
|
t.Error("skECDSAPublicKey: original mutated")
|
||||||
|
}
|
||||||
|
|
||||||
|
gotED := skKeyWithoutUP(skED)
|
||||||
|
if gotED == skED {
|
||||||
|
t.Error("skEd25519PublicKey: expected a clone, got the original pointer")
|
||||||
|
}
|
||||||
|
if !gotED.(*skEd25519PublicKey).noTouchRequired {
|
||||||
|
t.Error("skEd25519PublicKey clone: noTouchRequired not set")
|
||||||
|
}
|
||||||
|
if skED.noTouchRequired {
|
||||||
|
t.Error("skEd25519PublicKey: original mutated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Certificate wrapping SK: return a clone of the cert with a cloned
|
||||||
|
// SK key inside. Neither the original cert nor the original inner
|
||||||
|
// key must be mutated.
|
||||||
|
originalInnerEC := certEC.Key
|
||||||
|
gotCertEC := skKeyWithoutUP(certEC)
|
||||||
|
if gotCertEC == certEC {
|
||||||
|
t.Error("*Certificate(SK ecdsa): expected a clone, got the original pointer")
|
||||||
|
}
|
||||||
|
if got := gotCertEC.(*Certificate).Key.(*skECDSAPublicKey); !got.noTouchRequired {
|
||||||
|
t.Error("*Certificate(SK ecdsa): inner clone missing noTouchRequired")
|
||||||
|
}
|
||||||
|
if certEC.Key != originalInnerEC {
|
||||||
|
t.Error("*Certificate(SK ecdsa): original cert's Key pointer mutated")
|
||||||
|
}
|
||||||
|
if originalInnerEC.(*skECDSAPublicKey).noTouchRequired {
|
||||||
|
t.Error("*Certificate(SK ecdsa): original inner key mutated")
|
||||||
|
}
|
||||||
|
|
||||||
|
gotCertED := skKeyWithoutUP(certED)
|
||||||
|
if gotCertED == certED {
|
||||||
|
t.Error("*Certificate(SK ed25519): expected a clone, got the original pointer")
|
||||||
|
}
|
||||||
|
if got := gotCertED.(*Certificate).Key.(*skEd25519PublicKey); !got.noTouchRequired {
|
||||||
|
t.Error("*Certificate(SK ed25519): inner clone missing noTouchRequired")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-SK key inside a cert: return original unchanged (nothing to clone).
|
||||||
|
if got := skKeyWithoutUP(certRSA); got != certRSA {
|
||||||
|
t.Error("*Certificate(RSA): expected the original pointer back")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain non-SK key: return original unchanged.
|
||||||
|
if got := skKeyWithoutUP(rsaPub); got != rsaPub {
|
||||||
|
t.Error("rsaPublicKey: expected the original pointer back")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pathological: *Certificate whose Key is itself a *Certificate.
|
||||||
|
// The SSH cert format forbids this and parseCert rejects it, but
|
||||||
|
// a Go caller can still construct such a value. skKeyWithoutUP
|
||||||
|
// must not recurse into it (or panic); it returns the input
|
||||||
|
// unchanged. This also defends against a hypothetical cycle built
|
||||||
|
// from hand-constructed Certificate pointers.
|
||||||
|
nestedCert := &Certificate{Key: &Certificate{Key: skEC}}
|
||||||
|
if got := skKeyWithoutUP(nestedCert); got != nestedCert {
|
||||||
|
t.Error("*Certificate wrapping *Certificate: expected the original pointer back")
|
||||||
|
}
|
||||||
|
selfCycle := &Certificate{}
|
||||||
|
selfCycle.Key = selfCycle
|
||||||
|
if got := skKeyWithoutUP(selfCycle); got != selfCycle {
|
||||||
|
t.Error("self-referential *Certificate: expected the original pointer back")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoTouchAllowed(t *testing.T) {
|
||||||
|
ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sk := &skECDSAPublicKey{PublicKey: ecKey.PublicKey}
|
||||||
|
certNoExt := &Certificate{Key: sk}
|
||||||
|
certOptOut := &Certificate{
|
||||||
|
Key: sk,
|
||||||
|
Permissions: Permissions{Extensions: map[string]string{"no-touch-required": ""}},
|
||||||
|
}
|
||||||
|
// Non-empty value: OpenSSH writes "" but any value must be accepted
|
||||||
|
// because the check is presence-only.
|
||||||
|
certOptOutNonEmpty := &Certificate{
|
||||||
|
Key: sk,
|
||||||
|
Permissions: Permissions{Extensions: map[string]string{"no-touch-required": "yes"}},
|
||||||
|
}
|
||||||
|
// no-touch-required belongs in Extensions, never CriticalOptions.
|
||||||
|
// Putting it in CriticalOptions must NOT be treated as opt-out.
|
||||||
|
certCritOnly := &Certificate{
|
||||||
|
Key: sk,
|
||||||
|
Permissions: Permissions{CriticalOptions: map[string]string{"no-touch-required": ""}},
|
||||||
|
}
|
||||||
|
|
||||||
|
permsEmpty := &Permissions{}
|
||||||
|
permsOptOut := &Permissions{Extensions: map[string]string{"no-touch-required": ""}}
|
||||||
|
permsCritOnly := &Permissions{CriticalOptions: map[string]string{"no-touch-required": ""}}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
pub PublicKey
|
||||||
|
perms *Permissions
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"nil perms, no cert", sk, nil, false},
|
||||||
|
{"empty perms, no cert", sk, permsEmpty, false},
|
||||||
|
{"perms opt-out, raw key", sk, permsOptOut, true},
|
||||||
|
{"nil perms, cert opt-out", certOptOut, nil, true},
|
||||||
|
{"nil perms, cert opt-out non-empty value", certOptOutNonEmpty, nil, true},
|
||||||
|
{"nil perms, cert no ext", certNoExt, nil, false},
|
||||||
|
{"perms opt-out, cert no ext", certNoExt, permsOptOut, true},
|
||||||
|
{"empty perms, cert opt-out", certOptOut, permsEmpty, true},
|
||||||
|
// Negative controls: CriticalOptions must not waive UP.
|
||||||
|
{"critical-options only, raw key", sk, permsCritOnly, false},
|
||||||
|
{"critical-options only, cert", certCritOnly, nil, false},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := noTouchAllowed(tc.pub, tc.perms); got != tc.want {
|
||||||
|
t.Errorf("noTouchAllowed = %v, want %v", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSKUserPresenceEd25519(t *testing.T) {
|
||||||
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
h := skTestHarness{
|
||||||
|
format: "sk-ssh-ed25519@openssh.com",
|
||||||
|
application: "ssh:",
|
||||||
|
data: []byte("test data"),
|
||||||
|
signBlob: func(blob []byte) []byte {
|
||||||
|
return ed25519.Sign(priv, blob)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pk := &skEd25519PublicKey{
|
||||||
|
application: h.application,
|
||||||
|
PublicKey: pub,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pk.Verify(h.data, h.sign(t, flagUserPresence)); err != nil {
|
||||||
|
t.Errorf("Verify failed with UP=1: %v", err)
|
||||||
|
}
|
||||||
|
sigNoUP := h.sign(t, 0)
|
||||||
|
if err := pk.Verify(h.data, sigNoUP); !errors.Is(err, errSKMissingUserPresence) {
|
||||||
|
t.Errorf("expected errSKMissingUserPresence, got: %v", err)
|
||||||
|
}
|
||||||
|
pk.noTouchRequired = true
|
||||||
|
if err := pk.Verify(h.data, sigNoUP); err != nil {
|
||||||
|
t.Errorf("Verify with noTouchRequired failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewSignerWithAlgos(t *testing.T) {
|
func TestNewSignerWithAlgos(t *testing.T) {
|
||||||
algorithSigner, ok := testSigners["rsa"].(AlgorithmSigner)
|
algorithSigner, ok := testSigners["rsa"].(AlgorithmSigner)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -160,8 +160,13 @@ 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 {
|
||||||
_, ok := db.revoked[string(key.Marshal())]
|
if _, ok := db.revoked[string(key.Marshal())]; ok {
|
||||||
return ok
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := db.revoked[string(key.SignatureKey.Marshal())]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const markerCert = "@cert-authority"
|
const markerCert = "@cert-authority"
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ package knownhosts
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -272,7 +274,45 @@ func TestWildcardMatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(hanwen): test coverage for certificates.
|
func TestRevokedCA(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)
|
||||||
|
}
|
||||||
|
caKey := caSigner.PublicKey()
|
||||||
|
|
||||||
|
_, hostPriv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
hostKey, err := ssh.NewPublicKey(hostPriv.Public())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cert := &ssh.Certificate{
|
||||||
|
CertType: ssh.HostCert,
|
||||||
|
Key: hostKey,
|
||||||
|
ValidBefore: ssh.CertTimeInfinity,
|
||||||
|
ValidPrincipals: []string{"server.org"},
|
||||||
|
}
|
||||||
|
if err := cert.SignCert(rand.Reader, caSigner); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caLine := "ssh-ed25519 " + serialize(caKey)[len("ssh-ed25519 "):]
|
||||||
|
knownHostsData := "@revoked server.org " + caLine + "\n" +
|
||||||
|
"@cert-authority server.org " + caLine + "\n"
|
||||||
|
db := testDB(t, knownHostsData)
|
||||||
|
|
||||||
|
if !db.IsRevoked(cert) {
|
||||||
|
t.Error("IsRevoked returned false for certificate signed by revoked CA")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const testHostname = "hostname"
|
const testHostname = "hostname"
|
||||||
|
|
||||||
|
|||||||
+32
-4
@@ -91,9 +91,10 @@ type mux struct {
|
|||||||
|
|
||||||
incomingChannels chan NewChannel
|
incomingChannels chan NewChannel
|
||||||
|
|
||||||
globalSentMu sync.Mutex
|
globalSentMu sync.Mutex
|
||||||
globalResponses chan interface{}
|
globalSentPending atomic.Bool
|
||||||
incomingRequests chan *Request
|
globalResponses chan interface{}
|
||||||
|
incomingRequests chan *Request
|
||||||
|
|
||||||
errCond *sync.Cond
|
errCond *sync.Cond
|
||||||
err error
|
err error
|
||||||
@@ -141,6 +142,24 @@ func (m *mux) SendRequest(name string, wantReply bool, payload []byte) (bool, []
|
|||||||
if wantReply {
|
if wantReply {
|
||||||
m.globalSentMu.Lock()
|
m.globalSentMu.Lock()
|
||||||
defer m.globalSentMu.Unlock()
|
defer m.globalSentMu.Unlock()
|
||||||
|
|
||||||
|
// Open the gate so that responses arriving while this request is in
|
||||||
|
// flight are allowed to reach globalResponses. Any response arriving
|
||||||
|
// while no request is pending is dropped by handleGlobalPacket.
|
||||||
|
m.globalSentPending.Store(true)
|
||||||
|
defer m.globalSentPending.Store(false)
|
||||||
|
|
||||||
|
// Drain any spurious responses that may have been buffered. This prevents
|
||||||
|
// a previously buffered unexpected response from being consumed instead
|
||||||
|
// of the actual response for this request.
|
||||||
|
drain:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-m.globalResponses:
|
||||||
|
default:
|
||||||
|
break drain
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.sendMessage(globalRequestMsg{
|
if err := m.sendMessage(globalRequestMsg{
|
||||||
@@ -267,7 +286,16 @@ func (m *mux) handleGlobalPacket(packet []byte) error {
|
|||||||
mux: m,
|
mux: m,
|
||||||
}
|
}
|
||||||
case *globalRequestSuccessMsg, *globalRequestFailureMsg:
|
case *globalRequestSuccessMsg, *globalRequestFailureMsg:
|
||||||
m.globalResponses <- msg
|
// Drop responses that arrive when no SendRequest is waiting, to
|
||||||
|
// prevent a malicious peer from staging responses for a future
|
||||||
|
// caller.
|
||||||
|
if !m.globalSentPending.Load() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case m.globalResponses <- msg:
|
||||||
|
default:
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("not a global message %#v", msg))
|
panic(fmt.Sprintf("not a global message %#v", msg))
|
||||||
}
|
}
|
||||||
|
|||||||
+529
@@ -825,6 +825,50 @@ func TestMuxChannelWindowDeferredUpdates(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMuxChannelRejectRemovesFromMux(t *testing.T) {
|
||||||
|
serverMux, clientMux := muxPair()
|
||||||
|
defer serverMux.Close()
|
||||||
|
defer clientMux.Close()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
t.Cleanup(wg.Wait)
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
// The server waits for the channel creation request
|
||||||
|
newCh, ok := <-serverMux.incomingChannels
|
||||||
|
if !ok {
|
||||||
|
t.Error("failed to accept channel")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ch := newCh.(*channel)
|
||||||
|
|
||||||
|
if serverMux.chanList.getChan(ch.localId) == nil {
|
||||||
|
t.Errorf("channel %d is not in the chanList before Reject", ch.localId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ch.Reject(Prohibited, "rejecting this channel"); err != nil {
|
||||||
|
t.Errorf("Reject failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if serverMux.chanList.getChan(ch.localId) != nil {
|
||||||
|
t.Errorf("channel %d is still in the chanList after Reject", ch.localId)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, _, err := clientMux.OpenChannel("test_leak", nil)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error (channel rejected), but got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := err.(*OpenChannelError); !ok {
|
||||||
|
t.Errorf("expected *OpenChannelError, got: %T", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Don't ship code with debug=true.
|
// Don't ship code with debug=true.
|
||||||
func TestDebug(t *testing.T) {
|
func TestDebug(t *testing.T) {
|
||||||
if debugMux {
|
if debugMux {
|
||||||
@@ -837,3 +881,488 @@ func TestDebug(t *testing.T) {
|
|||||||
t.Error("transport debug switched on")
|
t.Error("transport debug switched on")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMuxUnexpectedGlobalResponsesDiscarded(t *testing.T) {
|
||||||
|
clientPipe, serverPipe := memPipe()
|
||||||
|
client := newMux(clientPipe)
|
||||||
|
defer serverPipe.Close()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
// Send multiple unexpected global responses, this should not block the
|
||||||
|
// globalResponses channel.
|
||||||
|
for i := range 5 {
|
||||||
|
err := serverPipe.writePacket(Marshal(globalRequestSuccessMsg{
|
||||||
|
Data: []byte{byte(i)},
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
done <- fmt.Errorf("send success msg %d: %w", i, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range 5 {
|
||||||
|
err := serverPipe.writePacket(Marshal(globalRequestFailureMsg{
|
||||||
|
Data: []byte{byte(i)},
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
done <- fmt.Errorf("send failure msg %d: %w", i, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now send a global request and wait for the response. This
|
||||||
|
// verifies the mux is still processing packets.
|
||||||
|
err := serverPipe.writePacket(Marshal(globalRequestMsg{
|
||||||
|
Type: "keepalive@golang.org",
|
||||||
|
WantReply: true,
|
||||||
|
Data: nil,
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
done <- fmt.Errorf("send global request: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
packet, err := serverPipe.readPacket()
|
||||||
|
if err != nil {
|
||||||
|
done <- fmt.Errorf("read packet: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
decoded, err := decode(packet)
|
||||||
|
if err != nil {
|
||||||
|
done <- fmt.Errorf("decode: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch decoded.(type) {
|
||||||
|
case *globalRequestSuccessMsg, *globalRequestFailureMsg:
|
||||||
|
// Expected response
|
||||||
|
default:
|
||||||
|
done <- fmt.Errorf("unexpected packet type: %T", decoded)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
done <- nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Handle the incoming request from the server and reply
|
||||||
|
req, ok := <-client.incomingRequests
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("incomingRequests channel closed unexpectedly")
|
||||||
|
}
|
||||||
|
if req.Type != "keepalive@golang.org" {
|
||||||
|
t.Fatalf("unexpected request type: %s", req.Type)
|
||||||
|
}
|
||||||
|
if err := req.Reply(true, nil); err != nil {
|
||||||
|
t.Fatalf("Reply: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := <-done; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxConcurrentGlobalRequests(t *testing.T) {
|
||||||
|
clientMux, serverMux := muxPair()
|
||||||
|
defer serverMux.Close()
|
||||||
|
defer clientMux.Close()
|
||||||
|
|
||||||
|
const numRequests = 50
|
||||||
|
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(serverDone)
|
||||||
|
for r := range serverMux.incomingRequests {
|
||||||
|
if r.WantReply {
|
||||||
|
replyData := append([]byte("reply:"), r.Payload...)
|
||||||
|
r.Reply(true, replyData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var clientWg sync.WaitGroup
|
||||||
|
clientWg.Add(numRequests)
|
||||||
|
|
||||||
|
errCh := make(chan error, numRequests)
|
||||||
|
|
||||||
|
for i := range numRequests {
|
||||||
|
go func(id int) {
|
||||||
|
defer clientWg.Done()
|
||||||
|
|
||||||
|
payloadStr := fmt.Sprintf("req-%d", id)
|
||||||
|
payload := []byte(payloadStr)
|
||||||
|
|
||||||
|
// This call blocks until the globalSentMu is acquired.
|
||||||
|
// The mutex ensures that even with many concurrent attempts,
|
||||||
|
// the "drain" and "send" logic happens atomically per request.
|
||||||
|
ok, data, err := clientMux.SendRequest("echo", true, payload)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- fmt.Errorf("req %d error: %v", id, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
errCh <- fmt.Errorf("req %d failed (want success)", id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "reply:" + payloadStr
|
||||||
|
if string(data) != expected {
|
||||||
|
errCh <- fmt.Errorf("req %d mismatch: got %q, want %q", id, string(data), expected)
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientWg.Wait()
|
||||||
|
close(errCh)
|
||||||
|
|
||||||
|
for err := range errCh {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clientMux.Close()
|
||||||
|
<-serverDone
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxGlobalResponseDroppedWhenIdle(t *testing.T) {
|
||||||
|
clientPipe, serverPipe := memPipe()
|
||||||
|
clientMux := newMux(clientPipe)
|
||||||
|
defer serverPipe.Close()
|
||||||
|
defer clientMux.Close()
|
||||||
|
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
// Send a spurious response while no SendRequest is pending.
|
||||||
|
if err := serverPipe.writePacket(Marshal(globalRequestSuccessMsg{
|
||||||
|
Data: []byte("spurious"),
|
||||||
|
})); err != nil {
|
||||||
|
errCh <- fmt.Errorf("send spurious: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Follow with a global request; once the client observes this on
|
||||||
|
// incomingRequests, the mux loop has necessarily processed (and
|
||||||
|
// dropped) the prior spurious response.
|
||||||
|
if err := serverPipe.writePacket(Marshal(globalRequestMsg{
|
||||||
|
Type: "sync@example.com",
|
||||||
|
WantReply: false,
|
||||||
|
})); err != nil {
|
||||||
|
errCh <- fmt.Errorf("send sync request: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errCh <- nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := <-errCh; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, ok := <-clientMux.incomingRequests
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("incomingRequests closed unexpectedly")
|
||||||
|
}
|
||||||
|
if req.Type != "sync@example.com" {
|
||||||
|
t.Fatalf("unexpected sync request type %q", req.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The spurious response preceded the sync request, so by now the mux
|
||||||
|
// loop has processed it. The pending-gate must have caused it to be
|
||||||
|
// dropped rather than buffered.
|
||||||
|
if n := len(clientMux.globalResponses); n != 0 {
|
||||||
|
t.Fatalf("globalResponses buffer should be empty after idle drop, has %d entries", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxStaleResponseDrained(t *testing.T) {
|
||||||
|
// Simulate a stale response sitting in globalResponses (e.g. a response
|
||||||
|
// that slipped in through the pending-gate on a prior SendRequest that
|
||||||
|
// exited without consuming it). The drain step in the next SendRequest
|
||||||
|
// must discard it so the caller receives the correct reply.
|
||||||
|
clientMux, serverMux := muxPair()
|
||||||
|
defer serverMux.Close()
|
||||||
|
defer clientMux.Close()
|
||||||
|
|
||||||
|
clientMux.globalResponses <- &globalRequestSuccessMsg{Data: []byte("stale")}
|
||||||
|
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(serverDone)
|
||||||
|
for req := range serverMux.incomingRequests {
|
||||||
|
if req.WantReply {
|
||||||
|
req.Reply(true, append([]byte("reply:"), req.Payload...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ok, data, err := clientMux.SendRequest("test", true, []byte("hello"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected success response")
|
||||||
|
}
|
||||||
|
if string(data) != "reply:hello" {
|
||||||
|
t.Fatalf("got %q, want %q (drain did not remove stale response)", data, "reply:hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
clientMux.Close()
|
||||||
|
<-serverDone
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxGlobalResponseAcceptedWhilePending(t *testing.T) {
|
||||||
|
// Positive control: when a SendRequest is actually pending, the
|
||||||
|
// response must be delivered (the gate is open).
|
||||||
|
clientMux, serverMux := muxPair()
|
||||||
|
defer serverMux.Close()
|
||||||
|
defer clientMux.Close()
|
||||||
|
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(serverDone)
|
||||||
|
for req := range serverMux.incomingRequests {
|
||||||
|
if req.WantReply {
|
||||||
|
req.Reply(true, []byte("pong"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ok, data, err := clientMux.SendRequest("ping", true, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SendRequest: %v", err)
|
||||||
|
}
|
||||||
|
if !ok || string(data) != "pong" {
|
||||||
|
t.Fatalf("unexpected response: ok=%v data=%q", ok, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientMux.Close()
|
||||||
|
<-serverDone
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChannelUnexpectedResponsesDiscarded(t *testing.T) {
|
||||||
|
// A malicious peer that spams channelRequestSuccess/Failure messages
|
||||||
|
// for an open, idle channel must not be able to stall the mux read
|
||||||
|
// loop by filling ch.msg. After the flood, the channel must still be
|
||||||
|
// usable: a subsequent legitimate SendRequest receives its reply.
|
||||||
|
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)
|
||||||
|
}()
|
||||||
|
|
||||||
|
clientCh, err := clientMux.openChannel("chan", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("openChannel: %v", err)
|
||||||
|
}
|
||||||
|
serverCh := <-serverRes
|
||||||
|
if serverCh == nil {
|
||||||
|
t.Fatal("server did not accept channel")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spam many unsolicited success/failure responses. More than chanSize
|
||||||
|
// to ensure ch.msg would overflow without the pending-gate.
|
||||||
|
const spam = chanSize * 4
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
for i := range spam {
|
||||||
|
if err := serverCh.ackRequest(i%2 == 0); err != nil {
|
||||||
|
done <- fmt.Errorf("ackRequest %d: %w", i, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Echo any legitimate request back.
|
||||||
|
for req := range serverCh.incomingRequests {
|
||||||
|
if req.WantReply {
|
||||||
|
if err := req.Reply(true, append([]byte("reply:"), req.Payload...)); err != nil {
|
||||||
|
done <- fmt.Errorf("reply: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
done <- nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
// If the flood had wedged the mux loop, this SendRequest would never
|
||||||
|
// receive a reply.
|
||||||
|
ok, err := clientCh.SendRequest("ping", true, []byte("hello"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SendRequest: %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected success reply")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up so the server goroutine can exit.
|
||||||
|
clientCh.Close()
|
||||||
|
serverCh.Close()
|
||||||
|
if err := <-done; err != nil {
|
||||||
|
if !errors.Is(err, io.EOF) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChannelConcurrentRequests(t *testing.T) {
|
||||||
|
writer, reader, mux := channelPair(t)
|
||||||
|
defer writer.Close()
|
||||||
|
defer reader.Close()
|
||||||
|
defer mux.Close()
|
||||||
|
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(serverDone)
|
||||||
|
for req := range writer.incomingRequests {
|
||||||
|
if req.WantReply {
|
||||||
|
req.Reply(true, append([]byte("reply:"), req.Payload...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
const numRequests = 50
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(numRequests)
|
||||||
|
errCh := make(chan error, numRequests)
|
||||||
|
|
||||||
|
for i := 0; i < numRequests; i++ {
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
payload := []byte(fmt.Sprintf("req-%d", id))
|
||||||
|
ok, err := reader.SendRequest("echo", true, payload)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- fmt.Errorf("req %d: %v", id, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
errCh <- fmt.Errorf("req %d: expected success", id)
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(errCh)
|
||||||
|
|
||||||
|
for err := range errCh {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.Close()
|
||||||
|
writer.Close()
|
||||||
|
<-serverDone
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChannelResponseDroppedWhenIdle(t *testing.T) {
|
||||||
|
// A spurious response arriving while no SendRequest is pending must
|
||||||
|
// be dropped rather than buffered in ch.msg.
|
||||||
|
writer, reader, mux := channelPair(t)
|
||||||
|
defer writer.Close()
|
||||||
|
defer reader.Close()
|
||||||
|
defer mux.Close()
|
||||||
|
|
||||||
|
// Server sends an unsolicited reply, then a request so we can
|
||||||
|
// synchronise: once the client observes the request, the mux loop has
|
||||||
|
// necessarily processed (and dropped) the prior spurious reply.
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
if err := writer.ackRequest(true); err != nil {
|
||||||
|
errCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := writer.SendRequest("sync", false, nil); err != nil {
|
||||||
|
errCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errCh <- nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
req := <-reader.incomingRequests
|
||||||
|
if req.Type != "sync" {
|
||||||
|
t.Fatalf("unexpected request type %q", req.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n := len(reader.msg); n != 0 {
|
||||||
|
t.Fatalf("ch.msg should be empty after idle drop, has %d entries", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := <-errCh; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChannelStaleResponseDrained(t *testing.T) {
|
||||||
|
// Simulate a stale response sitting in ch.msg (e.g. a response that
|
||||||
|
// slipped through the pending-gate on a prior SendRequest that exited
|
||||||
|
// without consuming it). The drain step in the next SendRequest must
|
||||||
|
// discard it so the caller receives the correct reply.
|
||||||
|
writer, reader, mux := channelPair(t)
|
||||||
|
defer writer.Close()
|
||||||
|
defer reader.Close()
|
||||||
|
defer mux.Close()
|
||||||
|
|
||||||
|
reader.msg <- &channelRequestSuccessMsg{PeersID: reader.remoteId}
|
||||||
|
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(serverDone)
|
||||||
|
for req := range writer.incomingRequests {
|
||||||
|
if req.WantReply {
|
||||||
|
req.Reply(false, append([]byte("nack:"), req.Payload...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ok, err := reader.SendRequest("test", true, []byte("hello"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SendRequest: %v", err)
|
||||||
|
}
|
||||||
|
// If the stale success had been consumed, ok would be true.
|
||||||
|
if ok {
|
||||||
|
t.Fatal("got stale success response; drain did not remove it")
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.Close()
|
||||||
|
writer.Close()
|
||||||
|
<-serverDone
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChannelResponseAcceptedWhilePending(t *testing.T) {
|
||||||
|
// Positive control: when a SendRequest is actually pending, the
|
||||||
|
// response must be delivered (the gate is open).
|
||||||
|
writer, reader, mux := channelPair(t)
|
||||||
|
defer writer.Close()
|
||||||
|
defer reader.Close()
|
||||||
|
defer mux.Close()
|
||||||
|
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(serverDone)
|
||||||
|
for req := range writer.incomingRequests {
|
||||||
|
if req.WantReply {
|
||||||
|
req.Reply(true, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ok, err := reader.SendRequest("ping", true, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SendRequest: %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected success")
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.Close()
|
||||||
|
writer.Close()
|
||||||
|
<-serverDone
|
||||||
|
}
|
||||||
|
|||||||
+114
-13
@@ -34,15 +34,20 @@ type Permissions struct {
|
|||||||
// or not supported.
|
// or not supported.
|
||||||
CriticalOptions map[string]string
|
CriticalOptions map[string]string
|
||||||
|
|
||||||
// Extensions are extra functionality that the server may
|
// Extensions are extra functionality that the server may offer on
|
||||||
// offer on authenticated connections. Lack of support for an
|
// authenticated connections. Lack of support for an extension does not
|
||||||
// extension does not preclude authenticating a user. Common
|
// preclude authenticating a user. Common extensions are
|
||||||
// extensions are "permit-agent-forwarding",
|
// "permit-agent-forwarding", "permit-X11-forwarding". In general the Go
|
||||||
// "permit-X11-forwarding". The Go SSH library currently does
|
// SSH library does not act on extensions and it is up to server
|
||||||
// not act on any extension, and it is up to server
|
// implementations to honor them; extensions can also be used to pass data
|
||||||
// implementations to honor them. Extensions can be used to
|
// from the authentication callbacks to the server application layer.
|
||||||
// pass data from the authentication callbacks to the server
|
//
|
||||||
// application layer.
|
// The one extension acted upon by this library is "no-touch-required",
|
||||||
|
// which applies only to security-key public keys
|
||||||
|
// (sk-ecdsa-sha2-nistp256@openssh.com and sk-ssh-ed25519@openssh.com).
|
||||||
|
// When present, it waives the default requirement that SK signatures
|
||||||
|
// assert user presence (i.e. a physical touch of the authenticator)
|
||||||
|
// during signature verification.
|
||||||
Extensions map[string]string
|
Extensions map[string]string
|
||||||
|
|
||||||
// ExtraData allows to store user defined data.
|
// ExtraData allows to store user defined data.
|
||||||
@@ -84,6 +89,79 @@ type ServerPreAuthConn interface {
|
|||||||
SendAuthBanner(string) error
|
SendAuthBanner(string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// noTouchRequiredExtension is the extension name used by OpenSSH in
|
||||||
|
// authorized_keys options and certificate extensions to mark keys
|
||||||
|
// whose signatures do not need to assert user presence (touch). See
|
||||||
|
// ssh-keygen(1) and sshd(8).
|
||||||
|
const noTouchRequiredExtension = "no-touch-required"
|
||||||
|
|
||||||
|
// noTouchAllowed reports whether the user presence requirement on
|
||||||
|
// SK signatures should be waived for this authentication attempt. The
|
||||||
|
// requirement is waived when the "no-touch-required" extension is
|
||||||
|
// present either in the Permissions returned by the auth callback
|
||||||
|
// (authorized_keys-level opt-out) or in the certificate's own
|
||||||
|
// Extensions (CA-level opt-out), matching OpenSSH behavior. OpenSSH
|
||||||
|
// reads the per-key opt-out only from cert Extensions and
|
||||||
|
// authorized_keys options (never from CriticalOptions); we follow the
|
||||||
|
// same rule.
|
||||||
|
func noTouchAllowed(pubKey PublicKey, perms *Permissions) bool {
|
||||||
|
if perms != nil {
|
||||||
|
if _, ok := perms.Extensions[noTouchRequiredExtension]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cert, ok := pubKey.(*Certificate); ok {
|
||||||
|
if _, ok := cert.Extensions[noTouchRequiredExtension]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// skKeyWithoutUP returns a PublicKey equivalent to pubKey but whose
|
||||||
|
// Verify accepts SK signatures with the user-presence flag clear. If
|
||||||
|
// pubKey is not (and does not wrap) an SK key, pubKey is returned
|
||||||
|
// unchanged. The returned value never mutates pubKey: for SK keys a
|
||||||
|
// shallow copy is made so that the noTouchRequired flag is set only on
|
||||||
|
// the clone.
|
||||||
|
//
|
||||||
|
// The implementation is iterative rather than recursive. When pubKey
|
||||||
|
// is a *Certificate we unwrap exactly one level to look at the inner
|
||||||
|
// key. The SSH cert format forbids Certificate.Key from being another
|
||||||
|
// Certificate (parseCert rejects it), but nothing stops callers from
|
||||||
|
// constructing such a value directly in Go; a recursive descent could
|
||||||
|
// otherwise be driven to unbounded depth by a hand-crafted or cyclic
|
||||||
|
// Certificate. A malformed input of that shape simply returns
|
||||||
|
// unchanged here.
|
||||||
|
func skKeyWithoutUP(pubKey PublicKey) PublicKey {
|
||||||
|
cert, isCert := pubKey.(*Certificate)
|
||||||
|
target := pubKey
|
||||||
|
if isCert {
|
||||||
|
target = cert.Key
|
||||||
|
}
|
||||||
|
var cloned PublicKey
|
||||||
|
switch k := target.(type) {
|
||||||
|
case *skECDSAPublicKey:
|
||||||
|
c := *k
|
||||||
|
c.noTouchRequired = true
|
||||||
|
cloned = &c
|
||||||
|
case *skEd25519PublicKey:
|
||||||
|
c := *k
|
||||||
|
c.noTouchRequired = true
|
||||||
|
cloned = &c
|
||||||
|
default:
|
||||||
|
// Not an SK key (or a pathological *Certificate wrapping
|
||||||
|
// another *Certificate): pubKey is already usable for Verify.
|
||||||
|
return pubKey
|
||||||
|
}
|
||||||
|
if !isCert {
|
||||||
|
return cloned
|
||||||
|
}
|
||||||
|
c := *cert
|
||||||
|
c.Key = cloned
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
|
||||||
// ServerConfig holds server specific configuration data.
|
// ServerConfig holds server specific configuration data.
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
// Config contains configuration shared between client and server.
|
// Config contains configuration shared between client and server.
|
||||||
@@ -242,8 +320,10 @@ func (c *pubKeyCache) add(candidate cachedPubKey) {
|
|||||||
type ServerConn struct {
|
type ServerConn struct {
|
||||||
Conn
|
Conn
|
||||||
|
|
||||||
// If the succeeding authentication callback returned a
|
// If the succeeding authentication callback returned a non-nil Permissions
|
||||||
// non-nil Permissions pointer, it is stored here.
|
// pointer, it is stored here. These are the permissions from the final,
|
||||||
|
// successful authentication method. Permissions returned by callbacks that
|
||||||
|
// return PartialSuccessError are not preserved and must be nil.
|
||||||
Permissions *Permissions
|
Permissions *Permissions
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -737,8 +817,15 @@ userAuthLoop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
signedData := buildDataSignedForAuth(sessionID, userAuthReq, algo, pubKeyData)
|
signedData := buildDataSignedForAuth(sessionID, userAuthReq, algo, pubKeyData)
|
||||||
|
// pubKey is reused below for VerifiedPublicKeyCallback and
|
||||||
if err := pubKey.Verify(signedData, sig); err != nil {
|
// must remain the key as presented by the client; derive a
|
||||||
|
// separate value for Verify that carries any applicable
|
||||||
|
// no-touch-required opt-out.
|
||||||
|
pubKeyForVerify := pubKey
|
||||||
|
if noTouchAllowed(pubKey, candidate.perms) {
|
||||||
|
pubKeyForVerify = skKeyWithoutUP(pubKey)
|
||||||
|
}
|
||||||
|
if err := pubKeyForVerify.Verify(signedData, sig); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,6 +837,13 @@ userAuthLoop:
|
|||||||
// considered verified and the callback must not run.
|
// considered verified and the callback must not run.
|
||||||
perms, authErr = config.VerifiedPublicKeyCallback(s, pubKey, perms, algo)
|
perms, authErr = config.VerifiedPublicKeyCallback(s, pubKey, perms, algo)
|
||||||
}
|
}
|
||||||
|
if authErr == nil && perms != nil && perms.CriticalOptions != nil {
|
||||||
|
if saco := perms.CriticalOptions[sourceAddressCriticalOption]; saco != "" {
|
||||||
|
if err := checkSourceAddress(s.RemoteAddr(), saco); err != nil {
|
||||||
|
authErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case "gssapi-with-mic":
|
case "gssapi-with-mic":
|
||||||
if authConfig.GSSAPIWithMICConfig == nil {
|
if authConfig.GSSAPIWithMICConfig == nil {
|
||||||
@@ -824,6 +918,13 @@ userAuthLoop:
|
|||||||
var failureMsg userAuthFailureMsg
|
var failureMsg userAuthFailureMsg
|
||||||
|
|
||||||
if partialSuccess, ok := authErr.(*PartialSuccessError); ok {
|
if partialSuccess, ok := authErr.(*PartialSuccessError); ok {
|
||||||
|
// Permissions are not preserved between authentication steps. To
|
||||||
|
// avoid confusion about the final state of the connection, we
|
||||||
|
// disallow returning non-nil Permissions combined with
|
||||||
|
// PartialSuccessError.
|
||||||
|
if perms != nil {
|
||||||
|
return nil, errors.New("ssh: permissions must be nil when returning PartialSuccessError")
|
||||||
|
}
|
||||||
// After a partial success error we don't allow changing the user
|
// After a partial success error we don't allow changing the user
|
||||||
// name and execute the NoClientAuthCallback.
|
// name and execute the NoClientAuthCallback.
|
||||||
partialSuccessReturned = true
|
partialSuccessReturned = true
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ package ssh
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -664,6 +669,44 @@ func TestVerifiedPubKeyCallbackError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVerifiedPubKeyCallbackSourceAddress(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
serverConf := &ServerConfig{
|
||||||
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
||||||
|
return &Permissions{
|
||||||
|
CriticalOptions: map[string]string{
|
||||||
|
sourceAddressCriticalOption: "192.168.99.99",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
serverConf.AddHostKey(testSigners["rsa"])
|
||||||
|
|
||||||
|
clientConf := ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
Auth: []AuthMethod{
|
||||||
|
PublicKeys(testSigners["rsa"]),
|
||||||
|
},
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
|
||||||
|
go NewServerConn(c1, serverConf)
|
||||||
|
|
||||||
|
_, _, _, err = NewClientConn(c2, "", &clientConf)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("client login succeed with VerifiedPublicKeyCallback returning mismatching source-address")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestVerifiedPublicCallbackPartialSuccessBadUsage(t *testing.T) {
|
func TestVerifiedPublicCallbackPartialSuccessBadUsage(t *testing.T) {
|
||||||
c1, c2, err := netPipe()
|
c1, c2, err := netPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -808,6 +851,60 @@ func TestVerifiedPublicKeyCallbackOnly(t *testing.T) {
|
|||||||
<-done
|
<-done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPartialSuccessWithNonNilPerms(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
serverConf := &ServerConfig{
|
||||||
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||||
|
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid credentials")
|
||||||
|
},
|
||||||
|
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
||||||
|
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||||
|
// Intentionally return non-nil Permissions along with a
|
||||||
|
// PartialSuccessError. Since permissions are reset between
|
||||||
|
// authentication steps, this constitutes invalid library usage
|
||||||
|
// and the server is expected to reject the connection.
|
||||||
|
return &Permissions{Extensions: map[string]string{"permit-port-forwarding": ""}}, &PartialSuccessError{
|
||||||
|
Next: ServerAuthCallbacks{
|
||||||
|
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
|
||||||
|
if string(password) == clientPassword {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid credentials")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
serverConf.AddHostKey(testSigners["rsa"])
|
||||||
|
|
||||||
|
clientConf := ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
Auth: []AuthMethod{
|
||||||
|
PublicKeys(testSigners["rsa"]),
|
||||||
|
Password(clientPassword),
|
||||||
|
},
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
|
||||||
|
go NewServerConn(c1, serverConf)
|
||||||
|
|
||||||
|
_, _, _, err = NewClientConn(c2, "", &clientConf)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("authentication succeeded unexpectedly; server should have rejected non-nil Permissions combined with PartialSuccessError")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type markerConn struct {
|
type markerConn struct {
|
||||||
closed uint32
|
closed uint32
|
||||||
used uint32
|
used uint32
|
||||||
@@ -850,3 +947,126 @@ func (*markerConn) RemoteAddr() net.Addr { return nil }
|
|||||||
func (*markerConn) SetDeadline(t time.Time) error { return nil }
|
func (*markerConn) SetDeadline(t time.Time) error { return nil }
|
||||||
func (*markerConn) SetReadDeadline(t time.Time) error { return nil }
|
func (*markerConn) SetReadDeadline(t time.Time) error { return nil }
|
||||||
func (*markerConn) SetWriteDeadline(t time.Time) error { return nil }
|
func (*markerConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||||
|
|
||||||
|
// skTestSigner is a Signer that produces SK-ECDSA signatures over
|
||||||
|
// user-auth data, simulating a FIDO/U2F authenticator. The flags
|
||||||
|
// byte (UP and other bits) is caller-controlled so tests can exercise
|
||||||
|
// the server's user-presence enforcement and its opt-out paths. This
|
||||||
|
// is test-only: the real Signer is hardware-backed.
|
||||||
|
type skTestSigner struct {
|
||||||
|
priv *ecdsa.PrivateKey
|
||||||
|
pub PublicKey
|
||||||
|
flags byte
|
||||||
|
application string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skTestSigner) PublicKey() PublicKey { return s.pub }
|
||||||
|
|
||||||
|
func (s *skTestSigner) Sign(r io.Reader, data []byte) (*Signature, error) {
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write([]byte(s.application))
|
||||||
|
appDigest := h.Sum(nil)
|
||||||
|
h.Reset()
|
||||||
|
h.Write(data)
|
||||||
|
dataDigest := h.Sum(nil)
|
||||||
|
var counter uint32 = 1
|
||||||
|
blob := struct {
|
||||||
|
ApplicationDigest []byte `ssh:"rest"`
|
||||||
|
Flags byte
|
||||||
|
Counter uint32
|
||||||
|
MessageDigest []byte `ssh:"rest"`
|
||||||
|
}{appDigest, s.flags, counter, dataDigest}
|
||||||
|
h.Reset()
|
||||||
|
h.Write(Marshal(blob))
|
||||||
|
digest := h.Sum(nil)
|
||||||
|
x, y, err := ecdsa.Sign(r, s.priv, digest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Signature{
|
||||||
|
Format: KeyAlgoSKECDSA256,
|
||||||
|
Blob: Marshal(struct{ R, S *big.Int }{x, y}),
|
||||||
|
Rest: Marshal(struct {
|
||||||
|
Flags byte
|
||||||
|
Counter uint32
|
||||||
|
}{s.flags, counter}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServerAuthSKUserPresence drives the full userAuthLoop with an SK
|
||||||
|
// public-key client and verifies the server's wiring of the UP check
|
||||||
|
// and its two opt-out paths: the per-key Permissions.Extensions route
|
||||||
|
// and (via cert) the cert-level route. It also confirms that non-SK
|
||||||
|
// clients are unaffected by the new code path.
|
||||||
|
func TestServerAuthSKUserPresence(t *testing.T) {
|
||||||
|
userKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
skPub := &skECDSAPublicKey{application: "ssh:", PublicKey: userKey.PublicKey}
|
||||||
|
|
||||||
|
runAuth := func(t *testing.T, signer Signer, perms *Permissions) error {
|
||||||
|
t.Helper()
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
serverConf := &ServerConfig{
|
||||||
|
PublicKeyCallback: func(ConnMetadata, PublicKey) (*Permissions, error) {
|
||||||
|
return perms, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
serverConf.AddHostKey(testSigners["ecdsa"])
|
||||||
|
|
||||||
|
serverErr := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
_, _, _, err := NewServerConn(c1, serverConf)
|
||||||
|
serverErr <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
Auth: []AuthMethod{PublicKeys(signer)},
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
_, _, _, clientErr := NewClientConn(c2, "", clientConf)
|
||||||
|
<-serverErr
|
||||||
|
return clientErr
|
||||||
|
}
|
||||||
|
|
||||||
|
optOut := &Permissions{Extensions: map[string]string{noTouchRequiredExtension: ""}}
|
||||||
|
|
||||||
|
t.Run("UP=1, default perms accepts", func(t *testing.T) {
|
||||||
|
s := &skTestSigner{priv: userKey, pub: skPub, flags: flagUserPresence, application: "ssh:"}
|
||||||
|
if err := runAuth(t, s, nil); err != nil {
|
||||||
|
t.Errorf("expected auth to succeed: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("UP=0, default perms rejects", func(t *testing.T) {
|
||||||
|
s := &skTestSigner{priv: userKey, pub: skPub, flags: 0, application: "ssh:"}
|
||||||
|
if err := runAuth(t, s, nil); err == nil {
|
||||||
|
t.Error("expected auth to fail with UP=0")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("UP=0, perms opt-out accepts", func(t *testing.T) {
|
||||||
|
s := &skTestSigner{priv: userKey, pub: skPub, flags: 0, application: "ssh:"}
|
||||||
|
if err := runAuth(t, s, optOut); err != nil {
|
||||||
|
t.Errorf("expected auth to succeed with opt-out: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("UP=0, perms CriticalOptions does NOT opt out", func(t *testing.T) {
|
||||||
|
s := &skTestSigner{priv: userKey, pub: skPub, flags: 0, application: "ssh:"}
|
||||||
|
critOnly := &Permissions{CriticalOptions: map[string]string{noTouchRequiredExtension: ""}}
|
||||||
|
if err := runAuth(t, s, critOnly); err == nil {
|
||||||
|
t.Error("no-touch-required in CriticalOptions must not waive UP")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("non-SK RSA signer unaffected", func(t *testing.T) {
|
||||||
|
if err := runAuth(t, testSigners["rsa"], nil); err != nil {
|
||||||
|
t.Errorf("plain RSA auth must still work: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ func TestAgentForward(t *testing.T) {
|
|||||||
t.Fatalf("Error adding key: %s", err)
|
t.Fatalf("Error adding key: %s", err)
|
||||||
}
|
}
|
||||||
if err := keyring.Add(agent.AddedKey{
|
if err := keyring.Add(agent.AddedKey{
|
||||||
PrivateKey: testPrivateKeys["ecdsa"],
|
PrivateKey: testPrivateKeys["ecdsa"],
|
||||||
ConfirmBeforeUse: true,
|
LifetimeSecs: 3600,
|
||||||
LifetimeSecs: 3600,
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatalf("Error adding key with constraints: %s", err)
|
t.Fatalf("Error adding key with constraints: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user