Compare commits
100
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18e0be4596 | ||
|
|
7a4a4d6bea | ||
|
|
1f7c531b64 | ||
|
|
234d0b5a5f | ||
|
|
a083cb74b6 | ||
|
|
b4dcfb54b8 | ||
|
|
b853e4b620 | ||
|
|
3f62bf119e | ||
|
|
86efde54dc | ||
|
|
a6cdac6084 | ||
|
|
39dc44e69c | ||
|
|
afebf4cb4e | ||
|
|
89f4e9bb5b | ||
|
|
71488c48c2 | ||
|
|
82adefa711 | ||
|
|
c757c9851f | ||
|
|
593c81af8a | ||
|
|
46efc8bc62 | ||
|
|
e0a54fdc4b | ||
|
|
f44d03d253 | ||
|
|
5ed494470b | ||
|
|
b07833c067 | ||
|
|
d701c51f7e | ||
|
|
999d053994 | ||
|
|
90f76b8ffe | ||
|
|
b53964a1ca | ||
|
|
626e40fc98 | ||
|
|
31914c699b | ||
|
|
f2135b814c | ||
|
|
ff03dafdb0 | ||
|
|
10b54ffa51 | ||
|
|
0f039f3a4a | ||
|
|
b27e1504e3 | ||
|
|
c1a3b97d70 | ||
|
|
b9229ecc70 | ||
|
|
e557b08ec2 | ||
|
|
4044611da8 | ||
|
|
e8ad5cb7aa | ||
|
|
fe1af3a418 | ||
|
|
243e02a382 | ||
|
|
cdce021fa6 | ||
|
|
d9474cc485 | ||
|
|
7626c50256 | ||
|
|
0471e7969e | ||
|
|
6435c37ab6 | ||
|
|
7d695da948 | ||
|
|
5b7f841599 | ||
|
|
0b316e7ee4 | ||
|
|
55aec0a86b | ||
|
|
5f2de1a9f1 | ||
|
|
45460e0797 | ||
|
|
d37c95e27d | ||
|
|
e2ffffe738 | ||
|
|
60e158ad30 | ||
|
|
1b77d23dc8 | ||
|
|
3872a2bc69 | ||
|
|
9f72eccdf6 | ||
|
|
8f405a4109 | ||
|
|
bb41b3dbe6 | ||
|
|
e04e721997 | ||
|
|
b315afd69a | ||
|
|
d4a85f4749 | ||
|
|
6f39c5226a | ||
|
|
4c4d20b72c | ||
|
|
e3e62d9601 | ||
|
|
5adb68b3e6 | ||
|
|
9beb694f97 | ||
|
|
e5306b2f3e | ||
|
|
d2fe592df1 | ||
|
|
d59570d1a3 | ||
|
|
4aab0d9d6a | ||
|
|
a1c0d99298 | ||
|
|
3c7c86938f | ||
|
|
533fb3f7e4 | ||
|
|
abbc44d451 | ||
|
|
e052873987 | ||
|
|
b61cf853a8 | ||
|
|
9c2cd33e8d | ||
|
|
890731877d | ||
|
|
ffd87b4878 | ||
|
|
4e7a7384ec | ||
|
|
b25012b37b | ||
|
|
6c195c8a97 | ||
|
|
f717e29698 | ||
|
|
e7c36ccb47 | ||
|
|
0fb843a472 | ||
|
|
e3d1254f1e | ||
|
|
a1ce0fee12 | ||
|
|
a749d17980 | ||
|
|
7ee59707ff | ||
|
|
44decbfe70 | ||
|
|
b8a14a8d65 | ||
|
|
9d9d507896 | ||
|
|
fd0b90d21f | ||
|
|
b9e53593a6 | ||
|
|
cc0e4fc1d4 | ||
|
|
a8e9237a21 | ||
|
|
03ca0dcccb | ||
|
|
8400f4a938 | ||
|
|
81c6cb34a8 |
+1
-1
@@ -221,7 +221,7 @@ func (c *Client) directoryURL() string {
|
|||||||
|
|
||||||
// CreateCert was part of the old version of ACME. It is incompatible with RFC 8555.
|
// CreateCert was part of the old version of ACME. It is incompatible with RFC 8555.
|
||||||
//
|
//
|
||||||
// Deprecated: this was for the pre-RFC 8555 version of ACME. Callers should use CreateOrderCert.
|
// Deprecated: this was for the pre-RFC 8555 version of ACME. Callers should use CreateCertFromOrder.
|
||||||
func (c *Client) CreateCert(ctx context.Context, csr []byte, exp time.Duration, bundle bool) (der [][]byte, certURL string, err error) {
|
func (c *Client) CreateCert(ctx context.Context, csr []byte, exp time.Duration, bundle bool) (der [][]byte, certURL string, err error) {
|
||||||
return nil, "", errPreRFC
|
return nil, "", errPreRFC
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -734,7 +734,7 @@ func TestTLSALPN01ChallengeCert(t *testing.T) {
|
|||||||
if !acmeExts[0].Critical {
|
if !acmeExts[0].Critical {
|
||||||
t.Errorf("acmeExt.Critical = %v; want true", acmeExts[0].Critical)
|
t.Errorf("acmeExt.Critical = %v; want true", acmeExts[0].Critical)
|
||||||
}
|
}
|
||||||
if bytes.Compare(acmeExts[0].Value, extValue) != 0 {
|
if !bytes.Equal(acmeExts[0].Value, extValue) {
|
||||||
t.Errorf("acmeExt.Value = %v; want %v", acmeExts[0].Value, extValue)
|
t.Errorf("acmeExt.Value = %v; want %v", acmeExts[0].Value, extValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-24
@@ -564,24 +564,21 @@ func encodeECDSAKey(w io.Writer, key *ecdsa.PrivateKey) error {
|
|||||||
// If the domain is already being verified, it waits for the existing verification to complete.
|
// If the domain is already being verified, it waits for the existing verification to complete.
|
||||||
// Either way, createCert blocks for the duration of the whole process.
|
// Either way, createCert blocks for the duration of the whole process.
|
||||||
func (m *Manager) createCert(ctx context.Context, ck certKey) (*tls.Certificate, error) {
|
func (m *Manager) createCert(ctx context.Context, ck certKey) (*tls.Certificate, error) {
|
||||||
// TODO: maybe rewrite this whole piece using sync.Once
|
state, owner, err := m.certState(ck)
|
||||||
state, err := m.certState(ck)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// state may exist if another goroutine is already working on it
|
// If another goroutine is already working on this state, wait for it
|
||||||
// in which case just wait for it to finish
|
// to finish by taking the read lock
|
||||||
if !state.locked {
|
if !owner {
|
||||||
state.RLock()
|
state.RLock()
|
||||||
defer state.RUnlock()
|
defer state.RUnlock()
|
||||||
return state.tlscert()
|
return state.tlscert()
|
||||||
}
|
}
|
||||||
|
|
||||||
// We are the first; state is locked.
|
// We are the first to work on this certKey, so state is write-locked.
|
||||||
// Unblock the readers when domain ownership is verified
|
// Unblock the readers when our work is complete.
|
||||||
// and we got the cert or the process failed.
|
|
||||||
defer state.Unlock()
|
defer state.Unlock()
|
||||||
state.locked = false
|
|
||||||
|
|
||||||
der, leaf, err := m.authorizedCert(ctx, state.key, ck)
|
der, leaf, err := m.authorizedCert(ctx, state.key, ck)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -611,10 +608,15 @@ func (m *Manager) createCert(ctx context.Context, ck certKey) (*tls.Certificate,
|
|||||||
return state.tlscert()
|
return state.tlscert()
|
||||||
}
|
}
|
||||||
|
|
||||||
// certState returns a new or existing certState.
|
// certState returns a new or existing certState along with a boolean
|
||||||
// If a new certState is returned, state.exist is false and the state is locked.
|
// indicating whether the caller is the owner of the state.
|
||||||
|
//
|
||||||
|
// The owner of the state is responsible for performing the ACME work and
|
||||||
|
// must unlock the state's write lock when done. Non-owner callers should
|
||||||
|
// wait on the state's read lock for the owner to finish.
|
||||||
|
//
|
||||||
// The returned error is non-nil only in the case where a new state could not be created.
|
// The returned error is non-nil only in the case where a new state could not be created.
|
||||||
func (m *Manager) certState(ck certKey) (*certState, error) {
|
func (m *Manager) certState(ck certKey) (*certState, bool, error) {
|
||||||
m.stateMu.Lock()
|
m.stateMu.Lock()
|
||||||
defer m.stateMu.Unlock()
|
defer m.stateMu.Unlock()
|
||||||
if m.state == nil {
|
if m.state == nil {
|
||||||
@@ -622,7 +624,7 @@ func (m *Manager) certState(ck certKey) (*certState, error) {
|
|||||||
}
|
}
|
||||||
// existing state
|
// existing state
|
||||||
if state, ok := m.state[ck]; ok {
|
if state, ok := m.state[ck]; ok {
|
||||||
return state, nil
|
return state, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// new locked state
|
// new locked state
|
||||||
@@ -636,16 +638,13 @@ func (m *Manager) certState(ck certKey) (*certState, error) {
|
|||||||
key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
state := &certState{
|
state := &certState{key: key}
|
||||||
key: key,
|
|
||||||
locked: true,
|
|
||||||
}
|
|
||||||
state.Lock() // will be unlocked by m.certState caller
|
state.Lock() // will be unlocked by m.certState caller
|
||||||
m.state[ck] = state
|
m.state[ck] = state
|
||||||
return state, nil
|
return state, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// authorizedCert starts the domain ownership verification process and requests a new cert upon success.
|
// authorizedCert starts the domain ownership verification process and requests a new cert upon success.
|
||||||
@@ -672,7 +671,7 @@ func (m *Manager) authorizedCert(ctx context.Context, key crypto.Signer, ck cert
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
chain, _, err := client.CreateOrderCert(ctx, o.FinalizeURL, csr, true)
|
chain, _, err := client.CreateCertFromOrder(ctx, o, csr, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -1035,10 +1034,9 @@ func (m *Manager) now() time.Time {
|
|||||||
// certState is ready when its mutex is unlocked for reading.
|
// certState is ready when its mutex is unlocked for reading.
|
||||||
type certState struct {
|
type certState struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
locked bool // locked for read/write
|
key crypto.Signer // private key for cert
|
||||||
key crypto.Signer // private key for cert
|
cert [][]byte // DER encoding
|
||||||
cert [][]byte // DER encoding
|
leaf *x509.Certificate // parsed cert[0]; always non-nil if cert != nil
|
||||||
leaf *x509.Certificate // parsed cert[0]; always non-nil if cert != nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// tlscert creates a tls.Certificate from s.key and s.cert.
|
// tlscert creates a tls.Certificate from s.key and s.cert.
|
||||||
|
|||||||
@@ -513,6 +513,33 @@ func TestGetCertificate_failedAttempt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGetCertificate_concurrent guards against a data race on certState
|
||||||
|
// fields between the goroutine performing the ACME work for a domain and
|
||||||
|
// the other goroutines waiting on the result. See golang/go#80119.
|
||||||
|
func TestGetCertificate_concurrent(t *testing.T) {
|
||||||
|
// An unreachable directory URL makes the owner goroutine fail
|
||||||
|
// quickly inside createCert, so its writes overlap with the
|
||||||
|
// non-owner goroutines reaching the same code path.
|
||||||
|
m := &Manager{
|
||||||
|
Prompt: AcceptTOS,
|
||||||
|
Client: &acme.Client{DirectoryURL: "http://127.0.0.1:1/"},
|
||||||
|
}
|
||||||
|
defer m.stopRenew()
|
||||||
|
|
||||||
|
start := make(chan struct{})
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i := 0; i < 64; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
<-start
|
||||||
|
m.GetCertificate(clientHelloInfo(exampleDomain, algECDSA))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
close(start)
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
func TestRevokeFailedAuthz(t *testing.T) {
|
func TestRevokeFailedAuthz(t *testing.T) {
|
||||||
ca := acmetest.NewCAServer(t)
|
ca := acmetest.NewCAServer(t)
|
||||||
// Make the authz unfulfillable on the client side, so it will be left
|
// Make the authz unfulfillable on the client side, so it will be left
|
||||||
|
|||||||
@@ -182,9 +182,9 @@ func (p *prober) runOrder(ctx context.Context, identifiers []acme.AuthzID) {
|
|||||||
log.Fatalf("WaitOrder(%q): %v", o.URI, err)
|
log.Fatalf("WaitOrder(%q): %v", o.URI, err)
|
||||||
}
|
}
|
||||||
csr, certkey := newCSR(identifiers)
|
csr, certkey := newCSR(identifiers)
|
||||||
der, curl, err := p.client.CreateOrderCert(ctx, o.FinalizeURL, csr, true)
|
der, curl, err := p.client.CreateCertFromOrder(ctx, o, csr, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("CreateOrderCert: %v", err)
|
log.Fatalf("CreateCertFromOrder: %v", err)
|
||||||
}
|
}
|
||||||
log.Printf("cert URL: %s", curl)
|
log.Printf("cert URL: %s", curl)
|
||||||
if err := checkCert(der, identifiers); err != nil {
|
if err := checkCert(der, identifiers); err != nil {
|
||||||
@@ -247,9 +247,9 @@ func (p *prober) runPreauthz(ctx context.Context, identifiers []acme.AuthzID) {
|
|||||||
log.Fatalf("WaitOrder(%q): %v", o.URI, err)
|
log.Fatalf("WaitOrder(%q): %v", o.URI, err)
|
||||||
}
|
}
|
||||||
csr, certkey := newCSR(identifiers)
|
csr, certkey := newCSR(identifiers)
|
||||||
der, curl, err := p.client.CreateOrderCert(ctx, o.FinalizeURL, csr, true)
|
der, curl, err := p.client.CreateCertFromOrder(ctx, o, csr, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("CreateOrderCert: %v", err)
|
log.Fatalf("CreateCertFromOrder: %v", err)
|
||||||
}
|
}
|
||||||
log.Printf("cert URL: %s", curl)
|
log.Printf("cert URL: %s", curl)
|
||||||
if err := checkCert(der, identifiers); err != nil {
|
if err := checkCert(der, identifiers); err != nil {
|
||||||
|
|||||||
+5
-8
@@ -103,9 +103,7 @@ func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid KeyID, nonce, ur
|
|||||||
}
|
}
|
||||||
payload = base64.RawURLEncoding.EncodeToString(cs)
|
payload = base64.RawURLEncoding.EncodeToString(cs)
|
||||||
}
|
}
|
||||||
hash := sha.New()
|
sig, err := jwsSign(key, sha, []byte(phead+"."+payload))
|
||||||
hash.Write([]byte(phead + "." + payload))
|
|
||||||
sig, err := jwsSign(key, sha, hash.Sum(nil))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -195,14 +193,13 @@ func jwkEncode(pub crypto.PublicKey) (string, error) {
|
|||||||
return "", ErrUnsupportedKey
|
return "", ErrUnsupportedKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// jwsSign signs the digest using the given key.
|
// jwsSign signs the payload using the given key.
|
||||||
// The hash is unused for ECDSA keys.
|
func jwsSign(key crypto.Signer, hash crypto.Hash, payload []byte) ([]byte, error) {
|
||||||
func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {
|
|
||||||
switch pub := key.Public().(type) {
|
switch pub := key.Public().(type) {
|
||||||
case *rsa.PublicKey:
|
case *rsa.PublicKey:
|
||||||
return key.Sign(rand.Reader, digest, hash)
|
return crypto.SignMessage(key, rand.Reader, payload, hash)
|
||||||
case *ecdsa.PublicKey:
|
case *ecdsa.PublicKey:
|
||||||
sigASN1, err := key.Sign(rand.Reader, digest, hash)
|
sigASN1, err := crypto.SignMessage(key, rand.Reader, payload, hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-5
@@ -14,6 +14,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
@@ -353,6 +354,22 @@ func (s *customTestSigner) Sign(io.Reader, []byte, crypto.SignerOpts) ([]byte, e
|
|||||||
return s.sig, nil
|
return s.sig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type customTestMessageSigner struct {
|
||||||
|
customTestSigner
|
||||||
|
msg []byte
|
||||||
|
opts crypto.SignerOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *customTestMessageSigner) Sign(io.Reader, []byte, crypto.SignerOpts) ([]byte, error) {
|
||||||
|
return nil, errors.New("customTestSigner: Sign() called instead of SignMessage()")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *customTestMessageSigner) SignMessage(_ io.Reader, msg []byte, opts crypto.SignerOpts) ([]byte, error) {
|
||||||
|
s.msg = msg
|
||||||
|
s.opts = opts
|
||||||
|
return s.sig, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestJWSEncodeJSONCustom(t *testing.T) {
|
func TestJWSEncodeJSONCustom(t *testing.T) {
|
||||||
claims := struct{ Msg string }{"hello"}
|
claims := struct{ Msg string }{"hello"}
|
||||||
const (
|
const (
|
||||||
@@ -391,24 +408,33 @@ func TestJWSEncodeJSONCustom(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
tt := []struct {
|
tt := []struct {
|
||||||
alg, phead string
|
name, phead string
|
||||||
pub crypto.PublicKey
|
pub crypto.PublicKey
|
||||||
stdsig, jwsig string
|
stdsig, jwsig string
|
||||||
|
msgSigner bool
|
||||||
}{
|
}{
|
||||||
{"ES256", es256phead, testKeyEC.Public(), es256stdsig, es256jwsig},
|
{"ES256", es256phead, testKeyEC.Public(), es256stdsig, es256jwsig, false},
|
||||||
{"RS256", rs256phead, testKey.Public(), testsig, testsig},
|
{"RS256", rs256phead, testKey.Public(), testsig, testsig, false},
|
||||||
|
{"ES256-MessageSigner", es256phead, testKeyEC.Public(), es256stdsig, es256jwsig, true},
|
||||||
|
{"RS256-MessageSigner", rs256phead, testKey.Public(), testsig, testsig, true},
|
||||||
}
|
}
|
||||||
for _, tc := range tt {
|
for _, tc := range tt {
|
||||||
tc := tc
|
tc := tc
|
||||||
t.Run(tc.alg, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
stdsig, err := base64.RawStdEncoding.DecodeString(tc.stdsig)
|
stdsig, err := base64.RawStdEncoding.DecodeString(tc.stdsig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("couldn't decode test vector: %v", err)
|
t.Errorf("couldn't decode test vector: %v", err)
|
||||||
}
|
}
|
||||||
signer := &customTestSigner{
|
cs := customTestSigner{
|
||||||
sig: stdsig,
|
sig: stdsig,
|
||||||
pub: tc.pub,
|
pub: tc.pub,
|
||||||
}
|
}
|
||||||
|
var signer crypto.Signer = &cs
|
||||||
|
var ms *customTestMessageSigner
|
||||||
|
if tc.msgSigner {
|
||||||
|
ms = &customTestMessageSigner{customTestSigner: cs}
|
||||||
|
signer = ms
|
||||||
|
}
|
||||||
|
|
||||||
b, err := jwsEncodeJSON(claims, signer, noKeyID, "nonce", "url")
|
b, err := jwsEncodeJSON(claims, signer, noKeyID, "nonce", "url")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -427,6 +453,14 @@ func TestJWSEncodeJSONCustom(t *testing.T) {
|
|||||||
if j.Sig != tc.jwsig {
|
if j.Sig != tc.jwsig {
|
||||||
t.Errorf("j.Sig = %q\nwant %q", j.Sig, tc.jwsig)
|
t.Errorf("j.Sig = %q\nwant %q", j.Sig, tc.jwsig)
|
||||||
}
|
}
|
||||||
|
if ms != nil {
|
||||||
|
if want := tc.phead + "." + payload; string(ms.msg) != want {
|
||||||
|
t.Errorf("msg = %q; want %q", ms.msg, want)
|
||||||
|
}
|
||||||
|
if ms.opts != crypto.SHA256 {
|
||||||
|
t.Errorf("opts = %v; want %v", ms.opts, crypto.SHA256)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -37,7 +37,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
// pebbleModVersion is the module version used for Pebble and Pebble's
|
// pebbleModVersion is the module version used for Pebble and Pebble's
|
||||||
// challenge test server. It is ignored if `-pebble-local-dir` is provided.
|
// challenge test server. It is ignored if `-pebble-local-dir` is provided.
|
||||||
pebbleModVersion = "v2.7.0"
|
pebbleModVersion = "v2.10.1"
|
||||||
// startingPort is the first port number used for binding interface
|
// startingPort is the first port number used for binding interface
|
||||||
// addresses. Each call to takeNextPort() will increment a port number
|
// addresses. Each call to takeNextPort() will increment a port number
|
||||||
// starting at this value.
|
// starting at this value.
|
||||||
@@ -383,7 +383,7 @@ func testIssuance(t *testing.T, env *environment, challSrv challengeServer) {
|
|||||||
order, err = client.WaitOrder(ctx, order.URI)
|
order, err = client.WaitOrder(ctx, order.URI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var orderErr *acme.OrderError
|
var orderErr *acme.OrderError
|
||||||
if errors.Is(err, orderErr) {
|
if errors.As(err, &orderErr) {
|
||||||
t.Fatalf("failed to wait for order %s: %s: %s", orderURL, err, orderErr.Problem)
|
t.Fatalf("failed to wait for order %s: %s: %s", orderURL, err, orderErr.Problem)
|
||||||
} else {
|
} else {
|
||||||
t.Fatalf("failed to wait for order %s: %s", orderURL, err)
|
t.Fatalf("failed to wait for order %s: %s", orderURL, err)
|
||||||
@@ -420,7 +420,7 @@ func testIssuance(t *testing.T, env *environment, challSrv challengeServer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Finalize the order by creating a certificate with our CSR.
|
// Finalize the order by creating a certificate with our CSR.
|
||||||
chain, _, err := client.CreateOrderCert(ctx, order.FinalizeURL, csrDer, true)
|
chain, _, err := client.CreateCertFromOrder(ctx, order, csrDer, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to finalize order %s with finalize URL %s: %v",
|
t.Fatalf("failed to finalize order %s with finalize URL %s: %v",
|
||||||
orderURL, order.FinalizeURL, err)
|
orderURL, order.FinalizeURL, err)
|
||||||
@@ -638,7 +638,7 @@ func startPebbleEnvironment(t *testing.T, config *environmentConfig) environment
|
|||||||
// Note: we specify -defaultIPv6 "" so that no AAAA records are served.
|
// Note: we specify -defaultIPv6 "" so that no AAAA records are served.
|
||||||
// The LUCI CI runners have issues with IPv6 connectivity on localhost.
|
// The LUCI CI runners have issues with IPv6 connectivity on localhost.
|
||||||
spawnServerProcess(t, binDir, "pebble-challtestsrv",
|
spawnServerProcess(t, binDir, "pebble-challtestsrv",
|
||||||
"-dns01", fmt.Sprintf(":%d", config.dnsPort),
|
"-dnsserver", fmt.Sprintf(":%d", config.dnsPort),
|
||||||
"-defaultIPv6", "",
|
"-defaultIPv6", "",
|
||||||
"-management", fmt.Sprintf(":%d", takeNextPort()),
|
"-management", fmt.Sprintf(":%d", takeNextPort()),
|
||||||
"-doh", "",
|
"-doh", "",
|
||||||
|
|||||||
+47
-5
@@ -197,7 +197,7 @@ func (c *Client) accountKeyRollover(ctx context.Context, newKey crypto.Signer) e
|
|||||||
// identify those with StatusPending status and fulfill a challenge using Accept.
|
// identify those with StatusPending status and fulfill a challenge using Accept.
|
||||||
// Once all authorizations are satisfied, the caller will typically want to poll
|
// Once all authorizations are satisfied, the caller will typically want to poll
|
||||||
// order status using WaitOrder until it's in StatusReady state.
|
// order status using WaitOrder until it's in StatusReady state.
|
||||||
// To finalize the order and obtain a certificate, the caller submits a CSR with CreateOrderCert.
|
// To finalize the order and obtain a certificate, the caller submits a CSR with CreateCertFromOrder.
|
||||||
func (c *Client) AuthorizeOrder(ctx context.Context, id []AuthzID, opt ...OrderOption) (*Order, error) {
|
func (c *Client) AuthorizeOrder(ctx context.Context, id []AuthzID, opt ...OrderOption) (*Order, error) {
|
||||||
dir, err := c.Discover(ctx)
|
dir, err := c.Discover(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -250,7 +250,11 @@ func (c *Client) GetOrder(ctx context.Context, url string) (*Order, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
return responseOrder(res)
|
o, err := responseOrder(res)
|
||||||
|
if err == nil && o.URI == "" {
|
||||||
|
o.URI = url
|
||||||
|
}
|
||||||
|
return o, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// WaitOrder polls an order from the given URL until it is in one of the final states,
|
// WaitOrder polls an order from the given URL until it is in one of the final states,
|
||||||
@@ -271,6 +275,9 @@ func (c *Client) WaitOrder(ctx context.Context, url string) (*Order, error) {
|
|||||||
}
|
}
|
||||||
o, err := responseOrder(res)
|
o, err := responseOrder(res)
|
||||||
res.Body.Close()
|
res.Body.Close()
|
||||||
|
if err == nil && o.URI == "" {
|
||||||
|
o.URI = url
|
||||||
|
}
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
// Skip and retry.
|
// Skip and retry.
|
||||||
@@ -338,11 +345,43 @@ func responseOrder(res *http.Response) (*Order, error) {
|
|||||||
// certificate chain. Otherwise, only a leaf certificate is returned.
|
// certificate chain. Otherwise, only a leaf certificate is returned.
|
||||||
// The returned URL can be used to re-fetch the certificate using FetchCert.
|
// The returned URL can be used to re-fetch the certificate using FetchCert.
|
||||||
//
|
//
|
||||||
// This method is only supported by CAs implementing RFC 8555. See CreateCert for pre-RFC CAs.
|
// This method is only supported by CAs implementing RFC 8555 that return a
|
||||||
|
// Location header for order finalization responses. Most users should prefer
|
||||||
|
// CreateCertFromOrder, an alternative that has better interoperation with RFC 8555
|
||||||
|
// compliant CAs. See CreateCert for pre-RFC CAs.
|
||||||
//
|
//
|
||||||
// CreateOrderCert returns an error if the CA's response is unreasonably large.
|
// CreateOrderCert returns an error if the CA's response is unreasonably large.
|
||||||
// Callers are encouraged to parse the returned value to ensure the certificate is valid and has the expected features.
|
// Callers are encouraged to parse the returned value to ensure the certificate is valid and has the expected features.
|
||||||
func (c *Client) CreateOrderCert(ctx context.Context, url string, csr []byte, bundle bool) (der [][]byte, certURL string, err error) {
|
func (c *Client) CreateOrderCert(ctx context.Context, url string, csr []byte, bundle bool) (der [][]byte, certURL string, err error) {
|
||||||
|
return c.createOrderCert(ctx, url, "", csr, bundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCertFromOrder submits the CSR (Certificate Signing Request) to a CA for
|
||||||
|
// the specified Order created with AuthorizeOrder.
|
||||||
|
//
|
||||||
|
// If the bundle argument is true, the returned value also contain the CA (issuer)
|
||||||
|
// certificate chain. Otherwise, only a leaf certificate is returned.
|
||||||
|
// The returned URL can be used to re-fetch the certificate using FetchCert.
|
||||||
|
//
|
||||||
|
// This method is only supported by CAs implementing RFC 8555.
|
||||||
|
// See CreateCert for pre-RFC CAs.
|
||||||
|
//
|
||||||
|
// CreateCertFromOrder returns an error if the CA's response is unreasonably large.
|
||||||
|
// Callers are encouraged to parse the returned value to ensure the certificate is valid and has the expected features.
|
||||||
|
func (c *Client) CreateCertFromOrder(ctx context.Context, order *Order, csr []byte, bundle bool) (der [][]byte, certURL string, err error) {
|
||||||
|
if order == nil {
|
||||||
|
return nil, "", errors.New("acme: nil order")
|
||||||
|
}
|
||||||
|
if order.URI == "" {
|
||||||
|
return nil, "", errors.New("acme: order URI is empty")
|
||||||
|
}
|
||||||
|
if order.FinalizeURL == "" {
|
||||||
|
return nil, "", errors.New("acme: order finalize URL is empty")
|
||||||
|
}
|
||||||
|
return c.createOrderCert(ctx, order.FinalizeURL, order.URI, csr, bundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) createOrderCert(ctx context.Context, finalizeURL, orderURL string, csr []byte, bundle bool) (der [][]byte, certURL string, err error) {
|
||||||
if _, err := c.Discover(ctx); err != nil { // required by c.accountKID
|
if _, err := c.Discover(ctx); err != nil { // required by c.accountKID
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
@@ -353,7 +392,7 @@ func (c *Client) CreateOrderCert(ctx context.Context, url string, csr []byte, bu
|
|||||||
}{
|
}{
|
||||||
CSR: base64.RawURLEncoding.EncodeToString(csr),
|
CSR: base64.RawURLEncoding.EncodeToString(csr),
|
||||||
}
|
}
|
||||||
res, err := c.post(ctx, nil, url, req, wantStatus(http.StatusOK))
|
res, err := c.post(ctx, nil, finalizeURL, req, wantStatus(http.StatusOK))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
@@ -365,7 +404,10 @@ func (c *Client) CreateOrderCert(ctx context.Context, url string, csr []byte, bu
|
|||||||
|
|
||||||
// Wait for CA to issue the cert if they haven't.
|
// Wait for CA to issue the cert if they haven't.
|
||||||
if o.Status != StatusValid {
|
if o.Status != StatusValid {
|
||||||
o, err = c.WaitOrder(ctx, o.URI)
|
if orderURL == "" {
|
||||||
|
orderURL = o.URI
|
||||||
|
}
|
||||||
|
o, err = c.WaitOrder(ctx, orderURL)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
|
|||||||
@@ -979,6 +979,80 @@ func TestRFC_CreateOrderCert(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRFC_CreateCertFromOrder tests that finalization responses work even when
|
||||||
|
// they omit a Location header. Such a header isn't required by RFC 8555 in this
|
||||||
|
// context and we should be able to complete an order without needing it.
|
||||||
|
func TestRFC_CreateCertFromOrder(t *testing.T) {
|
||||||
|
q := &x509.CertificateRequest{
|
||||||
|
Subject: pkix.Name{CommonName: "example.org"},
|
||||||
|
}
|
||||||
|
csr, err := x509.CreateCertificateRequest(rand.Reader, q, testKeyEC)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := &x509.Certificate{SerialNumber: big.NewInt(1)}
|
||||||
|
leaf, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &testKeyEC.PublicKey, testKeyEC)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := newACMEServer()
|
||||||
|
s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Location", s.url("/accounts/1"))
|
||||||
|
w.Write([]byte(`{"status": "valid"}`))
|
||||||
|
})
|
||||||
|
s.handle("/finalize", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprint(w, `{"status":"processing"}`)
|
||||||
|
})
|
||||||
|
s.handle("/orders/1", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprintf(w, `{"status":"valid", "certificate":%q}`, s.url("/crt"))
|
||||||
|
})
|
||||||
|
s.handle("/crt", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/pem-certificate-chain")
|
||||||
|
pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: leaf})
|
||||||
|
})
|
||||||
|
s.start()
|
||||||
|
defer s.close()
|
||||||
|
|
||||||
|
cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
|
||||||
|
order := &Order{URI: s.url("/orders/1"), FinalizeURL: s.url("/finalize")}
|
||||||
|
cert, curl, err := cl.CreateCertFromOrder(context.Background(), order, csr, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateCertFromOrder: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := x509.ParseCertificate(cert[0]); err != nil {
|
||||||
|
t.Errorf("ParseCertificate: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(cert[0], leaf) {
|
||||||
|
t.Errorf("cert and leaf bytes don't match")
|
||||||
|
}
|
||||||
|
if u := s.url("/crt"); curl != u {
|
||||||
|
t.Errorf("curl = %q; want %q", curl, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRFC_CreateCertFromOrderInvalidOrder tests that invalid CreateCertFromOrder
|
||||||
|
// calls fail before making a request.
|
||||||
|
func TestRFC_CreateCertFromOrderInvalidOrder(t *testing.T) {
|
||||||
|
cl := new(Client)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
order *Order
|
||||||
|
}{
|
||||||
|
{"nil", nil},
|
||||||
|
{"empty URI", &Order{FinalizeURL: "https://example.com/finalize"}},
|
||||||
|
{"empty finalize URL", &Order{URI: "https://example.com/order/1"}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if _, _, err := cl.CreateCertFromOrder(context.Background(), tt.order, nil, false); err == nil {
|
||||||
|
t.Fatal("CreateCertFromOrder returned nil error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRFC_AlreadyRevokedCert(t *testing.T) {
|
func TestRFC_AlreadyRevokedCert(t *testing.T) {
|
||||||
s := newACMEServer()
|
s := newACMEServer()
|
||||||
s.handle("/acme/revoke-cert", func(w http.ResponseWriter, r *http.Request) {
|
s.handle("/acme/revoke-cert", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
+11
-6
@@ -114,14 +114,15 @@ type Error struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Error) Error() string {
|
func (e *Error) Error() string {
|
||||||
str := fmt.Sprintf("%d %s: %s", e.StatusCode, e.ProblemType, e.Detail)
|
var sb strings.Builder
|
||||||
|
fmt.Fprintf(&sb, "%d %s: %s", e.StatusCode, e.ProblemType, e.Detail)
|
||||||
if len(e.Subproblems) > 0 {
|
if len(e.Subproblems) > 0 {
|
||||||
str += fmt.Sprintf("; subproblems:")
|
sb.WriteString("; subproblems:")
|
||||||
for _, sp := range e.Subproblems {
|
for _, sp := range e.Subproblems {
|
||||||
str += fmt.Sprintf("\n\t%s", sp)
|
fmt.Fprintf(&sb, "\n\t%s", sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return str
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizationError indicates that an authorization for an identifier
|
// AuthorizationError indicates that an authorization for an identifier
|
||||||
@@ -167,7 +168,11 @@ type OrderError struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (oe *OrderError) Error() string {
|
func (oe *OrderError) Error() string {
|
||||||
return fmt.Sprintf("acme: order %s status: %s", oe.OrderURL, oe.Status)
|
str := fmt.Sprintf("acme: order %s status: %s", oe.OrderURL, oe.Status)
|
||||||
|
if oe.Problem != nil {
|
||||||
|
str += fmt.Sprintf("; problem: %s", oe.Problem)
|
||||||
|
}
|
||||||
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
// RateLimit reports whether err represents a rate limit error and
|
// RateLimit reports whether err represents a rate limit error and
|
||||||
@@ -325,7 +330,7 @@ type Order struct {
|
|||||||
// Possible values are StatusPending, StatusReady, StatusProcessing, StatusValid and StatusInvalid.
|
// Possible values are StatusPending, StatusReady, StatusProcessing, StatusValid and StatusInvalid.
|
||||||
// Pending means the CA does not believe that the client has fulfilled the requirements.
|
// Pending means the CA does not believe that the client has fulfilled the requirements.
|
||||||
// Ready indicates that the client has fulfilled all the requirements and can submit a CSR
|
// Ready indicates that the client has fulfilled all the requirements and can submit a CSR
|
||||||
// to obtain a certificate. This is done with Client's CreateOrderCert.
|
// to obtain a certificate. This is done with Client's CreateCertFromOrder.
|
||||||
// Processing means the certificate is being issued.
|
// Processing means the certificate is being issued.
|
||||||
// Valid indicates the CA has issued the certificate. It can be downloaded
|
// Valid indicates the CA has issued the certificate. It can be downloaded
|
||||||
// from the Order's CertURL. This is done with Client's FetchCert.
|
// from the Order's CertURL. This is done with Client's FetchCert.
|
||||||
|
|||||||
@@ -217,3 +217,19 @@ func TestErrorStringerWithSubproblems(t *testing.T) {
|
|||||||
t.Errorf("Unexpected error string: wanted %q, got %q", expectedStr, err.Error())
|
t.Errorf("Unexpected error string: wanted %q, got %q", expectedStr, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOrderErrorProblem(t *testing.T) {
|
||||||
|
oe := &OrderError{
|
||||||
|
OrderURL: "url",
|
||||||
|
Status: "invalid",
|
||||||
|
Problem: &Error{
|
||||||
|
StatusCode: 400,
|
||||||
|
ProblemType: "type",
|
||||||
|
Detail: "detail",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
want := "acme: order url status: invalid; problem: 400 type: detail"
|
||||||
|
if got := oe.Error(); got != want {
|
||||||
|
t.Errorf("oe.Error() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+18
-17
@@ -17,8 +17,8 @@
|
|||||||
// It uses data-independent memory access, which is preferred for password
|
// It uses data-independent memory access, which is preferred for password
|
||||||
// hashing and password-based key derivation. Argon2i requires more passes over
|
// hashing and password-based key derivation. Argon2i requires more passes over
|
||||||
// memory than Argon2id to protect from trade-off attacks. The recommended
|
// memory than Argon2id to protect from trade-off attacks. The recommended
|
||||||
// parameters (taken from [RFC 9106 Section 7.3]) for non-interactive operations are time=3 and to
|
// parameters (taken from [RFC 9106 Section 7.3]) for non-interactive
|
||||||
// use the maximum available memory.
|
// operations are time=3 and to use the maximum available memory.
|
||||||
//
|
//
|
||||||
// # Argon2id
|
// # Argon2id
|
||||||
//
|
//
|
||||||
@@ -26,11 +26,14 @@
|
|||||||
// Argon2i and Argon2d. It uses data-independent memory access for the first
|
// Argon2i and Argon2d. It uses data-independent memory access for the first
|
||||||
// half of the first iteration over the memory and data-dependent memory access
|
// half of the first iteration over the memory and data-dependent memory access
|
||||||
// for the rest. Argon2id is side-channel resistant and provides better brute-
|
// for the rest. Argon2id is side-channel resistant and provides better brute-
|
||||||
// force cost savings due to time-memory tradeoffs than Argon2i. The recommended
|
// force cost savings due to time-memory tradeoffs than Argon2i. [RFC 9106
|
||||||
// parameters for non-interactive operations (taken from [RFC 9106 Section 7.3]) are time=1 and to
|
// Section 4] recommends time=1, memory=2*1024*1024 KiB (2 GiB), and threads=4
|
||||||
// use the maximum available memory.
|
// as the first recommended option. If much less memory is available, it
|
||||||
|
// recommends time=3, memory=64*1024 KiB (64 MiB), and threads=4 as the second
|
||||||
|
// recommended option.
|
||||||
//
|
//
|
||||||
// [argon2-specs.pdf]: https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf
|
// [argon2-specs.pdf]: https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf
|
||||||
|
// [RFC 9106 Section 4]: https://www.rfc-editor.org/rfc/rfc9106.html#section-4
|
||||||
// [RFC 9106 Section 7.3]: https://www.rfc-editor.org/rfc/rfc9106.html#section-7.3
|
// [RFC 9106 Section 7.3]: https://www.rfc-editor.org/rfc/rfc9106.html#section-7.3
|
||||||
package argon2
|
package argon2
|
||||||
|
|
||||||
@@ -59,9 +62,9 @@ const (
|
|||||||
//
|
//
|
||||||
// key := argon2.Key([]byte("some password"), salt, 3, 32*1024, 4, 32)
|
// key := argon2.Key([]byte("some password"), salt, 3, 32*1024, 4, 32)
|
||||||
//
|
//
|
||||||
// [RFC 9106 Section 7.3] recommends time=3, and memory=32*1024 as a sensible number.
|
// The example above uses time=3 and memory=32*1024. Argon2i generally
|
||||||
// If using that amount of memory (32 MB) is not possible in some contexts then
|
// requires more passes over memory than Argon2id. If in doubt, prefer IDKey
|
||||||
// the time parameter can be increased to compensate.
|
// and its Argon2id parameter recommendations.
|
||||||
//
|
//
|
||||||
// The time parameter specifies the number of passes over the memory and the
|
// The time parameter specifies the number of passes over the memory and the
|
||||||
// memory parameter specifies the size of the memory in KiB. For example
|
// memory parameter specifies the size of the memory in KiB. For example
|
||||||
@@ -69,8 +72,6 @@ const (
|
|||||||
// adjusted to the number of available CPUs. The cost parameters should be
|
// adjusted to the number of available CPUs. The cost parameters should be
|
||||||
// increased as memory latency and CPU parallelism increases. Remember to get a
|
// increased as memory latency and CPU parallelism increases. Remember to get a
|
||||||
// good random salt.
|
// good random salt.
|
||||||
//
|
|
||||||
// [RFC 9106 Section 7.3]: https://www.rfc-editor.org/rfc/rfc9106.html#section-7.3
|
|
||||||
func Key(password, salt []byte, time, memory uint32, threads uint8, keyLen uint32) []byte {
|
func Key(password, salt []byte, time, memory uint32, threads uint8, keyLen uint32) []byte {
|
||||||
return deriveKey(argon2i, password, salt, nil, nil, time, memory, threads, keyLen)
|
return deriveKey(argon2i, password, salt, nil, nil, time, memory, threads, keyLen)
|
||||||
}
|
}
|
||||||
@@ -83,20 +84,20 @@ func Key(password, salt []byte, time, memory uint32, threads uint8, keyLen uint3
|
|||||||
// For example, you can get a derived key for e.g. AES-256 (which needs a
|
// For example, you can get a derived key for e.g. AES-256 (which needs a
|
||||||
// 32-byte key) by doing:
|
// 32-byte key) by doing:
|
||||||
//
|
//
|
||||||
// key := argon2.IDKey([]byte("some password"), salt, 1, 64*1024, 4, 32)
|
// key := argon2.IDKey([]byte("some password"), salt, 1, 2*1024*1024, 4, 32)
|
||||||
//
|
//
|
||||||
// [RFC 9106 Section 7.3] recommends time=1, and memory=64*1024 as a sensible number.
|
// The example above uses the first [RFC 9106 Section 4] recommended option.
|
||||||
// If using that amount of memory (64 MB) is not possible in some contexts then
|
// If much less memory is available, the second recommended option is time=3,
|
||||||
// the time parameter can be increased to compensate.
|
// memory=64*1024 KiB (64 MiB), and threads=4.
|
||||||
//
|
//
|
||||||
// The time parameter specifies the number of passes over the memory and the
|
// The time parameter specifies the number of passes over the memory and the
|
||||||
// memory parameter specifies the size of the memory in KiB. For example
|
// memory parameter specifies the size of the memory in KiB. For example
|
||||||
// memory=64*1024 sets the memory cost to ~64 MB. The number of threads can be
|
// memory=2*1024*1024 sets the memory cost to ~2 GiB. The number of threads can
|
||||||
// adjusted to the numbers of available CPUs. The cost parameters should be
|
// be adjusted to the numbers of available CPUs. The cost parameters should be
|
||||||
// increased as memory latency and CPU parallelism increases. Remember to get a
|
// increased as memory latency and CPU parallelism increases. Remember to get a
|
||||||
// good random salt.
|
// good random salt.
|
||||||
//
|
//
|
||||||
// [RFC 9106 Section 7.3]: https://www.rfc-editor.org/rfc/rfc9106.html#section-7.3
|
// [RFC 9106 Section 4]: https://www.rfc-editor.org/rfc/rfc9106.html#section-4
|
||||||
func IDKey(password, salt []byte, time, memory uint32, threads uint8, keyLen uint32) []byte {
|
func IDKey(password, salt []byte, time, memory uint32, threads uint8, keyLen uint32) []byte {
|
||||||
return deriveKey(argon2id, password, salt, nil, nil, time, memory, threads, keyLen)
|
return deriveKey(argon2id, password, salt, nil, nil, time, memory, threads, keyLen)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ var (
|
|||||||
|
|
||||||
func TestArgon2(t *testing.T) {
|
func TestArgon2(t *testing.T) {
|
||||||
defer func(sse4 bool) { useSSE4 = sse4 }(useSSE4)
|
defer func(sse4 bool) { useSSE4 = sse4 }(useSSE4)
|
||||||
|
defer func(lsx bool) { useLSX = lsx }(useLSX)
|
||||||
|
|
||||||
if useSSE4 {
|
if useSSE4 {
|
||||||
t.Log("SSE4.1 version")
|
t.Log("SSE4.1 version")
|
||||||
@@ -32,6 +33,14 @@ func TestArgon2(t *testing.T) {
|
|||||||
testArgon2id(t)
|
testArgon2id(t)
|
||||||
useSSE4 = false
|
useSSE4 = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if useLSX {
|
||||||
|
t.Log("LSX version")
|
||||||
|
testArgon2i(t)
|
||||||
|
testArgon2d(t)
|
||||||
|
testArgon2id(t)
|
||||||
|
useLSX = false
|
||||||
|
}
|
||||||
t.Log("generic version")
|
t.Log("generic version")
|
||||||
testArgon2i(t)
|
testArgon2i(t)
|
||||||
testArgon2d(t)
|
testArgon2d(t)
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ package argon2
|
|||||||
|
|
||||||
var useSSE4 bool
|
var useSSE4 bool
|
||||||
|
|
||||||
|
// useLSX reports whether the loong64 LSX vector implementation should be
|
||||||
|
// used. It is set from cpu.Loong64.HasLSX in blamka_loong64.go's init().
|
||||||
|
var useLSX bool
|
||||||
|
|
||||||
func processBlockGeneric(out, in1, in2 *block, xor bool) {
|
func processBlockGeneric(out, in1, in2 *block, xor bool) {
|
||||||
var t block
|
var t block
|
||||||
for i := range t {
|
for i := range t {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright 2026 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build loong64 && gc && !purego
|
||||||
|
|
||||||
|
package argon2
|
||||||
|
|
||||||
|
import "golang.org/x/sys/cpu"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// useLSX (declared in blamka_generic.go) gates the LSX vector path.
|
||||||
|
// If the running CPU lacks LSX support, processBlockVX falls back
|
||||||
|
// to processBlockGeneric.
|
||||||
|
useLSX = cpu.Loong64.HasLSX
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:noescape
|
||||||
|
func blamkaLOONG64(b *block)
|
||||||
|
|
||||||
|
//go:noescape
|
||||||
|
func mixBlocksVX(out, a, b, c *block)
|
||||||
|
|
||||||
|
//go:noescape
|
||||||
|
func xorBlocksVX(out, a, b, c *block)
|
||||||
|
|
||||||
|
func processBlock(out, in1, in2 *block) {
|
||||||
|
processBlockVX(out, in1, in2, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func processBlockXOR(out, in1, in2 *block) {
|
||||||
|
processBlockVX(out, in1, in2, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func processBlockVX(out, in1, in2 *block, xor bool) {
|
||||||
|
if !useLSX {
|
||||||
|
processBlockGeneric(out, in1, in2, xor)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var t block
|
||||||
|
mixBlocksVX(&t, in1, in2, &t)
|
||||||
|
blamkaLOONG64(&t)
|
||||||
|
if xor {
|
||||||
|
xorBlocksVX(out, in1, in2, &t)
|
||||||
|
} else {
|
||||||
|
mixBlocksVX(out, in1, in2, &t)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
// Copyright 2026 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build loong64 && gc && !purego
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// HALF_ROUND applies one BlaMka half-round (mul-add-xor-rotate) to two
|
||||||
|
// groups of 4 columns, each packed 2-wide into V registers (v0/v2/v4/v6
|
||||||
|
// hold one pair of columns in lane0/lane1, v1/v3/v5/v7 hold the other).
|
||||||
|
// t0, t1 are scratch registers, one per independent column group.
|
||||||
|
#define HALF_ROUND(v0, v1, v2, v3, v4, v5, v6, v7, t0, t1) \
|
||||||
|
VMULWEVVWU v0, v2, t0; \
|
||||||
|
VADDV t0, t0, t0; \
|
||||||
|
VADDV v0, v2, v0; \
|
||||||
|
VADDV v0, t0, v0; \
|
||||||
|
VXORV v6, v0, v6; \
|
||||||
|
VROTRV $32, v6, v6; \
|
||||||
|
VMULWEVVWU v1, v3, t1; \
|
||||||
|
VADDV t1, t1, t1; \
|
||||||
|
VADDV v1, v3, v1; \
|
||||||
|
VADDV v1, t1, v1; \
|
||||||
|
VXORV v7, v1, v7; \
|
||||||
|
VROTRV $32, v7, v7; \
|
||||||
|
VMULWEVVWU v4, v6, t0; \
|
||||||
|
VADDV t0, t0, t0; \
|
||||||
|
VADDV v4, v6, v4; \
|
||||||
|
VADDV v4, t0, v4; \
|
||||||
|
VXORV v2, v4, v2; \
|
||||||
|
VROTRV $24, v2, v2; \
|
||||||
|
VMULWEVVWU v5, v7, t1; \
|
||||||
|
VADDV t1, t1, t1; \
|
||||||
|
VADDV v5, v7, v5; \
|
||||||
|
VADDV v5, t1, v5; \
|
||||||
|
VXORV v3, v5, v3; \
|
||||||
|
VROTRV $24, v3, v3; \
|
||||||
|
VMULWEVVWU v0, v2, t0; \
|
||||||
|
VADDV t0, t0, t0; \
|
||||||
|
VADDV v0, v2, v0; \
|
||||||
|
VADDV v0, t0, v0; \
|
||||||
|
VXORV v6, v0, v6; \
|
||||||
|
VROTRV $16, v6, v6; \
|
||||||
|
VMULWEVVWU v1, v3, t1; \
|
||||||
|
VADDV t1, t1, t1; \
|
||||||
|
VADDV v1, v3, v1; \
|
||||||
|
VADDV v1, t1, v1; \
|
||||||
|
VXORV v7, v1, v7; \
|
||||||
|
VROTRV $16, v7, v7; \
|
||||||
|
VMULWEVVWU v4, v6, t0; \
|
||||||
|
VADDV t0, t0, t0; \
|
||||||
|
VADDV v4, v6, v4; \
|
||||||
|
VADDV v4, t0, v4; \
|
||||||
|
VXORV v2, v4, v2; \
|
||||||
|
VROTRV $63, v2, v2; \
|
||||||
|
VMULWEVVWU v5, v7, t1; \
|
||||||
|
VADDV t1, t1, t1; \
|
||||||
|
VADDV v5, v7, v5; \
|
||||||
|
VADDV v5, t1, v5; \
|
||||||
|
VXORV v3, v5, v3; \
|
||||||
|
VROTRV $63, v3, v3
|
||||||
|
|
||||||
|
// SHUFFLE / SHUFFLE_INV: reused verbatim from blake2b_loong64.s -- amd64
|
||||||
|
// avo source confirms argon2's SHUFFLE/SHUFFLE_INV and blake2b's SHUFFLE
|
||||||
|
// are byte-for-byte identical macros (diagonalize / undiagonalize),
|
||||||
|
// operating only on V2..V7 (V0/V1 untouched), with V13 held at zero.
|
||||||
|
#define SHUFFLE \
|
||||||
|
VADDV V2, V13, V12; \
|
||||||
|
VADDV V6, V13, V16; \
|
||||||
|
VADDV V4, V13, V14; \
|
||||||
|
VADDV V5, V13, V4; \
|
||||||
|
VADDV V14, V13, V5; \
|
||||||
|
VSHUF4IV $9, V3, V2; \
|
||||||
|
VSHUF4IV $9, V12, V3; \
|
||||||
|
VSHUF4IV $3, V7, V6; \
|
||||||
|
VSHUF4IV $3, V16, V7
|
||||||
|
|
||||||
|
#define SHUFFLE_INV \
|
||||||
|
VADDV V2, V13, V12; \
|
||||||
|
VADDV V6, V13, V16; \
|
||||||
|
VADDV V4, V13, V14; \
|
||||||
|
VADDV V5, V13, V4; \
|
||||||
|
VADDV V14, V13, V5; \
|
||||||
|
VSHUF4IV $3, V3, V2; \
|
||||||
|
VSHUF4IV $3, V12, V3; \
|
||||||
|
VSHUF4IV $9, V7, V6; \
|
||||||
|
VSHUF4IV $9, V16, V7
|
||||||
|
|
||||||
|
// LOAD_MSG_0 / STORE_MSG_0: row pass. `off` is the byte offset of the
|
||||||
|
// 128-byte (16-uint64) row group; within the macro the 8 loads step by
|
||||||
|
// 16B (2 uint64 per V register). Called from blamkaLOONG64 with off
|
||||||
|
// stepping by 128B across the 8 row groups -- do not confuse the two
|
||||||
|
// strides.
|
||||||
|
#define LOAD_MSG_0(off) \
|
||||||
|
VMOVQ (off+0)(R4), V0; \
|
||||||
|
VMOVQ (off+16)(R4), V1; \
|
||||||
|
VMOVQ (off+32)(R4), V2; \
|
||||||
|
VMOVQ (off+48)(R4), V3; \
|
||||||
|
VMOVQ (off+64)(R4), V4; \
|
||||||
|
VMOVQ (off+80)(R4), V5; \
|
||||||
|
VMOVQ (off+96)(R4), V6; \
|
||||||
|
VMOVQ (off+112)(R4), V7
|
||||||
|
|
||||||
|
#define STORE_MSG_0(off) \
|
||||||
|
VMOVQ V0, (off+0)(R4); \
|
||||||
|
VMOVQ V1, (off+16)(R4); \
|
||||||
|
VMOVQ V2, (off+32)(R4); \
|
||||||
|
VMOVQ V3, (off+48)(R4); \
|
||||||
|
VMOVQ V4, (off+64)(R4); \
|
||||||
|
VMOVQ V5, (off+80)(R4); \
|
||||||
|
VMOVQ V6, (off+96)(R4); \
|
||||||
|
VMOVQ V7, (off+112)(R4)
|
||||||
|
|
||||||
|
// LOAD_MSG_1 / STORE_MSG_1: diagonal pass. `off` is the byte offset within
|
||||||
|
// a 128-byte row group; within the macro the 8 loads step by 128B (one
|
||||||
|
// element from each of the 8 row groups, mirroring blamka_generic.go's
|
||||||
|
// t[off], t[128+off], t[256+off], ...). Called from blamkaLOONG64 with
|
||||||
|
// off stepping by 16B (2 uint64) across the 8 diagonal groups -- the
|
||||||
|
// inner/outer strides are swapped relative to LOAD_MSG_0/STORE_MSG_0.
|
||||||
|
#define LOAD_MSG_1(off) \
|
||||||
|
VMOVQ (off+0)(R4), V0; \
|
||||||
|
VMOVQ (off+128)(R4), V1; \
|
||||||
|
VMOVQ (off+256)(R4), V2; \
|
||||||
|
VMOVQ (off+384)(R4), V3; \
|
||||||
|
VMOVQ (off+512)(R4), V4; \
|
||||||
|
VMOVQ (off+640)(R4), V5; \
|
||||||
|
VMOVQ (off+768)(R4), V6; \
|
||||||
|
VMOVQ (off+896)(R4), V7
|
||||||
|
|
||||||
|
#define STORE_MSG_1(off) \
|
||||||
|
VMOVQ V0, (off+0)(R4); \
|
||||||
|
VMOVQ V1, (off+128)(R4); \
|
||||||
|
VMOVQ V2, (off+256)(R4); \
|
||||||
|
VMOVQ V3, (off+384)(R4); \
|
||||||
|
VMOVQ V4, (off+512)(R4); \
|
||||||
|
VMOVQ V5, (off+640)(R4); \
|
||||||
|
VMOVQ V6, (off+768)(R4); \
|
||||||
|
VMOVQ V7, (off+896)(R4)
|
||||||
|
|
||||||
|
// BLAMKA_ROUND_0(off): one full row pass over the 16-uint64 group at
|
||||||
|
// byte offset off -- corresponds to a single call to blamkaGeneric
|
||||||
|
// with the "row" argument order (t[off]..t[off+15]).
|
||||||
|
#define BLAMKA_ROUND_0(off) \
|
||||||
|
LOAD_MSG_0(off); \
|
||||||
|
HALF_ROUND(V0, V1, V2, V3, V4, V5, V6, V7, V8, V9); \
|
||||||
|
SHUFFLE; \
|
||||||
|
HALF_ROUND(V0, V1, V2, V3, V4, V5, V6, V7, V8, V9); \
|
||||||
|
SHUFFLE_INV; \
|
||||||
|
STORE_MSG_0(off)
|
||||||
|
|
||||||
|
// BLAMKA_ROUND_1(off): one full diagonal pass over the 16 uint64s at
|
||||||
|
// diagonal offset off -- corresponds to a single call to blamkaGeneric
|
||||||
|
// with the "diagonal" argument order (t[off], t[off+1], t[16+off], ...).
|
||||||
|
#define BLAMKA_ROUND_1(off) \
|
||||||
|
LOAD_MSG_1(off); \
|
||||||
|
HALF_ROUND(V0, V1, V2, V3, V4, V5, V6, V7, V8, V9); \
|
||||||
|
SHUFFLE; \
|
||||||
|
HALF_ROUND(V0, V1, V2, V3, V4, V5, V6, V7, V8, V9); \
|
||||||
|
SHUFFLE_INV; \
|
||||||
|
STORE_MSG_1(off)
|
||||||
|
|
||||||
|
// func blamkaLOONG64(b *block)
|
||||||
|
TEXT ·blamkaLOONG64(SB), NOSPLIT, $0-8
|
||||||
|
MOVV b+0(FP), R4
|
||||||
|
|
||||||
|
VXORV V13, V13, V13 // V13 = 0, used by SHUFFLE/SHUFFLE_INV
|
||||||
|
|
||||||
|
// Round 0: 8 row passes, stride 128 bytes (16 uint64s).
|
||||||
|
BLAMKA_ROUND_0(0)
|
||||||
|
BLAMKA_ROUND_0(128)
|
||||||
|
BLAMKA_ROUND_0(256)
|
||||||
|
BLAMKA_ROUND_0(384)
|
||||||
|
BLAMKA_ROUND_0(512)
|
||||||
|
BLAMKA_ROUND_0(640)
|
||||||
|
BLAMKA_ROUND_0(768)
|
||||||
|
BLAMKA_ROUND_0(896)
|
||||||
|
|
||||||
|
// Round 1: 8 diagonal passes, stride 16 bytes (2 uint64s).
|
||||||
|
BLAMKA_ROUND_1(0)
|
||||||
|
BLAMKA_ROUND_1(16)
|
||||||
|
BLAMKA_ROUND_1(32)
|
||||||
|
BLAMKA_ROUND_1(48)
|
||||||
|
BLAMKA_ROUND_1(64)
|
||||||
|
BLAMKA_ROUND_1(80)
|
||||||
|
BLAMKA_ROUND_1(96)
|
||||||
|
BLAMKA_ROUND_1(112)
|
||||||
|
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func mixBlocksVX(out, a, b, c *block)
|
||||||
|
TEXT ·mixBlocksVX(SB), NOSPLIT, $0-32
|
||||||
|
MOVV out+0(FP), R4
|
||||||
|
MOVV a+8(FP), R5
|
||||||
|
MOVV b+16(FP), R6
|
||||||
|
MOVV c+24(FP), R7
|
||||||
|
MOVV $1024, R8
|
||||||
|
|
||||||
|
PCALIGN $16
|
||||||
|
mixloop:
|
||||||
|
VMOVQ (R5), V0
|
||||||
|
VMOVQ (R6), V1
|
||||||
|
VMOVQ (R7), V2
|
||||||
|
VXORV V0, V1, V0
|
||||||
|
VXORV V0, V2, V0
|
||||||
|
VMOVQ V0, (R4)
|
||||||
|
ADDV $16, R4
|
||||||
|
ADDV $16, R5
|
||||||
|
ADDV $16, R6
|
||||||
|
ADDV $16, R7
|
||||||
|
SUBV $16, R8
|
||||||
|
BNE R8, R0, mixloop
|
||||||
|
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func xorBlocksVX(out, a, b, c *block)
|
||||||
|
TEXT ·xorBlocksVX(SB), NOSPLIT, $0-32
|
||||||
|
MOVV out+0(FP), R4
|
||||||
|
MOVV a+8(FP), R5
|
||||||
|
MOVV b+16(FP), R6
|
||||||
|
MOVV c+24(FP), R7
|
||||||
|
MOVV $1024, R8
|
||||||
|
|
||||||
|
PCALIGN $16
|
||||||
|
xorloop:
|
||||||
|
VMOVQ (R5), V0
|
||||||
|
VMOVQ (R6), V1
|
||||||
|
VMOVQ (R7), V2
|
||||||
|
VMOVQ (R4), V3
|
||||||
|
VXORV V0, V1, V0
|
||||||
|
VXORV V0, V2, V0
|
||||||
|
VXORV V0, V3, V0
|
||||||
|
VMOVQ V0, (R4)
|
||||||
|
ADDV $16, R4
|
||||||
|
ADDV $16, R5
|
||||||
|
ADDV $16, R6
|
||||||
|
ADDV $16, R7
|
||||||
|
SUBV $16, R8
|
||||||
|
BNE R8, R0, xorloop
|
||||||
|
|
||||||
|
RET
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build !amd64 || purego || !gc
|
//go:build (!amd64 && !loong64) || purego || !gc
|
||||||
|
|
||||||
package argon2
|
package argon2
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright 2026 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build loong64 && gc && !purego
|
||||||
|
|
||||||
|
package blake2b
|
||||||
|
|
||||||
|
import "golang.org/x/sys/cpu"
|
||||||
|
|
||||||
|
//go:noescape
|
||||||
|
func hashBlocksVX(h *[8]uint64, c *[2]uint64, flag uint64, blocks []byte)
|
||||||
|
|
||||||
|
func hashBlocks(h *[8]uint64, c *[2]uint64, flag uint64, blocks []byte) {
|
||||||
|
if cpu.Loong64.HasLSX {
|
||||||
|
hashBlocksVX(h, c, flag, blocks)
|
||||||
|
} else {
|
||||||
|
hashBlocksGeneric(h, c, flag, blocks)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
// Copyright 2026 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build loong64 && gc && !purego
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
DATA ·iv0<>+0(SB)/8, $0x6a09e667f3bcc908
|
||||||
|
DATA ·iv0<>+8(SB)/8, $0xbb67ae8584caa73b
|
||||||
|
GLOBL ·iv0<>(SB), RODATA|NOPTR, $16
|
||||||
|
|
||||||
|
DATA ·iv1<>+0(SB)/8, $0x3c6ef372fe94f82b
|
||||||
|
DATA ·iv1<>+8(SB)/8, $0xa54ff53a5f1d36f1
|
||||||
|
GLOBL ·iv1<>(SB), RODATA|NOPTR, $16
|
||||||
|
|
||||||
|
DATA ·iv2<>+0(SB)/8, $0x510e527fade682d1
|
||||||
|
DATA ·iv2<>+8(SB)/8, $0x9b05688c2b3e6c1f
|
||||||
|
GLOBL ·iv2<>(SB), RODATA|NOPTR, $16
|
||||||
|
|
||||||
|
DATA ·iv3<>+0(SB)/8, $0x1f83d9abfb41bd6b
|
||||||
|
DATA ·iv3<>+8(SB)/8, $0x5be0cd19137e2179
|
||||||
|
GLOBL ·iv3<>(SB), RODATA|NOPTR, $16
|
||||||
|
|
||||||
|
#define SHUFFLE_1 \
|
||||||
|
VADDV V2, V13, V12; \ // V12 = V2
|
||||||
|
VADDV V6, V13, V16; \ // V16 = V6
|
||||||
|
VADDV V4, V13, V14; \ // V14 = V4
|
||||||
|
VADDV V5, V13, V4; \ // V4 = V5
|
||||||
|
VADDV V14, V13, V5; \ // V5 = V4
|
||||||
|
VSHUF4IV $9, V3, V2; \
|
||||||
|
VSHUF4IV $9, V12, V3; \
|
||||||
|
VSHUF4IV $3, V7, V6; \
|
||||||
|
VSHUF4IV $3, V16, V7; \
|
||||||
|
|
||||||
|
#define SHUFFLE_2 \
|
||||||
|
VADDV V2, V13, V12; \ // V12 = V2
|
||||||
|
VADDV V6, V13, V16; \ // V16 = V6
|
||||||
|
VADDV V4, V13, V14; \ // V14 = V4
|
||||||
|
VADDV V5, V13, V4; \ // V4 = V5
|
||||||
|
VADDV V14, V13, V5; \ // V5 = V4
|
||||||
|
VSHUF4IV $3, V3, V2; \
|
||||||
|
VSHUF4IV $3, V12, V3; \
|
||||||
|
VSHUF4IV $9, V7, V6; \
|
||||||
|
VSHUF4IV $9, V16, V7; \
|
||||||
|
|
||||||
|
#define LOAD_M(a, b, c, d, e, f, g, h) \
|
||||||
|
VMOVQ a, V8.V[0]; \
|
||||||
|
VMOVQ b, V8.V[1]; \
|
||||||
|
VMOVQ c, V9.V[0]; \
|
||||||
|
VMOVQ d, V9.V[1]; \
|
||||||
|
VMOVQ e, V10.V[0]; \
|
||||||
|
VMOVQ f, V10.V[1]; \
|
||||||
|
VMOVQ g, V11.V[0]; \
|
||||||
|
VMOVQ h, V11.V[1]; \
|
||||||
|
|
||||||
|
#define ROUND_0 \
|
||||||
|
VADDV V0, V8, V0; \
|
||||||
|
VADDV V1, V9, V1; \
|
||||||
|
VADDV V0, V2, V0; \
|
||||||
|
VADDV V1, V3, V1; \
|
||||||
|
VXORV V6, V0, V6; \
|
||||||
|
VXORV V7, V1, V7; \
|
||||||
|
VROTRV $32, V6, V6; \
|
||||||
|
VROTRV $32, V7, V7; \
|
||||||
|
VADDV V4, V6, V4; \
|
||||||
|
VADDV V5, V7, V5; \
|
||||||
|
VXORV V2, V4, V2; \
|
||||||
|
VXORV V3, V5, V3; \
|
||||||
|
VROTRV $24, V2, V2; \
|
||||||
|
VROTRV $24, V3, V3; \
|
||||||
|
VADDV V0, V10, V0; \
|
||||||
|
VADDV V1, V11, V1; \
|
||||||
|
VADDV V0, V2, V0; \
|
||||||
|
VADDV V1, V3, V1; \
|
||||||
|
VXORV V6, V0, V6; \
|
||||||
|
VXORV V7, V1, V7; \
|
||||||
|
VROTRV $16, V6, V6; \
|
||||||
|
VROTRV $16, V7, V7; \
|
||||||
|
VADDV V4, V6, V4; \
|
||||||
|
VADDV V5, V7, V5; \
|
||||||
|
VXORV V2, V4, V2; \
|
||||||
|
VXORV V3, V5, V3; \
|
||||||
|
VROTRV $63, V2, V2; \
|
||||||
|
VROTRV $63, V3, V3; \
|
||||||
|
|
||||||
|
#define ROUND_8 ROUND_0
|
||||||
|
|
||||||
|
// func hashBlocksVX(h *[8]uint64, c *[2]uint64, flag uint64, blocks []byte)
|
||||||
|
TEXT ·hashBlocksVX(SB), NOSPLIT, $0-48
|
||||||
|
MOVV h+0(FP), R4
|
||||||
|
MOVV c+8(FP), R5
|
||||||
|
MOVV flag+16(FP), R6
|
||||||
|
MOVV blocks_base+24(FP), R7
|
||||||
|
MOVV blocks_len+32(FP), R8
|
||||||
|
MOVV (R5), R9 // c0
|
||||||
|
MOVV 8(R5), R10 // c1
|
||||||
|
|
||||||
|
loop:
|
||||||
|
ADDV $0x80, R9
|
||||||
|
SGTU $0x80, R9, R11
|
||||||
|
ADDV R10, R11, R10
|
||||||
|
VMOVQ R9, V8.V[0]
|
||||||
|
VMOVQ R10, V8.V[1]
|
||||||
|
|
||||||
|
MOVV $·iv0<>(SB), R11
|
||||||
|
MOVV $·iv1<>(SB), R12
|
||||||
|
MOVV $·iv2<>(SB), R13
|
||||||
|
MOVV $·iv3<>(SB), R14
|
||||||
|
VMOVQ (R13), V6
|
||||||
|
VMOVQ (R11), V4
|
||||||
|
VMOVQ (R12), V5
|
||||||
|
MOVV (R14), R15 // v14
|
||||||
|
MOVV 8(R14), R16
|
||||||
|
VXORV V6, V8, V6
|
||||||
|
XOR R15, R6, R15
|
||||||
|
VMOVQ R16, V7.V[1]
|
||||||
|
VMOVQ R15, V7.V[0]
|
||||||
|
|
||||||
|
VMOVQ (R4), V0
|
||||||
|
VMOVQ 16(R4), V1
|
||||||
|
VMOVQ 32(R4), V2
|
||||||
|
VMOVQ 48(R4), V3
|
||||||
|
|
||||||
|
MOVV 0(R7), R11
|
||||||
|
MOVV 8(R7), R12
|
||||||
|
MOVV 16(R7), R13
|
||||||
|
MOVV 24(R7), R14
|
||||||
|
MOVV 32(R7), R15
|
||||||
|
MOVV 40(R7), R16
|
||||||
|
MOVV 48(R7), R17
|
||||||
|
MOVV 56(R7), R18
|
||||||
|
MOVV 64(R7), R19
|
||||||
|
MOVV 72(R7), R24
|
||||||
|
MOVV 80(R7), R25
|
||||||
|
MOVV 88(R7), R26
|
||||||
|
MOVV 96(R7), R27
|
||||||
|
MOVV 104(R7), R28
|
||||||
|
MOVV 112(R7), R29
|
||||||
|
MOVV 120(R7), R30
|
||||||
|
|
||||||
|
VXORV V13, V13, V13 // V13=0
|
||||||
|
|
||||||
|
LOAD_M(R11, R13, R15, R17, R12, R14, R16, R18)
|
||||||
|
ROUND_0
|
||||||
|
SHUFFLE_1
|
||||||
|
LOAD_M(R19, R25, R27, R29, R24, R26, R28, R30)
|
||||||
|
ROUND_8
|
||||||
|
SHUFFLE_2
|
||||||
|
|
||||||
|
LOAD_M(R29, R15, R24, R28, R25, R19, R30, R17)
|
||||||
|
ROUND_0
|
||||||
|
SHUFFLE_1
|
||||||
|
LOAD_M(R12, R11, R26, R16, R27, R13, R18, R14)
|
||||||
|
ROUND_8
|
||||||
|
SHUFFLE_2
|
||||||
|
|
||||||
|
LOAD_M(R26, R27, R16, R30, R19, R11, R13, R28)
|
||||||
|
ROUND_0
|
||||||
|
SHUFFLE_1
|
||||||
|
LOAD_M(R25, R14, R18, R24, R29, R17, R12, R15)
|
||||||
|
ROUND_8
|
||||||
|
SHUFFLE_2
|
||||||
|
|
||||||
|
LOAD_M(R18, R14, R28, R26, R24, R12, R27, R29)
|
||||||
|
ROUND_0
|
||||||
|
SHUFFLE_1
|
||||||
|
LOAD_M(R13, R16, R15, R30, R17, R25, R11, R19)
|
||||||
|
ROUND_8
|
||||||
|
SHUFFLE_2
|
||||||
|
|
||||||
|
LOAD_M(R24, R16, R13, R25, R11, R18, R15, R30)
|
||||||
|
ROUND_0
|
||||||
|
SHUFFLE_1
|
||||||
|
LOAD_M(R29, R26, R17, R14, R12, R27, R19, R28)
|
||||||
|
ROUND_8
|
||||||
|
SHUFFLE_2
|
||||||
|
|
||||||
|
LOAD_M(R13, R17, R11, R19, R27, R25, R26, R14)
|
||||||
|
ROUND_0
|
||||||
|
SHUFFLE_1
|
||||||
|
LOAD_M(R15, R18, R30, R12, R28, R16, R29, R24)
|
||||||
|
ROUND_8
|
||||||
|
SHUFFLE_2
|
||||||
|
|
||||||
|
LOAD_M(R27, R12, R29, R15, R16, R30, R28, R25)
|
||||||
|
ROUND_0
|
||||||
|
SHUFFLE_1
|
||||||
|
LOAD_M(R11, R17, R24, R19, R18, R14, R13, R26)
|
||||||
|
ROUND_8
|
||||||
|
SHUFFLE_2
|
||||||
|
|
||||||
|
LOAD_M(R28, R18, R27, R14, R26, R29, R12, R24)
|
||||||
|
ROUND_0
|
||||||
|
SHUFFLE_1
|
||||||
|
LOAD_M(R16, R30, R19, R13, R11, R15, R17, R25)
|
||||||
|
ROUND_8
|
||||||
|
SHUFFLE_2
|
||||||
|
|
||||||
|
LOAD_M(R17, R29, R26, R11, R30, R24, R14, R19)
|
||||||
|
ROUND_0
|
||||||
|
SHUFFLE_1
|
||||||
|
LOAD_M(R27, R28, R12, R25, R13, R18, R15, R16)
|
||||||
|
ROUND_8
|
||||||
|
SHUFFLE_2
|
||||||
|
|
||||||
|
LOAD_M(R25, R19, R18, R12, R13, R15, R17, R16)
|
||||||
|
ROUND_0
|
||||||
|
SHUFFLE_1
|
||||||
|
LOAD_M(R30, R24, R14, R28, R26, R29, R27, R11)
|
||||||
|
ROUND_8
|
||||||
|
SHUFFLE_2
|
||||||
|
|
||||||
|
LOAD_M(R11, R13, R15, R17, R12, R14, R16, R18)
|
||||||
|
ROUND_0
|
||||||
|
SHUFFLE_1
|
||||||
|
LOAD_M(R19, R25, R27, R29, R24, R26, R28, R30)
|
||||||
|
ROUND_8
|
||||||
|
SHUFFLE_2
|
||||||
|
|
||||||
|
LOAD_M(R29, R15, R24, R28, R25, R19, R30, R17)
|
||||||
|
ROUND_0
|
||||||
|
SHUFFLE_1
|
||||||
|
LOAD_M(R12, R11, R26, R16, R27, R13, R18, R14)
|
||||||
|
ROUND_8
|
||||||
|
SHUFFLE_2
|
||||||
|
|
||||||
|
VMOVQ (R4), V8
|
||||||
|
VMOVQ 16(R4), V9
|
||||||
|
VMOVQ 32(R4), V10
|
||||||
|
VMOVQ 48(R4), V11
|
||||||
|
VXORV V0, V4, V0
|
||||||
|
VXORV V1, V5, V1
|
||||||
|
VXORV V2, V6, V2
|
||||||
|
VXORV V3, V7, V3
|
||||||
|
VXORV V0, V8, V0
|
||||||
|
VXORV V1, V9, V1
|
||||||
|
VXORV V2, V10, V2
|
||||||
|
VXORV V3, V11, V3
|
||||||
|
VMOVQ V0, (R4)
|
||||||
|
VMOVQ V1, 16(R4)
|
||||||
|
VMOVQ V2, 32(R4)
|
||||||
|
VMOVQ V3, 48(R4)
|
||||||
|
|
||||||
|
SUBV $128, R8
|
||||||
|
ADDV $128, R7
|
||||||
|
BNE R8, R0, loop
|
||||||
|
|
||||||
|
MOVV R9, (R5)
|
||||||
|
MOVV R10, 8(R5)
|
||||||
|
|
||||||
|
RET
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build !amd64 || purego || !gc
|
//go:build (!amd64 && !loong64) || purego || !gc
|
||||||
|
|
||||||
package blake2b
|
package blake2b
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Copyright 2026 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build gc && !purego
|
||||||
|
|
||||||
|
package chacha20
|
||||||
|
|
||||||
|
import "golang.org/x/sys/cpu"
|
||||||
|
|
||||||
|
const bufSize = 256
|
||||||
|
|
||||||
|
//go:noescape
|
||||||
|
func xorKeyStreamVX(dst, src []byte, key *[8]uint32, nonce *[3]uint32, counter *uint32)
|
||||||
|
|
||||||
|
func (c *Cipher) xorKeyStreamBlocks(dst, src []byte) {
|
||||||
|
if cpu.Loong64.HasLSX {
|
||||||
|
xorKeyStreamVX(dst, src, &c.key, &c.nonce, &c.counter)
|
||||||
|
} else {
|
||||||
|
c.xorKeyStreamBlocksGeneric(dst, src)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
// Copyright 2026 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// derived from chacha_arm64.s
|
||||||
|
|
||||||
|
//go:build gc && !purego
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
DATA ·constants+0x00(SB)/4, $0x61707865
|
||||||
|
DATA ·constants+0x04(SB)/4, $0x3320646e
|
||||||
|
DATA ·constants+0x08(SB)/4, $0x79622d32
|
||||||
|
DATA ·constants+0x0c(SB)/4, $0x6b206574
|
||||||
|
GLOBL ·constants(SB), NOPTR|RODATA, $32
|
||||||
|
|
||||||
|
DATA ·incRotMatrix+0x00(SB)/4, $0x00000000
|
||||||
|
DATA ·incRotMatrix+0x04(SB)/4, $0x00000001
|
||||||
|
DATA ·incRotMatrix+0x08(SB)/4, $0x00000002
|
||||||
|
DATA ·incRotMatrix+0x0c(SB)/4, $0x00000003
|
||||||
|
GLOBL ·incRotMatrix(SB), NOPTR|RODATA, $32
|
||||||
|
|
||||||
|
#define NUM_ROUNDS 10
|
||||||
|
|
||||||
|
// func xorKeyStreamVX(dst, src []byte, key *[8]uint32, nonce *[3]uint32, counter *uint32)
|
||||||
|
TEXT ·xorKeyStreamVX(SB), NOSPLIT, $0
|
||||||
|
MOVV dst+0(FP), R4
|
||||||
|
MOVV src+24(FP), R5
|
||||||
|
MOVV src_len+32(FP), R6
|
||||||
|
MOVV key+48(FP), R7
|
||||||
|
MOVV nonce+56(FP), R8
|
||||||
|
MOVV counter+64(FP), R9
|
||||||
|
|
||||||
|
MOVV $·constants(SB), R10
|
||||||
|
MOVV $·incRotMatrix(SB), R11
|
||||||
|
|
||||||
|
MOVW (R9), R12
|
||||||
|
|
||||||
|
loop:
|
||||||
|
MOVV $NUM_ROUNDS, R15
|
||||||
|
// load 4-32bit data from incRotMatrix added to counter
|
||||||
|
VMOVQ (R11), V30
|
||||||
|
|
||||||
|
// load contants
|
||||||
|
VMOVQ (R10), V0.W4
|
||||||
|
VMOVQ 4(R10), V1.W4
|
||||||
|
VMOVQ 8(R10), V2.W4
|
||||||
|
VMOVQ 12(R10), V3.W4
|
||||||
|
|
||||||
|
// load keys
|
||||||
|
VMOVQ (R7), V4.W4
|
||||||
|
VMOVQ 4(R7), V5.W4
|
||||||
|
VMOVQ 8(R7), V6.W4
|
||||||
|
VMOVQ 12(R7), V7.W4
|
||||||
|
VMOVQ 16(R7), V8.W4
|
||||||
|
VMOVQ 20(R7), V9.W4
|
||||||
|
VMOVQ 24(R7), V10.W4
|
||||||
|
VMOVQ 28(R7), V11.W4
|
||||||
|
|
||||||
|
// load counter + nonce
|
||||||
|
VMOVQ (R9), V12.W4
|
||||||
|
|
||||||
|
VMOVQ (R8), V13.W4
|
||||||
|
VMOVQ 4(R8), V14.W4
|
||||||
|
VMOVQ 8(R8), V15.W4
|
||||||
|
|
||||||
|
// update counter
|
||||||
|
VADDW V30, V12, V12
|
||||||
|
|
||||||
|
chacha:
|
||||||
|
// V0..V3 += V4..V7
|
||||||
|
// V12..V15 <<<= ((V12..V15 XOR V0..V3), 16)
|
||||||
|
VADDW V0, V4, V0
|
||||||
|
VADDW V1, V5, V1
|
||||||
|
VADDW V2, V6, V2
|
||||||
|
VADDW V3, V7, V3
|
||||||
|
VXORV V12, V0, V12
|
||||||
|
VXORV V13, V1, V13
|
||||||
|
VXORV V14, V2, V14
|
||||||
|
VXORV V15, V3, V15
|
||||||
|
VROTRW $16, V12, V12
|
||||||
|
VROTRW $16, V13, V13
|
||||||
|
VROTRW $16, V14, V14
|
||||||
|
VROTRW $16, V15, V15
|
||||||
|
|
||||||
|
// V8..V11 += V12..V15
|
||||||
|
// V4..V7 <<<= ((V4..V7 XOR V8..V11), 12)
|
||||||
|
VADDW V8, V12, V8
|
||||||
|
VADDW V9, V13, V9
|
||||||
|
VADDW V10, V14, V10
|
||||||
|
VADDW V11, V15, V11
|
||||||
|
VXORV V4, V8, V4
|
||||||
|
VXORV V5, V9, V5
|
||||||
|
VXORV V6, V10, V6
|
||||||
|
VXORV V7, V11, V7
|
||||||
|
VROTRW $20, V4, V4
|
||||||
|
VROTRW $20, V5, V5
|
||||||
|
VROTRW $20, V6, V6
|
||||||
|
VROTRW $20, V7, V7
|
||||||
|
|
||||||
|
// V0..V3 += V4..V7
|
||||||
|
// V12..V15 <<<= ((V12..V15 XOR V0..V3), 8)
|
||||||
|
VADDW V0, V4, V0
|
||||||
|
VADDW V1, V5, V1
|
||||||
|
VADDW V2, V6, V2
|
||||||
|
VADDW V3, V7, V3
|
||||||
|
VXORV V12, V0, V12
|
||||||
|
VXORV V13, V1, V13
|
||||||
|
VXORV V14, V2, V14
|
||||||
|
VXORV V15, V3, V15
|
||||||
|
VROTRW $24, V12, V12
|
||||||
|
VROTRW $24, V13, V13
|
||||||
|
VROTRW $24, V14, V14
|
||||||
|
VROTRW $24, V15, V15
|
||||||
|
|
||||||
|
// V8..V11 += V12..V15
|
||||||
|
// V4..V7 <<<= ((V4..V7 XOR V8..V11), 7)
|
||||||
|
VADDW V12, V8, V8
|
||||||
|
VADDW V13, V9, V9
|
||||||
|
VADDW V14, V10, V10
|
||||||
|
VADDW V15, V11, V11
|
||||||
|
VXORV V4, V8, V4
|
||||||
|
VXORV V5, V9, V5
|
||||||
|
VXORV V6, V10, V6
|
||||||
|
VXORV V7, V11, V7
|
||||||
|
VROTRW $25, V4, V4
|
||||||
|
VROTRW $25, V5, V5
|
||||||
|
VROTRW $25, V6, V6
|
||||||
|
VROTRW $25, V7, V7
|
||||||
|
|
||||||
|
// V0..V3 += V5..V7, V4
|
||||||
|
// V15,V12-V14 <<<= ((V15,V12-V14 XOR V0..V3), 16)
|
||||||
|
VADDW V0, V5, V0
|
||||||
|
VADDW V1, V6, V1
|
||||||
|
VADDW V2, V7, V2
|
||||||
|
VADDW V3, V4, V3
|
||||||
|
VXORV V15, V0, V15
|
||||||
|
VXORV V12, V1, V12
|
||||||
|
VXORV V13, V2, V13
|
||||||
|
VXORV V14, V3, V14
|
||||||
|
VROTRW $16, V15, V15
|
||||||
|
VROTRW $16, V12, V12
|
||||||
|
VROTRW $16, V13, V13
|
||||||
|
VROTRW $16, V14, V14
|
||||||
|
|
||||||
|
// V10,V11,V8,V9 += V15,V12,V13,V14
|
||||||
|
// V5,V6,V7,V4 <<<= ((V5,V6,V7,V4 XOR V10,V11,V8,V9), 12)
|
||||||
|
VADDW V10, V15, V10
|
||||||
|
VADDW V11, V12, V11
|
||||||
|
VADDW V8, V13, V8
|
||||||
|
VADDW V9, V14, V9
|
||||||
|
VXORV V5, V10, V5
|
||||||
|
VXORV V6, V11, V6
|
||||||
|
VXORV V7, V8, V7
|
||||||
|
VXORV V4, V9, V4
|
||||||
|
VROTRW $20, V5, V5
|
||||||
|
VROTRW $20, V6, V6
|
||||||
|
VROTRW $20, V7, V7
|
||||||
|
VROTRW $20, V4, V4
|
||||||
|
|
||||||
|
// V0..V3 += V5..V7, V4
|
||||||
|
// V15,V12-V14 <<<= ((V15,V12-V14 XOR V0..V3), 8)
|
||||||
|
VADDW V5, V0, V0
|
||||||
|
VADDW V6, V1, V1
|
||||||
|
VADDW V7, V2, V2
|
||||||
|
VADDW V4, V3, V3
|
||||||
|
VXORV V15, V0, V15
|
||||||
|
VXORV V12, V1, V12
|
||||||
|
VXORV V13, V2, V13
|
||||||
|
VXORV V14, V3, V14
|
||||||
|
VROTRW $24, V15, V15
|
||||||
|
VROTRW $24, V12, V12
|
||||||
|
VROTRW $24, V13, V13
|
||||||
|
VROTRW $24, V14, V14
|
||||||
|
|
||||||
|
// V10,V11,V8,V9 += V15,V12,V13,V14
|
||||||
|
// V5,V6,V7,V4 <<<= ((V5,V6,V7,V4 XOR V10,V11,V8,V9), 7)
|
||||||
|
VADDW V15, V10, V10
|
||||||
|
VADDW V12, V11, V11
|
||||||
|
VADDW V13, V8, V8
|
||||||
|
VADDW V14, V9, V9
|
||||||
|
VXORV V5, V10, V5
|
||||||
|
VXORV V6, V11, V6
|
||||||
|
VXORV V7, V8, V7
|
||||||
|
VXORV V4, V9, V4
|
||||||
|
VROTRW $25, V5, V5
|
||||||
|
VROTRW $25, V6, V6
|
||||||
|
VROTRW $25, V7, V7
|
||||||
|
VROTRW $25, V4, V4
|
||||||
|
|
||||||
|
SUBV $1, R15
|
||||||
|
BNE R15, R0, chacha
|
||||||
|
|
||||||
|
// load origin contants
|
||||||
|
VMOVQ (R10), V16.W4
|
||||||
|
VMOVQ 4(R10), V17.W4
|
||||||
|
VMOVQ 8(R10), V18.W4
|
||||||
|
VMOVQ 12(R10), V19.W4
|
||||||
|
|
||||||
|
// load origin keys
|
||||||
|
VMOVQ (R7), V20.W4
|
||||||
|
VMOVQ 4(R7), V21.W4
|
||||||
|
VMOVQ 8(R7), V22.W4
|
||||||
|
VMOVQ 12(R7), V23.W4
|
||||||
|
VMOVQ 16(R7), V24.W4
|
||||||
|
VMOVQ 20(R7), V25.W4
|
||||||
|
VMOVQ 24(R7), V26.W4
|
||||||
|
VMOVQ 28(R7), V27.W4
|
||||||
|
|
||||||
|
// add back the initial state to generate the key stream
|
||||||
|
VADDW V30, V12, V12 // update counter in advance to prevent V30 from being overwritten
|
||||||
|
VADDW V16, V0, V0
|
||||||
|
VADDW V17, V1, V1
|
||||||
|
VADDW V18, V2, V2
|
||||||
|
VADDW V19, V3, V3
|
||||||
|
|
||||||
|
// load origin counter + nonce
|
||||||
|
VMOVQ (R9), V28.W4
|
||||||
|
VMOVQ (R8), V29.W4
|
||||||
|
VMOVQ 4(R8), V30.W4
|
||||||
|
VMOVQ 8(R8), V31.W4
|
||||||
|
|
||||||
|
VADDW V20, V4, V4
|
||||||
|
VADDW V21, V5, V5
|
||||||
|
VADDW V22, V6, V6
|
||||||
|
VADDW V23, V7, V7
|
||||||
|
VADDW V24, V8, V8
|
||||||
|
VADDW V25, V9, V9
|
||||||
|
VADDW V26, V10, V10
|
||||||
|
VADDW V27, V11, V11
|
||||||
|
VADDW V28, V12, V12
|
||||||
|
VADDW V29, V13, V13
|
||||||
|
VADDW V30, V14, V14
|
||||||
|
VADDW V31, V15, V15
|
||||||
|
|
||||||
|
// shuffle
|
||||||
|
VILVLW V0, V1, V16
|
||||||
|
VILVHW V0, V1, V17
|
||||||
|
VILVLW V2, V3, V18
|
||||||
|
VILVHW V2, V3, V19
|
||||||
|
VILVLW V4, V5 ,V20
|
||||||
|
VILVHW V4, V5, V21
|
||||||
|
VILVLW V6, V7, V22
|
||||||
|
VILVHW V6, V7, V23
|
||||||
|
VILVLW V8, V9, V24
|
||||||
|
VILVHW V8, V9, V25
|
||||||
|
VILVLW V10, V11, V26
|
||||||
|
VILVHW V10, V11, V27
|
||||||
|
VILVLW V12, V13, V28
|
||||||
|
VILVHW V12, V13, V29
|
||||||
|
VILVLW V14, V15, V30
|
||||||
|
VILVHW V14, V15, V31
|
||||||
|
VILVLV V16, V18, V0
|
||||||
|
VILVHV V16, V18, V4
|
||||||
|
VILVLV V17, V19, V8
|
||||||
|
VILVHV V17, V19, V12
|
||||||
|
|
||||||
|
// load src data from R5
|
||||||
|
VMOVQ 0(R5), V16
|
||||||
|
VMOVQ 16(R5), V17
|
||||||
|
VMOVQ 32(R5), V18
|
||||||
|
VMOVQ 48(R5), V19
|
||||||
|
|
||||||
|
VILVLV V20, V22, V1
|
||||||
|
VILVHV V20, V22, V5
|
||||||
|
VILVLV V21, V23, V9
|
||||||
|
VILVHV V21, V23, V13
|
||||||
|
|
||||||
|
VMOVQ 64(R5), V20
|
||||||
|
VMOVQ 80(R5), V21
|
||||||
|
VMOVQ 96(R5), V22
|
||||||
|
VMOVQ 112(R5), V23
|
||||||
|
|
||||||
|
VILVLV V24, V26, V2
|
||||||
|
VILVHV V24, V26, V6
|
||||||
|
VILVLV V25, V27, V10
|
||||||
|
VILVHV V25, V27, V14
|
||||||
|
|
||||||
|
VMOVQ 128(R5), V24
|
||||||
|
VMOVQ 144(R5), V25
|
||||||
|
VMOVQ 160(R5), V26
|
||||||
|
VMOVQ 176(R5), V27
|
||||||
|
|
||||||
|
VILVLV V28, V30, V3
|
||||||
|
VILVHV V28, V30, V7
|
||||||
|
VILVLV V29, V31, V11
|
||||||
|
VILVHV V29, V31, V15
|
||||||
|
|
||||||
|
VMOVQ 192(R5), V28
|
||||||
|
VMOVQ 208(R5), V29
|
||||||
|
VMOVQ 224(R5), V30
|
||||||
|
VMOVQ 240(R5), V31
|
||||||
|
|
||||||
|
VXORV V0, V16, V16
|
||||||
|
VXORV V1, V17, V17
|
||||||
|
VXORV V2, V18, V18
|
||||||
|
VXORV V3, V19, V19
|
||||||
|
|
||||||
|
VMOVQ V16, 0(R4)
|
||||||
|
VMOVQ V17, 16(R4)
|
||||||
|
VMOVQ V18, 32(R4)
|
||||||
|
VMOVQ V19, 48(R4)
|
||||||
|
|
||||||
|
VXORV V4, V20, V20
|
||||||
|
VXORV V5, V21, V21
|
||||||
|
VXORV V6, V22, V22
|
||||||
|
VXORV V7, V23, V23
|
||||||
|
|
||||||
|
VMOVQ V20, 64(R4)
|
||||||
|
VMOVQ V21, 80(R4)
|
||||||
|
VMOVQ V22, 96(R4)
|
||||||
|
VMOVQ V23, 112(R4)
|
||||||
|
|
||||||
|
VXORV V8, V24, V24
|
||||||
|
VXORV V9, V25, V25
|
||||||
|
VXORV V10, V26, V26
|
||||||
|
VXORV V11, V27, V27
|
||||||
|
|
||||||
|
VMOVQ V24, 128(R4)
|
||||||
|
VMOVQ V25, 144(R4)
|
||||||
|
VMOVQ V26, 160(R4)
|
||||||
|
VMOVQ V27, 176(R4)
|
||||||
|
|
||||||
|
VXORV V12, V28, V28
|
||||||
|
VXORV V13, V29, V29
|
||||||
|
VXORV V14, V30, V30
|
||||||
|
VXORV V15, V31, V31
|
||||||
|
|
||||||
|
VMOVQ V28, 192(R4)
|
||||||
|
VMOVQ V29, 208(R4)
|
||||||
|
VMOVQ V30, 224(R4)
|
||||||
|
VMOVQ V31, 240(R4)
|
||||||
|
|
||||||
|
ADD $4, R12, R12
|
||||||
|
MOVW R12, (R9) // update counter
|
||||||
|
|
||||||
|
ADDV $256, R4, R4
|
||||||
|
ADDV $256, R5, R5
|
||||||
|
SUBV $256, R6, R6
|
||||||
|
BNE R6, R0, loop
|
||||||
|
|
||||||
|
RET
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build (!arm64 && !s390x && !ppc64 && !ppc64le) || !gc || purego
|
//go:build (!arm64 && !loong64 && !s390x && !ppc64 && !ppc64le) || !gc || purego
|
||||||
|
|
||||||
package chacha20
|
package chacha20
|
||||||
|
|
||||||
|
|||||||
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
@@ -1,11 +1,11 @@
|
|||||||
module golang.org/x/crypto
|
module golang.org/x/crypto
|
||||||
|
|
||||||
go 1.25.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/net v0.51.0 // tagx:ignore
|
golang.org/x/net v0.58.0 // tagx:ignore
|
||||||
golang.org/x/sys v0.42.0
|
golang.org/x/sys v0.48.0
|
||||||
golang.org/x/term v0.41.0
|
golang.org/x/term v0.46.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/text v0.35.0 // indirect
|
require golang.org/x/text v0.42.0 // indirect
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||||
|
|||||||
+13
-8
@@ -11,6 +11,7 @@
|
|||||||
package hkdf
|
package hkdf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/hkdf"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"errors"
|
"errors"
|
||||||
"hash"
|
"hash"
|
||||||
@@ -24,15 +25,19 @@ import (
|
|||||||
// Expand invocations and different context values. Most common scenarios,
|
// Expand invocations and different context values. Most common scenarios,
|
||||||
// including the generation of multiple keys, should use New instead.
|
// including the generation of multiple keys, should use New instead.
|
||||||
func Extract(hash func() hash.Hash, secret, salt []byte) []byte {
|
func Extract(hash func() hash.Hash, secret, salt []byte) []byte {
|
||||||
if salt == nil {
|
// Use the stdlib Extract, which disables FIPS 140 enforcement of the HMAC
|
||||||
salt = make([]byte, hash().Size())
|
// key (which in HKDF is the salt). The only possible error is FIPS 140
|
||||||
|
// enforcement of the hash, which had to panic under this API anyway. We
|
||||||
|
// don't use the stdlib Expand, because it switched to returning a []byte
|
||||||
|
// instead of an io.Reader, and Expand uses the HMAC key as a key.
|
||||||
|
out, err := hkdf.Extract(hash, secret, salt)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
extractor := hmac.New(hash, salt)
|
return out
|
||||||
extractor.Write(secret)
|
|
||||||
return extractor.Sum(nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type hkdf struct {
|
type hkdfReader struct {
|
||||||
expander hash.Hash
|
expander hash.Hash
|
||||||
size int
|
size int
|
||||||
|
|
||||||
@@ -43,7 +48,7 @@ type hkdf struct {
|
|||||||
buf []byte
|
buf []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *hkdf) Read(p []byte) (int, error) {
|
func (f *hkdfReader) Read(p []byte) (int, error) {
|
||||||
// Check whether enough data can be generated
|
// Check whether enough data can be generated
|
||||||
need := len(p)
|
need := len(p)
|
||||||
remains := len(f.buf) + int(255-f.counter+1)*f.size
|
remains := len(f.buf) + int(255-f.counter+1)*f.size
|
||||||
@@ -84,7 +89,7 @@ func (f *hkdf) Read(p []byte) (int, error) {
|
|||||||
// 3.3. Most common scenarios will want to use New instead.
|
// 3.3. Most common scenarios will want to use New instead.
|
||||||
func Expand(hash func() hash.Hash, pseudorandomKey, info []byte) io.Reader {
|
func Expand(hash func() hash.Hash, pseudorandomKey, info []byte) io.Reader {
|
||||||
expander := hmac.New(hash, pseudorandomKey)
|
expander := hmac.New(hash, pseudorandomKey)
|
||||||
return &hkdf{expander, expander.Size(), info, 1, nil, nil}
|
return &hkdfReader{expander, expander.Size(), info, 1, nil, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a Reader, from which keys can be read, using the given hash,
|
// New returns a Reader, from which keys can be read, using the given hash,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build (!amd64 && !loong64 && !ppc64le && !ppc64 && !s390x) || !gc || purego
|
//go:build (!amd64 && !loong64 && !ppc64le && !ppc64 && !riscv64 && !s390x) || !gc || purego
|
||||||
|
|
||||||
package poly1305
|
package poly1305
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build gc && !purego && (amd64 || loong64 || ppc64 || ppc64le)
|
//go:build gc && !purego && (amd64 || loong64 || ppc64 || ppc64le || riscv64)
|
||||||
|
|
||||||
package poly1305
|
package poly1305
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
// Copyright 2026 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build gc && !purego
|
||||||
|
|
||||||
|
#define LOAD64U(base, offset, t0, t1, t2, t3, dst) \
|
||||||
|
MOVBU (offset+0*1)(base), t0; \
|
||||||
|
MOVBU (offset+1*1)(base), t1; \
|
||||||
|
MOVBU (offset+2*1)(base), t2; \
|
||||||
|
MOVBU (offset+3*1)(base), t3; \
|
||||||
|
SLL $8, t1; \
|
||||||
|
SLL $16, t2; \
|
||||||
|
SLL $24, t3; \
|
||||||
|
OR t1, t0; \
|
||||||
|
OR t3, t2; \
|
||||||
|
OR t2, t0, dst; \
|
||||||
|
MOVBU (offset+4*1)(base), t0; \
|
||||||
|
MOVBU (offset+5*1)(base), t1; \
|
||||||
|
MOVBU (offset+6*1)(base), t2; \
|
||||||
|
MOVBU (offset+7*1)(base), t3; \
|
||||||
|
SLL $32, t0; \
|
||||||
|
SLL $40, t1; \
|
||||||
|
SLL $48, t2; \
|
||||||
|
SLL $56, t3; \
|
||||||
|
OR t1, t0; \
|
||||||
|
OR t3, t2; \
|
||||||
|
OR t2, t0; \
|
||||||
|
OR t0, dst
|
||||||
|
|
||||||
|
// func update(state *macState, msg []byte)
|
||||||
|
TEXT ·update(SB), $0-32
|
||||||
|
MOV state+0(FP), X5
|
||||||
|
MOV msg_base+8(FP), X6
|
||||||
|
MOV msg_len+16(FP), X7
|
||||||
|
|
||||||
|
MOV $16, X8
|
||||||
|
|
||||||
|
AND $7, X6, X28
|
||||||
|
|
||||||
|
MOV (0*8)(X5), X9 // h0
|
||||||
|
MOV (1*8)(X5), X10 // h1
|
||||||
|
MOV (2*8)(X5), X11 // h2
|
||||||
|
MOV (3*8)(X5), X12 // r0
|
||||||
|
MOV (4*8)(X5), X13 // r1
|
||||||
|
|
||||||
|
BLT X7, X8, tail
|
||||||
|
|
||||||
|
loop:
|
||||||
|
BEQZ X28, aligned_load
|
||||||
|
|
||||||
|
LOAD64U(X6, 0*8, X16, X18, X19, X20, X15) // msg[0:8]
|
||||||
|
LOAD64U(X6, 1*8, X16, X18, X19, X20, X17) // msg[8:16]
|
||||||
|
JMP block
|
||||||
|
|
||||||
|
aligned_load:
|
||||||
|
MOV (0*8)(X6), X15 // msg[0:8]
|
||||||
|
MOV (1*8)(X6), X17 // msg[8:16]
|
||||||
|
|
||||||
|
block:
|
||||||
|
ADD X15, X9 // h0 (x1 + y1 = z1', if z1' < x1 then z1' overflow)
|
||||||
|
SLTU X15, X9, X19 // h0.carry
|
||||||
|
ADD X17, X10, X22
|
||||||
|
SLTU X17, X22, X23
|
||||||
|
ADD X22, X19, X10 // h1
|
||||||
|
SLTU X22, X10, X19
|
||||||
|
OR X23, X19 // h1.carry
|
||||||
|
ADD $1, X19
|
||||||
|
ADD X19, X11 // h2
|
||||||
|
|
||||||
|
ADD $16, X6 // msg = msg[16:]
|
||||||
|
|
||||||
|
multiply:
|
||||||
|
MULHU X9, X12, X16 // h0r0.hi
|
||||||
|
MUL X9, X12, X15 // h0r0.lo
|
||||||
|
MULHU X10, X12, X17 // h1r0.hi
|
||||||
|
MUL X10, X12, X14 // h1r0.lo
|
||||||
|
ADD X14, X16
|
||||||
|
SLTU X14, X16, X19
|
||||||
|
ADD X19, X17
|
||||||
|
MUL X11, X12, X20
|
||||||
|
ADD X17, X20
|
||||||
|
MULHU X9, X13, X17 // h0r1.hi
|
||||||
|
MUL X9, X13, X14 // h0r1.lo
|
||||||
|
ADD X14, X16
|
||||||
|
SLTU X14, X16, X19
|
||||||
|
ADD X19, X17
|
||||||
|
MOV X17, X9
|
||||||
|
MUL X11, X13, X21 // h2r1
|
||||||
|
MULHU X10, X13, X17 // h1r1.hi
|
||||||
|
MUL X10, X13, X14 // h1r1.lo
|
||||||
|
ADD X14, X20
|
||||||
|
ADD X17, X21, X22
|
||||||
|
SLTU X14, X20, X19
|
||||||
|
ADD X22, X19, X21
|
||||||
|
ADD X9, X20
|
||||||
|
SLTU X9, X20, X19
|
||||||
|
ADD X19, X21
|
||||||
|
AND $3, X20, X11
|
||||||
|
AND $-4, X20, X18
|
||||||
|
ADD X18, X15, X9
|
||||||
|
ADD X21, X16, X22
|
||||||
|
SLTU X18, X9, X19
|
||||||
|
SLTU X21, X22, X23
|
||||||
|
ADD X22, X19, X10
|
||||||
|
SLTU X22, X10, X19
|
||||||
|
OR X19, X23, X19
|
||||||
|
ADD X19, X11
|
||||||
|
SLL $62, X21, X22
|
||||||
|
SRL $2, X20, X23
|
||||||
|
SRL $2, X21, X21
|
||||||
|
OR X22, X23, X20
|
||||||
|
ADD X20, X9, X9
|
||||||
|
ADD X21, X10, X22
|
||||||
|
SLTU X20, X9, X19
|
||||||
|
SLTU X21, X22, X23
|
||||||
|
ADD X22, X19, X10
|
||||||
|
SLTU X22, X10, X19
|
||||||
|
OR X19, X23, X19
|
||||||
|
ADD X19, X11, X11
|
||||||
|
|
||||||
|
SUB $16, X7, X7
|
||||||
|
BGE X7, X8, loop
|
||||||
|
|
||||||
|
tail:
|
||||||
|
BEQ X7, X0, done
|
||||||
|
MOV $1, X15
|
||||||
|
MOV $0, X16
|
||||||
|
ADD X7, X6, X6
|
||||||
|
|
||||||
|
flush_buffer:
|
||||||
|
MOVBU -1(X6), X20
|
||||||
|
SRL $56, X15, X19
|
||||||
|
SLL $8, X16, X23
|
||||||
|
SLL $8, X15, X15
|
||||||
|
OR X19, X23, X16
|
||||||
|
XOR X20, X15
|
||||||
|
SUB $1, X7, X7
|
||||||
|
SUB $1, X6, X6
|
||||||
|
BNE X7, X0, flush_buffer
|
||||||
|
|
||||||
|
ADD X15, X9
|
||||||
|
SLTU X15, X9, X19
|
||||||
|
ADD X16, X10, X22
|
||||||
|
SLTU X16, X22, X23
|
||||||
|
ADD X22, X19, X10
|
||||||
|
SLTU X22, X10, X19
|
||||||
|
OR X23, X19
|
||||||
|
ADD X19, X11
|
||||||
|
|
||||||
|
MOV $16, X7
|
||||||
|
JMP multiply
|
||||||
|
|
||||||
|
done:
|
||||||
|
MOV X9, (0*8)(X5) // h0
|
||||||
|
MOV X10, (1*8)(X5)
|
||||||
|
MOV X11, (2*8)(X5)
|
||||||
|
RET
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
This package runs a set of the Wycheproof tests provided by
|
|
||||||
https://github.com/google/wycheproof.
|
|
||||||
|
|
||||||
The JSON test files live in
|
|
||||||
https://github.com/google/wycheproof/tree/master/testvectors
|
|
||||||
and are being fetched and cached at a pinned version every time
|
|
||||||
these tests are run. To change the version of the wycheproof
|
|
||||||
repository that is being used for testing, update wycheproofModVer.
|
|
||||||
|
|
||||||
The structs for these tests are generated from the
|
|
||||||
schemas provided in https://github.com/google/wycheproof/tree/master/schemas
|
|
||||||
using https://github.com/a-h/generate.
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
// Copyright 2020 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 wycheproof
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/chacha20poly1305"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAEAD(t *testing.T) {
|
|
||||||
// AeadTestVector
|
|
||||||
type AeadTestVector struct {
|
|
||||||
|
|
||||||
// additional authenticated data
|
|
||||||
Aad string `json:"aad,omitempty"`
|
|
||||||
|
|
||||||
// A brief description of the test case
|
|
||||||
Comment string `json:"comment,omitempty"`
|
|
||||||
|
|
||||||
// the ciphertext (without iv and tag)
|
|
||||||
Ct string `json:"ct,omitempty"`
|
|
||||||
|
|
||||||
// A list of flags
|
|
||||||
Flags []string `json:"flags,omitempty"`
|
|
||||||
|
|
||||||
// the nonce
|
|
||||||
Iv string `json:"iv,omitempty"`
|
|
||||||
|
|
||||||
// the key
|
|
||||||
Key string `json:"key,omitempty"`
|
|
||||||
|
|
||||||
// the plaintext
|
|
||||||
Msg string `json:"msg,omitempty"`
|
|
||||||
|
|
||||||
// Test result
|
|
||||||
Result string `json:"result,omitempty"`
|
|
||||||
|
|
||||||
// the authentication tag
|
|
||||||
Tag string `json:"tag,omitempty"`
|
|
||||||
|
|
||||||
// Identifier of the test case
|
|
||||||
TcId int `json:"tcId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notes a description of the labels used in the test vectors
|
|
||||||
type Notes struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// AeadTestGroup
|
|
||||||
type AeadTestGroup struct {
|
|
||||||
|
|
||||||
// the IV size in bits
|
|
||||||
IvSize int `json:"ivSize,omitempty"`
|
|
||||||
|
|
||||||
// the keySize in bits
|
|
||||||
KeySize int `json:"keySize,omitempty"`
|
|
||||||
|
|
||||||
// the expected size of the tag in bits
|
|
||||||
TagSize int `json:"tagSize,omitempty"`
|
|
||||||
Tests []*AeadTestVector `json:"tests,omitempty"`
|
|
||||||
Type interface{} `json:"type,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root
|
|
||||||
type Root struct {
|
|
||||||
|
|
||||||
// the primitive tested in the test file
|
|
||||||
Algorithm string `json:"algorithm,omitempty"`
|
|
||||||
|
|
||||||
// the version of the test vectors.
|
|
||||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
|
||||||
|
|
||||||
// additional documentation
|
|
||||||
Header []string `json:"header,omitempty"`
|
|
||||||
|
|
||||||
// a description of the labels used in the test vectors
|
|
||||||
Notes *Notes `json:"notes,omitempty"`
|
|
||||||
|
|
||||||
// the number of test vectors in this test
|
|
||||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
|
||||||
Schema interface{} `json:"schema,omitempty"`
|
|
||||||
TestGroups []*AeadTestGroup `json:"testGroups,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
testSealOpen := func(t *testing.T, aead cipher.AEAD, tv *AeadTestVector, recoverBadNonce func()) {
|
|
||||||
defer recoverBadNonce()
|
|
||||||
|
|
||||||
iv, tag, ct, msg, aad := decodeHex(tv.Iv), decodeHex(tv.Tag), decodeHex(tv.Ct), decodeHex(tv.Msg), decodeHex(tv.Aad)
|
|
||||||
|
|
||||||
genCT := aead.Seal(nil, iv, msg, aad)
|
|
||||||
genMsg, err := aead.Open(nil, iv, genCT, aad)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to decrypt generated ciphertext: %s", err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(genMsg, msg) {
|
|
||||||
t.Errorf("unexpected roundtripped plaintext: got %x, want %x", genMsg, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctWithTag := append(ct, tag...)
|
|
||||||
msg2, err := aead.Open(nil, iv, ctWithTag, aad)
|
|
||||||
wantPass := shouldPass(tv.Result, tv.Flags, nil)
|
|
||||||
if !wantPass && err == nil {
|
|
||||||
t.Error("decryption succeeded when it should've failed")
|
|
||||||
} else if wantPass {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("decryption failed: %s", err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(genCT, ctWithTag) {
|
|
||||||
t.Errorf("generated ciphertext doesn't match expected: got %x, want %x", genCT, ctWithTag)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(msg, msg2) {
|
|
||||||
t.Errorf("decrypted ciphertext doesn't match expected: got %x, want %x", msg2, msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vectors := map[string]func(*testing.T, []byte) cipher.AEAD{
|
|
||||||
"aes_gcm_test.json": func(t *testing.T, key []byte) cipher.AEAD {
|
|
||||||
aesCipher, err := aes.NewCipher(key)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to construct cipher: %s", err)
|
|
||||||
}
|
|
||||||
aead, err := cipher.NewGCM(aesCipher)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to construct cipher: %s", err)
|
|
||||||
}
|
|
||||||
return aead
|
|
||||||
},
|
|
||||||
"chacha20_poly1305_test.json": func(t *testing.T, key []byte) cipher.AEAD {
|
|
||||||
aead, err := chacha20poly1305.New(key)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to construct cipher: %s", err)
|
|
||||||
}
|
|
||||||
return aead
|
|
||||||
},
|
|
||||||
"xchacha20_poly1305_test.json": func(t *testing.T, key []byte) cipher.AEAD {
|
|
||||||
aead, err := chacha20poly1305.NewX(key)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to construct cipher: %s", err)
|
|
||||||
}
|
|
||||||
return aead
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for file, cipherInit := range vectors {
|
|
||||||
var root Root
|
|
||||||
readTestVector(t, file, &root)
|
|
||||||
for _, tg := range root.TestGroups {
|
|
||||||
for _, tv := range tg.Tests {
|
|
||||||
testName := fmt.Sprintf("%s #%d", file, tv.TcId)
|
|
||||||
if tv.Comment != "" {
|
|
||||||
testName += fmt.Sprintf(" %s", tv.Comment)
|
|
||||||
}
|
|
||||||
t.Run(testName, func(t *testing.T) {
|
|
||||||
aead := cipherInit(t, decodeHex(tv.Key))
|
|
||||||
testSealOpen(t, aead, tv, func() {
|
|
||||||
// A bad nonce causes a panic in AEAD.Seal and AEAD.Open,
|
|
||||||
// so should be recovered. Fail the test if it broke for
|
|
||||||
// some other reason.
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
if tg.IvSize/8 == aead.NonceSize() {
|
|
||||||
t.Error("unexpected panic")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
// Copyright 2019 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 wycheproof
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAesCbc(t *testing.T) {
|
|
||||||
// IndCpaTestVector
|
|
||||||
type IndCpaTestVector struct {
|
|
||||||
|
|
||||||
// A brief description of the test case
|
|
||||||
Comment string `json:"comment,omitempty"`
|
|
||||||
|
|
||||||
// the raw ciphertext (without IV)
|
|
||||||
Ct string `json:"ct,omitempty"`
|
|
||||||
|
|
||||||
// A list of flags
|
|
||||||
Flags []string `json:"flags,omitempty"`
|
|
||||||
|
|
||||||
// the initialization vector
|
|
||||||
Iv string `json:"iv,omitempty"`
|
|
||||||
|
|
||||||
// the key
|
|
||||||
Key string `json:"key,omitempty"`
|
|
||||||
|
|
||||||
// the plaintext
|
|
||||||
Msg string `json:"msg,omitempty"`
|
|
||||||
|
|
||||||
// Test result
|
|
||||||
Result string `json:"result,omitempty"`
|
|
||||||
|
|
||||||
// Identifier of the test case
|
|
||||||
TcId int `json:"tcId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notes a description of the labels used in the test vectors
|
|
||||||
type Notes struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// IndCpaTestGroup
|
|
||||||
type IndCpaTestGroup struct {
|
|
||||||
|
|
||||||
// the IV size in bits
|
|
||||||
IvSize int `json:"ivSize,omitempty"`
|
|
||||||
|
|
||||||
// the keySize in bits
|
|
||||||
KeySize int `json:"keySize,omitempty"`
|
|
||||||
|
|
||||||
// the expected size of the tag in bits
|
|
||||||
TagSize int `json:"tagSize,omitempty"`
|
|
||||||
Tests []*IndCpaTestVector `json:"tests,omitempty"`
|
|
||||||
Type interface{} `json:"type,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root
|
|
||||||
type Root struct {
|
|
||||||
|
|
||||||
// the primitive tested in the test file
|
|
||||||
Algorithm string `json:"algorithm,omitempty"`
|
|
||||||
|
|
||||||
// the version of the test vectors.
|
|
||||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
|
||||||
|
|
||||||
// additional documentation
|
|
||||||
Header []string `json:"header,omitempty"`
|
|
||||||
|
|
||||||
// a description of the labels used in the test vectors
|
|
||||||
Notes *Notes `json:"notes,omitempty"`
|
|
||||||
|
|
||||||
// the number of test vectors in this test
|
|
||||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
|
||||||
Schema interface{} `json:"schema,omitempty"`
|
|
||||||
TestGroups []*IndCpaTestGroup `json:"testGroups,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var root Root
|
|
||||||
readTestVector(t, "aes_cbc_pkcs5_test.json", &root)
|
|
||||||
for _, tg := range root.TestGroups {
|
|
||||||
tests:
|
|
||||||
for _, tv := range tg.Tests {
|
|
||||||
block, err := aes.NewCipher(decodeHex(tv.Key))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("#%d: %v", tv.TcId, err)
|
|
||||||
}
|
|
||||||
mode := cipher.NewCBCDecrypter(block, decodeHex(tv.Iv))
|
|
||||||
ct := decodeHex(tv.Ct)
|
|
||||||
if len(ct)%aes.BlockSize != 0 {
|
|
||||||
panic(fmt.Sprintf("#%d: ciphertext is not a multiple of the block size", tv.TcId))
|
|
||||||
}
|
|
||||||
mode.CryptBlocks(ct, ct) // decrypt the block in place
|
|
||||||
|
|
||||||
// Skip the tests that are broken due to bad padding. Panic if there are any
|
|
||||||
// tests left that are invalid for some other reason in the future, to
|
|
||||||
// evaluate what to do with those tests.
|
|
||||||
for _, flag := range tv.Flags {
|
|
||||||
if flag == "BadPadding" {
|
|
||||||
continue tests
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !shouldPass(tv.Result, tv.Flags, nil) {
|
|
||||||
panic(fmt.Sprintf("#%d: found an invalid test that is broken for some reason other than bad padding", tv.TcId))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the PKCS#5 padding from the given ciphertext to validate it
|
|
||||||
padding := ct[len(ct)-1]
|
|
||||||
paddingNum := int(padding)
|
|
||||||
for i := paddingNum; i > 0; i-- {
|
|
||||||
if ct[len(ct)-i] != padding { // panic if the padding is unexpectedly bad
|
|
||||||
panic(fmt.Sprintf("#%d: bad padding at index=%d of %v", tv.TcId, i, ct))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ct = ct[:len(ct)-paddingNum]
|
|
||||||
|
|
||||||
if got, want := hex.EncodeToString(ct), tv.Msg; got != want {
|
|
||||||
t.Errorf("#%d, type: %s, comment: %q, decoded ciphertext not equal: %s, want %s", tv.TcId, tv.Result, tv.Comment, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
// Copyright 2022 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 boringcrypto
|
|
||||||
|
|
||||||
package wycheproof
|
|
||||||
|
|
||||||
const boringcryptoEnabled = true
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
// Copyright 2019 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 wycheproof
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/dsa"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
wdsa "golang.org/x/crypto/internal/wycheproof/internal/dsa"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDsa(t *testing.T) {
|
|
||||||
// AsnSignatureTestVector
|
|
||||||
type AsnSignatureTestVector struct {
|
|
||||||
|
|
||||||
// A brief description of the test case
|
|
||||||
Comment string `json:"comment,omitempty"`
|
|
||||||
|
|
||||||
// A list of flags
|
|
||||||
Flags []string `json:"flags,omitempty"`
|
|
||||||
|
|
||||||
// The message to sign
|
|
||||||
Msg string `json:"msg,omitempty"`
|
|
||||||
|
|
||||||
// Test result
|
|
||||||
Result string `json:"result,omitempty"`
|
|
||||||
|
|
||||||
// An ASN encoded signature for msg
|
|
||||||
Sig string `json:"sig,omitempty"`
|
|
||||||
|
|
||||||
// Identifier of the test case
|
|
||||||
TcId int `json:"tcId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DsaPublicKey
|
|
||||||
type DsaPublicKey struct {
|
|
||||||
|
|
||||||
// the generator of the multiplicative subgroup
|
|
||||||
G string `json:"g,omitempty"`
|
|
||||||
|
|
||||||
// the key size in bits
|
|
||||||
KeySize int `json:"keySize,omitempty"`
|
|
||||||
|
|
||||||
// the modulus p
|
|
||||||
P string `json:"p,omitempty"`
|
|
||||||
|
|
||||||
// the order of the generator g
|
|
||||||
Q string `json:"q,omitempty"`
|
|
||||||
|
|
||||||
// the key type
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
|
|
||||||
// the public key value
|
|
||||||
Y string `json:"y,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DsaTestGroup
|
|
||||||
type DsaTestGroup struct {
|
|
||||||
|
|
||||||
// unenocded DSA public key
|
|
||||||
Key *DsaPublicKey `json:"key,omitempty"`
|
|
||||||
|
|
||||||
// DER encoded public key
|
|
||||||
KeyDer string `json:"keyDer,omitempty"`
|
|
||||||
|
|
||||||
// Pem encoded public key
|
|
||||||
KeyPem string `json:"keyPem,omitempty"`
|
|
||||||
|
|
||||||
// the hash function used for DSA
|
|
||||||
Sha string `json:"sha,omitempty"`
|
|
||||||
Tests []*AsnSignatureTestVector `json:"tests,omitempty"`
|
|
||||||
Type interface{} `json:"type,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notes a description of the labels used in the test vectors
|
|
||||||
type Notes struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root
|
|
||||||
type Root struct {
|
|
||||||
|
|
||||||
// the primitive tested in the test file
|
|
||||||
Algorithm string `json:"algorithm,omitempty"`
|
|
||||||
|
|
||||||
// the version of the test vectors.
|
|
||||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
|
||||||
|
|
||||||
// additional documentation
|
|
||||||
Header []string `json:"header,omitempty"`
|
|
||||||
|
|
||||||
// a description of the labels used in the test vectors
|
|
||||||
Notes *Notes `json:"notes,omitempty"`
|
|
||||||
|
|
||||||
// the number of test vectors in this test
|
|
||||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
|
||||||
Schema interface{} `json:"schema,omitempty"`
|
|
||||||
TestGroups []*DsaTestGroup `json:"testGroups,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
flagsShouldPass := map[string]bool{
|
|
||||||
// An encoded ASN.1 integer missing a leading zero is invalid, but accepted by some implementations.
|
|
||||||
"NoLeadingZero": false,
|
|
||||||
}
|
|
||||||
|
|
||||||
var root Root
|
|
||||||
readTestVector(t, "dsa_test.json", &root)
|
|
||||||
for _, tg := range root.TestGroups {
|
|
||||||
pub := decodePublicKey(tg.KeyDer).(*dsa.PublicKey)
|
|
||||||
h := parseHash(tg.Sha).New()
|
|
||||||
for _, sig := range tg.Tests {
|
|
||||||
h.Reset()
|
|
||||||
h.Write(decodeHex(sig.Msg))
|
|
||||||
hashed := h.Sum(nil)
|
|
||||||
hashed = hashed[:pub.Q.BitLen()/8] // Truncate to the byte-length of the subgroup (Q)
|
|
||||||
got := wdsa.VerifyASN1(pub, hashed, decodeHex(sig.Sig))
|
|
||||||
if want := shouldPass(sig.Result, sig.Flags, flagsShouldPass); got != want {
|
|
||||||
t.Errorf("tcid: %d, type: %s, comment: %q, wanted success: %t", sig.TcId, sig.Result, sig.Comment, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
// Copyright 2022 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 wycheproof
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/ecdh"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestECDHStdLib(t *testing.T) {
|
|
||||||
type ECDHTestVector struct {
|
|
||||||
// A brief description of the test case
|
|
||||||
Comment string `json:"comment,omitempty"`
|
|
||||||
// A list of flags
|
|
||||||
Flags []string `json:"flags,omitempty"`
|
|
||||||
// the private key
|
|
||||||
Private string `json:"private,omitempty"`
|
|
||||||
// Encoded public key
|
|
||||||
Public string `json:"public,omitempty"`
|
|
||||||
// Test result
|
|
||||||
Result string `json:"result,omitempty"`
|
|
||||||
// The shared secret key
|
|
||||||
Shared string `json:"shared,omitempty"`
|
|
||||||
// Identifier of the test case
|
|
||||||
TcID int `json:"tcId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ECDHTestGroup struct {
|
|
||||||
Curve string `json:"curve,omitempty"`
|
|
||||||
Tests []*ECDHTestVector `json:"tests,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Root struct {
|
|
||||||
TestGroups []*ECDHTestGroup `json:"testGroups,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
flagsShouldPass := map[string]bool{
|
|
||||||
// We don't support compressed points.
|
|
||||||
"CompressedPoint": false,
|
|
||||||
// We don't support decoding custom curves.
|
|
||||||
"UnnamedCurve": false,
|
|
||||||
// WrongOrder and UnusedParam are only found with UnnamedCurve.
|
|
||||||
"WrongOrder": false,
|
|
||||||
"UnusedParam": false,
|
|
||||||
|
|
||||||
// X25519 specific flags
|
|
||||||
"Twist": true,
|
|
||||||
"SmallPublicKey": false,
|
|
||||||
"LowOrderPublic": false,
|
|
||||||
"ZeroSharedSecret": false,
|
|
||||||
"NonCanonicalPublic": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// curveToCurve is a map of all elliptic curves supported
|
|
||||||
// by crypto/elliptic, which can subsequently be parsed and tested.
|
|
||||||
curveToCurve := map[string]ecdh.Curve{
|
|
||||||
"secp256r1": ecdh.P256(),
|
|
||||||
"secp384r1": ecdh.P384(),
|
|
||||||
"secp521r1": ecdh.P521(),
|
|
||||||
"curve25519": ecdh.X25519(),
|
|
||||||
}
|
|
||||||
|
|
||||||
curveToKeySize := map[string]int{
|
|
||||||
"secp256r1": 32,
|
|
||||||
"secp384r1": 48,
|
|
||||||
"secp521r1": 66,
|
|
||||||
"curve25519": 32,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range []string{
|
|
||||||
"ecdh_secp256r1_ecpoint_test.json",
|
|
||||||
"ecdh_secp384r1_ecpoint_test.json",
|
|
||||||
"ecdh_secp521r1_ecpoint_test.json",
|
|
||||||
"x25519_test.json",
|
|
||||||
} {
|
|
||||||
var root Root
|
|
||||||
readTestVector(t, f, &root)
|
|
||||||
for _, tg := range root.TestGroups {
|
|
||||||
if _, ok := curveToCurve[tg.Curve]; !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, tt := range tg.Tests {
|
|
||||||
tg, tt := tg, tt
|
|
||||||
t.Run(fmt.Sprintf("%s/%d", tg.Curve, tt.TcID), func(t *testing.T) {
|
|
||||||
t.Logf("Type: %v", tt.Result)
|
|
||||||
t.Logf("Flags: %q", tt.Flags)
|
|
||||||
t.Log(tt.Comment)
|
|
||||||
|
|
||||||
shouldPass := shouldPass(tt.Result, tt.Flags, flagsShouldPass)
|
|
||||||
|
|
||||||
curve := curveToCurve[tg.Curve]
|
|
||||||
p := decodeHex(tt.Public)
|
|
||||||
pub, err := curve.NewPublicKey(p)
|
|
||||||
if err != nil {
|
|
||||||
if shouldPass {
|
|
||||||
t.Errorf("NewPublicKey: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
privBytes := decodeHex(tt.Private)
|
|
||||||
if len(privBytes) != curveToKeySize[tg.Curve] {
|
|
||||||
t.Skipf("non-standard key size %d", len(privBytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
priv, err := curve.NewPrivateKey(privBytes)
|
|
||||||
if err != nil {
|
|
||||||
if shouldPass {
|
|
||||||
t.Errorf("NewPrivateKey: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
shared := decodeHex(tt.Shared)
|
|
||||||
x, err := priv.ECDH(pub)
|
|
||||||
if err != nil {
|
|
||||||
if tg.Curve == "curve25519" && !shouldPass {
|
|
||||||
// ECDH is expected to only return an error when using X25519,
|
|
||||||
// in all other cases an error is unexpected.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Fatalf("ECDH: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes.Equal(shared, x) != shouldPass {
|
|
||||||
if shouldPass {
|
|
||||||
t.Errorf("ECDH = %x, want %x", shared, x)
|
|
||||||
} else {
|
|
||||||
t.Errorf("ECDH = %x, want anything else", shared)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
// Copyright 2019 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 wycheproof
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/asn1"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/cryptobyte"
|
|
||||||
casn1 "golang.org/x/crypto/cryptobyte/asn1"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestECDH(t *testing.T) {
|
|
||||||
type ECDHTestVector struct {
|
|
||||||
// A brief description of the test case
|
|
||||||
Comment string `json:"comment,omitempty"`
|
|
||||||
// A list of flags
|
|
||||||
Flags []string `json:"flags,omitempty"`
|
|
||||||
// the private key
|
|
||||||
Private string `json:"private,omitempty"`
|
|
||||||
// Encoded public key
|
|
||||||
Public string `json:"public,omitempty"`
|
|
||||||
// Test result
|
|
||||||
Result string `json:"result,omitempty"`
|
|
||||||
// The shared secret key
|
|
||||||
Shared string `json:"shared,omitempty"`
|
|
||||||
// Identifier of the test case
|
|
||||||
TcID int `json:"tcId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ECDHTestGroup struct {
|
|
||||||
Curve string `json:"curve,omitempty"`
|
|
||||||
Tests []*ECDHTestVector `json:"tests,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Root struct {
|
|
||||||
TestGroups []*ECDHTestGroup `json:"testGroups,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
flagsShouldPass := map[string]bool{
|
|
||||||
// ParsePKIXPublicKey doesn't support compressed points, but we test
|
|
||||||
// them against UnmarshalCompressed anyway.
|
|
||||||
"CompressedPoint": true,
|
|
||||||
// We don't support decoding custom curves.
|
|
||||||
"UnnamedCurve": false,
|
|
||||||
// WrongOrder and UnusedParam are only found with UnnamedCurve.
|
|
||||||
"WrongOrder": false,
|
|
||||||
"UnusedParam": false,
|
|
||||||
}
|
|
||||||
|
|
||||||
// supportedCurves is a map of all elliptic curves supported
|
|
||||||
// by crypto/elliptic, which can subsequently be parsed and tested.
|
|
||||||
supportedCurves := map[string]bool{
|
|
||||||
"secp224r1": true,
|
|
||||||
"secp256r1": true,
|
|
||||||
"secp384r1": true,
|
|
||||||
"secp521r1": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
var root Root
|
|
||||||
readTestVector(t, "ecdh_test.json", &root)
|
|
||||||
for _, tg := range root.TestGroups {
|
|
||||||
if !supportedCurves[tg.Curve] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, tt := range tg.Tests {
|
|
||||||
tg, tt := tg, tt
|
|
||||||
t.Run(fmt.Sprintf("%s/%d", tg.Curve, tt.TcID), func(t *testing.T) {
|
|
||||||
t.Logf("Type: %v", tt.Result)
|
|
||||||
t.Logf("Flags: %q", tt.Flags)
|
|
||||||
t.Log(tt.Comment)
|
|
||||||
|
|
||||||
shouldPass := shouldPass(tt.Result, tt.Flags, flagsShouldPass)
|
|
||||||
|
|
||||||
p := decodeHex(tt.Public)
|
|
||||||
pp, err := x509.ParsePKIXPublicKey(p)
|
|
||||||
if err != nil {
|
|
||||||
pp, err = decodeCompressedPKIX(p)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if shouldPass {
|
|
||||||
t.Errorf("unexpected parsing error: %s", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pub := pp.(*ecdsa.PublicKey)
|
|
||||||
|
|
||||||
priv := decodeHex(tt.Private)
|
|
||||||
shared := decodeHex(tt.Shared)
|
|
||||||
|
|
||||||
x, _ := pub.Curve.ScalarMult(pub.X, pub.Y, priv)
|
|
||||||
xBytes := make([]byte, (pub.Curve.Params().BitSize+7)/8)
|
|
||||||
got := bytes.Equal(shared, x.FillBytes(xBytes))
|
|
||||||
|
|
||||||
if want := shouldPass; got != want {
|
|
||||||
t.Errorf("wanted success %v, got %v", want, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeCompressedPKIX(der []byte) (interface{}, error) {
|
|
||||||
s := cryptobyte.String(der)
|
|
||||||
var s1, s2 cryptobyte.String
|
|
||||||
var algoOID, namedCurveOID asn1.ObjectIdentifier
|
|
||||||
var pointDER []byte
|
|
||||||
if !s.ReadASN1(&s1, casn1.SEQUENCE) || !s.Empty() ||
|
|
||||||
!s1.ReadASN1(&s2, casn1.SEQUENCE) ||
|
|
||||||
!s2.ReadASN1ObjectIdentifier(&algoOID) ||
|
|
||||||
!s2.ReadASN1ObjectIdentifier(&namedCurveOID) || !s2.Empty() ||
|
|
||||||
!s1.ReadASN1BitStringAsBytes(&pointDER) || !s1.Empty() {
|
|
||||||
return nil, errors.New("failed to parse PKIX structure")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !algoOID.Equal(oidPublicKeyECDSA) {
|
|
||||||
return nil, errors.New("wrong algorithm OID")
|
|
||||||
}
|
|
||||||
namedCurve := namedCurveFromOID(namedCurveOID)
|
|
||||||
if namedCurve == nil {
|
|
||||||
return nil, errors.New("unsupported elliptic curve")
|
|
||||||
}
|
|
||||||
x, y := elliptic.UnmarshalCompressed(namedCurve, pointDER)
|
|
||||||
if x == nil {
|
|
||||||
return nil, errors.New("failed to unmarshal elliptic curve point")
|
|
||||||
}
|
|
||||||
pub := &ecdsa.PublicKey{
|
|
||||||
Curve: namedCurve,
|
|
||||||
X: x,
|
|
||||||
Y: y,
|
|
||||||
}
|
|
||||||
return pub, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}
|
|
||||||
oidNamedCurveP224 = asn1.ObjectIdentifier{1, 3, 132, 0, 33}
|
|
||||||
oidNamedCurveP256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7}
|
|
||||||
oidNamedCurveP384 = asn1.ObjectIdentifier{1, 3, 132, 0, 34}
|
|
||||||
oidNamedCurveP521 = asn1.ObjectIdentifier{1, 3, 132, 0, 35}
|
|
||||||
)
|
|
||||||
|
|
||||||
func namedCurveFromOID(oid asn1.ObjectIdentifier) elliptic.Curve {
|
|
||||||
switch {
|
|
||||||
case oid.Equal(oidNamedCurveP224):
|
|
||||||
return elliptic.P224()
|
|
||||||
case oid.Equal(oidNamedCurveP256):
|
|
||||||
return elliptic.P256()
|
|
||||||
case oid.Equal(oidNamedCurveP384):
|
|
||||||
return elliptic.P384()
|
|
||||||
case oid.Equal(oidNamedCurveP521):
|
|
||||||
return elliptic.P521()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
// Copyright 2019 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 wycheproof
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/cryptobyte"
|
|
||||||
"golang.org/x/crypto/cryptobyte/asn1"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestECDSA(t *testing.T) {
|
|
||||||
type ASNSignatureTestVector struct {
|
|
||||||
// A brief description of the test case
|
|
||||||
Comment string `json:"comment"`
|
|
||||||
// A list of flags
|
|
||||||
Flags []string `json:"flags"`
|
|
||||||
// The message to sign
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
// Test result
|
|
||||||
Result string `json:"result"`
|
|
||||||
// An ASN.1 encoded signature for msg
|
|
||||||
Sig string `json:"sig"`
|
|
||||||
// Identifier of the test case
|
|
||||||
TcID int `json:"tcId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ECPublicKey struct {
|
|
||||||
// The EC group used by this public key
|
|
||||||
Curve interface{} `json:"curve"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ECDSATestGroup struct {
|
|
||||||
// Unencoded EC public key
|
|
||||||
Key *ECPublicKey `json:"key"`
|
|
||||||
// DER encoded public key
|
|
||||||
KeyDER string `json:"keyDer"`
|
|
||||||
// the hash function used for ECDSA
|
|
||||||
SHA string `json:"sha"`
|
|
||||||
Tests []*ASNSignatureTestVector `json:"tests"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Root struct {
|
|
||||||
TestGroups []*ECDSATestGroup `json:"testGroups"`
|
|
||||||
}
|
|
||||||
|
|
||||||
flagsShouldPass := map[string]bool{
|
|
||||||
// An encoded ASN.1 integer missing a leading zero is invalid, but
|
|
||||||
// accepted by some implementations.
|
|
||||||
"MissingZero": false,
|
|
||||||
// A signature using a weaker hash than the EC params is not a security
|
|
||||||
// risk, as long as the hash is secure.
|
|
||||||
// https://www.imperialviolet.org/2014/05/25/strengthmatching.html
|
|
||||||
"WeakHash": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// supportedCurves is a map of all elliptic curves supported
|
|
||||||
// by crypto/elliptic, which can subsequently be parsed and tested.
|
|
||||||
supportedCurves := map[string]bool{
|
|
||||||
"secp224r1": true,
|
|
||||||
"secp256r1": true,
|
|
||||||
"secp384r1": true,
|
|
||||||
"secp521r1": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
var root Root
|
|
||||||
readTestVector(t, "ecdsa_test.json", &root)
|
|
||||||
for _, tg := range root.TestGroups {
|
|
||||||
curve := tg.Key.Curve.(string)
|
|
||||||
if !supportedCurves[curve] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pub := decodePublicKey(tg.KeyDER).(*ecdsa.PublicKey)
|
|
||||||
h := parseHash(tg.SHA).New()
|
|
||||||
for _, sig := range tg.Tests {
|
|
||||||
h.Reset()
|
|
||||||
h.Write(decodeHex(sig.Msg))
|
|
||||||
hashed := h.Sum(nil)
|
|
||||||
sigBytes := decodeHex(sig.Sig)
|
|
||||||
got := ecdsa.VerifyASN1(pub, hashed, sigBytes)
|
|
||||||
if want := shouldPass(sig.Result, sig.Flags, flagsShouldPass); got != want {
|
|
||||||
t.Errorf("tcid: %d, type: %s, comment: %q, VerifyASN1 wanted success: %t", sig.TcID, sig.Result, sig.Comment, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
var r, s big.Int
|
|
||||||
var inner cryptobyte.String
|
|
||||||
input := cryptobyte.String(sigBytes)
|
|
||||||
if !input.ReadASN1(&inner, asn1.SEQUENCE) ||
|
|
||||||
!input.Empty() ||
|
|
||||||
!inner.ReadASN1Integer(&r) ||
|
|
||||||
!inner.ReadASN1Integer(&s) ||
|
|
||||||
!inner.Empty() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
got = ecdsa.Verify(pub, hashed, &r, &s)
|
|
||||||
if want := shouldPass(sig.Result, sig.Flags, flagsShouldPass); got != want {
|
|
||||||
t.Errorf("tcid: %d, type: %s, comment: %q, Verify wanted success: %t", sig.TcID, sig.Result, sig.Comment, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
// Copyright 2019 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 wycheproof
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ed25519"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEddsa(t *testing.T) {
|
|
||||||
// Jwk the private key in webcrypto format
|
|
||||||
type Jwk struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key unencoded key pair
|
|
||||||
type Key struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notes a description of the labels used in the test vectors
|
|
||||||
type Notes struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignatureTestVector
|
|
||||||
type SignatureTestVector struct {
|
|
||||||
|
|
||||||
// A brief description of the test case
|
|
||||||
Comment string `json:"comment,omitempty"`
|
|
||||||
|
|
||||||
// A list of flags
|
|
||||||
Flags []string `json:"flags,omitempty"`
|
|
||||||
|
|
||||||
// The message to sign
|
|
||||||
Msg string `json:"msg,omitempty"`
|
|
||||||
|
|
||||||
// Test result
|
|
||||||
Result string `json:"result,omitempty"`
|
|
||||||
|
|
||||||
// A signature for msg
|
|
||||||
Sig string `json:"sig,omitempty"`
|
|
||||||
|
|
||||||
// Identifier of the test case
|
|
||||||
TcId int `json:"tcId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// EddsaTestGroup
|
|
||||||
type EddsaTestGroup struct {
|
|
||||||
|
|
||||||
// the private key in webcrypto format
|
|
||||||
Jwk *Jwk `json:"jwk,omitempty"`
|
|
||||||
|
|
||||||
// unencoded key pair
|
|
||||||
Key *Key `json:"key,omitempty"`
|
|
||||||
|
|
||||||
// Asn encoded public key
|
|
||||||
KeyDer string `json:"keyDer,omitempty"`
|
|
||||||
|
|
||||||
// Pem encoded public key
|
|
||||||
KeyPem string `json:"keyPem,omitempty"`
|
|
||||||
Tests []*SignatureTestVector `json:"tests,omitempty"`
|
|
||||||
Type interface{} `json:"type,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root
|
|
||||||
type Root struct {
|
|
||||||
|
|
||||||
// the primitive tested in the test file
|
|
||||||
Algorithm string `json:"algorithm,omitempty"`
|
|
||||||
|
|
||||||
// the version of the test vectors.
|
|
||||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
|
||||||
|
|
||||||
// additional documentation
|
|
||||||
Header []string `json:"header,omitempty"`
|
|
||||||
|
|
||||||
// a description of the labels used in the test vectors
|
|
||||||
Notes *Notes `json:"notes,omitempty"`
|
|
||||||
|
|
||||||
// the number of test vectors in this test
|
|
||||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
|
||||||
Schema interface{} `json:"schema,omitempty"`
|
|
||||||
TestGroups []*EddsaTestGroup `json:"testGroups,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var root Root
|
|
||||||
readTestVector(t, "eddsa_test.json", &root)
|
|
||||||
for _, tg := range root.TestGroups {
|
|
||||||
pub := decodePublicKey(tg.KeyDer).(ed25519.PublicKey)
|
|
||||||
for _, sig := range tg.Tests {
|
|
||||||
got := ed25519.Verify(pub, decodeHex(sig.Msg), decodeHex(sig.Sig))
|
|
||||||
if want := shouldPass(sig.Result, sig.Flags, nil); got != want {
|
|
||||||
t.Errorf("tcid: %d, type: %s, comment: %q, wanted success: %t", sig.TcId, sig.Result, sig.Comment, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
// Copyright 2019 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 wycheproof
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/hkdf"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHkdf(t *testing.T) {
|
|
||||||
|
|
||||||
// HkdfTestVector
|
|
||||||
type HkdfTestVector struct {
|
|
||||||
|
|
||||||
// A brief description of the test case
|
|
||||||
Comment string `json:"comment,omitempty"`
|
|
||||||
|
|
||||||
// A list of flags
|
|
||||||
Flags []string `json:"flags,omitempty"`
|
|
||||||
|
|
||||||
// the key (input key material)
|
|
||||||
Ikm string `json:"ikm,omitempty"`
|
|
||||||
|
|
||||||
// additional information used in the key derivation
|
|
||||||
Info string `json:"info,omitempty"`
|
|
||||||
|
|
||||||
// the generated bytes (output key material)
|
|
||||||
Okm string `json:"okm,omitempty"`
|
|
||||||
|
|
||||||
// Test result
|
|
||||||
Result string `json:"result,omitempty"`
|
|
||||||
|
|
||||||
// the salt for the key derivation
|
|
||||||
Salt string `json:"salt,omitempty"`
|
|
||||||
|
|
||||||
// the size of the output in bytes
|
|
||||||
Size int `json:"size,omitempty"`
|
|
||||||
|
|
||||||
// Identifier of the test case
|
|
||||||
TcId int `json:"tcId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notes a description of the labels used in the test vectors
|
|
||||||
type Notes struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// HkdfTestGroup
|
|
||||||
type HkdfTestGroup struct {
|
|
||||||
|
|
||||||
// the size of the ikm in bits
|
|
||||||
KeySize int `json:"keySize,omitempty"`
|
|
||||||
Tests []*HkdfTestVector `json:"tests,omitempty"`
|
|
||||||
Type interface{} `json:"type,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root
|
|
||||||
type Root struct {
|
|
||||||
|
|
||||||
// the primitive tested in the test file
|
|
||||||
Algorithm string `json:"algorithm,omitempty"`
|
|
||||||
|
|
||||||
// the version of the test vectors.
|
|
||||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
|
||||||
|
|
||||||
// additional documentation
|
|
||||||
Header []string `json:"header,omitempty"`
|
|
||||||
|
|
||||||
// a description of the labels used in the test vectors
|
|
||||||
Notes *Notes `json:"notes,omitempty"`
|
|
||||||
|
|
||||||
// the number of test vectors in this test
|
|
||||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
|
||||||
Schema interface{} `json:"schema,omitempty"`
|
|
||||||
TestGroups []*HkdfTestGroup `json:"testGroups,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
fileHashAlgorithms := map[string]string{
|
|
||||||
"hkdf_sha1_test.json": "SHA-1",
|
|
||||||
"hkdf_sha256_test.json": "SHA-256",
|
|
||||||
"hkdf_sha384_test.json": "SHA-384",
|
|
||||||
"hkdf_sha512_test.json": "SHA-512",
|
|
||||||
}
|
|
||||||
|
|
||||||
for f := range fileHashAlgorithms {
|
|
||||||
var root Root
|
|
||||||
readTestVector(t, f, &root)
|
|
||||||
for _, tg := range root.TestGroups {
|
|
||||||
for _, tv := range tg.Tests {
|
|
||||||
h := parseHash(fileHashAlgorithms[f]).New
|
|
||||||
hkdf := hkdf.New(h, decodeHex(tv.Ikm), decodeHex(tv.Salt), decodeHex(tv.Info))
|
|
||||||
key := make([]byte, tv.Size)
|
|
||||||
wantPass := shouldPass(tv.Result, tv.Flags, nil)
|
|
||||||
_, err := io.ReadFull(hkdf, key)
|
|
||||||
if (err == nil) != wantPass {
|
|
||||||
t.Errorf("tcid: %d, type: %s, comment: %q, wanted success: %t, got: %v", tv.TcId, tv.Result, tv.Comment, wantPass, err)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
continue // don't validate output text if reading failed
|
|
||||||
}
|
|
||||||
if got, want := key, decodeHex(tv.Okm); !bytes.Equal(got, want) {
|
|
||||||
t.Errorf("tcid: %d, type: %s, comment: %q, output bytes don't match", tv.TcId, tv.Result, tv.Comment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
// Copyright 2020 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 wycheproof
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/hmac"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHMAC(t *testing.T) {
|
|
||||||
// MacTestVector
|
|
||||||
type MacTestVector struct {
|
|
||||||
|
|
||||||
// A brief description of the test case
|
|
||||||
Comment string `json:"comment,omitempty"`
|
|
||||||
|
|
||||||
// A list of flags
|
|
||||||
Flags []string `json:"flags,omitempty"`
|
|
||||||
|
|
||||||
// the key
|
|
||||||
Key string `json:"key,omitempty"`
|
|
||||||
|
|
||||||
// the plaintext
|
|
||||||
Msg string `json:"msg,omitempty"`
|
|
||||||
|
|
||||||
// Test result
|
|
||||||
Result string `json:"result,omitempty"`
|
|
||||||
|
|
||||||
// the authentication tag
|
|
||||||
Tag string `json:"tag,omitempty"`
|
|
||||||
|
|
||||||
// Identifier of the test case
|
|
||||||
TcId int `json:"tcId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MacTestGroup
|
|
||||||
type MacTestGroup struct {
|
|
||||||
|
|
||||||
// the keySize in bits
|
|
||||||
KeySize int `json:"keySize,omitempty"`
|
|
||||||
|
|
||||||
// the expected size of the tag in bits
|
|
||||||
TagSize int `json:"tagSize,omitempty"`
|
|
||||||
Tests []*MacTestVector `json:"tests,omitempty"`
|
|
||||||
Type interface{} `json:"type,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notes a description of the labels used in the test vectors
|
|
||||||
type Notes struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root
|
|
||||||
type Root struct {
|
|
||||||
|
|
||||||
// the primitive tested in the test file
|
|
||||||
Algorithm string `json:"algorithm,omitempty"`
|
|
||||||
|
|
||||||
// the version of the test vectors.
|
|
||||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
|
||||||
|
|
||||||
// additional documentation
|
|
||||||
Header []string `json:"header,omitempty"`
|
|
||||||
|
|
||||||
// a description of the labels used in the test vectors
|
|
||||||
Notes *Notes `json:"notes,omitempty"`
|
|
||||||
|
|
||||||
// the number of test vectors in this test
|
|
||||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
|
||||||
Schema interface{} `json:"schema,omitempty"`
|
|
||||||
TestGroups []*MacTestGroup `json:"testGroups,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
fileHashAlgs := map[string]string{
|
|
||||||
"hmac_sha1_test.json": "SHA-1",
|
|
||||||
"hmac_sha224_test.json": "SHA-224",
|
|
||||||
"hmac_sha256_test.json": "SHA-256",
|
|
||||||
"hmac_sha384_test.json": "SHA-384",
|
|
||||||
"hmac_sha512_test.json": "SHA-512",
|
|
||||||
}
|
|
||||||
|
|
||||||
for f := range fileHashAlgs {
|
|
||||||
var root Root
|
|
||||||
readTestVector(t, f, &root)
|
|
||||||
for _, tg := range root.TestGroups {
|
|
||||||
h := parseHash(fileHashAlgs[f])
|
|
||||||
// Skip test vectors where the tag length does not equal the
|
|
||||||
// hash length, since crypto/hmac does not support generating
|
|
||||||
// these truncated tags.
|
|
||||||
if tg.TagSize/8 != h.Size() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, tv := range tg.Tests {
|
|
||||||
hm := hmac.New(h.New, decodeHex(tv.Key))
|
|
||||||
hm.Write(decodeHex(tv.Msg))
|
|
||||||
tag := hm.Sum(nil)
|
|
||||||
got := hmac.Equal(decodeHex(tv.Tag), tag)
|
|
||||||
if want := shouldPass(tv.Result, tv.Flags, nil); want != got {
|
|
||||||
t.Errorf("%s, tcid: %d, type: %s, comment: %q, unexpected result", f, tv.TcId, tv.Result, tv.Comment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
// Copyright 2019 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 dsa provides an internal version of dsa.Verify
|
|
||||||
// that is used for the Wycheproof tests.
|
|
||||||
package dsa
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/dsa"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/cryptobyte"
|
|
||||||
"golang.org/x/crypto/cryptobyte/asn1"
|
|
||||||
)
|
|
||||||
|
|
||||||
// VerifyASN1 verifies the ASN1 encoded signature, sig, of hash using the
|
|
||||||
// public key, pub. Its return value records whether the signature is valid.
|
|
||||||
func VerifyASN1(pub *dsa.PublicKey, hash, sig []byte) bool {
|
|
||||||
var (
|
|
||||||
r, s = &big.Int{}, &big.Int{}
|
|
||||||
inner cryptobyte.String
|
|
||||||
)
|
|
||||||
input := cryptobyte.String(sig)
|
|
||||||
if !input.ReadASN1(&inner, asn1.SEQUENCE) ||
|
|
||||||
!input.Empty() ||
|
|
||||||
!inner.ReadASN1Integer(r) ||
|
|
||||||
!inner.ReadASN1Integer(s) ||
|
|
||||||
!inner.Empty() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return dsa.Verify(pub, hash, r, s)
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
// Copyright 2022 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 !boringcrypto
|
|
||||||
|
|
||||||
package wycheproof
|
|
||||||
|
|
||||||
const boringcryptoEnabled = false
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
// Copyright 2020 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 wycheproof
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRSAOAEPDecrypt(t *testing.T) {
|
|
||||||
// Notes a description of the labels used in the test vectors
|
|
||||||
type Notes struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// RsaesOaepTestVector
|
|
||||||
type RsaesOaepTestVector struct {
|
|
||||||
|
|
||||||
// A brief description of the test case
|
|
||||||
Comment string `json:"comment,omitempty"`
|
|
||||||
|
|
||||||
// An encryption of msg
|
|
||||||
Ct string `json:"ct,omitempty"`
|
|
||||||
|
|
||||||
// A list of flags
|
|
||||||
Flags []string `json:"flags,omitempty"`
|
|
||||||
|
|
||||||
// The label used for the encryption
|
|
||||||
Label string `json:"label,omitempty"`
|
|
||||||
|
|
||||||
// The encrypted message
|
|
||||||
Msg string `json:"msg,omitempty"`
|
|
||||||
|
|
||||||
// Test result
|
|
||||||
Result string `json:"result,omitempty"`
|
|
||||||
|
|
||||||
// Identifier of the test case
|
|
||||||
TcId int `json:"tcId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RsaesOaepTestGroup
|
|
||||||
type RsaesOaepTestGroup struct {
|
|
||||||
|
|
||||||
// The private exponent
|
|
||||||
D string `json:"d,omitempty"`
|
|
||||||
|
|
||||||
// The public exponent
|
|
||||||
E string `json:"e,omitempty"`
|
|
||||||
|
|
||||||
// the message generating function (e.g. MGF1)
|
|
||||||
Mgf string `json:"mgf,omitempty"`
|
|
||||||
|
|
||||||
// The hash function used for the message generating function.
|
|
||||||
MgfSha string `json:"mgfSha,omitempty"`
|
|
||||||
|
|
||||||
// The modulus of the key
|
|
||||||
N string `json:"n,omitempty"`
|
|
||||||
|
|
||||||
// Pem encoded private key
|
|
||||||
PrivateKeyPem string `json:"privateKeyPem,omitempty"`
|
|
||||||
|
|
||||||
// Pkcs 8 encoded private key.
|
|
||||||
PrivateKeyPkcs8 string `json:"privateKeyPkcs8,omitempty"`
|
|
||||||
|
|
||||||
// The hash function for hashing the label.
|
|
||||||
Sha string `json:"sha,omitempty"`
|
|
||||||
Tests []*RsaesOaepTestVector `json:"tests,omitempty"`
|
|
||||||
Type interface{} `json:"type,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root
|
|
||||||
type Root struct {
|
|
||||||
|
|
||||||
// the primitive tested in the test file
|
|
||||||
Algorithm string `json:"algorithm,omitempty"`
|
|
||||||
|
|
||||||
// the version of the test vectors.
|
|
||||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
|
||||||
|
|
||||||
// additional documentation
|
|
||||||
Header []string `json:"header,omitempty"`
|
|
||||||
|
|
||||||
// a description of the labels used in the test vectors
|
|
||||||
Notes *Notes `json:"notes,omitempty"`
|
|
||||||
|
|
||||||
// the number of test vectors in this test
|
|
||||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
|
||||||
Schema interface{} `json:"schema,omitempty"`
|
|
||||||
TestGroups []*RsaesOaepTestGroup `json:"testGroups,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// rsa.DecryptOAEP doesn't support using a different hash for the
|
|
||||||
// MGF and the label, so skip all of the test vectors that use
|
|
||||||
// these unbalanced constructions. rsa_oaep_misc_test.json contains
|
|
||||||
// both balanced and unbalanced constructions so in that case
|
|
||||||
// we just filter out any test groups where MgfSha != Sha
|
|
||||||
files := []string{
|
|
||||||
"rsa_oaep_2048_sha1_mgf1sha1_test.json",
|
|
||||||
"rsa_oaep_2048_sha224_mgf1sha224_test.json",
|
|
||||||
"rsa_oaep_2048_sha256_mgf1sha256_test.json",
|
|
||||||
"rsa_oaep_2048_sha384_mgf1sha384_test.json",
|
|
||||||
"rsa_oaep_2048_sha512_mgf1sha512_test.json",
|
|
||||||
"rsa_oaep_3072_sha256_mgf1sha256_test.json",
|
|
||||||
"rsa_oaep_3072_sha512_mgf1sha512_test.json",
|
|
||||||
"rsa_oaep_4096_sha256_mgf1sha256_test.json",
|
|
||||||
"rsa_oaep_4096_sha512_mgf1sha512_test.json",
|
|
||||||
"rsa_oaep_misc_test.json",
|
|
||||||
}
|
|
||||||
|
|
||||||
flagsShouldPass := map[string]bool{
|
|
||||||
// rsa.DecryptOAEP happily supports small key sizes
|
|
||||||
"SmallModulus": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range files {
|
|
||||||
var root Root
|
|
||||||
readTestVector(t, f, &root)
|
|
||||||
for _, tg := range root.TestGroups {
|
|
||||||
if tg.MgfSha != tg.Sha {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
priv, err := x509.ParsePKCS8PrivateKey(decodeHex(tg.PrivateKeyPkcs8))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s failed to parse PKCS #8 private key: %s", f, err)
|
|
||||||
}
|
|
||||||
hash := parseHash(tg.Sha)
|
|
||||||
for _, tv := range tg.Tests {
|
|
||||||
t.Run(fmt.Sprintf("%s #%d", f, tv.TcId), func(t *testing.T) {
|
|
||||||
wantPass := shouldPass(tv.Result, tv.Flags, flagsShouldPass)
|
|
||||||
plaintext, err := rsa.DecryptOAEP(hash.New(), nil, priv.(*rsa.PrivateKey), decodeHex(tv.Ct), decodeHex(tv.Label))
|
|
||||||
if wantPass {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("comment: %s, expected success: %s", tv.Comment, err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(plaintext, decodeHex(tv.Msg)) {
|
|
||||||
t.Errorf("comment: %s, unexpected plaintext: got %x, want %s", tv.Comment, plaintext, tv.Msg)
|
|
||||||
}
|
|
||||||
} else if err == nil {
|
|
||||||
t.Errorf("comment: %s, expected failure", tv.Comment)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
// Copyright 2019 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 wycheproof
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rsa"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRsaPss(t *testing.T) {
|
|
||||||
// KeyJwk Public key in JWK format
|
|
||||||
type KeyJwk struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notes a description of the labels used in the test vectors
|
|
||||||
type Notes struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignatureTestVector
|
|
||||||
type SignatureTestVector struct {
|
|
||||||
|
|
||||||
// A brief description of the test case
|
|
||||||
Comment string `json:"comment,omitempty"`
|
|
||||||
|
|
||||||
// A list of flags
|
|
||||||
Flags []string `json:"flags,omitempty"`
|
|
||||||
|
|
||||||
// The message to sign
|
|
||||||
Msg string `json:"msg,omitempty"`
|
|
||||||
|
|
||||||
// Test result
|
|
||||||
Result string `json:"result,omitempty"`
|
|
||||||
|
|
||||||
// A signature for msg
|
|
||||||
Sig string `json:"sig,omitempty"`
|
|
||||||
|
|
||||||
// Identifier of the test case
|
|
||||||
TcId int `json:"tcId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RsassaPkcs1TestGroup
|
|
||||||
type RsassaPkcs1TestGroup struct {
|
|
||||||
|
|
||||||
// The private exponent
|
|
||||||
D string `json:"d,omitempty"`
|
|
||||||
|
|
||||||
// The public exponent
|
|
||||||
E string `json:"e,omitempty"`
|
|
||||||
|
|
||||||
// ASN encoding of the sequence [n, e]
|
|
||||||
KeyAsn string `json:"keyAsn,omitempty"`
|
|
||||||
|
|
||||||
// ASN encoding of the public key
|
|
||||||
KeyDer string `json:"keyDer,omitempty"`
|
|
||||||
|
|
||||||
// Public key in JWK format
|
|
||||||
KeyJwk *KeyJwk `json:"keyJwk,omitempty"`
|
|
||||||
|
|
||||||
// Pem encoded public key
|
|
||||||
KeyPem string `json:"keyPem,omitempty"`
|
|
||||||
|
|
||||||
// the size of the modulus in bits
|
|
||||||
KeySize int `json:"keySize,omitempty"`
|
|
||||||
|
|
||||||
// The modulus of the key
|
|
||||||
N string `json:"n,omitempty"`
|
|
||||||
|
|
||||||
// The salt length
|
|
||||||
SLen int `json:"sLen,omitempty"`
|
|
||||||
|
|
||||||
// the hash function used for the message
|
|
||||||
Sha string `json:"sha,omitempty"`
|
|
||||||
Tests []*SignatureTestVector `json:"tests,omitempty"`
|
|
||||||
Type interface{} `json:"type,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root
|
|
||||||
type Root struct {
|
|
||||||
|
|
||||||
// the primitive tested in the test file
|
|
||||||
Algorithm string `json:"algorithm,omitempty"`
|
|
||||||
|
|
||||||
// the version of the test vectors.
|
|
||||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
|
||||||
|
|
||||||
// additional documentation
|
|
||||||
Header []string `json:"header,omitempty"`
|
|
||||||
|
|
||||||
// a description of the labels used in the test vectors
|
|
||||||
Notes *Notes `json:"notes,omitempty"`
|
|
||||||
|
|
||||||
// the number of test vectors in this test
|
|
||||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
|
||||||
Schema interface{} `json:"schema,omitempty"`
|
|
||||||
TestGroups []*RsassaPkcs1TestGroup `json:"testGroups,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
flagsShouldPass := map[string]bool{
|
|
||||||
// A signature using a weaker hash than the EC params is not a security risk, as long as the hash is secure.
|
|
||||||
// https://www.imperialviolet.org/2014/05/25/strengthmatching.html
|
|
||||||
"WeakHash": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// filesOverrideToPassZeroSLen is a map of all test files
|
|
||||||
// and which TcIds that should be overridden to pass if the
|
|
||||||
// rsa.PSSOptions.SaltLength is zero.
|
|
||||||
// These tests expect a failure with a PSSOptions.SaltLength: 0
|
|
||||||
// and a signature that uses a different salt length. However,
|
|
||||||
// a salt length of 0 is defined as rsa.PSSSaltLengthAuto which
|
|
||||||
// works deterministically to auto-detect the length when
|
|
||||||
// verifying, so these tests actually pass as they should.
|
|
||||||
filesOverrideToPassZeroSLen := map[string][]int{
|
|
||||||
"rsa_pss_2048_sha1_mgf1_20_test.json": []int{46, 47},
|
|
||||||
"rsa_pss_2048_sha256_mgf1_0_test.json": []int{67, 68},
|
|
||||||
"rsa_pss_2048_sha256_mgf1_32_test.json": []int{67, 68},
|
|
||||||
"rsa_pss_3072_sha256_mgf1_32_test.json": []int{67, 68},
|
|
||||||
"rsa_pss_4096_sha256_mgf1_32_test.json": []int{67, 68},
|
|
||||||
"rsa_pss_4096_sha512_mgf1_32_test.json": []int{136, 137},
|
|
||||||
// "rsa_pss_misc_test.json": nil, // TODO: This ones seems to be broken right now, but can enable later on.
|
|
||||||
}
|
|
||||||
|
|
||||||
if !boringcryptoEnabled {
|
|
||||||
// boringcrypto doesn't support the truncated SHA-512 hashes, so only
|
|
||||||
// test them if boringcrypto isn't enabled.
|
|
||||||
filesOverrideToPassZeroSLen["rsa_pss_2048_sha512_256_mgf1_28_test.json"] = []int{13, 14, 15}
|
|
||||||
filesOverrideToPassZeroSLen["rsa_pss_2048_sha512_256_mgf1_32_test.json"] = []int{13, 14}
|
|
||||||
}
|
|
||||||
|
|
||||||
for f := range filesOverrideToPassZeroSLen {
|
|
||||||
var root Root
|
|
||||||
readTestVector(t, f, &root)
|
|
||||||
for _, tg := range root.TestGroups {
|
|
||||||
pub := decodePublicKey(tg.KeyDer).(*rsa.PublicKey)
|
|
||||||
ch := parseHash(tg.Sha)
|
|
||||||
h := ch.New()
|
|
||||||
opts := &rsa.PSSOptions{
|
|
||||||
Hash: ch,
|
|
||||||
SaltLength: rsa.PSSSaltLengthAuto,
|
|
||||||
}
|
|
||||||
// Run all the tests twice: the first time with the salt length
|
|
||||||
// as PSSSaltLengthAuto, and the second time with the salt length
|
|
||||||
// explicitly set to tg.SLen.
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
for _, sig := range tg.Tests {
|
|
||||||
h.Reset()
|
|
||||||
h.Write(decodeHex(sig.Msg))
|
|
||||||
hashed := h.Sum(nil)
|
|
||||||
err := rsa.VerifyPSS(pub, ch, hashed, decodeHex(sig.Sig), opts)
|
|
||||||
want := shouldPass(sig.Result, sig.Flags, flagsShouldPass)
|
|
||||||
if opts.SaltLength == 0 {
|
|
||||||
for _, id := range filesOverrideToPassZeroSLen[f] {
|
|
||||||
if sig.TcId == id {
|
|
||||||
want = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (err == nil) != want {
|
|
||||||
t.Errorf("file: %v, tcid: %d, type: %s, opts.SaltLength: %v, comment: %q, wanted success: %t", f, sig.TcId, sig.Result, opts.SaltLength, sig.Comment, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update opts.SaltLength for the second run of the tests.
|
|
||||||
opts.SaltLength = tg.SLen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
// Copyright 2019 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 wycheproof
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rsa"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRsa(t *testing.T) {
|
|
||||||
// KeyJwk Public key in JWK format
|
|
||||||
type KeyJwk struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notes a description of the labels used in the test vectors
|
|
||||||
type Notes struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignatureTestVector
|
|
||||||
type SignatureTestVector struct {
|
|
||||||
|
|
||||||
// A brief description of the test case
|
|
||||||
Comment string `json:"comment,omitempty"`
|
|
||||||
|
|
||||||
// A list of flags
|
|
||||||
Flags []string `json:"flags,omitempty"`
|
|
||||||
|
|
||||||
// The message to sign
|
|
||||||
Msg string `json:"msg,omitempty"`
|
|
||||||
|
|
||||||
// Test result
|
|
||||||
Result string `json:"result,omitempty"`
|
|
||||||
|
|
||||||
// A signature for msg
|
|
||||||
Sig string `json:"sig,omitempty"`
|
|
||||||
|
|
||||||
// Identifier of the test case
|
|
||||||
TcId int `json:"tcId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RsassaPkcs1TestGroup
|
|
||||||
type RsassaPkcs1TestGroup struct {
|
|
||||||
|
|
||||||
// The private exponent
|
|
||||||
D string `json:"d,omitempty"`
|
|
||||||
|
|
||||||
// The public exponent
|
|
||||||
E string `json:"e,omitempty"`
|
|
||||||
|
|
||||||
// ASN encoding of the sequence [n, e]
|
|
||||||
KeyAsn string `json:"keyAsn,omitempty"`
|
|
||||||
|
|
||||||
// ASN encoding of the public key
|
|
||||||
KeyDer string `json:"keyDer,omitempty"`
|
|
||||||
|
|
||||||
// Public key in JWK format
|
|
||||||
KeyJwk *KeyJwk `json:"keyJwk,omitempty"`
|
|
||||||
|
|
||||||
// Pem encoded public key
|
|
||||||
KeyPem string `json:"keyPem,omitempty"`
|
|
||||||
|
|
||||||
// the size of the modulus in bits
|
|
||||||
KeySize int `json:"keySize,omitempty"`
|
|
||||||
|
|
||||||
// The modulus of the key
|
|
||||||
N string `json:"n,omitempty"`
|
|
||||||
|
|
||||||
// the hash function used for the message
|
|
||||||
Sha string `json:"sha,omitempty"`
|
|
||||||
Tests []*SignatureTestVector `json:"tests,omitempty"`
|
|
||||||
Type interface{} `json:"type,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root
|
|
||||||
type Root struct {
|
|
||||||
|
|
||||||
// the primitive tested in the test file
|
|
||||||
Algorithm string `json:"algorithm,omitempty"`
|
|
||||||
|
|
||||||
// the version of the test vectors.
|
|
||||||
GeneratorVersion string `json:"generatorVersion,omitempty"`
|
|
||||||
|
|
||||||
// additional documentation
|
|
||||||
Header []string `json:"header,omitempty"`
|
|
||||||
|
|
||||||
// a description of the labels used in the test vectors
|
|
||||||
Notes *Notes `json:"notes,omitempty"`
|
|
||||||
|
|
||||||
// the number of test vectors in this test
|
|
||||||
NumberOfTests int `json:"numberOfTests,omitempty"`
|
|
||||||
Schema interface{} `json:"schema,omitempty"`
|
|
||||||
TestGroups []*RsassaPkcs1TestGroup `json:"testGroups,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
flagsShouldPass := map[string]bool{
|
|
||||||
// Omitting the parameter field in an ASN encoded integer is a legacy behavior.
|
|
||||||
"MissingNull": false,
|
|
||||||
// Keys with a modulus less than 2048 bits are supported by crypto/rsa.
|
|
||||||
"SmallModulus": true,
|
|
||||||
// Small public keys are supported by crypto/rsa.
|
|
||||||
"SmallPublicKey": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
var root Root
|
|
||||||
readTestVector(t, "rsa_signature_test.json", &root)
|
|
||||||
for _, tg := range root.TestGroups {
|
|
||||||
pub := decodePublicKey(tg.KeyDer).(*rsa.PublicKey)
|
|
||||||
ch := parseHash(tg.Sha)
|
|
||||||
h := ch.New()
|
|
||||||
for _, sig := range tg.Tests {
|
|
||||||
h.Reset()
|
|
||||||
h.Write(decodeHex(sig.Msg))
|
|
||||||
hashed := h.Sum(nil)
|
|
||||||
err := rsa.VerifyPKCS1v15(pub, ch, hashed, decodeHex(sig.Sig))
|
|
||||||
want := shouldPass(sig.Result, sig.Flags, flagsShouldPass)
|
|
||||||
if (err == nil) != want {
|
|
||||||
t.Errorf("tcid: %d, type: %s, comment: %q, wanted success: %t", sig.TcId, sig.Result, sig.Comment, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
// Copyright 2019 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 wycheproof runs a set of the Wycheproof tests
|
|
||||||
// provided by https://github.com/google/wycheproof.
|
|
||||||
package wycheproof
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
_ "crypto/sha1"
|
|
||||||
_ "crypto/sha256"
|
|
||||||
_ "crypto/sha512"
|
|
||||||
)
|
|
||||||
|
|
||||||
const wycheproofModVer = "v0.0.0-20191219022705-2196000605e4"
|
|
||||||
|
|
||||||
var wycheproofTestVectorsDir string
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
flag.Parse()
|
|
||||||
if flag.Lookup("test.short").Value.(flag.Getter).Get().(bool) {
|
|
||||||
log.Println("skipping test that downloads testdata via 'go mod download' in short mode")
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
if _, err := exec.LookPath("go"); err != nil {
|
|
||||||
log.Printf("skipping test because 'go' command is unavailable: %v", err)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
if os.Getenv("GO_BUILDER_FLAKY_NET") != "" {
|
|
||||||
log.Printf("skipping test because GO_BUILDER_FLAKY_NET is set")
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download the JSON test files from github.com/google/wycheproof
|
|
||||||
// using `go mod download -json` so the cached source of the testdata
|
|
||||||
// can be used in the following tests.
|
|
||||||
path := "github.com/google/wycheproof@" + wycheproofModVer
|
|
||||||
cmd := exec.Command("go", "mod", "download", "-json", path)
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to run `go mod download -json %s`, output: %s", path, output)
|
|
||||||
}
|
|
||||||
var dm struct {
|
|
||||||
Dir string // absolute path to cached source root directory
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(output, &dm); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
// Now that the module has been downloaded, use the absolute path of the
|
|
||||||
// cached source as the root directory for all tests going forward.
|
|
||||||
wycheproofTestVectorsDir = filepath.Join(dm.Dir, "testvectors")
|
|
||||||
os.Exit(m.Run())
|
|
||||||
}
|
|
||||||
|
|
||||||
func readTestVector(t *testing.T, f string, dest interface{}) {
|
|
||||||
b, err := os.ReadFile(filepath.Join(wycheproofTestVectorsDir, f))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read json file: %v", err)
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(b, &dest); err != nil {
|
|
||||||
t.Fatalf("failed to unmarshal json file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeHex(s string) []byte {
|
|
||||||
b, err := hex.DecodeString(s)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodePublicKey(der string) interface{} {
|
|
||||||
d := decodeHex(der)
|
|
||||||
pub, err := x509.ParsePKIXPublicKey(d)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("failed to parse DER encoded public key: %v", err))
|
|
||||||
}
|
|
||||||
return pub
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseHash(h string) crypto.Hash {
|
|
||||||
switch h {
|
|
||||||
case "SHA-1":
|
|
||||||
return crypto.SHA1
|
|
||||||
case "SHA-256":
|
|
||||||
return crypto.SHA256
|
|
||||||
case "SHA-224":
|
|
||||||
return crypto.SHA224
|
|
||||||
case "SHA-384":
|
|
||||||
return crypto.SHA384
|
|
||||||
case "SHA-512":
|
|
||||||
return crypto.SHA512
|
|
||||||
case "SHA-512/224":
|
|
||||||
return crypto.SHA512_224
|
|
||||||
case "SHA-512/256":
|
|
||||||
return crypto.SHA512_256
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("could not identify SHA hash algorithm: %q", h))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// shouldPass returns whether or not the test should pass.
|
|
||||||
// flagsShouldPass is a map associated with whether or not
|
|
||||||
// a flag for an "acceptable" result should pass.
|
|
||||||
// Every possible flag value that's associated with an
|
|
||||||
// "acceptable" result should be explicitly specified,
|
|
||||||
// otherwise the test will panic.
|
|
||||||
func shouldPass(result string, flags []string, flagsShouldPass map[string]bool) bool {
|
|
||||||
switch result {
|
|
||||||
case "valid":
|
|
||||||
return true
|
|
||||||
case "invalid":
|
|
||||||
return false
|
|
||||||
case "acceptable":
|
|
||||||
for _, flag := range flags {
|
|
||||||
pass, ok := flagsShouldPass[flag]
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Sprintf("unspecified flag: %q", flag))
|
|
||||||
}
|
|
||||||
if !pass {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true // There are no flags, or all are meant to pass.
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unexpected result: %v", result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+12
-5
@@ -85,7 +85,8 @@ type certID struct {
|
|||||||
|
|
||||||
// https://tools.ietf.org/html/rfc2560#section-4.1.1
|
// https://tools.ietf.org/html/rfc2560#section-4.1.1
|
||||||
type ocspRequest struct {
|
type ocspRequest struct {
|
||||||
TBSRequest tbsRequest
|
TBSRequest tbsRequest
|
||||||
|
OptionalSignature asn1.RawValue `asn1:"explicit,tag:0,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type tbsRequest struct {
|
type tbsRequest struct {
|
||||||
@@ -321,10 +322,10 @@ type Request struct {
|
|||||||
func (req *Request) Marshal() ([]byte, error) {
|
func (req *Request) Marshal() ([]byte, error) {
|
||||||
hashAlg := getOIDFromHashAlgorithm(req.HashAlgorithm)
|
hashAlg := getOIDFromHashAlgorithm(req.HashAlgorithm)
|
||||||
if hashAlg == nil {
|
if hashAlg == nil {
|
||||||
return nil, errors.New("Unknown hash algorithm")
|
return nil, errors.New("unknown hash algorithm")
|
||||||
}
|
}
|
||||||
return asn1.Marshal(ocspRequest{
|
return asn1.Marshal(ocspRequest{
|
||||||
tbsRequest{
|
TBSRequest: tbsRequest{
|
||||||
Version: 0,
|
Version: 0,
|
||||||
RequestList: []request{
|
RequestList: []request{
|
||||||
{
|
{
|
||||||
@@ -418,8 +419,10 @@ func (p ParseError) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ParseRequest parses an OCSP request in DER form. It only supports
|
// ParseRequest parses an OCSP request in DER form. It only supports
|
||||||
// requests for a single certificate. Signed requests are not supported.
|
// requests for a single certificate identifier. If a request includes
|
||||||
// If a request includes a signature, it will result in a ParseError.
|
// multiple certificate identifiers, only the first will be included in
|
||||||
|
// the parsed Request. Signed requests are not supported. If a request
|
||||||
|
// includes a signature, it will result in a ParseError.
|
||||||
func ParseRequest(bytes []byte) (*Request, error) {
|
func ParseRequest(bytes []byte) (*Request, error) {
|
||||||
var req ocspRequest
|
var req ocspRequest
|
||||||
rest, err := asn1.Unmarshal(bytes, &req)
|
rest, err := asn1.Unmarshal(bytes, &req)
|
||||||
@@ -430,6 +433,10 @@ func ParseRequest(bytes []byte) (*Request, error) {
|
|||||||
return nil, ParseError("trailing data in OCSP request")
|
return nil, ParseError("trailing data in OCSP request")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(req.OptionalSignature.FullBytes) > 0 {
|
||||||
|
return nil, ParseError("signed OCSP requests are not supported")
|
||||||
|
}
|
||||||
|
|
||||||
if len(req.TBSRequest.RequestList) == 0 {
|
if len(req.TBSRequest.RequestList) == 0 {
|
||||||
return nil, ParseError("OCSP request contains no request body")
|
return nil, ParseError("OCSP request contains no request body")
|
||||||
}
|
}
|
||||||
|
|||||||
+127
-1
@@ -15,10 +15,15 @@ import (
|
|||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
"math/big"
|
"math/big"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/cryptobyte"
|
||||||
|
cbasn1 "golang.org/x/crypto/cryptobyte/asn1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestOCSPDecode(t *testing.T) {
|
func TestOCSPDecode(t *testing.T) {
|
||||||
@@ -190,7 +195,7 @@ func TestOCSPRequest(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytes.Compare(expectedBytes, marshaledRequest) != 0 {
|
if !bytes.Equal(expectedBytes, marshaledRequest) {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"Marshaled request doesn't match expected: wanted %x, got %x",
|
"Marshaled request doesn't match expected: wanted %x, got %x",
|
||||||
expectedBytes,
|
expectedBytes,
|
||||||
@@ -199,6 +204,127 @@ func TestOCSPRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOCSPRequestErrors(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []byte
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "signed request",
|
||||||
|
input: testOCSPRequest(1, true),
|
||||||
|
wantErr: "signed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "trailing data",
|
||||||
|
input: append(testOCSPRequest(1, false), 0x00),
|
||||||
|
wantErr: "trailing",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
_, err := ParseRequest(tc.input)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf(
|
||||||
|
"ParseRequest succeeded, want ParseError containing %q",
|
||||||
|
tc.wantErr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var parseError ParseError
|
||||||
|
if !errors.As(err, &parseError) {
|
||||||
|
t.Fatalf("ParseRequest returned %T, want ParseError", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(parseError.Error(), tc.wantErr) {
|
||||||
|
t.Fatalf(
|
||||||
|
"ParseRequest error = %q, want it to contain %q",
|
||||||
|
parseError.Error(), tc.wantErr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOCSPMultipleCertRequest(t *testing.T) {
|
||||||
|
req, err := ParseRequest(testOCSPRequest(2, false))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseRequest errored for multiple request input: %q", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testOCSPRequest uses the request cert counter as the issuerNameHash,
|
||||||
|
// issuerKeyHash and serial. We expect the first request cert to be used
|
||||||
|
// (counter=1), not the second (counter=2).
|
||||||
|
wantHash := []byte{0x01}
|
||||||
|
if actual := req.IssuerNameHash; !bytes.Equal(actual, wantHash) {
|
||||||
|
t.Errorf(
|
||||||
|
"ParseRequest.IssuerNameHash returned = %q, wanted %q",
|
||||||
|
actual, wantHash,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if actual := req.IssuerKeyHash; !bytes.Equal(actual, wantHash) {
|
||||||
|
t.Errorf("ParseRequest.IssuerKeyHash returned = %q, wanted %q",
|
||||||
|
actual, wantHash)
|
||||||
|
}
|
||||||
|
wantSerial := big.NewInt(0x01)
|
||||||
|
if actual := req.SerialNumber; actual.Cmp(wantSerial) != 0 {
|
||||||
|
t.Errorf("ParseRequest.SerialNumber returned = %q, wanted %q",
|
||||||
|
actual, wantHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testOCSPRequest constructs an unsupported OCSP request in DER form for testing.
|
||||||
|
//
|
||||||
|
// We build these by hand using cryptobyte to specifically target shapes that are
|
||||||
|
// not supported by ParseRequest, and can't be created with CreateRequest.
|
||||||
|
//
|
||||||
|
// The resulting requestList will contain numCerts RequestList entries. If
|
||||||
|
// includeSignature is true, the request will carry a signature field (with an
|
||||||
|
// arbitrary signatureAlgorithm and signature).
|
||||||
|
//
|
||||||
|
// See RFC 6960, §4.1.1.
|
||||||
|
func testOCSPRequest(numCerts int, includeSignature bool) []byte {
|
||||||
|
sha1AlgID, err := asn1.Marshal(pkix.AlgorithmIdentifier{
|
||||||
|
Algorithm: asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26},
|
||||||
|
Parameters: asn1.RawValue{Tag: 5 /* ASN.1 NULL */},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var b cryptobyte.Builder
|
||||||
|
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // OCSPRequest
|
||||||
|
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // tbsRequest
|
||||||
|
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // requestList
|
||||||
|
for i := 0; i < numCerts; i++ {
|
||||||
|
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // Request
|
||||||
|
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // CertID
|
||||||
|
b.AddBytes(sha1AlgID) // hashAlgorithm
|
||||||
|
b.AddASN1OctetString([]byte{byte(i + 1)}) // issuerNameHash
|
||||||
|
b.AddASN1OctetString([]byte{byte(i + 1)}) // issuerKeyHash
|
||||||
|
b.AddASN1Int64(int64(i + 1)) // serialNumber
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if includeSignature {
|
||||||
|
// optionalSignature [0] EXPLICIT Signature
|
||||||
|
b.AddASN1(cbasn1.Tag(0).ContextSpecific().Constructed(), func(b *cryptobyte.Builder) {
|
||||||
|
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // Signature
|
||||||
|
b.AddBytes(sha1AlgID) // signatureAlgorithm
|
||||||
|
b.AddASN1BitString([]byte{0x09, 0x0A, 0x0B, 0x0C}) // signature
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
der, err := b.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return der
|
||||||
|
}
|
||||||
|
|
||||||
func TestOCSPResponse(t *testing.T) {
|
func TestOCSPResponse(t *testing.T) {
|
||||||
leafCert, _ := hex.DecodeString(leafCertHex)
|
leafCert, _ := hex.DecodeString(leafCertHex)
|
||||||
leaf, err := x509.ParseCertificate(leafCert)
|
leaf, err := x509.ParseCertificate(leafCert)
|
||||||
|
|||||||
@@ -5,11 +5,12 @@
|
|||||||
// Package armor implements OpenPGP ASCII Armor, see RFC 4880. OpenPGP Armor is
|
// Package armor implements OpenPGP ASCII Armor, see RFC 4880. OpenPGP Armor is
|
||||||
// very similar to PEM except that it has an additional CRC checksum.
|
// very similar to PEM except that it has an additional CRC checksum.
|
||||||
//
|
//
|
||||||
// Deprecated: this package is unmaintained except for security fixes. New
|
// Deprecated: this package is unsafe by design, and has numerous known security
|
||||||
// applications should consider a more focused, modern alternative to OpenPGP
|
// issues. It is not maintained, and should not be used. New applications should
|
||||||
// for their specific task. If you are required to interoperate with OpenPGP
|
// consider a more focused, modern alternative to OpenPGP for their specific
|
||||||
// systems and need a maintained package, consider a community fork.
|
// task. If you are required to interoperate with OpenPGP systems and need a
|
||||||
// See https://golang.org/issue/44226.
|
// maintained package, consider github.com/ProtonMail/go-crypto/openpgp, which
|
||||||
|
// is a maintained fork that aims to be a drop-in replacement for this package.
|
||||||
package armor
|
package armor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -8,11 +8,12 @@
|
|||||||
// Clearsigned messages are cryptographically signed, but the contents of the
|
// Clearsigned messages are cryptographically signed, but the contents of the
|
||||||
// message are kept in plaintext so that it can be read without special tools.
|
// message are kept in plaintext so that it can be read without special tools.
|
||||||
//
|
//
|
||||||
// Deprecated: this package is unmaintained except for security fixes. New
|
// Deprecated: this package is unsafe by design, and has numerous known security
|
||||||
// applications should consider a more focused, modern alternative to OpenPGP
|
// issues. It is not maintained, and should not be used. New applications should
|
||||||
// for their specific task. If you are required to interoperate with OpenPGP
|
// consider a more focused, modern alternative to OpenPGP for their specific
|
||||||
// systems and need a maintained package, consider a community fork.
|
// task. If you are required to interoperate with OpenPGP systems and need a
|
||||||
// See https://golang.org/issue/44226.
|
// maintained package, consider github.com/ProtonMail/go-crypto/openpgp, which
|
||||||
|
// is a maintained fork that aims to be a drop-in replacement for this package.
|
||||||
package clearsign
|
package clearsign
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
|
|
||||||
// Package errors contains common error types for the OpenPGP packages.
|
// Package errors contains common error types for the OpenPGP packages.
|
||||||
//
|
//
|
||||||
// Deprecated: this package is unmaintained except for security fixes. New
|
// Deprecated: this package is unsafe by design, and has numerous known security
|
||||||
// applications should consider a more focused, modern alternative to OpenPGP
|
// issues. It is not maintained, and should not be used. New applications should
|
||||||
// for their specific task. If you are required to interoperate with OpenPGP
|
// consider a more focused, modern alternative to OpenPGP for their specific
|
||||||
// systems and need a maintained package, consider a community fork.
|
// task. If you are required to interoperate with OpenPGP systems and need a
|
||||||
// See https://golang.org/issue/44226.
|
// maintained package, consider github.com/ProtonMail/go-crypto/openpgp, which
|
||||||
|
// is a maintained fork that aims to be a drop-in replacement for this package.
|
||||||
package errors
|
package errors
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -5,11 +5,12 @@
|
|||||||
// Package packet implements parsing and serialization of OpenPGP packets, as
|
// Package packet implements parsing and serialization of OpenPGP packets, as
|
||||||
// specified in RFC 4880.
|
// specified in RFC 4880.
|
||||||
//
|
//
|
||||||
// Deprecated: this package is unmaintained except for security fixes. New
|
// Deprecated: this package is unsafe by design, and has numerous known security
|
||||||
// applications should consider a more focused, modern alternative to OpenPGP
|
// issues. It is not maintained, and should not be used. New applications should
|
||||||
// for their specific task. If you are required to interoperate with OpenPGP
|
// consider a more focused, modern alternative to OpenPGP for their specific
|
||||||
// systems and need a maintained package, consider a community fork.
|
// task. If you are required to interoperate with OpenPGP systems and need a
|
||||||
// See https://golang.org/issue/44226.
|
// maintained package, consider github.com/ProtonMail/go-crypto/openpgp, which
|
||||||
|
// is a maintained fork that aims to be a drop-in replacement for this package.
|
||||||
package packet
|
package packet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
+6
-5
@@ -4,11 +4,12 @@
|
|||||||
|
|
||||||
// Package openpgp implements high level operations on OpenPGP messages.
|
// Package openpgp implements high level operations on OpenPGP messages.
|
||||||
//
|
//
|
||||||
// Deprecated: this package is unmaintained except for security fixes. New
|
// Deprecated: this package is unsafe by design, and has numerous known security
|
||||||
// applications should consider a more focused, modern alternative to OpenPGP
|
// issues. It is not maintained, and should not be used. New applications should
|
||||||
// for their specific task. If you are required to interoperate with OpenPGP
|
// consider a more focused, modern alternative to OpenPGP for their specific
|
||||||
// systems and need a maintained package, consider a community fork.
|
// task. If you are required to interoperate with OpenPGP systems and need a
|
||||||
// See https://golang.org/issue/44226.
|
// maintained package, consider github.com/ProtonMail/go-crypto/openpgp, which
|
||||||
|
// is a maintained fork that aims to be a drop-in replacement for this package.
|
||||||
package openpgp
|
package openpgp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
+6
-5
@@ -5,11 +5,12 @@
|
|||||||
// Package s2k implements the various OpenPGP string-to-key transforms as
|
// Package s2k implements the various OpenPGP string-to-key transforms as
|
||||||
// specified in RFC 4800 section 3.7.1.
|
// specified in RFC 4800 section 3.7.1.
|
||||||
//
|
//
|
||||||
// Deprecated: this package is unmaintained except for security fixes. New
|
// Deprecated: this package is unsafe by design, and has numerous known security
|
||||||
// applications should consider a more focused, modern alternative to OpenPGP
|
// issues. It is not maintained, and should not be used. New applications should
|
||||||
// for their specific task. If you are required to interoperate with OpenPGP
|
// consider a more focused, modern alternative to OpenPGP for their specific
|
||||||
// systems and need a maintained package, consider a community fork.
|
// task. If you are required to interoperate with OpenPGP systems and need a
|
||||||
// See https://golang.org/issue/44226.
|
// maintained package, consider github.com/ProtonMail/go-crypto/openpgp, which
|
||||||
|
// is a maintained fork that aims to be a drop-in replacement for this package.
|
||||||
package s2k
|
package s2k
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
+13
-60
@@ -2,24 +2,17 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
/*
|
// Package pbkdf2 implements the key derivation function PBKDF2 as defined in
|
||||||
Package pbkdf2 implements the key derivation function PBKDF2 as defined in RFC
|
// RFC 8018 (PKCS #5 v2.1).
|
||||||
2898 / PKCS #5 v2.0.
|
//
|
||||||
|
// This package is a wrapper for the PBKDF2 implementation in the
|
||||||
A key derivation function is useful when encrypting data based on a password
|
// [crypto/pbkdf2] package. It is [frozen] and is not accepting new features.
|
||||||
or any other not-fully-random data. It uses a pseudorandom function to derive
|
//
|
||||||
a secure encryption key based on the password.
|
// [frozen]: https://go.dev/wiki/Frozen
|
||||||
|
|
||||||
While v2.0 of the standard defines only one pseudorandom function to use,
|
|
||||||
HMAC-SHA1, the drafted v2.1 specification allows use of all five FIPS Approved
|
|
||||||
Hash Functions SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512 for HMAC. To
|
|
||||||
choose, you can pass the `New` functions from the different SHA packages to
|
|
||||||
pbkdf2.Key.
|
|
||||||
*/
|
|
||||||
package pbkdf2
|
package pbkdf2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/hmac"
|
"crypto/pbkdf2"
|
||||||
"hash"
|
"hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,51 +20,11 @@ import (
|
|||||||
// []byte of length keylen that can be used as cryptographic key. The key is
|
// []byte of length keylen that can be used as cryptographic key. The key is
|
||||||
// derived based on the method described as PBKDF2 with the HMAC variant using
|
// derived based on the method described as PBKDF2 with the HMAC variant using
|
||||||
// the supplied hash function.
|
// the supplied hash function.
|
||||||
//
|
|
||||||
// For example, to use a HMAC-SHA-1 based PBKDF2 key derivation function, you
|
|
||||||
// can get a derived key for e.g. AES-256 (which needs a 32-byte key) by
|
|
||||||
// doing:
|
|
||||||
//
|
|
||||||
// dk := pbkdf2.Key([]byte("some password"), salt, 4096, 32, sha1.New)
|
|
||||||
//
|
|
||||||
// Remember to get a good random salt. At least 8 bytes is recommended by the
|
|
||||||
// RFC.
|
|
||||||
//
|
|
||||||
// Using a higher iteration count will increase the cost of an exhaustive
|
|
||||||
// search but will also make derivation proportionally slower.
|
|
||||||
func Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte {
|
func Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte {
|
||||||
prf := hmac.New(h, password)
|
out, err := pbkdf2.Key(h, string(password), salt, iter, keyLen)
|
||||||
hashLen := prf.Size()
|
if err != nil {
|
||||||
numBlocks := (keyLen + hashLen - 1) / hashLen
|
// FIPS 140 enforcement, or an invalid key length.
|
||||||
|
panic(err)
|
||||||
var buf [4]byte
|
|
||||||
dk := make([]byte, 0, numBlocks*hashLen)
|
|
||||||
U := make([]byte, hashLen)
|
|
||||||
for block := 1; block <= numBlocks; block++ {
|
|
||||||
// N.B.: || means concatenation, ^ means XOR
|
|
||||||
// for each block T_i = U_1 ^ U_2 ^ ... ^ U_iter
|
|
||||||
// U_1 = PRF(password, salt || uint(i))
|
|
||||||
prf.Reset()
|
|
||||||
prf.Write(salt)
|
|
||||||
buf[0] = byte(block >> 24)
|
|
||||||
buf[1] = byte(block >> 16)
|
|
||||||
buf[2] = byte(block >> 8)
|
|
||||||
buf[3] = byte(block)
|
|
||||||
prf.Write(buf[:4])
|
|
||||||
dk = prf.Sum(dk)
|
|
||||||
T := dk[len(dk)-hashLen:]
|
|
||||||
copy(U, T)
|
|
||||||
|
|
||||||
// U_n = PRF(password, U_(n-1))
|
|
||||||
for n := 2; n <= iter; n++ {
|
|
||||||
prf.Reset()
|
|
||||||
prf.Write(U)
|
|
||||||
U = U[:0]
|
|
||||||
U = prf.Sum(U)
|
|
||||||
for x := range U {
|
|
||||||
T[x] ^= U[x]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return dk[:keyLen]
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ func pbDecrypterFor(algorithm pkix.AlgorithmIdentifier, password []byte) (cipher
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if params.Iterations < 0 || params.Iterations > maxIterations {
|
||||||
|
return nil, 0, NotImplementedError("iteration count is invalid or too high")
|
||||||
|
}
|
||||||
|
|
||||||
key := cipherType.deriveKey(params.Salt, password, params.Iterations)
|
key := cipherType.deriveKey(params.Salt, password, params.Iterations)
|
||||||
iv := cipherType.deriveIV(params.Salt, password, params.Iterations)
|
iv := cipherType.deriveIV(params.Salt, password, params.Iterations)
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,46 @@ import (
|
|||||||
|
|
||||||
var sha1WithTripleDES = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1, 12, 1, 3})
|
var sha1WithTripleDES = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1, 12, 1, 3})
|
||||||
|
|
||||||
|
func TestPbDecrypterForIterationLimit(t *testing.T) {
|
||||||
|
pass, _ := bmpString("Sesame open")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
iterations int
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"at limit", maxIterations, false},
|
||||||
|
{"over limit", maxIterations + 1, true},
|
||||||
|
{"negative", -1, true},
|
||||||
|
{"max int", 1<<31 - 1, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
params, _ := asn1.Marshal(pbeParams{
|
||||||
|
Salt: []byte{1, 2, 3, 4, 5, 6, 7, 8},
|
||||||
|
Iterations: tt.iterations,
|
||||||
|
})
|
||||||
|
alg := pkix.AlgorithmIdentifier{
|
||||||
|
Algorithm: sha1WithTripleDES,
|
||||||
|
Parameters: asn1.RawValue{
|
||||||
|
FullBytes: params,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, _, err := pbDecrypterFor(alg, pass)
|
||||||
|
if tt.wantErr {
|
||||||
|
if _, ok := err.(NotImplementedError); !ok {
|
||||||
|
t.Errorf("iterations=%d: got %v, want NotImplementedError", tt.iterations, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("iterations=%d: unexpected error: %v", tt.iterations, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPbDecrypterFor(t *testing.T) {
|
func TestPbDecrypterFor(t *testing.T) {
|
||||||
params, _ := asn1.Marshal(pbeParams{
|
params, _ := asn1.Marshal(pbeParams{
|
||||||
Salt: []byte{1, 2, 3, 4, 5, 6, 7, 8},
|
Salt: []byte{1, 2, 3, 4, 5, 6, 7, 8},
|
||||||
|
|||||||
@@ -27,11 +27,19 @@ var (
|
|||||||
oidSHA1 = asn1.ObjectIdentifier([]int{1, 3, 14, 3, 2, 26})
|
oidSHA1 = asn1.ObjectIdentifier([]int{1, 3, 14, 3, 2, 26})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// maxIterations is a safety limit to prevent CPU exhaustion from
|
||||||
|
// crafted PKCS#12 files with unreasonable iteration counts.
|
||||||
|
const maxIterations = 1 << 20 // ~1 million
|
||||||
|
|
||||||
func verifyMac(macData *macData, message, password []byte) error {
|
func verifyMac(macData *macData, message, password []byte) error {
|
||||||
if !macData.Mac.Algorithm.Algorithm.Equal(oidSHA1) {
|
if !macData.Mac.Algorithm.Algorithm.Equal(oidSHA1) {
|
||||||
return NotImplementedError("unknown digest algorithm: " + macData.Mac.Algorithm.Algorithm.String())
|
return NotImplementedError("unknown digest algorithm: " + macData.Mac.Algorithm.Algorithm.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if macData.Iterations < 0 || macData.Iterations > maxIterations {
|
||||||
|
return NotImplementedError("iteration count is invalid or too high")
|
||||||
|
}
|
||||||
|
|
||||||
key := pbkdf(sha1Sum, 20, 64, macData.MacSalt, password, macData.Iterations, 3, 20)
|
key := pbkdf(sha1Sum, 20, 64, macData.MacSalt, password, macData.Iterations, 3, 20)
|
||||||
|
|
||||||
mac := hmac.New(sha1.New, key)
|
mac := hmac.New(sha1.New, key)
|
||||||
|
|||||||
@@ -5,10 +5,52 @@
|
|||||||
package pkcs12
|
package pkcs12
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/x509/pkix"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestVerifyMacIterationLimit(t *testing.T) {
|
||||||
|
password, _ := bmpString("Sesame open")
|
||||||
|
message := []byte{11, 12, 13, 14, 15}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
iterations int
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"at limit", maxIterations, false},
|
||||||
|
{"over limit", maxIterations + 1, true},
|
||||||
|
{"negative", -1, true},
|
||||||
|
{"max int", 1<<31 - 1, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
td := macData{
|
||||||
|
Mac: digestInfo{
|
||||||
|
Algorithm: pkix.AlgorithmIdentifier{
|
||||||
|
Algorithm: oidSHA1,
|
||||||
|
},
|
||||||
|
Digest: nil, // will fail MAC check, but iteration check comes first
|
||||||
|
},
|
||||||
|
MacSalt: []byte{1, 2, 3, 4, 5, 6, 7, 8},
|
||||||
|
Iterations: tt.iterations,
|
||||||
|
}
|
||||||
|
err := verifyMac(&td, message, password)
|
||||||
|
if tt.wantErr {
|
||||||
|
if _, ok := err.(NotImplementedError); !ok {
|
||||||
|
t.Errorf("iterations=%d: got %v, want NotImplementedError", tt.iterations, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, ok := err.(NotImplementedError); ok {
|
||||||
|
t.Errorf("iterations=%d: got unexpected NotImplementedError", tt.iterations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestVerifyMac(t *testing.T) {
|
func TestVerifyMac(t *testing.T) {
|
||||||
td := macData{
|
td := macData{
|
||||||
Mac: digestInfo{
|
Mac: digestInfo{
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2026 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build loong64 && !purego && gc
|
||||||
|
|
||||||
|
package salsa
|
||||||
|
|
||||||
|
import "golang.org/x/sys/cpu"
|
||||||
|
|
||||||
|
// xorKeyStreamVX is implemented in salsa20_loong64.s.
|
||||||
|
//
|
||||||
|
//go:noescape
|
||||||
|
func xorKeyStreamVX(out, in *byte, n uint64, nonce, key *byte)
|
||||||
|
|
||||||
|
// XORKeyStream crypts bytes from in to out using the given key and counters.
|
||||||
|
// In and out must overlap entirely or not at all. Counter
|
||||||
|
// contains the raw salsa20 counter bytes (both nonce and block counter).
|
||||||
|
func XORKeyStream(out, in []byte, counter *[16]byte, key *[32]byte) {
|
||||||
|
if len(in) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = out[len(in)-1]
|
||||||
|
if cpu.Loong64.HasLSX {
|
||||||
|
xorKeyStreamVX(&out[0], &in[0], uint64(len(in)), &counter[0], &key[0])
|
||||||
|
} else {
|
||||||
|
genericXORKeyStream(out, in, counter, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,454 @@
|
|||||||
|
// Copyright 2026 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build !purego && gc
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
DATA ·constants+0x00(SB)/4, $0x61707865
|
||||||
|
DATA ·constants+0x04(SB)/4, $0x3320646e
|
||||||
|
DATA ·constants+0x08(SB)/4, $0x79622d32
|
||||||
|
DATA ·constants+0x0c(SB)/4, $0x6b206574
|
||||||
|
GLOBL ·constants(SB), NOPTR|RODATA, $32
|
||||||
|
|
||||||
|
#define NUM_ROUNDS 10
|
||||||
|
|
||||||
|
// func xorKeyStreamVX(out, in *byte, n uint64, counter, key *byte)
|
||||||
|
TEXT ·xorKeyStreamVX(SB), $0-40
|
||||||
|
MOVV out+0(FP), R4
|
||||||
|
MOVV in+8(FP), R5
|
||||||
|
MOVV n+16(FP), R6
|
||||||
|
MOVV counter+24(FP), R7
|
||||||
|
MOVV key+32(FP), R8
|
||||||
|
MOVV $·constants(SB), R10
|
||||||
|
|
||||||
|
BGE R0, R6, ret
|
||||||
|
|
||||||
|
MOVV 8(R7), R9 // counter[8:16]
|
||||||
|
|
||||||
|
loop256:
|
||||||
|
MOVV $NUM_ROUNDS, R15
|
||||||
|
VXORV V30, V30, V30 // V30 = 0
|
||||||
|
|
||||||
|
// load contants
|
||||||
|
VMOVQ (R10), V0.W4
|
||||||
|
VMOVQ 4(R10), V5.W4
|
||||||
|
VMOVQ 8(R10), V10.W4
|
||||||
|
VMOVQ 12(R10), V15.W4
|
||||||
|
|
||||||
|
// load keys
|
||||||
|
VMOVQ (R8), V1.W4
|
||||||
|
VMOVQ 4(R8), V2.W4
|
||||||
|
VMOVQ 8(R8), V3.W4
|
||||||
|
VMOVQ 12(R8), V4.W4
|
||||||
|
VMOVQ 16(R8), V11.W4
|
||||||
|
VMOVQ 20(R8), V12.W4
|
||||||
|
VMOVQ 24(R8), V13.W4
|
||||||
|
VMOVQ 28(R8), V14.W4
|
||||||
|
|
||||||
|
// load and update counter
|
||||||
|
VMOVQ (R7), V6.W4
|
||||||
|
VMOVQ 4(R7), V7.W4
|
||||||
|
|
||||||
|
ADDV $1, R9, R11
|
||||||
|
ADDV $2, R9, R12
|
||||||
|
ADDV $3, R9, R13
|
||||||
|
VMOVQ R9, V8.W[0]
|
||||||
|
VMOVQ R11, V8.W[1]
|
||||||
|
VMOVQ R12, V8.W[2]
|
||||||
|
VMOVQ R13, V8.W[3]
|
||||||
|
SRLV $32, R9, R14
|
||||||
|
SRLV $32, R11, R11
|
||||||
|
SRLV $32, R12, R12
|
||||||
|
SRLV $32, R13, R13
|
||||||
|
VMOVQ R14, V9.W[0]
|
||||||
|
VMOVQ R11, V9.W[1]
|
||||||
|
VMOVQ R12, V9.W[2]
|
||||||
|
VMOVQ R13, V9.W[3]
|
||||||
|
|
||||||
|
// backup V8 and V9
|
||||||
|
VADDV V8, V30, V24 // V21 = V8
|
||||||
|
VADDV V9, V30, V25 // V22 = V9
|
||||||
|
|
||||||
|
salsa20_256:
|
||||||
|
VADDW V0, V12, V26
|
||||||
|
VADDW V5, V1, V27
|
||||||
|
VADDW V10, V6, V28
|
||||||
|
VADDW V15, V11, V29
|
||||||
|
VROTRW $25, V26, V26
|
||||||
|
VROTRW $25, V27, V27
|
||||||
|
VROTRW $25, V28, V28
|
||||||
|
VROTRW $25, V29, V29
|
||||||
|
VXORV V4, V26, V4
|
||||||
|
VXORV V9, V27, V9
|
||||||
|
VXORV V14, V28, V14
|
||||||
|
VXORV V3, V29, V3
|
||||||
|
VADDW V4, V0, V26
|
||||||
|
VADDW V9, V5, V27
|
||||||
|
VADDW V14, V10, V28
|
||||||
|
VADDW V3, V15, V29
|
||||||
|
VROTRW $23, V26, V26
|
||||||
|
VROTRW $23, V27, V27
|
||||||
|
VROTRW $23, V28, V28
|
||||||
|
VROTRW $23, V29, V29
|
||||||
|
VXORV V8, V26, V8
|
||||||
|
VXORV V13, V27, V13
|
||||||
|
VXORV V2, V28, V2
|
||||||
|
VXORV V7, V29, V7
|
||||||
|
VADDW V8, V4, V26
|
||||||
|
VADDW V13, V9, V27
|
||||||
|
VADDW V2, V14, V28
|
||||||
|
VADDW V7, V3, V29
|
||||||
|
VROTRW $19, V26, V26
|
||||||
|
VROTRW $19, V27, V27
|
||||||
|
VROTRW $19, V28, V28
|
||||||
|
VROTRW $19, V29, V29
|
||||||
|
VXORV V12, V26, V12
|
||||||
|
VXORV V1, V27, V1
|
||||||
|
VXORV V6, V28, V6
|
||||||
|
VXORV V11, V29, V11
|
||||||
|
VADDW V12, V8, V26
|
||||||
|
VADDW V1, V13, V27
|
||||||
|
VADDW V6, V2, V28
|
||||||
|
VADDW V11, V7, V29
|
||||||
|
VROTRW $14, V26, V26
|
||||||
|
VROTRW $14, V27, V27
|
||||||
|
VROTRW $14, V28, V28
|
||||||
|
VROTRW $14, V29, V29
|
||||||
|
VXORV V0, V26, V0
|
||||||
|
VXORV V5, V27, V5
|
||||||
|
VXORV V10, V28, V10
|
||||||
|
VXORV V15, V29, V15
|
||||||
|
|
||||||
|
VADDW V0, V3, V26
|
||||||
|
VADDW V5, V4, V27
|
||||||
|
VADDW V10, V9, V28
|
||||||
|
VADDW V15, V14, V29
|
||||||
|
VROTRW $25, V26, V26
|
||||||
|
VROTRW $25, V27, V27
|
||||||
|
VROTRW $25, V28, V28
|
||||||
|
VROTRW $25, V29, V29
|
||||||
|
VXORV V1, V26, V1
|
||||||
|
VXORV V6, V27, V6
|
||||||
|
VXORV V11, V28, V11
|
||||||
|
VXORV V12, V29, V12
|
||||||
|
VADDW V1, V0, V26
|
||||||
|
VADDW V6, V5, V27
|
||||||
|
VADDW V11, V10, V28
|
||||||
|
VADDW V12, V15, V29
|
||||||
|
VROTRW $23, V26, V26
|
||||||
|
VROTRW $23, V27, V27
|
||||||
|
VROTRW $23, V28, V28
|
||||||
|
VROTRW $23, V29, V29
|
||||||
|
VXORV V2, V26, V2
|
||||||
|
VXORV V7, V27, V7
|
||||||
|
VXORV V8, V28, V8
|
||||||
|
VXORV V13, V29, V13
|
||||||
|
VADDW V2, V1, V26
|
||||||
|
VADDW V7, V6, V27
|
||||||
|
VADDW V8, V11, V28
|
||||||
|
VADDW V13, V12, V29
|
||||||
|
VROTRW $19, V26, V26
|
||||||
|
VROTRW $19, V27, V27
|
||||||
|
VROTRW $19, V28, V28
|
||||||
|
VROTRW $19, V29, V29
|
||||||
|
VXORV V3, V26, V3
|
||||||
|
VXORV V4, V27, V4
|
||||||
|
VXORV V9, V28, V9
|
||||||
|
VXORV V14, V29, V14
|
||||||
|
VADDW V3, V2, V26
|
||||||
|
VADDW V4, V7, V27
|
||||||
|
VADDW V9, V8, V28
|
||||||
|
VADDW V14, V13, V29
|
||||||
|
VROTRW $14, V26, V26
|
||||||
|
VROTRW $14, V27, V27
|
||||||
|
VROTRW $14, V28, V28
|
||||||
|
VROTRW $14, V29, V29
|
||||||
|
VXORV V0, V26, V0
|
||||||
|
VXORV V5, V27, V5
|
||||||
|
VXORV V10, V28, V10
|
||||||
|
VXORV V15, V29, V15
|
||||||
|
|
||||||
|
SUBV $1, R15
|
||||||
|
BNE R15, R0, salsa20_256
|
||||||
|
|
||||||
|
// load origin contants
|
||||||
|
VMOVQ (R10), V16.W4
|
||||||
|
VMOVQ 4(R10), V21.W4
|
||||||
|
VMOVQ 8(R10), V26.W4
|
||||||
|
VMOVQ 12(R10), V31.W4
|
||||||
|
|
||||||
|
// load origin keys
|
||||||
|
VMOVQ (R8), V17.W4
|
||||||
|
VMOVQ 4(R8), V18.W4
|
||||||
|
VMOVQ 8(R8), V19.W4
|
||||||
|
VMOVQ 12(R8), V20.W4
|
||||||
|
VMOVQ 16(R8), V27.W4
|
||||||
|
VMOVQ 20(R8), V28.W4
|
||||||
|
VMOVQ 24(R8), V29.W4
|
||||||
|
VMOVQ 28(R8), V30.W4
|
||||||
|
|
||||||
|
// load origin counter
|
||||||
|
VMOVQ (R7), V22.W4
|
||||||
|
VMOVQ 4(R7), V23.W4
|
||||||
|
|
||||||
|
// add back the initial state to generate the key stream
|
||||||
|
VADDW V0, V16, V0
|
||||||
|
VADDW V1, V17, V1
|
||||||
|
VADDW V2, V18, V2
|
||||||
|
VADDW V3, V19, V3
|
||||||
|
VADDW V4, V20, V4
|
||||||
|
VADDW V5, V21, V5
|
||||||
|
VADDW V6, V22, V6
|
||||||
|
VADDW V7, V23, V7
|
||||||
|
VADDW V8, V24, V8
|
||||||
|
VADDW V9, V25, V9
|
||||||
|
VADDW V10, V26, V10
|
||||||
|
VADDW V11, V27, V11
|
||||||
|
VADDW V12, V28, V12
|
||||||
|
VADDW V13, V29, V13
|
||||||
|
VADDW V14, V30, V14
|
||||||
|
VADDW V15, V31, V15
|
||||||
|
|
||||||
|
// shuffle
|
||||||
|
VILVLW V0, V1, V16
|
||||||
|
VILVHW V0, V1, V17
|
||||||
|
VILVLW V2, V3, V18
|
||||||
|
VILVHW V2, V3, V19
|
||||||
|
VILVLW V4, V5 ,V20
|
||||||
|
VILVHW V4, V5, V21
|
||||||
|
VILVLW V6, V7, V22
|
||||||
|
VILVHW V6, V7, V23
|
||||||
|
VILVLW V8, V9, V24
|
||||||
|
VILVHW V8, V9, V25
|
||||||
|
VILVLW V10, V11, V26
|
||||||
|
VILVHW V10, V11, V27
|
||||||
|
VILVLW V12, V13, V28
|
||||||
|
VILVHW V12, V13, V29
|
||||||
|
VILVLW V14, V15, V30
|
||||||
|
VILVHW V14, V15, V31
|
||||||
|
VILVLV V16, V18, V0
|
||||||
|
VILVHV V16, V18, V4
|
||||||
|
VILVLV V17, V19, V8
|
||||||
|
VILVHV V17, V19, V12
|
||||||
|
VILVLV V20, V22, V1
|
||||||
|
VILVHV V20, V22, V5
|
||||||
|
VILVLV V21, V23, V9
|
||||||
|
VILVHV V21, V23, V13
|
||||||
|
VILVLV V24, V26, V2
|
||||||
|
VILVHV V24, V26, V6
|
||||||
|
VILVLV V25, V27, V10
|
||||||
|
VILVHV V25, V27, V14
|
||||||
|
VILVLV V28, V30, V3
|
||||||
|
VILVHV V28, V30, V7
|
||||||
|
VILVLV V29, V31, V11
|
||||||
|
VILVHV V29, V31, V15
|
||||||
|
|
||||||
|
SGTU $256, R6, R11
|
||||||
|
BNE R11, R0, less_than_256
|
||||||
|
|
||||||
|
// load src data from R5
|
||||||
|
VMOVQ 0(R5), V16
|
||||||
|
VMOVQ 16(R5), V17
|
||||||
|
VMOVQ 32(R5), V18
|
||||||
|
VMOVQ 48(R5), V19
|
||||||
|
VMOVQ 64(R5), V20
|
||||||
|
VMOVQ 80(R5), V21
|
||||||
|
VMOVQ 96(R5), V22
|
||||||
|
VMOVQ 112(R5), V23
|
||||||
|
VMOVQ 128(R5), V24
|
||||||
|
VMOVQ 144(R5), V25
|
||||||
|
VMOVQ 160(R5), V26
|
||||||
|
VMOVQ 176(R5), V27
|
||||||
|
VMOVQ 192(R5), V28
|
||||||
|
VMOVQ 208(R5), V29
|
||||||
|
VMOVQ 224(R5), V30
|
||||||
|
VMOVQ 240(R5), V31
|
||||||
|
|
||||||
|
VXORV V0, V16, V16
|
||||||
|
VXORV V1, V17, V17
|
||||||
|
VXORV V2, V18, V18
|
||||||
|
VXORV V3, V19, V19
|
||||||
|
VXORV V4, V20, V20
|
||||||
|
VXORV V5, V21, V21
|
||||||
|
VXORV V6, V22, V22
|
||||||
|
VXORV V7, V23, V23
|
||||||
|
VXORV V8, V24, V24
|
||||||
|
VXORV V9, V25, V25
|
||||||
|
VXORV V10, V26, V26
|
||||||
|
VXORV V11, V27, V27
|
||||||
|
VXORV V12, V28, V28
|
||||||
|
VXORV V13, V29, V29
|
||||||
|
VXORV V14, V30, V30
|
||||||
|
VXORV V15, V31, V31
|
||||||
|
|
||||||
|
VMOVQ V16, 0(R4)
|
||||||
|
VMOVQ V17, 16(R4)
|
||||||
|
VMOVQ V18, 32(R4)
|
||||||
|
VMOVQ V19, 48(R4)
|
||||||
|
VMOVQ V20, 64(R4)
|
||||||
|
VMOVQ V21, 80(R4)
|
||||||
|
VMOVQ V22, 96(R4)
|
||||||
|
VMOVQ V23, 112(R4)
|
||||||
|
VMOVQ V24, 128(R4)
|
||||||
|
VMOVQ V25, 144(R4)
|
||||||
|
VMOVQ V26, 160(R4)
|
||||||
|
VMOVQ V27, 176(R4)
|
||||||
|
VMOVQ V28, 192(R4)
|
||||||
|
VMOVQ V29, 208(R4)
|
||||||
|
VMOVQ V30, 224(R4)
|
||||||
|
VMOVQ V31, 240(R4)
|
||||||
|
|
||||||
|
ADDV $4, R9, R9 // update counter
|
||||||
|
|
||||||
|
SUBV $256, R6, R6
|
||||||
|
ADDV $256, R4, R4
|
||||||
|
ADDV $256, R5, R5
|
||||||
|
SGTU $256, R6, R11
|
||||||
|
BEQ R11, R0, loop256
|
||||||
|
BEQ R6, R0, ret
|
||||||
|
|
||||||
|
less_than_256:
|
||||||
|
VXORV V30, V30, V30 // V30=0
|
||||||
|
SGTU $128, R6, R11
|
||||||
|
BNE R11, R0, less_than_128
|
||||||
|
SUBV $128, R6
|
||||||
|
VMOVQ (R5), V16
|
||||||
|
VMOVQ 16(R5), V17
|
||||||
|
VMOVQ 32(R5), V18
|
||||||
|
VMOVQ 48(R5), V19
|
||||||
|
VMOVQ 64(R5), V20
|
||||||
|
VMOVQ 80(R5), V21
|
||||||
|
VMOVQ 96(R5), V22
|
||||||
|
VMOVQ 112(R5), V23
|
||||||
|
VXORV V0, V16, V16
|
||||||
|
VXORV V1, V17, V17
|
||||||
|
VXORV V2, V18, V18
|
||||||
|
VXORV V3, V19, V19
|
||||||
|
VXORV V4, V20, V20
|
||||||
|
VXORV V5, V21, V21
|
||||||
|
VXORV V6, V22, V22
|
||||||
|
VXORV V7, V23, V23
|
||||||
|
VMOVQ V16, (R4)
|
||||||
|
VMOVQ V17, 16(R4)
|
||||||
|
VMOVQ V18, 32(R4)
|
||||||
|
VMOVQ V19, 48(R4)
|
||||||
|
VMOVQ V20, 64(R4)
|
||||||
|
VMOVQ V21, 80(R4)
|
||||||
|
VMOVQ V22, 96(R4)
|
||||||
|
VMOVQ V23, 112(R4)
|
||||||
|
BEQ R6, R0, ret
|
||||||
|
ADDV $128, R5, R5
|
||||||
|
ADDV $128, R4, R4
|
||||||
|
VADDV V8, V30, V0
|
||||||
|
VADDV V9, V30, V1
|
||||||
|
VADDV V10, V30, V2
|
||||||
|
VADDV V11, V30, V3
|
||||||
|
VADDV V12, V30, V4
|
||||||
|
VADDV V13, V30, V5
|
||||||
|
VADDV V14, V30, V6
|
||||||
|
VADDV V15, V30, V7
|
||||||
|
|
||||||
|
less_than_128:
|
||||||
|
SGTU $64, R6, R11
|
||||||
|
BNE R11, R0, less_than_64
|
||||||
|
SUBV $64, R6
|
||||||
|
VMOVQ (R5), V16
|
||||||
|
VMOVQ 16(R5), V17
|
||||||
|
VMOVQ 32(R5), V18
|
||||||
|
VMOVQ 48(R5), V19
|
||||||
|
VXORV V0, V16, V16
|
||||||
|
VXORV V1, V17, V17
|
||||||
|
VXORV V2, V18, V18
|
||||||
|
VXORV V3, V19, V19
|
||||||
|
VMOVQ V16, (R4)
|
||||||
|
VMOVQ V17, 16(R4)
|
||||||
|
VMOVQ V18, 32(R4)
|
||||||
|
VMOVQ V19, 48(R4)
|
||||||
|
BEQ R6, R0, ret
|
||||||
|
ADDV $64, R5
|
||||||
|
ADDV $64, R4
|
||||||
|
VADDV V4, V30, V0
|
||||||
|
VADDV V5, V30, V1
|
||||||
|
VADDV V6, V30, V2
|
||||||
|
VADDV V7, V30, V3
|
||||||
|
|
||||||
|
less_than_64:
|
||||||
|
SGTU $32, R6, R11
|
||||||
|
BNE R11, R0, less_than_32
|
||||||
|
SUBV $32, R6
|
||||||
|
VMOVQ (R5), V16
|
||||||
|
VMOVQ 16(R5), V17
|
||||||
|
VXORV V0, V16, V16
|
||||||
|
VXORV V1, V17, V17
|
||||||
|
VMOVQ V16, (R4)
|
||||||
|
VMOVQ V17, 16(R4)
|
||||||
|
BEQ R6, R0, ret
|
||||||
|
ADDV $32, R5
|
||||||
|
ADDV $32, R4
|
||||||
|
VADDV V2, V30, V0
|
||||||
|
VADDV V3, V30, V1
|
||||||
|
|
||||||
|
less_than_32:
|
||||||
|
SGTU $16, R6, R11
|
||||||
|
BNE R11, R0, less_than_16
|
||||||
|
SUBV $16, R6
|
||||||
|
VMOVQ (R5), V16
|
||||||
|
VXORV V16, V0, V16
|
||||||
|
VMOVQ V16, (R4)
|
||||||
|
BEQ R6, R0, ret
|
||||||
|
ADDV $16, R5
|
||||||
|
ADDV $16, R4
|
||||||
|
VADDV V1, V30, V0
|
||||||
|
|
||||||
|
less_than_16:
|
||||||
|
SGTU $8, R6, R11
|
||||||
|
BNE R11, R0, less_than_8
|
||||||
|
SUBV $8, R6
|
||||||
|
VMOVQ V0.V[0], R11
|
||||||
|
VMOVQ V0.V[1], R13
|
||||||
|
MOVV (R5), R12
|
||||||
|
XOR R11, R12, R12
|
||||||
|
MOVV R12, (R4)
|
||||||
|
BEQ R6, R0, ret
|
||||||
|
ADDV $8, R5
|
||||||
|
ADDV $8, R4
|
||||||
|
VMOVQ R13, V0.V[0]
|
||||||
|
|
||||||
|
less_than_8:
|
||||||
|
SGTU $4, R6, R11
|
||||||
|
BNE R11, R0, less_than_4
|
||||||
|
SUBV $4, R6
|
||||||
|
VMOVQ V0.W[0], R11
|
||||||
|
VMOVQ V0.W[1], R13
|
||||||
|
MOVWU (R5), R12
|
||||||
|
XOR R11, R12, R12
|
||||||
|
MOVWU R12, (R4)
|
||||||
|
BEQ R6, R0, ret
|
||||||
|
ADDV $4, R5
|
||||||
|
ADDV $4, R4
|
||||||
|
VMOVQ R13, V0.W[0]
|
||||||
|
|
||||||
|
less_than_4:
|
||||||
|
SGTU $2, R6, R11
|
||||||
|
BNE R11, R0, less_than_2
|
||||||
|
SUBV $2, R6
|
||||||
|
VMOVQ V0.H[0], R11
|
||||||
|
VMOVQ V0.H[1], R13
|
||||||
|
MOVHU (R5), R12
|
||||||
|
XOR R11, R12, R12
|
||||||
|
MOVHU R12, (R4)
|
||||||
|
BEQ R6, R0, ret
|
||||||
|
ADDV $2, R5
|
||||||
|
ADDV $2, R4
|
||||||
|
VMOVQ R13, V0.H[0]
|
||||||
|
|
||||||
|
less_than_2:
|
||||||
|
VMOVQ V0.B[0], R11
|
||||||
|
MOVBU (R5), R12
|
||||||
|
XOR R11, R12, R12
|
||||||
|
MOVBU R12, (R4)
|
||||||
|
|
||||||
|
ret:
|
||||||
|
RET
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build !amd64 || purego || !gc
|
//go:build (!amd64 && !loong64) || purego || !gc
|
||||||
|
|
||||||
package salsa
|
package salsa
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build amd64 && !purego && gc
|
//go:build (amd64 || loong64) && !purego && gc
|
||||||
|
|
||||||
package salsa
|
package salsa
|
||||||
|
|
||||||
+233
-3
@@ -26,6 +26,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
@@ -307,17 +308,50 @@ func parseKey(in []byte) (out *Key, rest []byte, err error) {
|
|||||||
}, record.Rest, nil
|
}, record.Rest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pipelineMaxInFlight is the maximum number of outstanding requests the
|
||||||
|
// client will pipeline to the agent before applying backpressure.
|
||||||
|
const pipelineMaxInFlight = 32
|
||||||
|
|
||||||
// client is a client for an ssh-agent process.
|
// client is a client for an ssh-agent process.
|
||||||
|
//
|
||||||
|
// Exactly one of pipeline / (mu, conn) is set, chosen by NewClient
|
||||||
|
// based on whether the underlying transport implements io.Closer.
|
||||||
type client struct {
|
type client struct {
|
||||||
// conn is typically a *net.UnixConn
|
// pipeline, if non-nil, dispatches requests over a pipelined
|
||||||
|
// connection: requests are written as soon as the wire is
|
||||||
|
// available and responses are routed back to per-call reply
|
||||||
|
// channels in FIFO order by a background reader goroutine.
|
||||||
|
pipeline *pipeline
|
||||||
|
|
||||||
|
// mu and conn are used in fully-serialized mode, when the
|
||||||
|
// transport does not implement io.Closer. Each call takes mu,
|
||||||
|
// writes its request, reads the matching response, and releases
|
||||||
|
// mu before returning. There is no background goroutine.
|
||||||
|
mu sync.Mutex
|
||||||
conn io.ReadWriter
|
conn io.ReadWriter
|
||||||
// mu is used to prevent concurrent access to the agent
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient returns an Agent that talks to an ssh-agent process over
|
// NewClient returns an Agent that talks to an ssh-agent process over
|
||||||
// the given connection.
|
// the given connection.
|
||||||
|
//
|
||||||
|
// If rw also implements io.Closer (like *net.UnixConn and ssh.Channel
|
||||||
|
// do), the returned client pipelines concurrent requests over the
|
||||||
|
// connection: callers can issue Sign and other operations from
|
||||||
|
// multiple goroutines and they will be written to the agent as soon
|
||||||
|
// as the wire is available, rather than waiting for the previous
|
||||||
|
// responses. The ssh-agent protocol still requires responses to be
|
||||||
|
// returned in request order, so a slow request delays subsequent
|
||||||
|
// responses on the same connection (head-of-line blocking).
|
||||||
|
//
|
||||||
|
// Pipelining requires io.Closer because, on a Write error, the
|
||||||
|
// background reader goroutine must be unblocked by closing the
|
||||||
|
// underlying connection. When rw does not implement io.Closer
|
||||||
|
// this is not possible, so NewClient falls back to fully
|
||||||
|
// serializing each request: a single in-flight call at a time.
|
||||||
func NewClient(rw io.ReadWriter) ExtendedAgent {
|
func NewClient(rw io.ReadWriter) ExtendedAgent {
|
||||||
|
if rwc, ok := rw.(io.ReadWriteCloser); ok {
|
||||||
|
return &client{pipeline: newPipeline(rwc)}
|
||||||
|
}
|
||||||
return &client{conn: rw}
|
return &client{conn: rw}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,6 +374,16 @@ func (c *client) call(req []byte) (reply interface{}, err error) {
|
|||||||
// bytes of the response are returned; no unmarshalling is
|
// bytes of the response are returned; no unmarshalling is
|
||||||
// performed on the response.
|
// performed on the response.
|
||||||
func (c *client) callRaw(req []byte) (reply []byte, err error) {
|
func (c *client) callRaw(req []byte) (reply []byte, err error) {
|
||||||
|
if c.pipeline != nil {
|
||||||
|
return c.pipeline.call(req)
|
||||||
|
}
|
||||||
|
return c.serialCall(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serialCall implements the fully-serialized request/response path
|
||||||
|
// used when the transport is not an io.Closer. It writes req under mu
|
||||||
|
// and reads the matching response before returning.
|
||||||
|
func (c *client) serialCall(req []byte) (reply []byte, err error) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
@@ -577,6 +621,9 @@ func (c *client) insertKey(s interface{}, comment string, constraints []byte) er
|
|||||||
Constraints: constraints,
|
Constraints: constraints,
|
||||||
})
|
})
|
||||||
case ed25519.PrivateKey:
|
case ed25519.PrivateKey:
|
||||||
|
if len(k) != ed25519.PrivateKeySize {
|
||||||
|
return fmt.Errorf("agent: bad ED25519 key size: %d", len(k))
|
||||||
|
}
|
||||||
req = ssh.Marshal(ed25519KeyMsg{
|
req = ssh.Marshal(ed25519KeyMsg{
|
||||||
Type: ssh.KeyAlgoED25519,
|
Type: ssh.KeyAlgoED25519,
|
||||||
Pub: []byte(k)[32:],
|
Pub: []byte(k)[32:],
|
||||||
@@ -588,6 +635,9 @@ func (c *client) insertKey(s interface{}, comment string, constraints []byte) er
|
|||||||
// general idiom is to pass ed25519.PrivateKey by value, not by pointer.
|
// general idiom is to pass ed25519.PrivateKey by value, not by pointer.
|
||||||
// We still support the pointer variant for backwards compatibility.
|
// We still support the pointer variant for backwards compatibility.
|
||||||
case *ed25519.PrivateKey:
|
case *ed25519.PrivateKey:
|
||||||
|
if len(*k) != ed25519.PrivateKeySize {
|
||||||
|
return fmt.Errorf("agent: bad ED25519 key size: %d", len(*k))
|
||||||
|
}
|
||||||
req = ssh.Marshal(ed25519KeyMsg{
|
req = ssh.Marshal(ed25519KeyMsg{
|
||||||
Type: ssh.KeyAlgoED25519,
|
Type: ssh.KeyAlgoED25519,
|
||||||
Pub: []byte(*k)[32:],
|
Pub: []byte(*k)[32:],
|
||||||
@@ -663,6 +713,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)
|
||||||
@@ -705,6 +762,9 @@ func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string
|
|||||||
Constraints: constraints,
|
Constraints: constraints,
|
||||||
})
|
})
|
||||||
case ed25519.PrivateKey:
|
case ed25519.PrivateKey:
|
||||||
|
if len(k) != ed25519.PrivateKeySize {
|
||||||
|
return fmt.Errorf("agent: bad ED25519 key size: %d", len(k))
|
||||||
|
}
|
||||||
req = ssh.Marshal(ed25519CertMsg{
|
req = ssh.Marshal(ed25519CertMsg{
|
||||||
Type: cert.Type(),
|
Type: cert.Type(),
|
||||||
CertBytes: cert.Marshal(),
|
CertBytes: cert.Marshal(),
|
||||||
@@ -717,6 +777,9 @@ func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string
|
|||||||
// general idiom is to pass ed25519.PrivateKey by value, not by pointer.
|
// general idiom is to pass ed25519.PrivateKey by value, not by pointer.
|
||||||
// We still support the pointer variant for backwards compatibility.
|
// We still support the pointer variant for backwards compatibility.
|
||||||
case *ed25519.PrivateKey:
|
case *ed25519.PrivateKey:
|
||||||
|
if len(*k) != ed25519.PrivateKeySize {
|
||||||
|
return fmt.Errorf("agent: bad ED25519 key size: %d", len(*k))
|
||||||
|
}
|
||||||
req = ssh.Marshal(ed25519CertMsg{
|
req = ssh.Marshal(ed25519CertMsg{
|
||||||
Type: cert.Type(),
|
Type: cert.Type(),
|
||||||
CertBytes: cert.Marshal(),
|
CertBytes: cert.Marshal(),
|
||||||
@@ -854,3 +917,170 @@ func (c *client) Extension(extensionType string, contents []byte) ([]byte, error
|
|||||||
|
|
||||||
return buf, nil
|
return buf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pipelineResult carries either a raw agent reply or an error back to a
|
||||||
|
// caller waiting on the response channel.
|
||||||
|
type pipelineResult struct {
|
||||||
|
reply []byte
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// pipeline implements request pipelining over a single agent connection.
|
||||||
|
//
|
||||||
|
// Writers serialize on writeMu to both register a reply channel in the
|
||||||
|
// pending FIFO queue and write the request bytes on the wire; the two
|
||||||
|
// must be atomic so the queue order matches the wire order. A single
|
||||||
|
// reader goroutine decodes responses from the connection and dispatches
|
||||||
|
// each one to the channel at the head of the queue.
|
||||||
|
//
|
||||||
|
// pending is a chan-of-chan acting as a FIFO queue with a fixed
|
||||||
|
// capacity of pipelineMaxInFlight. The outer channel provides ordering
|
||||||
|
// (reads happen in send order) and natural backpressure (a full queue
|
||||||
|
// blocks new writers). Each inner channel is buffered with capacity
|
||||||
|
// one and is sent to exactly once: either by the reader goroutine
|
||||||
|
// with the agent reply, or by shutdown with the terminal error during
|
||||||
|
// drain. The cap-one buffer makes the producer's send non-blocking,
|
||||||
|
// so the reader and shutdown never have to wait for the caller to be
|
||||||
|
// scheduled on the receive.
|
||||||
|
//
|
||||||
|
// When the reader goroutine exits (on read error or protocol
|
||||||
|
// violation), it closes exitCh to wake any writer blocked on the
|
||||||
|
// pending queue, then serializes with any in-flight writer to close
|
||||||
|
// the pending channel, and finally drains the remaining entries
|
||||||
|
// delivering the terminal error to each waiting caller. The
|
||||||
|
// pipeline relies on conn implementing io.Closer so a writer that
|
||||||
|
// hits a Write error can close the connection to unblock the reader
|
||||||
|
// goroutine; NewClient is responsible for only constructing a
|
||||||
|
// pipeline when this guarantee holds.
|
||||||
|
type pipeline struct {
|
||||||
|
conn io.ReadWriteCloser
|
||||||
|
|
||||||
|
writeMu sync.Mutex
|
||||||
|
// pending is the FIFO queue of reply channels with capacity
|
||||||
|
// pipelineMaxInFlight. See type-level documentation.
|
||||||
|
pending chan chan pipelineResult
|
||||||
|
exitCh chan struct{}
|
||||||
|
|
||||||
|
// err carries the terminal error to callers blocked on a closed
|
||||||
|
// pipeline. It is stored exactly once by the reader goroutine
|
||||||
|
// before exitCh is closed; every read happens after observing
|
||||||
|
// exitCh closed, so the load synchronises through the close and
|
||||||
|
// is guaranteed to return the stored value (never nil).
|
||||||
|
err atomic.Pointer[error]
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPipeline(conn io.ReadWriteCloser) *pipeline {
|
||||||
|
p := &pipeline{
|
||||||
|
conn: conn,
|
||||||
|
pending: make(chan chan pipelineResult, pipelineMaxInFlight),
|
||||||
|
exitCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
go p.readLoop()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// readLoop decodes responses from conn and dispatches them in FIFO order
|
||||||
|
// to reply channels in pending. On any failure it invokes shutdown.
|
||||||
|
func (p *pipeline) readLoop() {
|
||||||
|
var finalErr error
|
||||||
|
for {
|
||||||
|
var sizeBuf [4]byte
|
||||||
|
if _, err := io.ReadFull(p.conn, sizeBuf[:]); err != nil {
|
||||||
|
finalErr = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
respSize := binary.BigEndian.Uint32(sizeBuf[:])
|
||||||
|
if respSize > maxAgentResponseBytes {
|
||||||
|
finalErr = errors.New("response too large")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buf := make([]byte, respSize)
|
||||||
|
if _, err := io.ReadFull(p.conn, buf); err != nil {
|
||||||
|
finalErr = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Successful writes always enqueue before sending bytes, so
|
||||||
|
// pending has a waiting channel for this response.
|
||||||
|
ch := <-p.pending
|
||||||
|
// The reply channel is buffered with capacity 1 and is only
|
||||||
|
// ever written to once, so this send cannot block.
|
||||||
|
ch <- pipelineResult{reply: buf}
|
||||||
|
}
|
||||||
|
p.shutdown(clientErr(finalErr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdown is called exactly once, from readLoop, when the reader is
|
||||||
|
// terminating. It unblocks pending writers and fails all in-flight
|
||||||
|
// requests with finalErr.
|
||||||
|
func (p *pipeline) shutdown(finalErr error) {
|
||||||
|
// Publish the terminal error before closing exitCh so any
|
||||||
|
// writer that subsequently observes exitCh closed sees err.
|
||||||
|
p.err.Store(&finalErr)
|
||||||
|
|
||||||
|
// Wake any writer blocked waiting for a slot in the pending queue.
|
||||||
|
close(p.exitCh)
|
||||||
|
|
||||||
|
// Wait for any writer currently inside its critical section to
|
||||||
|
// complete. After this lock, no new writer can reach the send on
|
||||||
|
// pending: they will observe exitCh closed in the select and bail
|
||||||
|
// out before attempting the send.
|
||||||
|
p.writeMu.Lock()
|
||||||
|
close(p.pending)
|
||||||
|
p.writeMu.Unlock()
|
||||||
|
|
||||||
|
// Drain entries that were enqueued but never answered, delivering
|
||||||
|
// the terminal error to their waiting callers. The reply channels
|
||||||
|
// are buffered (cap 1) and written to exactly once, so these sends
|
||||||
|
// cannot block.
|
||||||
|
for ch := range p.pending {
|
||||||
|
ch <- pipelineResult{err: finalErr}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// call sends req to the agent and returns the matching raw response.
|
||||||
|
func (p *pipeline) call(req []byte) ([]byte, error) {
|
||||||
|
replyCh := make(chan pipelineResult, 1)
|
||||||
|
|
||||||
|
p.writeMu.Lock()
|
||||||
|
|
||||||
|
// Priority check: if the reader has already finished shutdown,
|
||||||
|
// pending is closed and sending to it would panic. Bail out now.
|
||||||
|
// Once we pass this check while holding writeMu, shutdown cannot
|
||||||
|
// complete close(pending) until we release writeMu, so the send
|
||||||
|
// below is safe against concurrent closure.
|
||||||
|
select {
|
||||||
|
case <-p.exitCh:
|
||||||
|
p.writeMu.Unlock()
|
||||||
|
return nil, *p.err.Load()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue the reply channel before writing the request, so FIFO
|
||||||
|
// order on the wire matches FIFO order in the pending queue. The
|
||||||
|
// exitCh arm handles the case where the reader errors while we
|
||||||
|
// block on a full queue.
|
||||||
|
select {
|
||||||
|
case p.pending <- replyCh:
|
||||||
|
case <-p.exitCh:
|
||||||
|
p.writeMu.Unlock()
|
||||||
|
return nil, *p.err.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := make([]byte, 4+len(req))
|
||||||
|
binary.BigEndian.PutUint32(msg, uint32(len(req)))
|
||||||
|
copy(msg[4:], req)
|
||||||
|
_, werr := p.conn.Write(msg)
|
||||||
|
p.writeMu.Unlock()
|
||||||
|
|
||||||
|
if werr != nil {
|
||||||
|
// The connection is in an undefined state. Close it so the
|
||||||
|
// reader unblocks promptly and triggers shutdown for every
|
||||||
|
// other in-flight caller. NewClient guarantees conn is a
|
||||||
|
// real io.Closer when the pipeline is in use.
|
||||||
|
p.conn.Close()
|
||||||
|
return nil, clientErr(werr)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := <-replyCh
|
||||||
|
return res.reply, res.err
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,649 @@
|
|||||||
|
// Copyright 2026 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPipelineParallelSigns(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
keyring := NewKeyring()
|
||||||
|
if err := keyring.Add(AddedKey{PrivateKey: testPrivateKeys["rsa"]}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
go ServeAgent(keyring, c2)
|
||||||
|
|
||||||
|
client := NewClient(c1)
|
||||||
|
pubKey := testPublicKeys["rsa"]
|
||||||
|
|
||||||
|
const N = 64
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errCh := make(chan error, N)
|
||||||
|
for i := range N {
|
||||||
|
wg.Go(func() {
|
||||||
|
data := []byte{byte(i), byte(i >> 8)}
|
||||||
|
sig, err := client.Sign(pubKey, data)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := pubKey.Verify(data, sig); err != nil {
|
||||||
|
errCh <- err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
close(errCh)
|
||||||
|
for err := range errCh {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPipelineBackpressure(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = pipelineMaxInFlight + 8
|
||||||
|
|
||||||
|
// Fake agent: the reader drains requests from the wire independently
|
||||||
|
// of response production, so the write path is never self-blocked.
|
||||||
|
// The responder only writes a canned success reply once unblocked
|
||||||
|
// via gate.
|
||||||
|
var reqsRead int64
|
||||||
|
gate := make(chan struct{}, total)
|
||||||
|
pending := make(chan struct{}, total+pipelineMaxInFlight)
|
||||||
|
readerDone := make(chan struct{})
|
||||||
|
responderDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(readerDone)
|
||||||
|
defer close(pending)
|
||||||
|
for {
|
||||||
|
var sizeBuf [4]byte
|
||||||
|
if _, err := io.ReadFull(c2, sizeBuf[:]); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
size := binary.BigEndian.Uint32(sizeBuf[:])
|
||||||
|
req := make([]byte, size)
|
||||||
|
if _, err := io.ReadFull(c2, req); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
atomic.AddInt64(&reqsRead, 1)
|
||||||
|
pending <- struct{}{}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer close(responderDone)
|
||||||
|
for range pending {
|
||||||
|
<-gate
|
||||||
|
var out [5]byte
|
||||||
|
binary.BigEndian.PutUint32(out[:4], 1)
|
||||||
|
out[4] = agentSuccess
|
||||||
|
if _, err := c2.Write(out[:]); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer func() {
|
||||||
|
c1.Close()
|
||||||
|
c2.Close()
|
||||||
|
<-readerDone
|
||||||
|
<-responderDone
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := NewClient(c1)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errs := make(chan error, total)
|
||||||
|
for range total {
|
||||||
|
wg.Go(func() {
|
||||||
|
// RemoveAll maps to a simpleCall expecting agentSuccess.
|
||||||
|
if err := client.RemoveAll(); err != nil {
|
||||||
|
errs <- err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the client to fill the pipeline.
|
||||||
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if atomic.LoadInt64(&reqsRead) >= pipelineMaxInFlight {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
if got := atomic.LoadInt64(&reqsRead); got != pipelineMaxInFlight {
|
||||||
|
t.Errorf("requests in flight before any response: got %d, want %d", got, pipelineMaxInFlight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release all responses; every caller should eventually complete.
|
||||||
|
for range total {
|
||||||
|
gate <- struct{}{}
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
close(errs)
|
||||||
|
for err := range errs {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := atomic.LoadInt64(&reqsRead); got != total {
|
||||||
|
t.Errorf("total requests received: got %d, want %d", got, total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPipelineConnCloseFailsInFlight(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server that reads requests but never replies, so all calls are
|
||||||
|
// parked waiting for responses.
|
||||||
|
serverExit := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(serverExit)
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
for {
|
||||||
|
if _, err := c2.Read(buf); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := NewClient(c1)
|
||||||
|
|
||||||
|
const N = 8
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errs := make(chan error, N)
|
||||||
|
for range N {
|
||||||
|
wg.Go(func() {
|
||||||
|
errs <- client.RemoveAll()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give callers time to enter the pipeline, then tear down the conn.
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
c1.Close()
|
||||||
|
c2.Close()
|
||||||
|
|
||||||
|
// All callers must return promptly with an error.
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(doneCh)
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-doneCh:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("pipelined callers did not complete after conn close")
|
||||||
|
}
|
||||||
|
|
||||||
|
close(errs)
|
||||||
|
for err := range errs {
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error after conn close, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subsequent calls also fail fast rather than hanging.
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() { errCh <- client.RemoveAll() }()
|
||||||
|
select {
|
||||||
|
case err := <-errCh:
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error on post-close call, got nil")
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("post-close call hung")
|
||||||
|
}
|
||||||
|
|
||||||
|
<-serverExit
|
||||||
|
}
|
||||||
|
|
||||||
|
// readWriter exposes only io.Reader and io.Writer, hiding any Close
|
||||||
|
// method on the wrapped value so NewClient cannot take the pipelined
|
||||||
|
// path.
|
||||||
|
type readWriter struct {
|
||||||
|
io.Reader
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewClientSerialFallback(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
keyring := NewKeyring()
|
||||||
|
if err := keyring.Add(AddedKey{PrivateKey: testPrivateKeys["rsa"]}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
go ServeAgent(keyring, c2)
|
||||||
|
|
||||||
|
rw := readWriter{Reader: c1, Writer: c1}
|
||||||
|
ag := NewClient(rw)
|
||||||
|
|
||||||
|
ci, ok := ag.(*client)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("NewClient: got %T, want *client", ag)
|
||||||
|
}
|
||||||
|
if ci.pipeline != nil {
|
||||||
|
t.Error("NewClient with non-Closer rw: pipeline must be nil")
|
||||||
|
}
|
||||||
|
if ci.conn == nil {
|
||||||
|
t.Error("NewClient with non-Closer rw: conn must be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The serialized path correctly handles a request/response cycle.
|
||||||
|
keys, err := ag.List()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("List: %v", err)
|
||||||
|
}
|
||||||
|
if len(keys) != 1 {
|
||||||
|
t.Errorf("List: got %d keys, want 1", len(keys))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSerialFallbackParallelSigns(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
keyring := NewKeyring()
|
||||||
|
if err := keyring.Add(AddedKey{PrivateKey: testPrivateKeys["rsa"]}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
go ServeAgent(keyring, c2)
|
||||||
|
|
||||||
|
rw := readWriter{Reader: c1, Writer: c1}
|
||||||
|
ag := NewClient(rw)
|
||||||
|
|
||||||
|
if ci, ok := ag.(*client); !ok {
|
||||||
|
t.Fatalf("NewClient: got %T, want *client", ag)
|
||||||
|
} else if ci.pipeline != nil {
|
||||||
|
t.Fatal("pipeline must be nil for non-Closer rw")
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey := testPublicKeys["rsa"]
|
||||||
|
|
||||||
|
const N = 64
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errCh := make(chan error, N)
|
||||||
|
for i := range N {
|
||||||
|
wg.Go(func() {
|
||||||
|
data := []byte{byte(i), byte(i >> 8)}
|
||||||
|
sig, err := ag.Sign(pubKey, data)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := pubKey.Verify(data, sig); err != nil {
|
||||||
|
errCh <- err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
close(errCh)
|
||||||
|
for err := range errCh {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeFailConn wraps a net.Conn and forces Write to return an error when
|
||||||
|
// the fail flag is set. Close delegates to the underlying conn so the test
|
||||||
|
// can observe whether the pipeline closed it.
|
||||||
|
type writeFailConn struct {
|
||||||
|
net.Conn
|
||||||
|
fail atomic.Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *writeFailConn) Write(p []byte) (int, error) {
|
||||||
|
if c.fail.Load() {
|
||||||
|
return 0, errors.New("synthetic write failure")
|
||||||
|
}
|
||||||
|
return c.Conn.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPipelineWriteErrorClosesConn(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
// Peer that reads and discards, exiting when the conn closes.
|
||||||
|
serverExit := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(serverExit)
|
||||||
|
io.Copy(io.Discard, c2)
|
||||||
|
}()
|
||||||
|
|
||||||
|
wrapper := &writeFailConn{Conn: c1}
|
||||||
|
wrapper.fail.Store(true)
|
||||||
|
|
||||||
|
client := NewClient(wrapper)
|
||||||
|
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() { errCh <- client.RemoveAll() }()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-errCh:
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error on forced write failure, got nil")
|
||||||
|
}
|
||||||
|
// The caller whose Write failed must receive the actual
|
||||||
|
// write error, not the reader's "closed connection" error
|
||||||
|
// from the post-shutdown drain.
|
||||||
|
if !strings.Contains(err.Error(), "synthetic write failure") {
|
||||||
|
t.Errorf("got %q, want error containing %q", err, "synthetic write failure")
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("RemoveAll hung after write failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The pipeline must have closed the underlying conn, so the peer
|
||||||
|
// goroutine returns from its Read.
|
||||||
|
select {
|
||||||
|
case <-serverExit:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("connection was not closed after write failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
// After shutdown, subsequent calls must also fail promptly.
|
||||||
|
errCh2 := make(chan error, 1)
|
||||||
|
go func() { errCh2 <- client.RemoveAll() }()
|
||||||
|
select {
|
||||||
|
case err := <-errCh2:
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error on post-shutdown call, got nil")
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("post-shutdown call hung")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPipelineResponseTooLarge(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
// Server: read the request, then reply with a size header that
|
||||||
|
// exceeds maxAgentResponseBytes. The size check fires before the
|
||||||
|
// body is read, so no body needs to follow.
|
||||||
|
serverDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(serverDone)
|
||||||
|
var sizeBuf [4]byte
|
||||||
|
if _, err := io.ReadFull(c2, sizeBuf[:]); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
size := binary.BigEndian.Uint32(sizeBuf[:])
|
||||||
|
if _, err := io.CopyN(io.Discard, c2, int64(size)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var out [4]byte
|
||||||
|
binary.BigEndian.PutUint32(out[:], maxAgentResponseBytes+1)
|
||||||
|
c2.Write(out[:])
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := NewClient(c1)
|
||||||
|
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() { errCh <- client.RemoveAll() }()
|
||||||
|
select {
|
||||||
|
case err := <-errCh:
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "response too large") {
|
||||||
|
t.Errorf("got %q, want error containing %q", err, "response too large")
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("call hung")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subsequent calls must also fail: the pipeline is shut down.
|
||||||
|
errCh2 := make(chan error, 1)
|
||||||
|
go func() { errCh2 <- client.RemoveAll() }()
|
||||||
|
select {
|
||||||
|
case err := <-errCh2:
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error on post-shutdown call, got nil")
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("post-shutdown call hung")
|
||||||
|
}
|
||||||
|
|
||||||
|
<-serverDone
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPipelineShutdownWithBlockedWriter(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
|
||||||
|
// Server drains requests off the wire so writes never block on
|
||||||
|
// backpressure from the transport, but never replies. The reader
|
||||||
|
// goroutine therefore stays parked on Read, never dequeues from
|
||||||
|
// pending, and the queue fills up.
|
||||||
|
var reqsRead int64
|
||||||
|
serverExit := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(serverExit)
|
||||||
|
for {
|
||||||
|
var sizeBuf [4]byte
|
||||||
|
if _, err := io.ReadFull(c2, sizeBuf[:]); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
size := binary.BigEndian.Uint32(sizeBuf[:])
|
||||||
|
if _, err := io.CopyN(io.Discard, c2, int64(size)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
atomic.AddInt64(&reqsRead, 1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := NewClient(c1)
|
||||||
|
|
||||||
|
const total = pipelineMaxInFlight + 4
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errs := make(chan error, total)
|
||||||
|
for range total {
|
||||||
|
wg.Go(func() {
|
||||||
|
errs <- client.RemoveAll()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the queue to fill: once pipelineMaxInFlight requests
|
||||||
|
// are on the wire, pending is at capacity and the +1 writer is
|
||||||
|
// blocked in the second-select arm; the rest are queued behind
|
||||||
|
// writeMu.
|
||||||
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if atomic.LoadInt64(&reqsRead) >= pipelineMaxInFlight {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
if got := atomic.LoadInt64(&reqsRead); got < pipelineMaxInFlight {
|
||||||
|
t.Fatalf("requests on wire: got %d, want >= %d", got, pipelineMaxInFlight)
|
||||||
|
}
|
||||||
|
// Give the +1 writer a moment to actually park in the second
|
||||||
|
// select before we trigger shutdown.
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
// Trigger shutdown: closing the server side makes the client's
|
||||||
|
// reader hit EOF, which calls shutdown(...).
|
||||||
|
c2.Close()
|
||||||
|
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(doneCh)
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-doneCh:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("blocked callers did not complete after shutdown")
|
||||||
|
}
|
||||||
|
|
||||||
|
close(errs)
|
||||||
|
for err := range errs {
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error after shutdown, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<-serverExit
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiDeviceAgent simulates an ssh-agent backed by numDevices signing
|
||||||
|
// devices, each of which takes latency to process a request. Incoming
|
||||||
|
// requests are round-robin dispatched to devices, and responses are
|
||||||
|
// serialized back to the wire in request order as the protocol requires.
|
||||||
|
//
|
||||||
|
// The simulation does not emit a semantically meaningful reply; it just
|
||||||
|
// writes a 1-byte SSH_AGENT_SUCCESS after the simulated latency has
|
||||||
|
// elapsed. This is enough to exercise callRaw/simpleCall (RemoveAll).
|
||||||
|
func multiDeviceAgent(t testing.TB, conn io.ReadWriteCloser, numDevices int, latency time.Duration) (done <-chan struct{}) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
type job struct {
|
||||||
|
ready chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
devices := make([]chan job, numDevices)
|
||||||
|
var devicesWG sync.WaitGroup
|
||||||
|
for i := range numDevices {
|
||||||
|
devices[i] = make(chan job, 1024)
|
||||||
|
ch := devices[i]
|
||||||
|
devicesWG.Go(func() {
|
||||||
|
for j := range ch {
|
||||||
|
time.Sleep(latency)
|
||||||
|
close(j.ready)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order-preserving responder: responses are written in the same
|
||||||
|
// order the requests were read, regardless of which device finished
|
||||||
|
// first.
|
||||||
|
orderCh := make(chan chan struct{}, 1024)
|
||||||
|
responderDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(responderDone)
|
||||||
|
for ready := range orderCh {
|
||||||
|
<-ready
|
||||||
|
var out [5]byte
|
||||||
|
binary.BigEndian.PutUint32(out[:4], 1)
|
||||||
|
out[4] = agentSuccess
|
||||||
|
if _, err := conn.Write(out[:]); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
finished := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(finished)
|
||||||
|
defer func() {
|
||||||
|
close(orderCh)
|
||||||
|
<-responderDone
|
||||||
|
for _, d := range devices {
|
||||||
|
close(d)
|
||||||
|
}
|
||||||
|
devicesWG.Wait()
|
||||||
|
}()
|
||||||
|
next := 0
|
||||||
|
for {
|
||||||
|
var sizeBuf [4]byte
|
||||||
|
if _, err := io.ReadFull(conn, sizeBuf[:]); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
size := binary.BigEndian.Uint32(sizeBuf[:])
|
||||||
|
if _, err := io.CopyN(io.Discard, conn, int64(size)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ready := make(chan struct{})
|
||||||
|
devices[next%numDevices] <- job{ready: ready}
|
||||||
|
next++
|
||||||
|
orderCh <- ready
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return finished
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkPipelineMultiDevice measures throughput of the pipelined
|
||||||
|
// agent client against a simulated multi-device agent, varying the
|
||||||
|
// number of backend devices.
|
||||||
|
//
|
||||||
|
// Setup: each simulated signing device takes 2ms per request. The
|
||||||
|
// protocol serializes responses in request order, but pipelining lets
|
||||||
|
// the client keep multiple devices busy concurrently; throughput
|
||||||
|
// should scale roughly linearly with numDevices up to the pipeline
|
||||||
|
// depth (pipelineMaxInFlight).
|
||||||
|
//
|
||||||
|
// Run with: go test -benchmem -bench BenchmarkPipelineMultiDevice -benchtime=2s ./ssh/agent/
|
||||||
|
func BenchmarkPipelineMultiDevice(b *testing.B) {
|
||||||
|
const (
|
||||||
|
deviceLatency = 2 * time.Millisecond
|
||||||
|
parallelism = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, numDevices := range []int{1, 2, 4, 8, 16} {
|
||||||
|
b.Run(fmt.Sprintf("Devices=%d", numDevices), func(b *testing.B) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
serverDone := multiDeviceAgent(b, c2, numDevices, deviceLatency)
|
||||||
|
|
||||||
|
client := NewClient(c1)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.SetParallelism(parallelism)
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
for pb.Next() {
|
||||||
|
if err := client.RemoveAll(); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
b.StopTimer()
|
||||||
|
|
||||||
|
c1.Close()
|
||||||
|
c2.Close()
|
||||||
|
<-serverDone
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ package agent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -594,3 +595,96 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddRejectsShortEd25519Key(t *testing.T) {
|
||||||
|
short := ed25519.PrivateKey(make([]byte, 16))
|
||||||
|
// insertCert reaches its ed25519 branch only when AddedKey.Certificate
|
||||||
|
// is non-nil. The length check returns before cert.Type/Marshal run, so
|
||||||
|
// an empty certificate is sufficient to drive the path.
|
||||||
|
emptyCert := &ssh.Certificate{}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
add AddedKey
|
||||||
|
}{
|
||||||
|
{"insertKey value", AddedKey{PrivateKey: short}},
|
||||||
|
{"insertKey pointer", AddedKey{PrivateKey: &short}},
|
||||||
|
{"insertCert value", AddedKey{PrivateKey: short, Certificate: emptyCert}},
|
||||||
|
{"insertCert pointer", AddedKey{PrivateKey: &short, Certificate: emptyCert}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
client, cleanup := startKeyringAgent(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
err := client.Add(tc.add)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Add accepted ed25519 key shorter than 64 bytes")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "bad ED25519 key size") {
|
||||||
|
t.Errorf("got error %q, want substring %q", err.Error(), "bad ED25519 key size")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ func ForwardToAgent(client *ssh.Client, keyring Agent) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
go ssh.DiscardRequests(reqs)
|
go ssh.DiscardRequests(reqs)
|
||||||
|
go io.Copy(io.Discard, channel.Stderr())
|
||||||
go func() {
|
go func() {
|
||||||
ServeAgent(keyring, channel)
|
ServeAgent(keyring, channel)
|
||||||
channel.Close()
|
channel.Close()
|
||||||
@@ -72,6 +73,7 @@ func ForwardToRemote(client *ssh.Client, addr string) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
go ssh.DiscardRequests(reqs)
|
go ssh.DiscardRequests(reqs)
|
||||||
|
go io.Copy(io.Discard, channel.Stderr())
|
||||||
go forwardUnixSocket(channel, addr)
|
go forwardUnixSocket(channel, addr)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
+19
-6
@@ -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{}
|
||||||
}
|
}
|
||||||
@@ -102,7 +104,7 @@ func (r *keyring) Unlock(passphrase []byte) error {
|
|||||||
if !r.locked {
|
if !r.locked {
|
||||||
return errors.New("agent: not locked")
|
return errors.New("agent: not locked")
|
||||||
}
|
}
|
||||||
if 1 != subtle.ConstantTimeCompare(passphrase, r.passphrase) {
|
if subtle.ConstantTimeCompare(passphrase, r.passphrase) != 1 {
|
||||||
return fmt.Errorf("agent: incorrect passphrase")
|
return fmt.Errorf("agent: incorrect passphrase")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+79
-10
@@ -240,13 +240,35 @@ func setConstraints(key *AddedKey, constraintBytes []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkRSAKeyParams enforces the same bounds as parseRSA in the ssh
|
||||||
|
// package, and additionally caps the prime factors. Without this,
|
||||||
|
// the rsa.PrivateKey built from an Add request would call Precompute()
|
||||||
|
// on arbitrary inputs; the CRT coefficient recomputation is cubic in
|
||||||
|
// |p| and can consume excessive CPU on oversized keys.
|
||||||
|
func checkRSAKeyParams(N, E, P, Q *big.Int) error {
|
||||||
|
if N.BitLen() > 16384 {
|
||||||
|
return errors.New("agent: RSA modulus too large")
|
||||||
|
}
|
||||||
|
if P.BitLen() > 8192 || Q.BitLen() > 8192 {
|
||||||
|
return errors.New("agent: RSA prime too large")
|
||||||
|
}
|
||||||
|
if E.BitLen() > 24 {
|
||||||
|
return errors.New("agent: RSA public exponent too large")
|
||||||
|
}
|
||||||
|
e := E.Int64()
|
||||||
|
if e < 3 || e&1 == 0 {
|
||||||
|
return errors.New("agent: incorrect RSA public exponent")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseRSAKey(req []byte) (*AddedKey, error) {
|
func parseRSAKey(req []byte) (*AddedKey, error) {
|
||||||
var k rsaKeyMsg
|
var k rsaKeyMsg
|
||||||
if err := ssh.Unmarshal(req, &k); err != nil {
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if k.E.BitLen() > 30 {
|
if err := checkRSAKeyParams(k.N, k.E, k.P, k.Q); err != nil {
|
||||||
return nil, errors.New("agent: RSA public exponent too large")
|
return nil, err
|
||||||
}
|
}
|
||||||
priv := &rsa.PrivateKey{
|
priv := &rsa.PrivateKey{
|
||||||
PublicKey: rsa.PublicKey{
|
PublicKey: rsa.PublicKey{
|
||||||
@@ -270,6 +292,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}
|
||||||
@@ -279,19 +304,60 @@ func parseEd25519Key(req []byte) (*AddedKey, error) {
|
|||||||
return addedKey, nil
|
return addedKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkDSAParams(param *dsa.Parameters) error {
|
||||||
|
// SSH specifies FIPS 186-2, which only provided a single size
|
||||||
|
// (1024 bits) DSA key. FIPS 186-3 allows for larger key
|
||||||
|
// sizes, which would confuse SSH.
|
||||||
|
if l := param.P.BitLen(); l != 1024 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
func parseDSAKey(req []byte) (*AddedKey, error) {
|
func parseDSAKey(req []byte) (*AddedKey, error) {
|
||||||
var k dsaKeyMsg
|
var k dsaKeyMsg
|
||||||
if err := ssh.Unmarshal(req, &k); err != nil {
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
params := dsa.Parameters{
|
||||||
|
P: k.P,
|
||||||
|
Q: k.Q,
|
||||||
|
G: k.G,
|
||||||
|
}
|
||||||
|
if err := checkDSAParams(¶ms); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The public value Y must be a non-zero element of the group, i.e. strictly
|
||||||
|
// between 0 and P, to prevent a maliciously oversized Y from slowing
|
||||||
|
// signature operations.
|
||||||
|
if k.Y.Sign() <= 0 || k.Y.Cmp(k.P) >= 0 {
|
||||||
|
return nil, errors.New("agent: DSA public value Y out of range")
|
||||||
|
}
|
||||||
|
|
||||||
priv := &dsa.PrivateKey{
|
priv := &dsa.PrivateKey{
|
||||||
PublicKey: dsa.PublicKey{
|
PublicKey: dsa.PublicKey{
|
||||||
Parameters: dsa.Parameters{
|
Parameters: params,
|
||||||
P: k.P,
|
Y: k.Y,
|
||||||
Q: k.Q,
|
|
||||||
G: k.G,
|
|
||||||
},
|
|
||||||
Y: k.Y,
|
|
||||||
},
|
},
|
||||||
X: k.X,
|
X: k.X,
|
||||||
}
|
}
|
||||||
@@ -336,6 +402,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 {
|
||||||
@@ -393,8 +462,8 @@ func parseRSACert(req []byte) (*AddedKey, error) {
|
|||||||
return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err)
|
return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if rsaPub.E.BitLen() > 30 {
|
if err := checkRSAKeyParams(rsaPub.N, rsaPub.E, k.P, k.Q); err != nil {
|
||||||
return nil, errors.New("agent: RSA public exponent too large")
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
priv := rsa.PrivateKey{
|
priv := rsa.PrivateKey{
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/big"
|
||||||
pseudorand "math/rand"
|
pseudorand "math/rand"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
@@ -88,6 +90,96 @@ func TestSetupForwardAgent(t *testing.T) {
|
|||||||
conn.Close()
|
conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestForwardAgentDiscardsStderr(t *testing.T) {
|
||||||
|
a, b, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer a.Close()
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
serverConf := ssh.ServerConfig{
|
||||||
|
NoClientAuth: true,
|
||||||
|
}
|
||||||
|
serverConf.AddHostKey(testSigners["rsa"])
|
||||||
|
incoming := make(chan *ssh.ServerConn, 1)
|
||||||
|
go func() {
|
||||||
|
conn, _, _, err := ssh.NewServerConn(a, &serverConf)
|
||||||
|
incoming <- conn
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("NewServerConn error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
conf := ssh.ClientConfig{
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
conn, chans, reqs, err := ssh.NewClientConn(b, "", &conf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewClientConn: %v", err)
|
||||||
|
}
|
||||||
|
client := ssh.NewClient(conn, chans, reqs)
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if err := ForwardToAgent(client, NewKeyring()); err != nil {
|
||||||
|
t.Fatalf("ForwardToAgent: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := <-incoming
|
||||||
|
if server == nil {
|
||||||
|
t.Fatal("Unable to get server")
|
||||||
|
}
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
ch, chanReqs, err := server.OpenChannel(channelType, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("OpenChannel(%q): %v", channelType, err)
|
||||||
|
}
|
||||||
|
go ssh.DiscardRequests(chanReqs)
|
||||||
|
defer ch.Close()
|
||||||
|
|
||||||
|
// Write more than the default channel receive window (2 MiB) on the
|
||||||
|
// stderr stream. If the forwarder did not drain it the remote window
|
||||||
|
// would be exhausted and this write would block forever, because no
|
||||||
|
// window-adjust messages are emitted for an extended stream that is
|
||||||
|
// never read.
|
||||||
|
const payload = 4 * 1024 * 1024
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, 32*1024)
|
||||||
|
remaining := payload
|
||||||
|
for remaining > 0 {
|
||||||
|
n := len(buf)
|
||||||
|
if remaining < n {
|
||||||
|
n = remaining
|
||||||
|
}
|
||||||
|
w, err := ch.Stderr().Write(buf[:n])
|
||||||
|
if err != nil {
|
||||||
|
done <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
remaining -= w
|
||||||
|
}
|
||||||
|
done <- nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("write to stderr: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
t.Fatal("write to stderr blocked: ForwardToAgent is not draining channel.Stderr()")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The agent must still be reachable on the main stream after the
|
||||||
|
// stderr traffic.
|
||||||
|
if _, err := NewClient(ch).List(); err != nil {
|
||||||
|
t.Fatalf("List after stderr flood: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestV1ProtocolMessages(t *testing.T) {
|
func TestV1ProtocolMessages(t *testing.T) {
|
||||||
c1, c2, err := netPipe()
|
c1, c2, err := netPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -154,6 +246,75 @@ func addKeyToAgent(key crypto.PrivateKey) error {
|
|||||||
return verifyKey(sshAgent)
|
return verifyKey(sshAgent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseDSAKeyHugeQ(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)
|
||||||
|
|
||||||
|
req := ssh.Marshal(dsaKeyMsg{
|
||||||
|
Type: ssh.InsecureKeyAlgoDSA,
|
||||||
|
P: P,
|
||||||
|
Q: Q,
|
||||||
|
G: G,
|
||||||
|
Y: Y,
|
||||||
|
X: big.NewInt(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := parseDSAKey(req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("parseDSAKey accepted a DSA key with large Q")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedError := "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 TestParseDSAKeyYOutOfRange(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) {
|
||||||
|
req := ssh.Marshal(dsaKeyMsg{
|
||||||
|
Type: ssh.InsecureKeyAlgoDSA,
|
||||||
|
P: P,
|
||||||
|
Q: Q,
|
||||||
|
G: G,
|
||||||
|
Y: tc.Y,
|
||||||
|
X: big.NewInt(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := parseDSAKey(req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("parseDSAKey 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 TestKeyTypes(t *testing.T) {
|
func TestKeyTypes(t *testing.T) {
|
||||||
for k, v := range testPrivateKeys {
|
for k, v := range testPrivateKeys {
|
||||||
if err := addKeyToAgent(v); err != nil {
|
if err := addKeyToAgent(v); err != nil {
|
||||||
@@ -271,3 +432,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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ func TestBufferReadwrite(t *testing.T) {
|
|||||||
b.write(alphabet[5:15])
|
b.write(alphabet[5:15])
|
||||||
r, _ = b.Read(make([]byte, 10))
|
r, _ = b.Read(make([]byte, 10))
|
||||||
r2, _ := b.Read(make([]byte, 10))
|
r2, _ := b.Read(make([]byte, 10))
|
||||||
if r != 10 || r2 != 5 || 15 != r+r2 {
|
if r != 10 || r2 != 5 || r+r2 != 15 {
|
||||||
t.Fatal("Expected written == read == 15")
|
t.Fatal("Expected written == read == 15")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-27
@@ -10,6 +10,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -229,15 +230,20 @@ func parseCert(in []byte, privAlgo string) (*Certificate, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
c.Reserved = g.Reserved
|
c.Reserved = g.Reserved
|
||||||
|
// Reject a certificate whose signature key is itself a certificate before
|
||||||
|
// parsing it. Certificates signed by certificates are not supported (see
|
||||||
|
// PROTOCOL.certkeys), and rejecting after ParsePublicKey returns would allow
|
||||||
|
// a chain of nested certificates to recurse once per level, exhausting the
|
||||||
|
// goroutine stack.
|
||||||
|
if sigAlgo, _, ok := parseString(g.SignatureKey); !ok {
|
||||||
|
return nil, errShortRead
|
||||||
|
} else if _, ok := certKeyAlgoNames[string(sigAlgo)]; ok {
|
||||||
|
return nil, fmt.Errorf("ssh: the signature key type %q is invalid for certificates", sigAlgo)
|
||||||
|
}
|
||||||
k, err := ParsePublicKey(g.SignatureKey)
|
k, err := ParsePublicKey(g.SignatureKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// The Type() function is intended to return only certificate key types, but
|
|
||||||
// we use certKeyAlgoNames anyway for safety, to match [Certificate.Type].
|
|
||||||
if _, ok := certKeyAlgoNames[k.Type()]; ok {
|
|
||||||
return nil, fmt.Errorf("ssh: the signature key type %q is invalid for certificates", k.Type())
|
|
||||||
}
|
|
||||||
c.SignatureKey = k
|
c.SignatureKey = k
|
||||||
c.Signature, rest, ok = parseSignatureBody(g.Signature)
|
c.Signature, rest, ok = parseSignatureBody(g.Signature)
|
||||||
if !ok || len(rest) > 0 {
|
if !ok || len(rest) > 0 {
|
||||||
@@ -300,8 +306,11 @@ const sourceAddressCriticalOption = "source-address"
|
|||||||
// minimally, the IsAuthority callback should be set.
|
// minimally, the IsAuthority callback should be set.
|
||||||
type CertChecker struct {
|
type CertChecker struct {
|
||||||
// SupportedCriticalOptions lists the CriticalOptions that the
|
// SupportedCriticalOptions lists the CriticalOptions that the
|
||||||
// server application layer understands. These are only used
|
// application layer understands. A certificate carrying a critical
|
||||||
// for user certificates.
|
// option that is not listed here is rejected.
|
||||||
|
// CertChecker.Authenticate additionally accepts the source-address
|
||||||
|
// option, which the server enforces on the Permissions that
|
||||||
|
// Authenticate returns.
|
||||||
SupportedCriticalOptions []string
|
SupportedCriticalOptions []string
|
||||||
|
|
||||||
// IsUserAuthority should return true if the key is recognized as an
|
// IsUserAuthority should return true if the key is recognized as an
|
||||||
@@ -348,6 +357,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)
|
||||||
}
|
}
|
||||||
@@ -361,8 +373,9 @@ func (c *CertChecker) CheckHostKey(addr string, remote net.Addr, key PublicKey)
|
|||||||
return c.CheckCert(hostname, cert)
|
return c.CheckCert(hostname, cert)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate checks a user certificate. Authenticate can be used as
|
// Authenticate checks a user certificate. Authenticate can be used as a value
|
||||||
// a value for ServerConfig.PublicKeyCallback.
|
// for ServerConfig.PublicKeyCallback. The source-address critical option is
|
||||||
|
// allowed, as it will be enforced by the server.
|
||||||
func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permissions, error) {
|
func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permissions, error) {
|
||||||
cert, ok := pubKey.(*Certificate)
|
cert, ok := pubKey.(*Certificate)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -375,11 +388,17 @@ 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")
|
||||||
}
|
}
|
||||||
|
// The source-address critical option is enforced by serverAuthenticate,
|
||||||
if err := c.CheckCert(conn.User(), cert); err != nil {
|
// so it is supported regardless of SupportedCriticalOptions
|
||||||
|
cc := *c
|
||||||
|
cc.SupportedCriticalOptions = append(slices.Clip(cc.SupportedCriticalOptions), sourceAddressCriticalOption)
|
||||||
|
if err := cc.CheckCert(conn.User(), cert); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,27 +406,15 @@ func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permis
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CheckCert checks CriticalOptions, ValidPrincipals, revocation, timestamp and
|
// CheckCert checks CriticalOptions, ValidPrincipals, revocation, timestamp and
|
||||||
// the signature of the certificate.
|
// the signature of the certificate. Critical options that are not listed in
|
||||||
|
// SupportedCriticalOptions are rejected.
|
||||||
func (c *CertChecker) CheckCert(principal string, cert *Certificate) error {
|
func (c *CertChecker) CheckCert(principal string, cert *Certificate) error {
|
||||||
if c.IsRevoked != nil && c.IsRevoked(cert) {
|
if c.IsRevoked != nil && c.IsRevoked(cert) {
|
||||||
return fmt.Errorf("ssh: certificate serial %d revoked", cert.Serial)
|
return fmt.Errorf("ssh: certificate serial %d revoked", cert.Serial)
|
||||||
}
|
}
|
||||||
|
|
||||||
for opt := range cert.CriticalOptions {
|
for opt := range cert.CriticalOptions {
|
||||||
// sourceAddressCriticalOption will be enforced by
|
if !slices.Contains(c.SupportedCriticalOptions, opt) {
|
||||||
// serverAuthenticate
|
|
||||||
if opt == sourceAddressCriticalOption {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, supp := range c.SupportedCriticalOptions {
|
|
||||||
if supp == opt {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("ssh: unsupported critical option %q in certificate", opt)
|
return fmt.Errorf("ssh: unsupported critical option %q in certificate", opt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -438,7 +445,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,10 +9,13 @@ 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"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -43,6 +46,34 @@ func TestParseCert(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseCertNestedSignatureKey(t *testing.T) {
|
||||||
|
signer, err := NewSignerFromKey(testPrivateKeys["ed25519"])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cert := &Certificate{
|
||||||
|
Key: signer.PublicKey(),
|
||||||
|
CertType: UserCert,
|
||||||
|
ValidBefore: CertTimeInfinity,
|
||||||
|
}
|
||||||
|
if err := cert.SignCert(rand.Reader, signer); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner := *cert
|
||||||
|
cert.SignatureKey = &inner
|
||||||
|
blob := cert.Marshal()
|
||||||
|
|
||||||
|
_, err = ParsePublicKey(blob)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("ParsePublicKey: expected error for certificate signed by a certificate, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "invalid for certificates") {
|
||||||
|
t.Errorf("ParsePublicKey: got error %q, want it to mention the signature key is invalid for certificates", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cert generated by ssh-keygen OpenSSH_6.8p1 OS X 10.10.3
|
// Cert generated by ssh-keygen OpenSSH_6.8p1 OS X 10.10.3
|
||||||
// % ssh-keygen -s ca -I testcert -O source-address=192.168.1.0/24 -O force-command=/bin/sleep user.pub
|
// % ssh-keygen -s ca -I testcert -O source-address=192.168.1.0/24 -O force-command=/bin/sleep user.pub
|
||||||
// user.pub key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDACh1rt2DXfV3hk6fszSQcQ/rueMId0kVD9U7nl8cfEnFxqOCrNT92g4laQIGl2mn8lsGZfTLg8ksHq3gkvgO3oo/0wHy4v32JeBOHTsN5AL4gfHNEhWeWb50ev47hnTsRIt9P4dxogeUo/hTu7j9+s9lLpEQXCvq6xocXQt0j8MV9qZBBXFLXVT3cWIkSqOdwt/5ZBg+1GSrc7WfCXVWgTk4a20uPMuJPxU4RQwZW6X3+O8Pqo8C3cW0OzZRFP6gUYUKUsTI5WntlS+LAxgw1mZNsozFGdbiOPRnEryE3SRldh9vjDR3tin1fGpA5P7+CEB/bqaXtG3V+F2OkqaMN
|
// user.pub key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDACh1rt2DXfV3hk6fszSQcQ/rueMId0kVD9U7nl8cfEnFxqOCrNT92g4laQIGl2mn8lsGZfTLg8ksHq3gkvgO3oo/0wHy4v32JeBOHTsN5AL4gfHNEhWeWb50ev47hnTsRIt9P4dxogeUo/hTu7j9+s9lLpEQXCvq6xocXQt0j8MV9qZBBXFLXVT3cWIkSqOdwt/5ZBg+1GSrc7WfCXVWgTk4a20uPMuJPxU4RQwZW6X3+O8Pqo8C3cW0OzZRFP6gUYUKUsTI5WntlS+LAxgw1mZNsozFGdbiOPRnEryE3SRldh9vjDR3tin1fGpA5P7+CEB/bqaXtG3V+F2OkqaMN
|
||||||
@@ -128,6 +159,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"},
|
||||||
@@ -257,6 +385,82 @@ func TestHostKeyCert(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type testConnMeta struct {
|
||||||
|
ConnMetadata
|
||||||
|
user string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c testConnMeta) User() string { return c.user }
|
||||||
|
|
||||||
|
func TestCertCriticalOptions(t *testing.T) {
|
||||||
|
appended := make([]string, 2)
|
||||||
|
appended[0], appended[1] = "supported-option", "appended-option"
|
||||||
|
supported := appended[:1]
|
||||||
|
|
||||||
|
checker := &CertChecker{
|
||||||
|
SupportedCriticalOptions: supported,
|
||||||
|
IsHostAuthority: func(p PublicKey, addr string) bool {
|
||||||
|
return bytes.Equal(testPublicKeys["ecdsa"].Marshal(), p.Marshal())
|
||||||
|
},
|
||||||
|
IsUserAuthority: func(p PublicKey) bool {
|
||||||
|
return bytes.Equal(testPublicKeys["ecdsa"].Marshal(), p.Marshal())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
opts map[string]string
|
||||||
|
// succeed is the expected outcome of CheckHostKey, for a host
|
||||||
|
// certificate, and of CheckCert, for a user certificate.
|
||||||
|
succeed bool
|
||||||
|
// authSucceed is the expected outcome of Authenticate.
|
||||||
|
authSucceed bool
|
||||||
|
}{
|
||||||
|
{name: "no critical options", opts: nil, succeed: true, authSucceed: true},
|
||||||
|
{name: "source-address", opts: map[string]string{sourceAddressCriticalOption: "192.168.1.0/24"}, authSucceed: true},
|
||||||
|
{name: "unknown option", opts: map[string]string{"unknown-option": ""}},
|
||||||
|
{name: "supported option", opts: map[string]string{"supported-option": ""}, succeed: true, authSucceed: true},
|
||||||
|
} {
|
||||||
|
hostCert := &Certificate{
|
||||||
|
ValidPrincipals: []string{"hostname"},
|
||||||
|
Key: testPublicKeys["rsa"],
|
||||||
|
ValidBefore: CertTimeInfinity,
|
||||||
|
CertType: HostCert,
|
||||||
|
Permissions: Permissions{CriticalOptions: test.opts},
|
||||||
|
}
|
||||||
|
if err := hostCert.SignCert(rand.Reader, testSigners["ecdsa"]); err != nil {
|
||||||
|
t.Fatalf("SignCert: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checker.CheckHostKey("hostname:22", nil, hostCert); (err == nil) != test.succeed {
|
||||||
|
t.Errorf("CheckHostKey(%s): got %v, want success=%v", test.name, err, test.succeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
userCert := &Certificate{
|
||||||
|
ValidPrincipals: []string{"user"},
|
||||||
|
Key: testPublicKeys["rsa"],
|
||||||
|
ValidBefore: CertTimeInfinity,
|
||||||
|
CertType: UserCert,
|
||||||
|
Permissions: Permissions{CriticalOptions: test.opts},
|
||||||
|
}
|
||||||
|
if err := userCert.SignCert(rand.Reader, testSigners["ecdsa"]); err != nil {
|
||||||
|
t.Fatalf("SignCert: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checker.CheckCert("user", userCert); (err == nil) != test.succeed {
|
||||||
|
t.Errorf("CheckCert(%s): got %v, want success=%v", test.name, err, test.succeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := checker.Authenticate(testConnMeta{user: "user"}, userCert); (err == nil) != test.authSucceed {
|
||||||
|
t.Errorf("Authenticate(%s): got %v, want success=%v", test.name, err, test.authSucceed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if appended[1] != "appended-option" {
|
||||||
|
t.Errorf("Authenticate overwrote the caller's slice: got %q, want %q", appended[1], "appended-option")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type legacyRSASigner struct {
|
type legacyRSASigner struct {
|
||||||
Signer
|
Signer
|
||||||
}
|
}
|
||||||
|
|||||||
+123
-32
@@ -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
|
||||||
@@ -166,6 +173,12 @@ type channel struct {
|
|||||||
// (for outbound channels) or received (for inbound channels).
|
// (for outbound channels) or received (for inbound channels).
|
||||||
decided bool
|
decided bool
|
||||||
|
|
||||||
|
// established is set to true once the channel is open and may carry normal
|
||||||
|
// channel traffic: for an outbound channel when the peer's open
|
||||||
|
// confirmation is received, for an inbound channel when the local side
|
||||||
|
// accepts it. It is set and read from different goroutines.
|
||||||
|
established atomic.Bool
|
||||||
|
|
||||||
// direction contains either channelOutbound, for channels created
|
// direction contains either channelOutbound, for channels created
|
||||||
// locally, or channelInbound, for channels created by the peer.
|
// locally, or channelInbound, for channels created by the peer.
|
||||||
direction channelDirection
|
direction channelDirection
|
||||||
@@ -177,6 +190,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
|
||||||
|
|
||||||
@@ -203,6 +222,10 @@ type channel struct {
|
|||||||
// packetPool has a buffer for each extended channel ID to
|
// packetPool has a buffer for each extended channel ID to
|
||||||
// save allocations during writes.
|
// save allocations during writes.
|
||||||
packetPool map[uint32][]byte
|
packetPool map[uint32][]byte
|
||||||
|
|
||||||
|
// closeOnce guards close so it is idempotent: closing the internal Go
|
||||||
|
// channels (msg, incomingRequests) more than once would panic.
|
||||||
|
closeOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
// writePacket sends a packet. If the packet is a channel close, it updates
|
// writePacket sends a packet. If the packet is a channel close, it updates
|
||||||
@@ -251,7 +274,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
|
||||||
}
|
}
|
||||||
@@ -327,7 +350,18 @@ func (ch *channel) handleData(packet []byte) error {
|
|||||||
if extended == 1 {
|
if extended == 1 {
|
||||||
ch.extPending.write(data)
|
ch.extPending.write(data)
|
||||||
} else if extended > 0 {
|
} else if extended > 0 {
|
||||||
// discard other extended data.
|
// RFC 4254, Section 5.2 defines no extended data types other
|
||||||
|
// than stderr (type 1, handled above) and this package provides
|
||||||
|
// no API to read them, so the data is discarded. Credit its
|
||||||
|
// window back immediately: it can never be read, so the
|
||||||
|
// deduction above would otherwise shrink the window permanently.
|
||||||
|
// adjustWindow returns io.EOF if the local side has already
|
||||||
|
// sent a channel close; ignore it like ReadExtended does, since
|
||||||
|
// an error returned here would terminate the mux read loop and
|
||||||
|
// tear down the whole connection.
|
||||||
|
if err := ch.adjustWindow(length); err != nil && err != io.EOF {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ch.pending.write(data)
|
ch.pending.write(data)
|
||||||
}
|
}
|
||||||
@@ -380,17 +414,19 @@ func (c *channel) ReadExtended(data []byte, extended uint32) (n int, err error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *channel) close() {
|
func (c *channel) close() {
|
||||||
c.pending.eof()
|
c.closeOnce.Do(func() {
|
||||||
c.extPending.eof()
|
c.pending.eof()
|
||||||
close(c.msg)
|
c.extPending.eof()
|
||||||
close(c.incomingRequests)
|
close(c.msg)
|
||||||
c.writeMu.Lock()
|
close(c.incomingRequests)
|
||||||
// This is not necessary for a normal channel teardown, but if
|
c.writeMu.Lock()
|
||||||
// there was another error, it is.
|
// This is not necessary for a normal channel teardown, but if
|
||||||
c.sentClose = true
|
// there was another error, it is.
|
||||||
c.writeMu.Unlock()
|
c.sentClose = true
|
||||||
// Unblock writers.
|
c.writeMu.Unlock()
|
||||||
c.remoteWin.close()
|
// Unblock writers.
|
||||||
|
c.remoteWin.close()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// responseMessageReceived is called when a success or failure message is
|
// responseMessageReceived is called when a success or failure message is
|
||||||
@@ -404,10 +440,20 @@ func (ch *channel) responseMessageReceived() error {
|
|||||||
return errors.New("ssh: duplicate response received for channel")
|
return errors.New("ssh: duplicate response received for channel")
|
||||||
}
|
}
|
||||||
ch.decided = true
|
ch.decided = true
|
||||||
|
ch.established.Store(true)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *channel) handlePacket(packet []byte) error {
|
func (ch *channel) handlePacket(packet []byte) error {
|
||||||
|
// Only the open response is expected before the channel is established.
|
||||||
|
if !ch.established.Load() {
|
||||||
|
switch packet[0] {
|
||||||
|
case msgChannelOpenConfirm, msgChannelOpenFailure:
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch packet[0] {
|
switch packet[0] {
|
||||||
case msgChannelData, msgChannelExtendedData:
|
case msgChannelData, msgChannelExtendedData:
|
||||||
return ch.handleData(packet)
|
return ch.handleData(packet)
|
||||||
@@ -460,27 +506,41 @@ 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
|
// No other message type is expected on an established channel.
|
||||||
|
return fmt.Errorf("ssh: unexpected message type %d on channel %d", packet[0], ch.localId)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mux) newChannel(chanType string, direction channelDirection, extraData []byte) *channel {
|
func (m *mux) newChannel(chanType string, direction channelDirection, extraData []byte) *channel {
|
||||||
ch := &channel{
|
ch := &channel{
|
||||||
remoteWin: window{Cond: newCond()},
|
remoteWin: window{Cond: newCond()},
|
||||||
myWindow: channelWindowSize,
|
myWindow: channelWindowSize,
|
||||||
pending: newBuffer(),
|
maxIncomingPayload: channelMaxPacket,
|
||||||
extPending: newBuffer(),
|
pending: newBuffer(),
|
||||||
direction: direction,
|
extPending: newBuffer(),
|
||||||
incomingRequests: make(chan *Request, chanSize),
|
direction: direction,
|
||||||
msg: make(chan interface{}, chanSize),
|
incomingRequests: make(chan *Request, chanSize),
|
||||||
chanType: chanType,
|
msg: make(chan interface{}, chanSize),
|
||||||
extraData: extraData,
|
chanType: chanType,
|
||||||
mux: m,
|
extraData: extraData,
|
||||||
packetPool: make(map[uint32][]byte),
|
mux: m,
|
||||||
|
packetPool: make(map[uint32][]byte),
|
||||||
}
|
}
|
||||||
ch.localId = m.chanList.add(ch)
|
m.chanList.add(ch)
|
||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,7 +564,6 @@ func (ch *channel) Accept() (Channel, <-chan *Request, error) {
|
|||||||
if ch.decided {
|
if ch.decided {
|
||||||
return nil, nil, errDecidedAlready
|
return nil, nil, errDecidedAlready
|
||||||
}
|
}
|
||||||
ch.maxIncomingPayload = channelMaxPacket
|
|
||||||
confirm := channelOpenConfirmMsg{
|
confirm := channelOpenConfirmMsg{
|
||||||
PeersID: ch.remoteId,
|
PeersID: ch.remoteId,
|
||||||
MyID: ch.localId,
|
MyID: ch.localId,
|
||||||
@@ -512,6 +571,7 @@ func (ch *channel) Accept() (Channel, <-chan *Request, error) {
|
|||||||
MaxPacketSize: ch.maxIncomingPayload,
|
MaxPacketSize: ch.maxIncomingPayload,
|
||||||
}
|
}
|
||||||
ch.decided = true
|
ch.decided = true
|
||||||
|
ch.established.Store(true)
|
||||||
if err := ch.sendMessage(confirm); err != nil {
|
if err := ch.sendMessage(confirm); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -530,7 +590,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 +656,27 @@ 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 _, ok := <-ch.msg:
|
||||||
|
if !ok {
|
||||||
|
break drain
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break drain
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := channelRequestMsg{
|
msg := channelRequestMsg{
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
// 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 (
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscardedExtendedDataReturnsWindowCredit(t *testing.T) {
|
||||||
|
client, _, serverChans := forwardingPair(t)
|
||||||
|
|
||||||
|
clientCh, reqs, err := client.OpenChannel("test", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("OpenChannel: %v", err)
|
||||||
|
}
|
||||||
|
go DiscardRequests(reqs)
|
||||||
|
defer clientCh.Close()
|
||||||
|
|
||||||
|
serverCh := (<-serverChans).(*channel)
|
||||||
|
defer serverCh.Close()
|
||||||
|
|
||||||
|
// Write more than the default channel receive window (2 MiB) of
|
||||||
|
// extended data with a type code other than stderr. The peer
|
||||||
|
// discards such data: if it did not return the window credit the
|
||||||
|
// write would block forever once the window is exhausted.
|
||||||
|
const payload = 4 * 1024 * 1024
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, 32*1024)
|
||||||
|
remaining := payload
|
||||||
|
for remaining > 0 {
|
||||||
|
n := min(len(buf), remaining)
|
||||||
|
nw, err := serverCh.WriteExtended(buf[:n], 2)
|
||||||
|
if err != nil {
|
||||||
|
done <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
remaining -= nw
|
||||||
|
}
|
||||||
|
done <- nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("write of discarded extended data: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
t.Fatal("write of discarded extended data blocked: window credit is not returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The main stream must still be usable.
|
||||||
|
want := []byte("main stream after extended data")
|
||||||
|
if _, err := serverCh.Write(want); err != nil {
|
||||||
|
t.Fatalf("write to main stream: %v", err)
|
||||||
|
}
|
||||||
|
got := make([]byte, len(want))
|
||||||
|
if _, err := io.ReadFull(clientCh, got); err != nil {
|
||||||
|
t.Fatalf("read from main stream: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != string(want) {
|
||||||
|
t.Fatalf("read %q from main stream, want %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reading the main stream payload above orders the test after all
|
||||||
|
// preceding packets: everything received has now been either read or
|
||||||
|
// discarded, so the full window credit must have been accounted.
|
||||||
|
// Anything less indicates a partial-credit leak that the liveness
|
||||||
|
// check above would not detect.
|
||||||
|
cc := clientCh.(*channel)
|
||||||
|
cc.windowMu.Lock()
|
||||||
|
window := cc.myWindow + cc.myConsumed
|
||||||
|
cc.windowMu.Unlock()
|
||||||
|
if window != channelWindowSize {
|
||||||
|
t.Errorf("myWindow+myConsumed = %d, want %d", window, channelWindowSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscardedExtendedDataAfterClose(t *testing.T) {
|
||||||
|
client, _, serverChans := forwardingPair(t)
|
||||||
|
|
||||||
|
clientCh, reqs, err := client.OpenChannel("test", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("OpenChannel: %v", err)
|
||||||
|
}
|
||||||
|
go DiscardRequests(reqs)
|
||||||
|
serverCh := <-serverChans
|
||||||
|
defer serverCh.Close()
|
||||||
|
|
||||||
|
ch := clientCh.(*channel)
|
||||||
|
// Close the channel locally: sentClose is now set, but the channel
|
||||||
|
// stays in the mux chanList until the peer's close arrives, so
|
||||||
|
// in-flight data packets are still dispatched to handleData.
|
||||||
|
if err := ch.Close(); err != nil {
|
||||||
|
t.Fatalf("Close: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deliver extended data packets with an unknown type code, enough to
|
||||||
|
// cross the adjustWindow send threshold of 3*maxIncomingPayload.
|
||||||
|
// The window adjust message cannot be sent after the close and
|
||||||
|
// adjustWindow fails with io.EOF: handleData must swallow that
|
||||||
|
// error, since any error it returns terminates the mux read loop
|
||||||
|
// and tears down every channel on the connection.
|
||||||
|
const packetLen = 32 * 1024
|
||||||
|
packet := make([]byte, 13+packetLen)
|
||||||
|
packet[0] = msgChannelExtendedData
|
||||||
|
binary.BigEndian.PutUint32(packet[1:], ch.localId)
|
||||||
|
binary.BigEndian.PutUint32(packet[5:], 2)
|
||||||
|
binary.BigEndian.PutUint32(packet[9:], packetLen)
|
||||||
|
for range 5 {
|
||||||
|
if err := ch.handleData(packet); err != nil {
|
||||||
|
t.Fatalf("handleData after local close: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-2
@@ -129,6 +129,10 @@ func init() {
|
|||||||
// You should expect that an active attacker can recover plaintext if
|
// You should expect that an active attacker can recover plaintext if
|
||||||
// you do.
|
// you do.
|
||||||
cipherModes[InsecureCipherAES128CBC] = &cipherMode{16, aes.BlockSize, newAESCBCCipher}
|
cipherModes[InsecureCipherAES128CBC] = &cipherMode{16, aes.BlockSize, newAESCBCCipher}
|
||||||
|
// AES-192 and AES-256 reuse the same CBC packet cipher; aes.NewCipher
|
||||||
|
// selects the key schedule from the key length.
|
||||||
|
cipherModes[InsecureCipherAES192CBC] = &cipherMode{24, aes.BlockSize, newAESCBCCipher}
|
||||||
|
cipherModes[InsecureCipherAES256CBC] = &cipherMode{32, aes.BlockSize, newAESCBCCipher}
|
||||||
cipherModes[InsecureCipherTripleDESCBC] = &cipherMode{24, des.BlockSize, newTripleDESCBCCipher}
|
cipherModes[InsecureCipherTripleDESCBC] = &cipherMode{24, des.BlockSize, newTripleDESCBCCipher}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,7 +411,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)]
|
||||||
@@ -586,7 +590,7 @@ func (c *cbcCipher) writeCipherPacket(seqNum uint32, w io.Writer, rand io.Reader
|
|||||||
|
|
||||||
// Length of encrypted portion of the packet (header, payload, padding).
|
// Length of encrypted portion of the packet (header, payload, padding).
|
||||||
// Enforce minimum padding and packet size.
|
// Enforce minimum padding and packet size.
|
||||||
encLength := maxUInt32(prefixLen+len(packet)+cbcMinPaddingSize, cbcMinPaddingSize)
|
encLength := maxUInt32(prefixLen+len(packet)+cbcMinPaddingSize, cbcMinPacketSize)
|
||||||
// Enforce block size.
|
// Enforce block size.
|
||||||
encLength = (encLength + effectiveBlockSize - 1) / effectiveBlockSize * effectiveBlockSize
|
encLength = (encLength + effectiveBlockSize - 1) / effectiveBlockSize * effectiveBlockSize
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,32 @@ func NewClientConn(c net.Conn, addr string, config *ClientConfig) (Conn, <-chan
|
|||||||
return conn, conn.mux.incomingChannels, conn.mux.incomingRequests, nil
|
return conn, conn.mux.incomingChannels, conn.mux.incomingRequests, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewControlClientConn establishes an SSH connection over an OpenSSH
|
||||||
|
// ControlMaster socket c in proxy mode.
|
||||||
|
//
|
||||||
|
// Note that this package only implements the client side of the multiplexing
|
||||||
|
// protocol. The provided net.Conn must be a local, secure connection (such as a
|
||||||
|
// Unix domain socket) connected to an already-running OpenSSH process acting as
|
||||||
|
// the ControlMaster.
|
||||||
|
//
|
||||||
|
// WARNING: Because proxy mode bypasses the standard cryptographic handshake
|
||||||
|
// passing a standard network connection (e.g., TCP) will result in plaintext
|
||||||
|
// data leakage.
|
||||||
|
//
|
||||||
|
// The Request and NewChannel channels must be serviced or the connection
|
||||||
|
// will hang.
|
||||||
|
func NewControlClientConn(c net.Conn) (Conn, <-chan NewChannel, <-chan *Request, error) {
|
||||||
|
conn := &connection{
|
||||||
|
sshConn: sshConn{conn: c},
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if conn.transport, err = handshakeControlProxy(c); err != nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("ssh: control proxy handshake failed: %w", err)
|
||||||
|
}
|
||||||
|
conn.mux = newMux(conn.transport)
|
||||||
|
return conn, conn.mux.incomingChannels, conn.mux.incomingRequests, nil
|
||||||
|
}
|
||||||
|
|
||||||
// clientHandshake performs the client side key exchange. See RFC 4253 Section
|
// clientHandshake performs the client side key exchange. See RFC 4253 Section
|
||||||
// 7.
|
// 7.
|
||||||
func (c *connection) clientHandshake(dialAddress string, config *ClientConfig) error {
|
func (c *connection) clientHandshake(dialAddress string, config *ClientConfig) error {
|
||||||
@@ -197,6 +223,59 @@ type HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
|
|||||||
// the server. A BannerCallback receives the message sent by the remote server.
|
// the server. A BannerCallback receives the message sent by the remote server.
|
||||||
type BannerCallback func(message string) error
|
type BannerCallback func(message string) error
|
||||||
|
|
||||||
|
// ClientAuthContext contains information about the current state of the
|
||||||
|
// authentication process, passed to [ClientAuthCallback].
|
||||||
|
type ClientAuthContext struct {
|
||||||
|
// Metadata contains the connection metadata.
|
||||||
|
Metadata ConnMetadata
|
||||||
|
|
||||||
|
// Algorithms contains the negotiated algorithms.
|
||||||
|
Algorithms NegotiatedAlgorithms
|
||||||
|
|
||||||
|
// AllowedMethods lists the authentication methods currently accepted
|
||||||
|
// by the server. These are the protocol-level names defined in RFC 4252
|
||||||
|
// such as "publickey", "password".
|
||||||
|
AllowedMethods []string
|
||||||
|
|
||||||
|
// PartialSuccessMethods lists the authentication methods that have already
|
||||||
|
// succeeded, indicating a multi-step authentication flow. This list
|
||||||
|
// represents the exact sequence of partial successes and may contain
|
||||||
|
// duplicates if the same method succeeded multiple times.
|
||||||
|
PartialSuccessMethods []string
|
||||||
|
|
||||||
|
// TriedMethods lists the methods that have already been attempted and
|
||||||
|
// failed during this session. This list represents the exact sequence of
|
||||||
|
// failures and may contain duplicates. This allows the callback to also
|
||||||
|
// track the number of failed attempts for a specific method.
|
||||||
|
TriedMethods []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientAuthCallback is a hook invoked before each authentication attempt. It
|
||||||
|
// allows the client to dynamically select an authentication method based on the
|
||||||
|
// current context, server capabilities, or previous failures.
|
||||||
|
//
|
||||||
|
// The callback is invoked after the initial "none" authentication method, once
|
||||||
|
// the server's supported authentication methods are known.
|
||||||
|
//
|
||||||
|
// Return values:
|
||||||
|
// - (AuthMethod, nil): The client will attempt this specific method next.
|
||||||
|
// The returned method does NOT need to be present in [ClientConfig.Auth].
|
||||||
|
// This allows for dynamic authentication strategies (e.g., prompting
|
||||||
|
// for a password only if public key auth fails). Callers should inspect
|
||||||
|
// [ClientAuthContext.TriedMethods] to avoid repeatedly returning the
|
||||||
|
// same failing method.
|
||||||
|
// - (nil, nil): The client selects from [ClientConfig.Auth] the first
|
||||||
|
// instance of a method that has not been tried yet, or aborts if none
|
||||||
|
// are left. If authentication is not successful, the callback is invoked
|
||||||
|
// again before the following attempt.
|
||||||
|
// - (nil, error): The authentication process is aborted immediately,
|
||||||
|
// causing the ongoing SSH handshake to fail with the provided error.
|
||||||
|
//
|
||||||
|
// To bound resource use, the client caps the total number of authentication
|
||||||
|
// attempts (failures and partial successes combined) at 64. If the cap is
|
||||||
|
// exceeded the handshake aborts with an error.
|
||||||
|
type ClientAuthCallback func(ctx *ClientAuthContext) (AuthMethod, error)
|
||||||
|
|
||||||
// A ClientConfig structure is used to configure a Client. It must not be
|
// A ClientConfig structure is used to configure a Client. It must not be
|
||||||
// modified after having been passed to an SSH function.
|
// modified after having been passed to an SSH function.
|
||||||
type ClientConfig struct {
|
type ClientConfig struct {
|
||||||
@@ -210,6 +289,9 @@ type ClientConfig struct {
|
|||||||
// Auth contains possible authentication methods to use with the
|
// Auth contains possible authentication methods to use with the
|
||||||
// server. Only the first instance of a particular RFC 4252 method will
|
// server. Only the first instance of a particular RFC 4252 method will
|
||||||
// be used during authentication.
|
// be used during authentication.
|
||||||
|
//
|
||||||
|
// If AuthCallback is set, these AuthMethod are only used if the
|
||||||
|
// callback returns nil.
|
||||||
Auth []AuthMethod
|
Auth []AuthMethod
|
||||||
|
|
||||||
// HostKeyCallback is called during the cryptographic
|
// HostKeyCallback is called during the cryptographic
|
||||||
@@ -240,6 +322,9 @@ type ClientConfig struct {
|
|||||||
//
|
//
|
||||||
// A Timeout of zero means no timeout.
|
// A Timeout of zero means no timeout.
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
|
|
||||||
|
// AuthCallback, if non-nil, is invoked before each authentication attempt.
|
||||||
|
AuthCallback ClientAuthCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// InsecureIgnoreHostKey returns a function that can be used for
|
// InsecureIgnoreHostKey returns a function that can be used for
|
||||||
|
|||||||
+58
-18
@@ -21,6 +21,12 @@ const (
|
|||||||
authSuccess
|
authSuccess
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// maxAuthClientTried bounds the total number of authentication attempts
|
||||||
|
// (failures and partial successes combined) the client makes before
|
||||||
|
// aborting the loop, to prevent unbounded growth when an AuthCallback
|
||||||
|
// keeps supplying methods.
|
||||||
|
const maxAuthClientTried = 64
|
||||||
|
|
||||||
// clientAuthenticate authenticates with the remote server. See RFC 4252.
|
// clientAuthenticate authenticates with the remote server. See RFC 4252.
|
||||||
func (c *connection) clientAuthenticate(config *ClientConfig) error {
|
func (c *connection) clientAuthenticate(config *ClientConfig) error {
|
||||||
// initiate user auth session
|
// initiate user auth session
|
||||||
@@ -67,32 +73,62 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
|
|||||||
// then any untried methods suggested by the server.
|
// then any untried methods suggested by the server.
|
||||||
var tried []string
|
var tried []string
|
||||||
var lastMethods []string
|
var lastMethods []string
|
||||||
|
var partialSuccess []string
|
||||||
|
|
||||||
sessionID := c.transport.getSessionID()
|
sessionID := c.transport.getSessionID()
|
||||||
for auth := AuthMethod(new(noneAuth)); auth != nil; {
|
for auth := AuthMethod(new(noneAuth)); auth != nil; {
|
||||||
ok, methods, err := auth.auth(sessionID, config.User, c.transport, config.Rand, extensions)
|
ok, methods, err := auth.auth(sessionID, config.User, c.transport, config.Rand, extensions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// On disconnect, return error immediately
|
// On disconnect, return error immediately
|
||||||
if _, ok := err.(*disconnectMsg); ok {
|
if _, isDisconnect := err.(*disconnectMsg); isDisconnect {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// We return the error later if there is no other method left to
|
// We return the error later if there is no other method
|
||||||
// try.
|
// left to try.
|
||||||
ok = authFailure
|
ok = authFailure
|
||||||
}
|
}
|
||||||
if ok == authSuccess {
|
|
||||||
// success
|
switch ok {
|
||||||
|
case authSuccess:
|
||||||
return nil
|
return nil
|
||||||
} else if ok == authFailure {
|
case authPartialSuccess:
|
||||||
if m := auth.method(); !slices.Contains(tried, m) {
|
partialSuccess = append(partialSuccess, auth.method())
|
||||||
tried = append(tried, m)
|
case authFailure:
|
||||||
}
|
tried = append(tried, auth.method())
|
||||||
}
|
}
|
||||||
|
if len(partialSuccess)+len(tried) > maxAuthClientTried {
|
||||||
|
return fmt.Errorf("ssh: too many authentication attempts (%d), aborting",
|
||||||
|
len(partialSuccess)+len(tried))
|
||||||
|
}
|
||||||
|
|
||||||
if methods == nil {
|
if methods == nil {
|
||||||
methods = lastMethods
|
methods = lastMethods
|
||||||
}
|
}
|
||||||
lastMethods = methods
|
lastMethods = methods
|
||||||
|
|
||||||
|
// If AuthCallback is set it takes precedence: it picks the next
|
||||||
|
// AuthMethod dynamically. The returned method need not be in
|
||||||
|
// config.Auth. If the callback returns (nil, nil) we fall back to
|
||||||
|
// selecting the next untried method from config.Auth below; on
|
||||||
|
// (nil, error) the handshake aborts.
|
||||||
|
if config.AuthCallback != nil {
|
||||||
|
ctx := &ClientAuthContext{
|
||||||
|
Metadata: c,
|
||||||
|
Algorithms: c.Algorithms(),
|
||||||
|
AllowedMethods: slices.Clone(methods),
|
||||||
|
PartialSuccessMethods: slices.Clone(partialSuccess),
|
||||||
|
TriedMethods: slices.Clone(tried),
|
||||||
|
}
|
||||||
|
altAuth, cbErr := config.AuthCallback(ctx)
|
||||||
|
if cbErr != nil {
|
||||||
|
return cbErr
|
||||||
|
}
|
||||||
|
if altAuth != nil {
|
||||||
|
auth = altAuth
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auth = nil
|
auth = nil
|
||||||
|
|
||||||
findNext:
|
findNext:
|
||||||
@@ -274,10 +310,14 @@ func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (MultiA
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter algorithms based on those supported by MultiAlgorithmSigner.
|
// Filter algorithms based on those supported by MultiAlgorithmSigner.
|
||||||
|
// Iterate over the signer's algorithms first to preserve its preference order.
|
||||||
|
supportedKeyAlgos := algorithmsForKeyFormat(keyFormat)
|
||||||
var keyAlgos []string
|
var keyAlgos []string
|
||||||
for _, algo := range algorithmsForKeyFormat(keyFormat) {
|
for _, signerAlgo := range as.Algorithms() {
|
||||||
if slices.Contains(as.Algorithms(), underlyingAlgo(algo)) {
|
if idx := slices.IndexFunc(supportedKeyAlgos, func(algo string) bool {
|
||||||
keyAlgos = append(keyAlgos, algo)
|
return underlyingAlgo(algo) == signerAlgo
|
||||||
|
}); idx >= 0 {
|
||||||
|
keyAlgos = append(keyAlgos, supportedKeyAlgos[idx])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,11 +413,11 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
|
|||||||
return authFailure, nil, err
|
return authFailure, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If authentication succeeds or the list of available methods does not
|
// If authentication succeeds or partially succeeds, return immediately
|
||||||
// contain the "publickey" method, do not attempt to authenticate with any
|
// so the caller can select the next auth method. According to RFC 4252
|
||||||
// other keys. According to RFC 4252 Section 7, the latter can occur when
|
// Section 7, if the server no longer lists "publickey" among its
|
||||||
// additional authentication methods are required.
|
// allowed methods, do not attempt to authenticate with any other keys.
|
||||||
if success == authSuccess || !slices.Contains(methods, cb.method()) {
|
if success == authSuccess || success == authPartialSuccess || !slices.Contains(methods, cb.method()) {
|
||||||
return success, methods, err
|
return success, methods, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -758,7 +798,7 @@ func (g *gssAPIWithMICCallback) auth(session []byte, user string, c packetConn,
|
|||||||
return authFailure, nil, fmt.Errorf("GSS-API Error:\n"+
|
return authFailure, nil, fmt.Errorf("GSS-API Error:\n"+
|
||||||
"Major Status: %d\n"+
|
"Major Status: %d\n"+
|
||||||
"Minor Status: %d\n"+
|
"Minor Status: %d\n"+
|
||||||
"Error Message: %s\n", userAuthGSSAPIErrorResp.MajorStatus, userAuthGSSAPIErrorResp.MinorStatus,
|
"Error Message: %q\n", userAuthGSSAPIErrorResp.MajorStatus, userAuthGSSAPIErrorResp.MinorStatus,
|
||||||
userAuthGSSAPIErrorResp.Message)
|
userAuthGSSAPIErrorResp.Message)
|
||||||
case msgUserAuthGSSAPIToken:
|
case msgUserAuthGSSAPIToken:
|
||||||
userAuthGSSAPITokenReq := &userAuthGSSAPIToken{}
|
userAuthGSSAPITokenReq := &userAuthGSSAPIToken{}
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ package ssh
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -169,6 +171,723 @@ func TestClientAuthThirdKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// invalidRSASigner offers an RSA public key the server cannot parse: its public
|
||||||
|
// exponent is even, which parseRSA rejects regardless of any key size limit.
|
||||||
|
// The key is intentionally invalid for a structural reason rather than for its
|
||||||
|
// size, so the test stays meaningful even if the accepted modulus size changes.
|
||||||
|
// Its Sign method is never reached, because the key is rejected during the
|
||||||
|
// initial public key query.
|
||||||
|
type invalidRSASigner struct{}
|
||||||
|
|
||||||
|
func (invalidRSASigner) PublicKey() PublicKey {
|
||||||
|
n := new(big.Int).Lsh(big.NewInt(1), 2048)
|
||||||
|
pub, err := NewPublicKey(&rsa.PublicKey{N: n, E: 2}) // incorrect exponent.
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return pub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (invalidRSASigner) Sign(rand io.Reader, data []byte) (*Signature, error) {
|
||||||
|
return nil, errors.New("ssh: invalid test key must not be used to sign")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientAuthInvalidPublicKey(t *testing.T) {
|
||||||
|
config := &ClientConfig{
|
||||||
|
User: "testuser",
|
||||||
|
Auth: []AuthMethod{
|
||||||
|
PublicKeys(invalidRSASigner{}, testSigners["rsa"]),
|
||||||
|
},
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
if err := tryAuth(t, config); err != nil {
|
||||||
|
t.Fatalf("client auth failed but should have fallen back to a valid key: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// partialSuccessPublicKeyAndKbdInteractiveServer returns a server config
|
||||||
|
// where the rsa key gets partial success and the server offers both publickey
|
||||||
|
// and keyboard-interactive as next methods. The VerifiedPublicKeyCallback
|
||||||
|
// tracks state so that the same key only triggers partial success once,
|
||||||
|
// avoiding an impossible auth loop.
|
||||||
|
func partialSuccessPublicKeyAndKbdInteractiveServer() *ServerConfig {
|
||||||
|
rsaDone := false
|
||||||
|
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 !rsaDone && bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||||
|
rsaDone = true
|
||||||
|
return nil, &PartialSuccessError{
|
||||||
|
Next: ServerAuthCallbacks{
|
||||||
|
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")
|
||||||
|
},
|
||||||
|
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
|
||||||
|
answers, err := client("", "", []string{"token: "}, []bool{true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(answers) == 1 && answers[0] == "correct-token" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid token")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid credentials")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
serverConf.AddHostKey(testSigners["rsa"])
|
||||||
|
return serverConf
|
||||||
|
}
|
||||||
|
|
||||||
|
// partialSuccessMultiKeyServer returns a server config that requires two
|
||||||
|
// public keys in sequence: the first key (rsa) triggers partial success
|
||||||
|
// requiring a second key (ecdsa) to complete authentication.
|
||||||
|
func partialSuccessMultiKeyServer() *ServerConfig {
|
||||||
|
firstKeyDone := false
|
||||||
|
serverConf := &ServerConfig{
|
||||||
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||||
|
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid credentials")
|
||||||
|
},
|
||||||
|
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
||||||
|
if !firstKeyDone && bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||||
|
firstKeyDone = true
|
||||||
|
return nil, &PartialSuccessError{
|
||||||
|
Next: ServerAuthCallbacks{
|
||||||
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||||
|
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid credentials")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if firstKeyDone && bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid credentials")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
serverConf.AddHostKey(testSigners["rsa"])
|
||||||
|
return serverConf
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStaticMultiKeyPartialSuccess(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
Auth: []AuthMethod{
|
||||||
|
PublicKeys(testSigners["rsa"], testSigners["ecdsa"]),
|
||||||
|
},
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
|
||||||
|
go NewServerConn(c1, partialSuccessMultiKeyServer())
|
||||||
|
|
||||||
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client login error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStaticPartialSuccess(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
Auth: []AuthMethod{
|
||||||
|
PublicKeys(testSigners["rsa"]),
|
||||||
|
KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||||
|
return []string{"correct-token"}, nil
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
|
||||||
|
go NewServerConn(c1, partialSuccessPublicKeyAndKbdInteractiveServer())
|
||||||
|
|
||||||
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client login error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// partialSuccessKeyAThenKeyBServer returns a server config where keyA (rsa)
|
||||||
|
// gets partial success and the server offers BOTH publickey and
|
||||||
|
// keyboard-interactive. keyB (ecdsa) then completes auth successfully.
|
||||||
|
func partialSuccessKeyAThenKeyBServer() *ServerConfig {
|
||||||
|
keyADone := false
|
||||||
|
serverConf := &ServerConfig{
|
||||||
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||||
|
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid credentials")
|
||||||
|
},
|
||||||
|
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
|
||||||
|
if !keyADone && bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||||
|
keyADone = true
|
||||||
|
return nil, &PartialSuccessError{
|
||||||
|
Next: ServerAuthCallbacks{
|
||||||
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||||
|
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid credentials")
|
||||||
|
},
|
||||||
|
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
|
||||||
|
answers, err := client("", "", []string{"token: "}, []bool{true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(answers) == 1 && answers[0] == "correct-token" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid token")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if keyADone && bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid credentials")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
serverConf.AddHostKey(testSigners["rsa"])
|
||||||
|
return serverConf
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStaticPartialSuccessKeyAThenKeyB(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
Auth: []AuthMethod{
|
||||||
|
PublicKeys(testSigners["rsa"], testSigners["ecdsa"]),
|
||||||
|
},
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
|
||||||
|
go NewServerConn(c1, partialSuccessKeyAThenKeyBServer())
|
||||||
|
|
||||||
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client login error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// partialSuccessMultiKeyThenKbdInteractiveServer returns a server config
|
||||||
|
// that strictly requires keyA (rsa) AND keyB (ecdsa) as public keys before
|
||||||
|
// allowing keyboard-interactive. After keyA, ONLY publickey is offered
|
||||||
|
// (for keyB). After keyB, ONLY keyboard-interactive is offered.
|
||||||
|
func partialSuccessMultiKeyThenKbdInteractiveServer() *ServerConfig {
|
||||||
|
serverConf := &ServerConfig{
|
||||||
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||||
|
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||||
|
return nil, &PartialSuccessError{
|
||||||
|
Next: ServerAuthCallbacks{
|
||||||
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||||
|
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
|
||||||
|
return nil, &PartialSuccessError{
|
||||||
|
Next: ServerAuthCallbacks{
|
||||||
|
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
|
||||||
|
answers, err := client("", "", []string{"token: "}, []bool{true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(answers) == 1 && answers[0] == "correct-token" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid token")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid credentials")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid credentials")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
serverConf.AddHostKey(testSigners["rsa"])
|
||||||
|
return serverConf
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStaticMultiKeyPartialThenKbdInteractive(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
Auth: []AuthMethod{
|
||||||
|
PublicKeys(testSigners["rsa"], testSigners["ecdsa"]),
|
||||||
|
KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||||
|
return []string{"correct-token"}, nil
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
|
||||||
|
go NewServerConn(c1, partialSuccessMultiKeyThenKbdInteractiveServer())
|
||||||
|
|
||||||
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client login error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthMethodDynamic(t *testing.T) {
|
||||||
|
config := &ClientConfig{
|
||||||
|
User: "testuser",
|
||||||
|
Auth: nil,
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||||
|
if len(ctx.Metadata.SessionID()) == 0 {
|
||||||
|
t.Error("empty sessionID")
|
||||||
|
}
|
||||||
|
if ctx.Algorithms.HostKey == "" {
|
||||||
|
t.Error("empty Host Key")
|
||||||
|
}
|
||||||
|
if slices.Contains(ctx.AllowedMethods, "password") {
|
||||||
|
return Password(clientPassword), nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("auth callback error")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tryAuth(t, config); err != nil {
|
||||||
|
t.Fatalf("unable to dial remote side: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthCallbackError(t *testing.T) {
|
||||||
|
config := &ClientConfig{
|
||||||
|
User: "testuser",
|
||||||
|
Auth: []AuthMethod{
|
||||||
|
Password(clientPassword),
|
||||||
|
},
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||||
|
return nil, errors.New("auth callback error")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tryAuth(t, config); err == nil {
|
||||||
|
t.Fatal("authentication unexpectedly succeeded after AuthCallback returned an error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthCallbackPartialSuccess(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||||
|
if slices.Contains(ctx.AllowedMethods, "publickey") && !slices.Contains(ctx.PartialSuccessMethods, "publickey") {
|
||||||
|
return PublicKeys(testSigners["rsa"]), nil
|
||||||
|
}
|
||||||
|
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
|
||||||
|
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||||
|
return []string{"correct-token"}, nil
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v", ctx.AllowedMethods, ctx.PartialSuccessMethods)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
go NewServerConn(c1, partialSuccessPublicKeyAndKbdInteractiveServer())
|
||||||
|
|
||||||
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client login error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthCallbackMultiKeyPartialSuccess(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||||
|
if slices.Contains(ctx.AllowedMethods, "publickey") && !slices.Contains(ctx.PartialSuccessMethods, "publickey") {
|
||||||
|
return PublicKeys(testSigners["rsa"]), nil
|
||||||
|
}
|
||||||
|
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "publickey") {
|
||||||
|
return PublicKeys(testSigners["ecdsa"]), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v", ctx.AllowedMethods, ctx.PartialSuccessMethods)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
go NewServerConn(c1, partialSuccessMultiKeyServer())
|
||||||
|
|
||||||
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client login error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthCallbackMultiKeyPartialThenKbdInteractive(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||||
|
partialCount := len(ctx.PartialSuccessMethods)
|
||||||
|
if partialCount == 0 && slices.Contains(ctx.AllowedMethods, "publickey") {
|
||||||
|
return PublicKeys(testSigners["rsa"]), nil
|
||||||
|
}
|
||||||
|
if partialCount == 1 && slices.Contains(ctx.AllowedMethods, "publickey") {
|
||||||
|
return PublicKeys(testSigners["ecdsa"]), nil
|
||||||
|
}
|
||||||
|
if partialCount >= 2 && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
|
||||||
|
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||||
|
return []string{"correct-token"}, nil
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v tried=%v",
|
||||||
|
ctx.AllowedMethods, ctx.PartialSuccessMethods, ctx.TriedMethods)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
go NewServerConn(c1, partialSuccessMultiKeyThenKbdInteractiveServer())
|
||||||
|
|
||||||
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client login error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthCallbackPartialSuccessKeyAThenKeyB(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||||
|
if len(ctx.PartialSuccessMethods) == 0 && slices.Contains(ctx.AllowedMethods, "publickey") {
|
||||||
|
return PublicKeys(testSigners["rsa"]), nil
|
||||||
|
}
|
||||||
|
if len(ctx.PartialSuccessMethods) == 1 && slices.Contains(ctx.AllowedMethods, "publickey") {
|
||||||
|
return PublicKeys(testSigners["ecdsa"]), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v",
|
||||||
|
ctx.AllowedMethods, ctx.PartialSuccessMethods)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
go NewServerConn(c1, partialSuccessKeyAThenKeyBServer())
|
||||||
|
|
||||||
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client login error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthCallbackPartialSuccessWithStaticAuth(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
Auth: []AuthMethod{
|
||||||
|
PublicKeys(testSigners["rsa"]),
|
||||||
|
},
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||||
|
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
|
||||||
|
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||||
|
return []string{"correct-token"}, nil
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
go NewServerConn(c1, partialSuccessPublicKeyAndKbdInteractiveServer())
|
||||||
|
|
||||||
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client login error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func partialSuccessPubKeyForbiddenAfterPartialServer(maxTries int) *ServerConfig {
|
||||||
|
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()) {
|
||||||
|
return nil, &PartialSuccessError{
|
||||||
|
Next: ServerAuthCallbacks{
|
||||||
|
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||||
|
return nil, errors.New("legacy public key authentication is forbidden")
|
||||||
|
},
|
||||||
|
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
|
||||||
|
answers, err := client("", "", []string{"token: "}, []bool{true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(answers) == 1 && answers[0] == "correct-token" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid token")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid credentials")
|
||||||
|
},
|
||||||
|
MaxAuthTries: maxTries,
|
||||||
|
}
|
||||||
|
serverConf.AddHostKey(testSigners["rsa"])
|
||||||
|
return serverConf
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthCallbackPartialSuccessWithStaticMultiKey(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
Auth: []AuthMethod{
|
||||||
|
PublicKeys(
|
||||||
|
testSigners["rsa"],
|
||||||
|
testSigners["ecdsa"],
|
||||||
|
testSigners["ed25519"],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||||
|
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
|
||||||
|
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||||
|
return []string{"correct-token"}, nil
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
go NewServerConn(c1, partialSuccessPubKeyForbiddenAfterPartialServer(2))
|
||||||
|
|
||||||
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client login error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthCallbackPartialSuccessShortCircuit(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
serverConf := partialSuccessPubKeyForbiddenAfterPartialServer(2)
|
||||||
|
|
||||||
|
pubkeyAttempts := 0
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||||
|
if slices.Contains(ctx.AllowedMethods, "publickey") && !slices.Contains(ctx.PartialSuccessMethods, "publickey") {
|
||||||
|
pubkeyAttempts++
|
||||||
|
return PublicKeys(testSigners["rsa"], testSigners["ecdsa"], testSigners["ed25519"]), nil
|
||||||
|
}
|
||||||
|
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
|
||||||
|
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||||
|
return []string{"correct-token"}, nil
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v tried=%v",
|
||||||
|
ctx.AllowedMethods, ctx.PartialSuccessMethods, ctx.TriedMethods)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
go NewServerConn(c1, serverConf)
|
||||||
|
|
||||||
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client login error: %s", err)
|
||||||
|
}
|
||||||
|
if pubkeyAttempts != 1 {
|
||||||
|
t.Errorf("expected publickey to be attempted once before short-circuit, got %d", pubkeyAttempts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthCallbackTriedLoopBound(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
serverConf := &ServerConfig{
|
||||||
|
MaxAuthTries: -1,
|
||||||
|
PasswordCallback: func(conn ConnMetadata, pass []byte) (*Permissions, error) {
|
||||||
|
return nil, errors.New("password rejected")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
serverConf.AddHostKey(testSigners["rsa"])
|
||||||
|
|
||||||
|
invocations := 0
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||||
|
invocations++
|
||||||
|
return Password("wrong"), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
go NewServerConn(c1, serverConf)
|
||||||
|
|
||||||
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected the client to abort after exceeding the attempts cap")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "too many authentication attempts") {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if invocations != maxAuthClientTried {
|
||||||
|
t.Errorf("AuthCallback invoked %d times; expected %d",
|
||||||
|
invocations, maxAuthClientTried)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func alwaysPartialPubKeyServer() *ServerConfig {
|
||||||
|
var alwaysPartial func(ConnMetadata, PublicKey) (*Permissions, error)
|
||||||
|
alwaysPartial = func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
|
||||||
|
if !bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
|
||||||
|
return nil, errors.New("invalid credentials")
|
||||||
|
}
|
||||||
|
return nil, &PartialSuccessError{
|
||||||
|
Next: ServerAuthCallbacks{
|
||||||
|
PublicKeyCallback: alwaysPartial,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serverConf := &ServerConfig{
|
||||||
|
MaxAuthTries: -1,
|
||||||
|
PublicKeyCallback: alwaysPartial,
|
||||||
|
}
|
||||||
|
serverConf.AddHostKey(testSigners["rsa"])
|
||||||
|
return serverConf
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthCallbackPartialSuccessLoopBound(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
invocations := 0
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
|
||||||
|
invocations++
|
||||||
|
return PublicKeys(testSigners["rsa"]), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
go NewServerConn(c1, alwaysPartialPubKeyServer())
|
||||||
|
|
||||||
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected the client to abort after exceeding the attempts cap")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "too many authentication attempts") {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if invocations != maxAuthClientTried {
|
||||||
|
t.Errorf("AuthCallback invoked %d times; expected %d",
|
||||||
|
invocations, maxAuthClientTried)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAuthMethodPassword(t *testing.T) {
|
func TestAuthMethodPassword(t *testing.T) {
|
||||||
config := &ClientConfig{
|
config := &ClientConfig{
|
||||||
User: "testuser",
|
User: "testuser",
|
||||||
@@ -971,6 +1690,74 @@ func TestAuthMethodGSSAPIWithMIC(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type maliciousGSSAPIWithMICAuthMethod struct{}
|
||||||
|
|
||||||
|
func (maliciousGSSAPIWithMICAuthMethod) method() string {
|
||||||
|
// Reuse an advertised auth method name so the client attempts this
|
||||||
|
// AuthMethod even when gssapi-with-mic is not advertised.
|
||||||
|
return "password"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (maliciousGSSAPIWithMICAuthMethod) auth(session []byte, user string, c packetConn, rand io.Reader, _ map[string][]byte) (authResult, []string, error) {
|
||||||
|
req := &userAuthRequestMsg{
|
||||||
|
User: user,
|
||||||
|
Service: serviceSSH,
|
||||||
|
Method: "gssapi-with-mic",
|
||||||
|
}
|
||||||
|
req.Payload = appendU32(req.Payload, 1)
|
||||||
|
req.Payload = appendString(req.Payload, string(krb5OID))
|
||||||
|
if err := c.writePacket(Marshal(req)); err != nil {
|
||||||
|
return authFailure, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
packet, err := c.readPacket()
|
||||||
|
if err != nil {
|
||||||
|
return authFailure, nil, err
|
||||||
|
}
|
||||||
|
switch packet[0] {
|
||||||
|
case msgUserAuthFailure:
|
||||||
|
var msg userAuthFailureMsg
|
||||||
|
if err := Unmarshal(packet, &msg); err != nil {
|
||||||
|
return authFailure, nil, err
|
||||||
|
}
|
||||||
|
if msg.PartialSuccess {
|
||||||
|
return authPartialSuccess, msg.Methods, nil
|
||||||
|
}
|
||||||
|
return authFailure, msg.Methods, nil
|
||||||
|
default:
|
||||||
|
return authFailure, nil, unexpectedMessageError(msgUserAuthFailure, packet[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGSSAPIWithMICPartialConfigRejectedAtRuntime(t *testing.T) {
|
||||||
|
config := &ClientConfig{
|
||||||
|
User: "testuser",
|
||||||
|
Auth: []AuthMethod{
|
||||||
|
maliciousGSSAPIWithMICAuthMethod{},
|
||||||
|
},
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
|
||||||
|
clientErr, serverErrs := tryAuthBothSides(t, config, &GSSAPIWithMICConfig{
|
||||||
|
Server: &FakeServer{},
|
||||||
|
})
|
||||||
|
|
||||||
|
if clientErr == nil || !strings.Contains(clientErr.Error(), "ssh: unable to authenticate") {
|
||||||
|
t.Fatalf("client got %v, want authentication failure", clientErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, err := range serverErrs {
|
||||||
|
if err != nil && strings.Contains(err.Error(), "ssh: gssapi-with-mic auth not configured") {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("server auth errors = %v, want gssapi-with-mic auth not configured", serverErrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCompatibleAlgoAndSignatures(t *testing.T) {
|
func TestCompatibleAlgoAndSignatures(t *testing.T) {
|
||||||
type testcase struct {
|
type testcase struct {
|
||||||
algo string
|
algo string
|
||||||
@@ -1159,6 +1946,52 @@ func TestPickSignatureAlgorithm(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPickSignatureAlgorithmRespectsSignerPreference(t *testing.T) {
|
||||||
|
algoSigner, ok := testSigners["rsa"].(AlgorithmSigner)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("rsa test signer does not implement the AlgorithmSigner interface")
|
||||||
|
}
|
||||||
|
|
||||||
|
serverExtensions := map[string][]byte{
|
||||||
|
"server-sig-algs": []byte(KeyAlgoRSASHA256 + "," + KeyAlgoRSASHA512),
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
signerPrefs []string
|
||||||
|
expectedAlgo string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Signer prefers SHA512 then SHA256",
|
||||||
|
signerPrefs: []string{KeyAlgoRSASHA512, KeyAlgoRSASHA256},
|
||||||
|
expectedAlgo: KeyAlgoRSASHA512,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Signer prefers SHA256 then SHA512",
|
||||||
|
signerPrefs: []string{KeyAlgoRSASHA256, KeyAlgoRSASHA512},
|
||||||
|
expectedAlgo: KeyAlgoRSASHA256,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
orderedSigner, err := NewSignerWithAlgorithms(algoSigner, tc.signerPrefs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create ordered signer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, selectedAlgo, err := pickSignatureAlgorithm(orderedSigner, serverExtensions)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if selectedAlgo != tc.expectedAlgo {
|
||||||
|
t.Errorf("Algorithm mismatch; got %q want %q", selectedAlgo, tc.expectedAlgo)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// configurablePublicKeyCallback is a public key callback that allows to
|
// configurablePublicKeyCallback is a public key callback that allows to
|
||||||
// configure the signature algorithm and format. This way we can emulate the
|
// configure the signature algorithm and format. This way we can emulate the
|
||||||
// behavior of buggy clients.
|
// behavior of buggy clients.
|
||||||
|
|||||||
+9
-5
@@ -37,6 +37,8 @@ const (
|
|||||||
CipherAES192CTR = "aes192-ctr"
|
CipherAES192CTR = "aes192-ctr"
|
||||||
CipherAES256CTR = "aes256-ctr"
|
CipherAES256CTR = "aes256-ctr"
|
||||||
InsecureCipherAES128CBC = "aes128-cbc"
|
InsecureCipherAES128CBC = "aes128-cbc"
|
||||||
|
InsecureCipherAES192CBC = "aes192-cbc"
|
||||||
|
InsecureCipherAES256CBC = "aes256-cbc"
|
||||||
InsecureCipherTripleDESCBC = "3des-cbc"
|
InsecureCipherTripleDESCBC = "3des-cbc"
|
||||||
InsecureCipherRC4 = "arcfour"
|
InsecureCipherRC4 = "arcfour"
|
||||||
InsecureCipherRC4128 = "arcfour128"
|
InsecureCipherRC4128 = "arcfour128"
|
||||||
@@ -128,6 +130,8 @@ var (
|
|||||||
// package and which have security issues.
|
// package and which have security issues.
|
||||||
insecureCiphers = []string{
|
insecureCiphers = []string{
|
||||||
InsecureCipherAES128CBC,
|
InsecureCipherAES128CBC,
|
||||||
|
InsecureCipherAES192CBC,
|
||||||
|
InsecureCipherAES256CBC,
|
||||||
InsecureCipherTripleDESCBC,
|
InsecureCipherTripleDESCBC,
|
||||||
InsecureCipherRC4256,
|
InsecureCipherRC4256,
|
||||||
InsecureCipherRC4128,
|
InsecureCipherRC4128,
|
||||||
@@ -419,7 +423,7 @@ type AlgorithmNegotiationError struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *AlgorithmNegotiationError) Error() string {
|
func (a *AlgorithmNegotiationError) Error() string {
|
||||||
return fmt.Sprintf("ssh: no common algorithm for %s; we offered: %v, peer offered: %v",
|
return fmt.Sprintf("ssh: no common algorithm for %s; we offered: %q, peer offered: %q",
|
||||||
a.What, a.SupportedAlgorithms, a.RequestedAlgorithms)
|
a.What, a.SupportedAlgorithms, a.RequestedAlgorithms)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +441,7 @@ func (a *DirectionAlgorithms) rekeyBytes() int64 {
|
|||||||
// 2^(BLOCKSIZE/4) blocks. For all AES flavors BLOCKSIZE is
|
// 2^(BLOCKSIZE/4) blocks. For all AES flavors BLOCKSIZE is
|
||||||
// 128.
|
// 128.
|
||||||
switch a.Cipher {
|
switch a.Cipher {
|
||||||
case CipherAES128CTR, CipherAES192CTR, CipherAES256CTR, CipherAES128GCM, CipherAES256GCM, InsecureCipherAES128CBC:
|
case CipherAES128CTR, CipherAES192CTR, CipherAES256CTR, CipherAES128GCM, CipherAES256GCM, InsecureCipherAES128CBC, InsecureCipherAES192CBC, InsecureCipherAES256CBC:
|
||||||
return 16 * (1 << 32)
|
return 16 * (1 << 32)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -544,7 +548,7 @@ func (c *Config) SetDefaults() {
|
|||||||
if c.Rand == nil {
|
if c.Rand == nil {
|
||||||
c.Rand = rand.Reader
|
c.Rand = rand.Reader
|
||||||
}
|
}
|
||||||
if c.Ciphers == nil {
|
if len(c.Ciphers) == 0 {
|
||||||
c.Ciphers = defaultCiphers
|
c.Ciphers = defaultCiphers
|
||||||
}
|
}
|
||||||
var ciphers []string
|
var ciphers []string
|
||||||
@@ -556,7 +560,7 @@ func (c *Config) SetDefaults() {
|
|||||||
}
|
}
|
||||||
c.Ciphers = ciphers
|
c.Ciphers = ciphers
|
||||||
|
|
||||||
if c.KeyExchanges == nil {
|
if len(c.KeyExchanges) == 0 {
|
||||||
c.KeyExchanges = defaultKexAlgos
|
c.KeyExchanges = defaultKexAlgos
|
||||||
}
|
}
|
||||||
var kexs []string
|
var kexs []string
|
||||||
@@ -571,7 +575,7 @@ func (c *Config) SetDefaults() {
|
|||||||
}
|
}
|
||||||
c.KeyExchanges = kexs
|
c.KeyExchanges = kexs
|
||||||
|
|
||||||
if c.MACs == nil {
|
if len(c.MACs) == 0 {
|
||||||
c.MACs = defaultMACs
|
c.MACs = defaultMACs
|
||||||
}
|
}
|
||||||
var macs []string
|
var macs []string
|
||||||
|
|||||||
+10
-2
@@ -17,7 +17,7 @@ type OpenChannelError struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *OpenChannelError) Error() string {
|
func (e *OpenChannelError) Error() string {
|
||||||
return fmt.Sprintf("ssh: rejected: %s (%s)", e.Reason, e.Message)
|
return fmt.Sprintf("ssh: rejected: %s (%q)", e.Reason, e.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnMetadata holds metadata for the connection.
|
// ConnMetadata holds metadata for the connection.
|
||||||
@@ -91,9 +91,17 @@ func DiscardRequests(in <-chan *Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A connTransport represents the transport for a connection.
|
||||||
|
type connTransport interface {
|
||||||
|
packetConn
|
||||||
|
getAlgorithms() NegotiatedAlgorithms
|
||||||
|
getSessionID() []byte
|
||||||
|
waitSession() error
|
||||||
|
}
|
||||||
|
|
||||||
// A connection represents an incoming connection.
|
// A connection represents an incoming connection.
|
||||||
type connection struct {
|
type connection struct {
|
||||||
transport *handshakeTransport
|
transport connTransport
|
||||||
sshConn
|
sshConn
|
||||||
|
|
||||||
// The connection protocol.
|
// The connection protocol.
|
||||||
|
|||||||
+155
@@ -0,0 +1,155 @@
|
|||||||
|
// Copyright 2026 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/cryptobyte"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
muxProtocolVersion = 4
|
||||||
|
|
||||||
|
muxMsgHello = 0x00000001
|
||||||
|
muxCProxy = 0x1000000f
|
||||||
|
muxSProxy = 0x8000000f
|
||||||
|
)
|
||||||
|
|
||||||
|
const controlProxyRequestID = 0
|
||||||
|
|
||||||
|
// handshakeControlProxy attempts to establish a transport connection with an
|
||||||
|
// OpenSSH ControlMaster socket in proxy mode. For details see:
|
||||||
|
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.mux
|
||||||
|
func handshakeControlProxy(rw io.ReadWriteCloser) (connTransport, error) {
|
||||||
|
if err := controlProxyWritePacket(rw, func(b *cryptobyte.Builder) {
|
||||||
|
b.AddUint32(muxMsgHello)
|
||||||
|
b.AddUint32(muxProtocolVersion)
|
||||||
|
}); err != nil {
|
||||||
|
return nil, fmt.Errorf("mux hello write failed: %w", err)
|
||||||
|
}
|
||||||
|
if err := controlProxyWritePacket(rw, func(b *cryptobyte.Builder) {
|
||||||
|
b.AddUint32(muxCProxy)
|
||||||
|
b.AddUint32(controlProxyRequestID)
|
||||||
|
}); err != nil {
|
||||||
|
return nil, fmt.Errorf("mux client proxy write failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
messageType, body, err := controlProxyReadMessage(rw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("mux hello read failed: %w", err)
|
||||||
|
}
|
||||||
|
if messageType != muxMsgHello {
|
||||||
|
return nil, fmt.Errorf("expected hello response, got %v", messageType)
|
||||||
|
}
|
||||||
|
var v uint32
|
||||||
|
if !body.ReadUint32(&v) {
|
||||||
|
return nil, errors.New("EOF reading mux protocol version")
|
||||||
|
}
|
||||||
|
if v != muxProtocolVersion {
|
||||||
|
return nil, fmt.Errorf("mux server has unsupported version %v", v)
|
||||||
|
}
|
||||||
|
messageType, body, err = controlProxyReadMessage(rw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("mux server proxy read failed: %w", err)
|
||||||
|
}
|
||||||
|
if messageType != muxSProxy {
|
||||||
|
return nil, fmt.Errorf("expected server proxy response, got %v", messageType)
|
||||||
|
}
|
||||||
|
var reqID uint32
|
||||||
|
if !body.ReadUint32(&reqID) {
|
||||||
|
return nil, errors.New("EOF reading request id")
|
||||||
|
}
|
||||||
|
if reqID != controlProxyRequestID {
|
||||||
|
return nil, fmt.Errorf("expected request id %v, got %v", controlProxyRequestID, reqID)
|
||||||
|
}
|
||||||
|
return &controlProxyTransport{rw}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// controlProxyTransport implements the connTransport interface for
|
||||||
|
// ControlMaster connections. Each controlMessage has zero length padding and
|
||||||
|
// no MAC.
|
||||||
|
type controlProxyTransport struct {
|
||||||
|
rw io.ReadWriteCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *controlProxyTransport) Close() error {
|
||||||
|
return p.rw.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *controlProxyTransport) writePacket(controlMessage []byte) error {
|
||||||
|
return controlProxyWritePacket(p.rw, func(b *cryptobyte.Builder) {
|
||||||
|
b.AddUint8(0) // Padding length.
|
||||||
|
b.AddBytes(controlMessage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *controlProxyTransport) readPacket() ([]byte, error) {
|
||||||
|
buf, err := controlProxyReadPacket(p.rw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ssh: error reading control message: %w", err)
|
||||||
|
}
|
||||||
|
// Discard the padding length.
|
||||||
|
if len(buf) < 1 {
|
||||||
|
return nil, errors.New("ssh: EOF reading padding length")
|
||||||
|
}
|
||||||
|
if buf[0] != 0 {
|
||||||
|
return nil, errors.New("ssh: unexpected non-zero padding in control message")
|
||||||
|
}
|
||||||
|
return buf[1:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *controlProxyTransport) getAlgorithms() NegotiatedAlgorithms {
|
||||||
|
return NegotiatedAlgorithms{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *controlProxyTransport) getSessionID() []byte {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *controlProxyTransport) waitSession() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func controlProxyWritePacket(w io.Writer, f cryptobyte.BuilderContinuation) error {
|
||||||
|
var buf []byte
|
||||||
|
b := cryptobyte.NewBuilder(buf)
|
||||||
|
b.AddUint32LengthPrefixed(f)
|
||||||
|
out, err := b.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write(out)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func controlProxyReadPacket(r io.Reader) (cryptobyte.String, error) {
|
||||||
|
var l uint32
|
||||||
|
if err := binary.Read(r, binary.BigEndian, &l); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if l > maxPacket {
|
||||||
|
return nil, fmt.Errorf("message length %v exceeds maximum %v", l, maxPacket)
|
||||||
|
}
|
||||||
|
buf := make([]byte, l)
|
||||||
|
if _, err := io.ReadFull(r, buf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func controlProxyReadMessage(r io.Reader) (messageType uint32, body cryptobyte.String, err error) {
|
||||||
|
body, err = controlProxyReadPacket(r)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("error reading message body: %w", err)
|
||||||
|
}
|
||||||
|
if !body.ReadUint32(&messageType) {
|
||||||
|
return 0, nil, errors.New("EOF reading message type")
|
||||||
|
}
|
||||||
|
return messageType, body, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
// Copyright 2026 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestControlClientHandshake(t *testing.T) {
|
||||||
|
reqs := [][]byte{
|
||||||
|
// Hello request.
|
||||||
|
{0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04},
|
||||||
|
// Client proxy request.
|
||||||
|
{0x00, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00},
|
||||||
|
}
|
||||||
|
respsNormal := [][]byte{
|
||||||
|
// Hello response.
|
||||||
|
{0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04},
|
||||||
|
// Server proxy response.
|
||||||
|
{0x00, 0x00, 0x00, 0x08, 0x80, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00},
|
||||||
|
}
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
resps [][]byte
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal handshake",
|
||||||
|
resps: respsNormal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "length greater than max",
|
||||||
|
resps: [][]byte{
|
||||||
|
{0xff, 0xff, 0xff, 0xff},
|
||||||
|
respsNormal[1],
|
||||||
|
},
|
||||||
|
expectedErr: "message length 4294967295 exceeds maximum",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing hello response",
|
||||||
|
resps: [][]byte{
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
expectedErr: "use of closed network connection",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hello response too short",
|
||||||
|
resps: [][]byte{
|
||||||
|
{0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00},
|
||||||
|
respsNormal[1],
|
||||||
|
},
|
||||||
|
expectedErr: "EOF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad hello response type",
|
||||||
|
resps: [][]byte{
|
||||||
|
{0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04},
|
||||||
|
respsNormal[1],
|
||||||
|
},
|
||||||
|
expectedErr: "expected hello response, got 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad protocol version",
|
||||||
|
resps: [][]byte{
|
||||||
|
{0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00},
|
||||||
|
respsNormal[1],
|
||||||
|
},
|
||||||
|
expectedErr: "mux server has unsupported version 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing server proxy response",
|
||||||
|
resps: [][]byte{
|
||||||
|
respsNormal[0],
|
||||||
|
},
|
||||||
|
expectedErr: "use of closed network connection",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "server proxy response too short",
|
||||||
|
resps: [][]byte{
|
||||||
|
respsNormal[0],
|
||||||
|
{0x00, 0x00, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0f, 0x00, 0x00},
|
||||||
|
},
|
||||||
|
expectedErr: "EOF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad server proxy response type",
|
||||||
|
resps: [][]byte{
|
||||||
|
respsNormal[0],
|
||||||
|
{0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||||
|
},
|
||||||
|
expectedErr: "expected server proxy response, got 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad request id",
|
||||||
|
resps: [][]byte{
|
||||||
|
respsNormal[0],
|
||||||
|
{0x00, 0x00, 0x00, 0x08, 0x80, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x01},
|
||||||
|
},
|
||||||
|
expectedErr: "expected request id 0, got 1",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
done := make(chan error, 1)
|
||||||
|
|
||||||
|
ok := func() bool {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
_, _, _, err := NewControlClientConn(c2)
|
||||||
|
c2.Write([]byte{0}) // Dummy message to unblock the final read.
|
||||||
|
done <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for ; i < len(reqs) && i < len(tt.resps); i++ {
|
||||||
|
expected := reqs[i]
|
||||||
|
buf := make([]byte, len(expected))
|
||||||
|
if _, err := io.ReadFull(c1, buf); err != nil {
|
||||||
|
t.Errorf("error reading message %d: %v", i+1, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !bytes.Equal(buf, expected) {
|
||||||
|
t.Errorf(
|
||||||
|
"unexpected message %d: got %v, want %v",
|
||||||
|
i+1, buf, expected,
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err = c1.Write(tt.resps[i])
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error writing message %d: %v", i+1, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Wait for the next message so that the final response can be read.
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
c1.Read(buf)
|
||||||
|
return true
|
||||||
|
}()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := <-done
|
||||||
|
if tt.expectedErr != "" {
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tt.expectedErr) {
|
||||||
|
t.Fatalf("got err %q; want err containing %q", err, tt.expectedErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("got err %q; want no err", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestControlClientTransport(t *testing.T) {
|
||||||
|
type response struct {
|
||||||
|
status bool
|
||||||
|
payload []byte
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
resp []byte
|
||||||
|
respStatus bool
|
||||||
|
respPayload []byte
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful request",
|
||||||
|
resp: []byte{0x00, 0x00, 0x00, 0x02, 0x00, 0x51},
|
||||||
|
respStatus: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "failed request",
|
||||||
|
resp: []byte{0x00, 0x00, 0x00, 0x02, 0x00, 0x52},
|
||||||
|
respStatus: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "short response",
|
||||||
|
resp: []byte{0x00, 0x00, 0x00, 0x00},
|
||||||
|
expectedErr: "EOF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "response with payload",
|
||||||
|
resp: []byte{0x00, 0x00, 0x00, 0x05, 0x00, 0x51, 0x01, 0x02, 0x03},
|
||||||
|
respStatus: true,
|
||||||
|
respPayload: []byte{1, 2, 3},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
// Handshake responses.
|
||||||
|
c1.Write([]byte{
|
||||||
|
0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04,
|
||||||
|
0x00, 0x00, 0x00, 0x08, 0x80, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
})
|
||||||
|
|
||||||
|
conn, chans, reqs, err := NewControlClientConn(c2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
client := NewClient(conn, chans, reqs)
|
||||||
|
|
||||||
|
done := make(chan response, 1)
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
status, payload, err := client.SendRequest("hello", true, nil)
|
||||||
|
if err != nil {
|
||||||
|
done <- response{err: err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
done <- response{
|
||||||
|
status: status,
|
||||||
|
payload: payload,
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Discard handshake.
|
||||||
|
io.CopyN(io.Discard, c1, 24)
|
||||||
|
|
||||||
|
expectedReq := []byte{
|
||||||
|
0x00, 0x00, 0x00, 0x0c, 0x00, 0x50,
|
||||||
|
0x00, 0x00, 0x00, 0x05, 'h', 'e', 'l', 'l', 'o',
|
||||||
|
0x01,
|
||||||
|
}
|
||||||
|
buf := make([]byte, len(expectedReq))
|
||||||
|
if _, err := io.ReadFull(c1, buf); err != nil {
|
||||||
|
t.Fatalf("reading request: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(buf, expectedReq) {
|
||||||
|
t.Fatalf("got request %v; want %v", buf, expectedReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
c1.Write(tt.resp)
|
||||||
|
|
||||||
|
resp := <-done
|
||||||
|
if tt.expectedErr != "" {
|
||||||
|
if resp.err == nil || !strings.Contains(resp.err.Error(), tt.expectedErr) {
|
||||||
|
t.Fatalf("got err %q; want err containing %q", resp.err, tt.expectedErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.err != nil {
|
||||||
|
t.Fatalf("got err %q; want no err", resp.err)
|
||||||
|
}
|
||||||
|
if resp.status != tt.respStatus {
|
||||||
|
t.Fatalf("got status %v; want %v", resp.status, tt.respStatus)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(resp.payload, tt.respPayload) {
|
||||||
|
t.Errorf("got payload %v; want %v", resp.payload, tt.respPayload)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -162,7 +162,7 @@ func newClientTransport(conn keyingTransport, clientVersion, serverVersion []byt
|
|||||||
t.remoteAddr = addr
|
t.remoteAddr = addr
|
||||||
t.hostKeyCallback = config.HostKeyCallback
|
t.hostKeyCallback = config.HostKeyCallback
|
||||||
t.bannerCallback = config.BannerCallback
|
t.bannerCallback = config.BannerCallback
|
||||||
if config.HostKeyAlgorithms != nil {
|
if len(config.HostKeyAlgorithms) > 0 {
|
||||||
t.hostKeyAlgorithms = config.HostKeyAlgorithms
|
t.hostKeyAlgorithms = config.HostKeyAlgorithms
|
||||||
} else {
|
} else {
|
||||||
t.hostKeyAlgorithms = defaultHostKeyAlgos
|
t.hostKeyAlgorithms = defaultHostKeyAlgos
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ func TestHandshakeBasic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
want := []byte{msgRequestSuccess, byte(i)}
|
want := []byte{msgRequestSuccess, byte(i)}
|
||||||
if bytes.Compare(p, want) != 0 {
|
if !bytes.Equal(p, want) {
|
||||||
t.Errorf("message %d: got %v, want %v", i, p, want)
|
t.Errorf("message %d: got %v, want %v", i, p, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1336,7 +1336,7 @@ func TestAlgorithmNegotiationError(t *testing.T) {
|
|||||||
if !errors.As(err, &negotiationError) {
|
if !errors.As(err, &negotiationError) {
|
||||||
t.Fatalf("expected algorithm negotiation error, got %v", err)
|
t.Fatalf("expected algorithm negotiation error, got %v", err)
|
||||||
}
|
}
|
||||||
expectedErrorString := fmt.Sprintf("ssh: handshake failed: ssh: no common algorithm for client to server cipher; we offered: %v, peer offered: %v",
|
expectedErrorString := fmt.Sprintf("ssh: handshake failed: ssh: no common algorithm for client to server cipher; we offered: %q, peer offered: %q",
|
||||||
clientConf.Ciphers, serverConf.Ciphers)
|
clientConf.Ciphers, serverConf.Ciphers)
|
||||||
if err.Error() != expectedErrorString {
|
if err.Error() != expectedErrorString {
|
||||||
t.Fatalf("expected error string %q, got %q", expectedErrorString, err.Error())
|
t.Fatalf("expected error string %q, got %q", expectedErrorString, err.Error())
|
||||||
@@ -1352,7 +1352,7 @@ func TestAlgorithmNegotiationError(t *testing.T) {
|
|||||||
if !errors.As(err, &negotiationError) {
|
if !errors.As(err, &negotiationError) {
|
||||||
t.Fatalf("expected algorithm negotiation error, got %v", err)
|
t.Fatalf("expected algorithm negotiation error, got %v", err)
|
||||||
}
|
}
|
||||||
expectedErrorString = fmt.Sprintf("ssh: no common algorithm for client to server cipher; we offered: %v, peer offered: %v",
|
expectedErrorString = fmt.Sprintf("ssh: no common algorithm for client to server cipher; we offered: %q, peer offered: %q",
|
||||||
serverConf.Ciphers, clientConf.Ciphers)
|
serverConf.Ciphers, clientConf.Ciphers)
|
||||||
if err.Error() != expectedErrorString {
|
if err.Error() != expectedErrorString {
|
||||||
t.Fatalf("expected error string %q, got %q", expectedErrorString, err.Error())
|
t.Fatalf("expected error string %q, got %q", expectedErrorString, err.Error())
|
||||||
|
|||||||
+66
-9
@@ -16,6 +16,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
"slices"
|
"slices"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"golang.org/x/crypto/curve25519"
|
"golang.org/x/crypto/curve25519"
|
||||||
)
|
)
|
||||||
@@ -718,15 +719,9 @@ func (gex *dhGEXSHA) Server(c packetConn, randSource io.Reader, magics *handshak
|
|||||||
kexDHGexRequest.MaxBits, kexDHGexRequest.PreferredBits)
|
kexDHGexRequest.MaxBits, kexDHGexRequest.PreferredBits)
|
||||||
}
|
}
|
||||||
|
|
||||||
var p *big.Int
|
p, err := chooseDH(kexDHGexRequest)
|
||||||
// We hardcode sending Oakley Group 14 (2048 bits), Oakley Group 15 (3072
|
if err != nil {
|
||||||
// bits) or Oakley Group 16 (4096 bits), based on the requested max size.
|
return nil, err
|
||||||
if kexDHGexRequest.MaxBits < 3072 {
|
|
||||||
p, _ = new(big.Int).SetString(oakleyGroup14, 16)
|
|
||||||
} else if kexDHGexRequest.MaxBits < 4096 {
|
|
||||||
p, _ = new(big.Int).SetString(oakleyGroup15, 16)
|
|
||||||
} else {
|
|
||||||
p, _ = new(big.Int).SetString(oakleyGroup16, 16)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
g := big.NewInt(2)
|
g := big.NewInt(2)
|
||||||
@@ -805,3 +800,65 @@ func (gex *dhGEXSHA) Server(c packetConn, randSource io.Reader, magics *handshak
|
|||||||
Hash: gex.hashFunc,
|
Hash: gex.hashFunc,
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dhKEXGroup struct {
|
||||||
|
size int
|
||||||
|
p *big.Int
|
||||||
|
}
|
||||||
|
|
||||||
|
// supportedDHKEXGroups returns the DH groups the server is willing to offer
|
||||||
|
// for diffie-hellman-group-exchange-* key exchanges. The list is built lazily
|
||||||
|
// on first use to keep the hex-to-big.Int parse out of package initialization.
|
||||||
|
var supportedDHKEXGroups = sync.OnceValue(func() []dhKEXGroup {
|
||||||
|
specs := []struct {
|
||||||
|
size int
|
||||||
|
hex string
|
||||||
|
}{
|
||||||
|
{2048, oakleyGroup14},
|
||||||
|
{3072, oakleyGroup15},
|
||||||
|
{4096, oakleyGroup16},
|
||||||
|
}
|
||||||
|
out := make([]dhKEXGroup, 0, len(specs))
|
||||||
|
for _, s := range specs {
|
||||||
|
p, _ := new(big.Int).SetString(s.hex, 16)
|
||||||
|
out = append(out, dhKEXGroup{size: s.size, p: p})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
})
|
||||||
|
|
||||||
|
// chooseDH picks a DH group for the given client request, mirroring the
|
||||||
|
// algorithm used by OpenSSH's choose_dh in dh.c: prefer the smallest known
|
||||||
|
// group larger than or equal to the client's PreferredBits, and otherwise pick
|
||||||
|
// the largest group within the accepted [MinBits, MaxBits] range.
|
||||||
|
func chooseDH(req kexDHGexRequestMsg) (*big.Int, error) {
|
||||||
|
var best *big.Int
|
||||||
|
bestSize := 0
|
||||||
|
wantBits := int(req.PreferredBits)
|
||||||
|
|
||||||
|
for _, group := range supportedDHKEXGroups() {
|
||||||
|
if uint32(group.size) < req.MinBits || uint32(group.size) > req.MaxBits {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if bestSize == 0 {
|
||||||
|
best = group.p
|
||||||
|
bestSize = group.size
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
closerFromAbove := group.size >= wantBits && group.size < bestSize
|
||||||
|
closerFromBelow := group.size > bestSize && bestSize < wantBits
|
||||||
|
|
||||||
|
if closerFromAbove || closerFromBelow {
|
||||||
|
best = group.p
|
||||||
|
bestSize = group.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bestSize == 0 {
|
||||||
|
return nil, fmt.Errorf("ssh: no suitable DH group found for request min: %d, preferred: %d, max: %d",
|
||||||
|
req.MinBits, req.PreferredBits, req.MaxBits)
|
||||||
|
}
|
||||||
|
|
||||||
|
return best, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ package ssh
|
|||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -65,6 +66,88 @@ func TestKexes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestChooseDH(t *testing.T) {
|
||||||
|
oakley := map[int]string{
|
||||||
|
2048: oakleyGroup14,
|
||||||
|
3072: oakleyGroup15,
|
||||||
|
4096: oakleyGroup16,
|
||||||
|
}
|
||||||
|
expected := func(size int) *big.Int {
|
||||||
|
hex, ok := oakley[size]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("test setup: no Oakley group for size %d", size)
|
||||||
|
}
|
||||||
|
p, _ := new(big.Int).SetString(hex, 16)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
request kexDHGexRequestMsg
|
||||||
|
want int // expected bit size; 0 means error expected
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Standard 2048 request",
|
||||||
|
request: kexDHGexRequestMsg{MinBits: 1024, PreferredBits: 2048, MaxBits: 8192},
|
||||||
|
want: 2048,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Standard 3072 request",
|
||||||
|
request: kexDHGexRequestMsg{MinBits: 1024, PreferredBits: 3072, MaxBits: 8192},
|
||||||
|
want: 3072,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Standard 4096 request",
|
||||||
|
request: kexDHGexRequestMsg{MinBits: 1024, PreferredBits: 4096, MaxBits: 8192},
|
||||||
|
want: 4096,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Preferred 2500 -> Expect 3072 (round up)",
|
||||||
|
request: kexDHGexRequestMsg{MinBits: 1024, PreferredBits: 2500, MaxBits: 8192},
|
||||||
|
want: 3072,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Preferred 3500 -> Expect 4096 (round up)",
|
||||||
|
request: kexDHGexRequestMsg{MinBits: 1024, PreferredBits: 3500, MaxBits: 8192},
|
||||||
|
want: 4096,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Preferred too high (8000) -> Expect 4096 (cap at max available)",
|
||||||
|
request: kexDHGexRequestMsg{MinBits: 1024, PreferredBits: 8000, MaxBits: 8192},
|
||||||
|
want: 4096,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No group in range",
|
||||||
|
request: kexDHGexRequestMsg{MinBits: 2500, PreferredBits: 2500, MaxBits: 2900},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := chooseDH(tt.request)
|
||||||
|
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("chooseDH() error = %v, wantErr %t", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.wantErr {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got == nil {
|
||||||
|
t.Fatalf("chooseDH() returned nil big.Int but expected success")
|
||||||
|
}
|
||||||
|
if got.BitLen() != tt.want {
|
||||||
|
t.Errorf("chooseDH() got size = %d, want %d", got.BitLen(), tt.want)
|
||||||
|
}
|
||||||
|
if want := expected(tt.want); got.Cmp(want) != 0 {
|
||||||
|
t.Errorf("chooseDH() returned the wrong group for size %d", tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkKexes(b *testing.B) {
|
func BenchmarkKexes(b *testing.B) {
|
||||||
type kexResultErr struct {
|
type kexResultErr struct {
|
||||||
result *kexResult
|
result *kexResult
|
||||||
|
|||||||
+123
-13
@@ -76,7 +76,7 @@ func parsePubKey(in []byte, algo string) (pubKey PublicKey, rest []byte, err err
|
|||||||
case InsecureKeyAlgoDSA:
|
case InsecureKeyAlgoDSA:
|
||||||
return parseDSA(in)
|
return parseDSA(in)
|
||||||
case KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521:
|
case KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521:
|
||||||
return parseECDSA(in)
|
return parseECDSA(in, algo)
|
||||||
case KeyAlgoSKECDSA256:
|
case KeyAlgoSKECDSA256:
|
||||||
return parseSKECDSA(in)
|
return parseSKECDSA(in)
|
||||||
case KeyAlgoED25519:
|
case KeyAlgoED25519:
|
||||||
@@ -182,14 +182,19 @@ func ParseKnownHosts(in []byte) (marker string, hosts []string, pubKey PublicKey
|
|||||||
}
|
}
|
||||||
|
|
||||||
hosts := string(keyFields[0])
|
hosts := string(keyFields[0])
|
||||||
// keyFields[1] contains the key type (e.g. “ssh-rsa”).
|
// keyFields[1] contains the key type (e.g. "ssh-rsa"). This information
|
||||||
// However, that information is duplicated inside the
|
// is duplicated within the base64-encoded key blob. As OpenSSH's
|
||||||
// base64-encoded key and so is ignored here.
|
// sshkey_read does, we verify that the declared key type matches the
|
||||||
|
// type embedded in the key blob.
|
||||||
|
wantType := string(keyFields[1])
|
||||||
|
|
||||||
key := bytes.Join(keyFields[2:], []byte(" "))
|
key := bytes.Join(keyFields[2:], []byte(" "))
|
||||||
if pubKey, comment, err = parseAuthorizedKey(key); err != nil {
|
if pubKey, comment, err = parseAuthorizedKey(key); err != nil {
|
||||||
return "", nil, nil, "", nil, err
|
return "", nil, nil, "", nil, err
|
||||||
}
|
}
|
||||||
|
if pubKey.Type() != wantType {
|
||||||
|
return "", nil, nil, "", nil, fmt.Errorf("ssh: known hosts key type mismatch: human-readable type %q, encoded type %q", wantType, pubKey.Type())
|
||||||
|
}
|
||||||
|
|
||||||
return marker, strings.Split(hosts, ","), pubKey, comment, rest, nil
|
return marker, strings.Split(hosts, ","), pubKey, comment, rest, nil
|
||||||
}
|
}
|
||||||
@@ -228,10 +233,17 @@ func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []str
|
|||||||
}
|
}
|
||||||
|
|
||||||
if out, comment, err = parseAuthorizedKey(in[i:]); err == nil {
|
if out, comment, err = parseAuthorizedKey(in[i:]); err == nil {
|
||||||
return out, comment, options, rest, nil
|
// The first field contains the declared key type. As OpenSSH's
|
||||||
} else {
|
// sshkey_read does, we verify that it matches the type embedded in
|
||||||
lastErr = err
|
// the key blob. Without this check, a single-token option (e.g.
|
||||||
|
// "restrict") appearing in the key type position could be silently
|
||||||
|
// discarded along with its intended effect.
|
||||||
|
if string(in[:i]) == out.Type() {
|
||||||
|
return out, comment, options, rest, nil
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("ssh: authorized keys key type mismatch: human-readable type %q, encoded type %q", in[:i], out.Type())
|
||||||
}
|
}
|
||||||
|
lastErr = err
|
||||||
|
|
||||||
// No key type recognised. Maybe there's an options field at
|
// No key type recognised. Maybe there's an options field at
|
||||||
// the beginning.
|
// the beginning.
|
||||||
@@ -271,11 +283,15 @@ func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []str
|
|||||||
}
|
}
|
||||||
|
|
||||||
if out, comment, err = parseAuthorizedKey(in[i:]); err == nil {
|
if out, comment, err = parseAuthorizedKey(in[i:]); err == nil {
|
||||||
options = candidateOptions
|
// As above, the declared key type (here following the options
|
||||||
return out, comment, options, rest, nil
|
// field) must match the type embedded in the key blob.
|
||||||
} else {
|
if string(in[:i]) == out.Type() {
|
||||||
lastErr = err
|
options = candidateOptions
|
||||||
|
return out, comment, options, rest, nil
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("ssh: authorized keys key type mismatch: human-readable type %q, encoded type %q", in[:i], out.Type())
|
||||||
}
|
}
|
||||||
|
lastErr = err
|
||||||
|
|
||||||
in = rest
|
in = rest
|
||||||
continue
|
continue
|
||||||
@@ -469,6 +485,13 @@ func parseRSA(in []byte) (out PublicKey, rest []byte, err error) {
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 16384 bits is the largest RSA key OpenSSH will generate (ssh-keygen
|
||||||
|
// caps -b at 16384), so it is the practical upper bound for keys seen on
|
||||||
|
// the wire. Rejecting anything larger bounds the CPU spent verifying an
|
||||||
|
// attacker-supplied key and signature, mitigating a denial of service.
|
||||||
|
if w.N.BitLen() > 16384 {
|
||||||
|
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 +597,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 +637,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,
|
||||||
@@ -774,7 +823,7 @@ func supportedEllipticCurve(curve elliptic.Curve) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parseECDSA parses an ECDSA key according to RFC 5656, section 3.1.
|
// parseECDSA parses an ECDSA key according to RFC 5656, section 3.1.
|
||||||
func parseECDSA(in []byte) (out PublicKey, rest []byte, err error) {
|
func parseECDSA(in []byte, expectedType string) (out PublicKey, rest []byte, err error) {
|
||||||
var w struct {
|
var w struct {
|
||||||
Curve string
|
Curve string
|
||||||
KeyBytes []byte
|
KeyBytes []byte
|
||||||
@@ -785,6 +834,12 @@ func parseECDSA(in []byte) (out PublicKey, rest []byte, err error) {
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actualType := "ecdsa-sha2-" + w.Curve
|
||||||
|
if expectedType != actualType {
|
||||||
|
return nil, nil, fmt.Errorf("ssh: algorithm type mismatch: expected %q, found curve %q (type %q)",
|
||||||
|
expectedType, w.Curve, actualType)
|
||||||
|
}
|
||||||
|
|
||||||
key := new(ecdsa.PublicKey)
|
key := new(ecdsa.PublicKey)
|
||||||
|
|
||||||
switch w.Curve {
|
switch w.Curve {
|
||||||
@@ -869,11 +924,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 +1028,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 +1065,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 +1143,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
|
||||||
@@ -1408,6 +1489,17 @@ func passphraseProtectedOpenSSHKey(passphrase []byte) openSSHDecryptFunc {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenSSH does not impose an upper bound on the bcrypt round count
|
||||||
|
// stored in the key file, but bcrypt_pbkdf cost is linear in rounds:
|
||||||
|
// the default is 16, ssh-keygen lets users pick anything up to
|
||||||
|
// INT_MAX. Cap at 2048 (128x the default, a few seconds of CPU) so
|
||||||
|
// that an oversized value in the file cannot tie up the caller for
|
||||||
|
// months.
|
||||||
|
const maxRounds = 1 << 11
|
||||||
|
if opts.Rounds > maxRounds {
|
||||||
|
return nil, fmt.Errorf("ssh: bcrypt KDF rounds %d exceed maximum %d", opts.Rounds, maxRounds)
|
||||||
|
}
|
||||||
|
|
||||||
k, err := bcrypt_pbkdf.Key(passphrase, []byte(opts.Salt), int(opts.Rounds), 32+16)
|
k, err := bcrypt_pbkdf.Key(passphrase, []byte(opts.Salt), int(opts.Rounds), 32+16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1577,10 +1669,28 @@ func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.Priv
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mirror the validation done in parseRSA for public keys: cap the
|
||||||
|
// modulus at the OpenSSH-generated maximum, reject oversized or
|
||||||
|
// invalid exponents, and additionally bound the prime factors to
|
||||||
|
// avoid the expensive CRT coefficient recomputation in pk.Precompute.
|
||||||
|
if key.N.BitLen() > 16384 {
|
||||||
|
return nil, errors.New("ssh: rsa modulus too large")
|
||||||
|
}
|
||||||
|
if key.P.BitLen() > 8192 || key.Q.BitLen() > 8192 {
|
||||||
|
return nil, errors.New("ssh: rsa prime too large")
|
||||||
|
}
|
||||||
|
if key.E.BitLen() > 24 {
|
||||||
|
return nil, errors.New("ssh: exponent too large")
|
||||||
|
}
|
||||||
|
e := key.E.Int64()
|
||||||
|
if e < 3 || e&1 == 0 {
|
||||||
|
return nil, errors.New("ssh: incorrect exponent")
|
||||||
|
}
|
||||||
|
|
||||||
pk := &rsa.PrivateKey{
|
pk := &rsa.PrivateKey{
|
||||||
PublicKey: rsa.PublicKey{
|
PublicKey: rsa.PublicKey{
|
||||||
N: key.N,
|
N: key.N,
|
||||||
E: int(key.E.Int64()),
|
E: int(e),
|
||||||
},
|
},
|
||||||
D: key.D,
|
D: key.D,
|
||||||
Primes: []*big.Int{key.P, key.Q},
|
Primes: []*big.Int{key.P, key.Q},
|
||||||
|
|||||||
+720
-13
@@ -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,221 @@ func TestParseRSAPrivateKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseRSAModulusLimit(t *testing.T) {
|
||||||
|
// A 16384-bit modulus is the largest OpenSSH will generate and must be
|
||||||
|
// accepted.
|
||||||
|
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=="
|
||||||
|
if _, _, _, _, err := ParseAuthorizedKey([]byte(rsa16384)); err != nil {
|
||||||
|
t.Fatalf("ParseAuthorizedKey rejected a 16384-bit modulus: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A modulus larger than 16384 bits must be rejected to bound the cost of
|
||||||
|
// verifying an attacker-supplied key.
|
||||||
|
n := new(big.Int).Lsh(big.NewInt(1), 16384) // 16385 bits
|
||||||
|
pub, err := NewPublicKey(&rsa.PublicKey{N: n, E: 65537})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewPublicKey: %v", err)
|
||||||
|
}
|
||||||
|
_, err = ParsePublicKey(pub.Marshal())
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("ParsePublicKey accepted a modulus larger than 16384 bits; 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 TestParsePrivateKeyRSAModulus16384(t *testing.T) {
|
||||||
|
rsa16384 := []byte(`-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAIFwAAAAdzc2gtcn
|
||||||
|
NhAAAAAwEAAQAACAEAy58lBbrFRB4r18MqVA625r4nzxjTkMlN5LxoG7ARC9L7px5lPs4o
|
||||||
|
GyOrs/03MRRbBL0GOVVSBqhel5kzJTRN1cwLEBYliiQBB9XVcbPavAbZnrxCVIvGBT63D1
|
||||||
|
nnMoBgI42Y49rTHePNLcVkNVvj+dHHwf1kiBijNYOrIvcuTqt6upAmSzPH7f3s4YTSMpUj
|
||||||
|
KJy2+8n0OS42Oz0jZFoBBBoOSaalYEKMmcfJE29hLhOiSYoDZqJJsCVSCOVC3RjYEc+Cj1
|
||||||
|
uJCNiCkI3pymbCMc7bGE6Oc4FHzdCs8ck16VFwM/VI50I2I9ADR5UXbjTEuGq+gprUO5Oc
|
||||||
|
7NbXGaqcgfOCywWjpRsGd3ORs5o30/1Jk1d5b8l1fuwpJuvIbqunvqM0AVe6STRTKjn0PE
|
||||||
|
cSJ9Z/RKIq06BxKN1wqx0WC611ikyLZ5aNLCjRF0ER3b7T2eFr9ib1d8ngSPpz8oFoQnL0
|
||||||
|
/+umKRD+iIqAwzvoljC2DQTxOP+b+2TxkI0lpdU/uzIQmkTRMHXMqKxHr5o6dRtggv8Gov
|
||||||
|
Ednqu7mjxX9t2I0rz6ZAnp4xv7fwe4OWBqP/or1QyW+RGk+x9HOacv1CfIFdndetHbfM6n
|
||||||
|
q7TWbyrdOl4zj6IA+CU5gJdISCpXoQkDNz1TbuoNlAzeg2IUyPchWVf2nm4v6a5fzggCBK
|
||||||
|
/nTtU6O2bvyccr1fpbcyVKKi4aG+hE9pY1Vc45c4ucxqYzBog5An/wpcvVnbm5KkmV+Ok9
|
||||||
|
K89qZdVheGqyzjSIw8ww7EiX0Ybp3eHWJAR6fR5jUk6uJZi2xPzHpB5MHTvilnBlSQ1Ew7
|
||||||
|
tjsd1z0NM5fb4PznwJxE3c4WB2nMKw2qcE552f5h6htNISmo+gjWmKSjXeCdSKdlFdA6rH
|
||||||
|
RtNQg3MnwIFIezfj4KnK78YcBOTHfDnPVChGgSxJ57K56OiVIPgxXjiowfvY91aJX+arC7
|
||||||
|
9lCPrvIZyq2PHIdbssEzLY5ECgS5RA3z8pvXbxOAJOt0kXnJi2BL6puPjd1+1FQgoXObak
|
||||||
|
Uk7BEVZG4s2mq5nCNN41o30DRanrGJO1jUrC5tB6tpzOLPlv9QgwUvnhzK/vZFiiYkttja
|
||||||
|
FkZyZyEPfmEDBpdFH1DgOVh77Eq/mKClH79aa6xUNTtqd3Qby5Oqiq3dJb7RkGEIn69iv+
|
||||||
|
XHuTYhNYPHTwPZVuN324MdMoqiWOPHWR9Vvlc167tKYelR/UGeBildWcP94cIofaulF8kt
|
||||||
|
XLG6UQAzV5aStC50dHLD0RzMvQXx/UXqd5leIO4o6TQ4tQWhYf13sfisfAZ2us3WQnErl9
|
||||||
|
+j/tIBoPvCQgbFid6iWyq4Ou3L60JIdV9dvRstNZ38PEfx0ScpZbcUO87dQA2bZ+ECGMGF
|
||||||
|
fEVJTnm/R4HYwWmajvVCCNqnSPCW0lCJdVn+AVZBtnqDaH7taZUeQeaEfuNqWMOcqUNaYL
|
||||||
|
HPC+Yuzz3gMjLWZwSY5n0xOVHAIAXH4P+y9aCyM6VwJ8/k6laH3w+V+UUtfVX/w6M9efzH
|
||||||
|
VJfQ0onk8LwA0OhZ2OXb92eCWC7us+ebw/AoXi0ugBwVxSllxuV2ZbiJINkN4Ryx059/RO
|
||||||
|
8rt9lrCRa/I++BX/7ZeMtGyc2sNo20tWH0t+3Q4+viJerUcVrTk9XN45YfRaQCUCios04r
|
||||||
|
EPLKx5Q67WInstEAyOUoa0BIHb/CI0JA6/atwA8eg4NvdUf1VWf+BQ3vOxqmw3eFou97lV
|
||||||
|
JmLRugqleuCYTL1vxb3hQrZlnHwAJLt9WvBd6tqHQpf/juYQEVKW11eQE2+6fDsQHtmZGa
|
||||||
|
yzwseoMV2UBJ5AuuP5X7fofEwdVgmYaqQ8amF26KaRGfMWiFb9hIR1+dhvxDW+oDzzTzoJ
|
||||||
|
YF7xl3F5eViRo6zYODVHhgaE843Pz8Q6RmQ4RIYEkaPID9VVtoYZmzRduYWQuIghiOzMhj
|
||||||
|
pnNwcJudLmNsSgpiNUfbvCPfgt06TVpcaK1sPaeTEwnQ8aOJoPKG7HM5yYwuQ3AWwjTmCO
|
||||||
|
OuEVS550ALAeXPD0e/aBnlDfZ+6yktKRhK7lq36/KeaiRP6MWr+il4bkbgLMOdO0HFAkTd
|
||||||
|
1ZBlW/vUnGwDl9mxfVxwPMlQxc4aL5ovXHSN+Vwu9oIk8fcS8Snp7/ftvsUOpCLafVnKdV
|
||||||
|
Pw30qRHLT8Xo+tPy57wuZMac3na/xe3HCtWulspDRWVVIbVKQADlgRThCT44hlr+80ePAD
|
||||||
|
r0x5iZPTp85KYGwcPGoUFzJ8vzrHAg7wF3yan837T2syz3/MX1bLyFmPUPdZ5BOPzw1Ctp
|
||||||
|
6TEdibD1/DbW2s7tY7FGtJh+luiFXLZZhYud2+3kdUUFukEHJII2IrYWLkpBHcwTj7A0UP
|
||||||
|
aCFS0ryXW3AiWMayNjw3qULbuOv/HB0Px7QNOTkos8TywMBqdcITz1o5uk7QVRcZ3V47CS
|
||||||
|
5t8aNLdjanQoDX+XXdo8SSGd7LqIWLRjNRQTKNv6D0+WIfbFnOMZYwORALX9bpsushaGJg
|
||||||
|
tYxInvSVdcYER34gee5kpcPAzWt8mdNMOZDatmi67B5y0oZh6tUZZpWc5ej+SVfxAJJfB7
|
||||||
|
ajiwWrtMKPCF1DupOJ99jo57UvcVyGmwVzuKbCthWCobwK5UtYhnIzQci/a5j1sZqUYt+n
|
||||||
|
hRW2LX5nG9XlL923aXhbcnUHQfdQikH/ON4KPZCanAkQGN3vb9TPrOtDDRWNgC5oZXOzZB
|
||||||
|
YqzrXChLsnFTi+TcIvkAABxA2VU8G9lVPBsAAAAHc3NoLXJzYQAACAEAy58lBbrFRB4r18
|
||||||
|
MqVA625r4nzxjTkMlN5LxoG7ARC9L7px5lPs4oGyOrs/03MRRbBL0GOVVSBqhel5kzJTRN
|
||||||
|
1cwLEBYliiQBB9XVcbPavAbZnrxCVIvGBT63D1nnMoBgI42Y49rTHePNLcVkNVvj+dHHwf
|
||||||
|
1kiBijNYOrIvcuTqt6upAmSzPH7f3s4YTSMpUjKJy2+8n0OS42Oz0jZFoBBBoOSaalYEKM
|
||||||
|
mcfJE29hLhOiSYoDZqJJsCVSCOVC3RjYEc+Cj1uJCNiCkI3pymbCMc7bGE6Oc4FHzdCs8c
|
||||||
|
k16VFwM/VI50I2I9ADR5UXbjTEuGq+gprUO5Oc7NbXGaqcgfOCywWjpRsGd3ORs5o30/1J
|
||||||
|
k1d5b8l1fuwpJuvIbqunvqM0AVe6STRTKjn0PEcSJ9Z/RKIq06BxKN1wqx0WC611ikyLZ5
|
||||||
|
aNLCjRF0ER3b7T2eFr9ib1d8ngSPpz8oFoQnL0/+umKRD+iIqAwzvoljC2DQTxOP+b+2Tx
|
||||||
|
kI0lpdU/uzIQmkTRMHXMqKxHr5o6dRtggv8GovEdnqu7mjxX9t2I0rz6ZAnp4xv7fwe4OW
|
||||||
|
BqP/or1QyW+RGk+x9HOacv1CfIFdndetHbfM6nq7TWbyrdOl4zj6IA+CU5gJdISCpXoQkD
|
||||||
|
Nz1TbuoNlAzeg2IUyPchWVf2nm4v6a5fzggCBK/nTtU6O2bvyccr1fpbcyVKKi4aG+hE9p
|
||||||
|
Y1Vc45c4ucxqYzBog5An/wpcvVnbm5KkmV+Ok9K89qZdVheGqyzjSIw8ww7EiX0Ybp3eHW
|
||||||
|
JAR6fR5jUk6uJZi2xPzHpB5MHTvilnBlSQ1Ew7tjsd1z0NM5fb4PznwJxE3c4WB2nMKw2q
|
||||||
|
cE552f5h6htNISmo+gjWmKSjXeCdSKdlFdA6rHRtNQg3MnwIFIezfj4KnK78YcBOTHfDnP
|
||||||
|
VChGgSxJ57K56OiVIPgxXjiowfvY91aJX+arC79lCPrvIZyq2PHIdbssEzLY5ECgS5RA3z
|
||||||
|
8pvXbxOAJOt0kXnJi2BL6puPjd1+1FQgoXObakUk7BEVZG4s2mq5nCNN41o30DRanrGJO1
|
||||||
|
jUrC5tB6tpzOLPlv9QgwUvnhzK/vZFiiYkttjaFkZyZyEPfmEDBpdFH1DgOVh77Eq/mKCl
|
||||||
|
H79aa6xUNTtqd3Qby5Oqiq3dJb7RkGEIn69iv+XHuTYhNYPHTwPZVuN324MdMoqiWOPHWR
|
||||||
|
9Vvlc167tKYelR/UGeBildWcP94cIofaulF8ktXLG6UQAzV5aStC50dHLD0RzMvQXx/UXq
|
||||||
|
d5leIO4o6TQ4tQWhYf13sfisfAZ2us3WQnErl9+j/tIBoPvCQgbFid6iWyq4Ou3L60JIdV
|
||||||
|
9dvRstNZ38PEfx0ScpZbcUO87dQA2bZ+ECGMGFfEVJTnm/R4HYwWmajvVCCNqnSPCW0lCJ
|
||||||
|
dVn+AVZBtnqDaH7taZUeQeaEfuNqWMOcqUNaYLHPC+Yuzz3gMjLWZwSY5n0xOVHAIAXH4P
|
||||||
|
+y9aCyM6VwJ8/k6laH3w+V+UUtfVX/w6M9efzHVJfQ0onk8LwA0OhZ2OXb92eCWC7us+eb
|
||||||
|
w/AoXi0ugBwVxSllxuV2ZbiJINkN4Ryx059/RO8rt9lrCRa/I++BX/7ZeMtGyc2sNo20tW
|
||||||
|
H0t+3Q4+viJerUcVrTk9XN45YfRaQCUCios04rEPLKx5Q67WInstEAyOUoa0BIHb/CI0JA
|
||||||
|
6/atwA8eg4NvdUf1VWf+BQ3vOxqmw3eFou97lVJmLRugqleuCYTL1vxb3hQrZlnHwAJLt9
|
||||||
|
WvBd6tqHQpf/juYQEVKW11eQE2+6fDsQHtmZGayzwseoMV2UBJ5AuuP5X7fofEwdVgmYaq
|
||||||
|
Q8amF26KaRGfMWiFb9hIR1+dhvxDW+oDzzTzoJYF7xl3F5eViRo6zYODVHhgaE843Pz8Q6
|
||||||
|
RmQ4RIYEkaPID9VVtoYZmzRduYWQuIghiOzMhjpnNwcJudLmNsSgpiNUfbvCPfgt06TVpc
|
||||||
|
aK1sPaeTEwnQ8aOJoPKG7HM5yYwuQ3AWwjTmCOOuEVS550ALAeXPD0e/aBnlDfZ+6yktKR
|
||||||
|
hK7lq36/KeaiRP6MWr+il4bkbgLMOdO0HFAkTd1ZBlW/vUnGwDl9mxfVxwPMlQxc4aL5ov
|
||||||
|
XHSN+Vwu9oIk8fcS8Snp7/ftvsUOpCLafVnKdVPw30qRHLT8Xo+tPy57wuZMac3na/xe3H
|
||||||
|
CtWulspDRWVVIbVKQADlgRThCT44hlr+80ePADr0x5iZPTp85KYGwcPGoUFzJ8vzrHAg7w
|
||||||
|
F3yan837T2syz3/MX1bLyFmPUPdZ5BOPzw1Ctp6TEdibD1/DbW2s7tY7FGtJh+luiFXLZZ
|
||||||
|
hYud2+3kdUUFukEHJII2IrYWLkpBHcwTj7A0UPaCFS0ryXW3AiWMayNjw3qULbuOv/HB0P
|
||||||
|
x7QNOTkos8TywMBqdcITz1o5uk7QVRcZ3V47CS5t8aNLdjanQoDX+XXdo8SSGd7LqIWLRj
|
||||||
|
NRQTKNv6D0+WIfbFnOMZYwORALX9bpsushaGJgtYxInvSVdcYER34gee5kpcPAzWt8mdNM
|
||||||
|
OZDatmi67B5y0oZh6tUZZpWc5ej+SVfxAJJfB7ajiwWrtMKPCF1DupOJ99jo57UvcVyGmw
|
||||||
|
VzuKbCthWCobwK5UtYhnIzQci/a5j1sZqUYt+nhRW2LX5nG9XlL923aXhbcnUHQfdQikH/
|
||||||
|
ON4KPZCanAkQGN3vb9TPrOtDDRWNgC5oZXOzZBYqzrXChLsnFTi+TcIvkAAAADAQABAAAI
|
||||||
|
AFiWz8OzY5nkSozf022ozTiMqMM4eOt4OZR3yA+rxW7Qhz5JQiFWDirolQ6E71tCEOt51d
|
||||||
|
hh34MYA7ePJqpcHDUVRgbkq8ZzLaOcC/YhGtxNWqbuHymreibUB079fVICelFdjJQto0ZQ
|
||||||
|
0vbD93ojlYceFvu2Y+O2XGOu+mkHA7Wkc4vxpUd4qtZHcKUZZV4udpJ3xEC9t6ydB2k0i0
|
||||||
|
5gviprr6WphC/iJEvPmRMElVI3ppa6HgqsNsUVJ6DJJhMNeQwerR3z5CXeFMgRhhLSLFEB
|
||||||
|
P19O5jkomPXZgTTcpsDw9pEUeXhr3SQtnw+otP30pVXa0zH9bLLS4SZFvmXjTZ5YNKJhvL
|
||||||
|
XbkS+tL0nlob5wZ29cUnApRR5IXwsY8CX+NsgBN2ISKfEpe7lWZ4VGIocEknBo4ZsbJcBy
|
||||||
|
v08jI3FHMWlPLiOOY7M/uuCUJdLE8GTN52u7vXY1dYgqtwFd/d9TJnalrrAVPbhoEedfDC
|
||||||
|
0z2jDF4rE6vEFexJ5wWl3Q7p5iBMkpgZ3E0prcAYBL6H0EwTOdAuUnZAyhiMhs0pSMA7g0
|
||||||
|
EfguO/zcMsossKD96pwVGrbheFm6rH25OQLDU1LJUAr5s5t47DZbrqVM0zKggomfbG1kPW
|
||||||
|
m4wFDLAN6s1V0xj52b583MtMWh57lflc1tf6vgUmLRa7UOcY4w+7fQVCF/MYugmFjAd7jr
|
||||||
|
6JerNJ4vWpqjrkVeSqwfQe2cY0QmEPMZlzwYL7niefjaUc4tH7ugtkV2Q0M01+hM+6gtQQ
|
||||||
|
d2sh5K4wp82Qj49XLMJAKFkw6/PpMK4xHHLJChwdnCMS3kjpx+0lkaESDUSUwkadHkO/pJ
|
||||||
|
CvLGMYA4uMIxDkLDAaZHdMboss/5ybBHJsH/lfP9hbHfL7KUaZRr27FbxJ68HiJp3QAG6/
|
||||||
|
TVyn4RQiV657OLb0GdGDeg7jKJFstXTXJ/qPLmUb279cR+eNwpfmdpCOskSZ/lCovCfcWC
|
||||||
|
/oUHzUdZOqRHRwYZ8+4DbqVAk8C4YzZ1VlOwJCVe2R3VlBlMTJiGdu18aBFD6SlJ8eacyd
|
||||||
|
q4hruvaxoG+ErEEOv9B9jmAU2r4SO+DlCcFHI1V2D3dn7A9T0wteQ1wHoERwJM6GJIvcim
|
||||||
|
06gaNRdPD0AHHGCrFKTmukGSrjw8le0az7fOpDBIEkiQZ4wQjAhIUa0o8pFr+yqqcc9JUR
|
||||||
|
+Baf77cKi+GCkDudH2/lf4clABKjMVGvF7J0krG4TJ/JGla6s+QqWbsHdBlDrhLAZulPgM
|
||||||
|
kdGUbEcesXSBY6Me2A42wOUexMFRPk9Dau0UG0AE8hfMx/O2XC2QWFflMiSSUiA304dv8p
|
||||||
|
xpdFecvho6OiPeYa7KyWw0+dgZ00a+fkUuV2+kIbbB3W6tu4R3sHshR1qJa49q5ZpOqQdq
|
||||||
|
yzvLSsWt3gDq2xwUYc5qVwhqMt0wEmax68qSZAyqmgwsOomfrpi4+20adOFImJ0q9jvHkk
|
||||||
|
Kq18x2iAu85HB3VYFU6dPe4WbzkYxk/ir72zFPNqlVDspVWGgCaLw8b/cPesPM03M67ebj
|
||||||
|
mlFL0bgD19qJyBnIzdeH4PUJafXw1a8/BepGOozAgXnzMgi0WmRIb+3Zf0YQ7Q5Wtu1DVX
|
||||||
|
rQ3/nz9L4DsdsVJzTwSKfS1xDnnWDuALa6ARAvRV2Y21CWLDMVCeFi/f+Y8EYljbhU63u7
|
||||||
|
KOKyG5jOr72qsjVwthfbYrCjZp0pRgksiDnzlZADXEWoe7Jbr8OR6rRZl8yFr0idknKQKU
|
||||||
|
kdAP34tWvcsi92kkwlF5l9kIdodzn3zRPIGoNllu7m/+hxqlYxOL5DWYEDOwDu3AvHnAgy
|
||||||
|
nVYVH1+/KV3jnqClH6xi5+sqLNoJMFjPF5bgT5ou/g6sSITVQXTEK+k3HWGJ699/ocmwW7
|
||||||
|
87om+9R+XINknHcfU6vCJmkNuVC/k7bPyBLzFrimLOob1GumYxlATh0/K9TV29DiqfagX+
|
||||||
|
+tPl5EwjTyrrOnT7PEbOF9EUaNEcODGeesFmOU6eVDfkGLOiffnD45PLzidpQ0dO/s69m/
|
||||||
|
RBzsUR+WsALG2aZciUweW5W7ER+d7H9HMWuEPueeQ52xn0SfQKo1yvrk/zWOnsG0CXH90W
|
||||||
|
uWm0qrzCOu2CqMaslGEGTV+IBIxQxbs42KKbhMnl6CsSqHJ0gJNe8akBojR6zaTQLQgqn0
|
||||||
|
ViQE/IopHxMucE7kI2tfZ/zujoB6aGNCkkrE6LCzi8f2iY3sw79dHO6ilSTbIZfJAszsfm
|
||||||
|
WLTRsi+y8/ZmUP+KbemU6XoIVfQ6XD6TATAlJX+Xqps/tDnK7RPBxhEvJaoGRRTHnPsxrH
|
||||||
|
KfmMgHoLmvLgvzwPXskwbij8IAuZTGilOjc9O38UF/byxtNlCiA6kswOUgOm0EOWFcZn1T
|
||||||
|
gl6pPSfHe8RnjBrdyAX2VOwYe71aESqJCVwcBxvLFj3YDbJIqHhvMQDdtoFaPOd7yPjkun
|
||||||
|
WeMlJO4MpoT5VCfVcIW3RP3hCwSpgonJ76Gjnd2yf87kpIYmWPOOVyS8CuOVxS24R4192q
|
||||||
|
hF9z+DWQb34dGEparBoZ2leK0Ex9ApMvg1naJINkSWPArFnikm5uA+bS6lQXEoQ4xJNHVs
|
||||||
|
lH8DG2D67UARTkErnZWiio3gIfdgqKkjdE7+ii918b12Qs9YrZi7i0pQtpfrtMbhBzD+uF
|
||||||
|
0HcEwi0O37c3DhVqNMAhu6Xm30PljXjjtZGedyO8V93C9VkA5E0D1VppVHAT+vM233IIX0
|
||||||
|
zlAAAEAG001autfUEdND18iVd4pEgjRMX2oOtUd/N/AQFFAGHu4U2bo29czCpKLX7J8m79
|
||||||
|
FcF0Q+jFLaK7VrVQmHa+3jKhYrpNjKj6+yx9XgRAQ5L/b+4hTSd8/J+vABvXhz00NgmYvk
|
||||||
|
WX0JLdi8FudfEIbTA9gOjgZNjmuxxT38ER8UYzFJts/4U9ZWbqn/7AZtBn95bKsvbVZkf9
|
||||||
|
CIyfJkOEuiUWj697d+hpHIspMSyBpLjjVsG+zRu9euhSHWbP3nHUERvuiOo5jyCwcFLKZn
|
||||||
|
ve+He4MX+np0PUm7Ug/Tsb8bouIysc+ncTBDuZXwRjf10Wlt1ZZI5UsmvImBLS7D18y4wd
|
||||||
|
zu1NzcHYRQ1BuiGSpvvy4xWo2szpmhYqu/8pBINgAVRyhHTJ5PwXKzULggC4L1gVAqLs6J
|
||||||
|
xUhlgsbm4CADpmVR6oy+8tzi8XHwu40nxTeWY5Cr75yio8+C/UwPiGKo9SSXaveXtBPLqB
|
||||||
|
RAMFupBQ3zIP9/BoNBjxZ8qINThI+qr7Q9jbKwLGVqAvGx5k4IEjQxTHdgvNQFJj47tKM+
|
||||||
|
6ClytN3CS+paNgT+UywJpLzyGppOZvuxDiq6WkF+DXd9KFefJG3XRHQEGcq0WKY4gkL8oO
|
||||||
|
CaVPbeciSOrxuudjun26oQurWyqstckbWDdtONwpLPECA1oRqfDzUicI6lTQIoWcPWrida
|
||||||
|
XAU85XWyxgkhVTYo3jPPoDurzwjeNE8CaPrD870FFBpyDkoSmh9S0qfuwxrZac1TKeqc2N
|
||||||
|
RtK0V1XYoHh4JDWZUiVy2jK/2CluVvLnDrf1V7Diz25B/HLFUiZ2uKGta+ccTZWMhDdnOH
|
||||||
|
d1+E7dlS623833w2P0c7BbEUbBWuUY3EOnUAz2mqdw12kaHLlSHUrHY1IBVQrmCn498Rr9
|
||||||
|
TbgT6sO5aTOROU8Y2iEaXXxzmCZYYKJA3Of3VXBXUb4CDx1aBdFm/PpvjI9S5qgEdgVwmK
|
||||||
|
9nT0Ag3mmnKpefdRirEbQUy9Cb6JaC+UXlQ8s0ToHs8Sm1LxbFtWnemJ0Sgcxjqv6BSDe/
|
||||||
|
GO+6bhXXwwNldR7NX8+3HS2cnEjEMjdOZChbwKCF+WCf7v0U6+fu9UvOibFGpDLAbeTKRv
|
||||||
|
dC1QZqGNvdadx0zB5mzlfZ6K+ANY9DPrKQjn3o2OrRYNDGM80yiIzyYMd4g+cSU81ge+s4
|
||||||
|
7UYBXRYVlB+yeFfmnyiVyf4SEaayr7TjUK60B+OwPPqTQAucEtc9TYKmQzVQzxmhTDW6k4
|
||||||
|
15DfIZfyMcPmWljoLq8dmLQl2I53jm0SQ4gQx7rYpOoJiVejkT2DCGHu2w47QWBYuEhkdv
|
||||||
|
rNniy5fZBw+9OBuIujsfEpxMGAQ58PmTH5+1KNqHL40AAAQBAOmmLGLXkJabeN8qcIOaTP
|
||||||
|
DY+c1++lOuMGpleUMH4fIP5EbLyYOldBFV5klrDKmCM1dc1/jUF/kppgMhOdr8XNhoBZNH
|
||||||
|
BrzibkbE41BI8XvqWNIT6UMrHxgnEsij/F9rVw75KMJu0oeZgmNaAg+hm8+nOwV7PL7/GG
|
||||||
|
IrbXPKpoTtKXob3w3ig1+AJBbz+tlyjL2sfjG8M+DUgaa8R4SCOk6msVWhUfGWHLXB/Cgl
|
||||||
|
xX5wqmSvmq8wxctvdSvJ/xL4tCV4SNvhbj4BKg+Tc9wpRR04r/8DFAb/e+bLao7v1sl0OV
|
||||||
|
65+qtsmng93d8jp0wSD9ZNHCplqdEixyEMMyrOOCFp3vPYunz3GMosDkciR8ClnxBc6kg4
|
||||||
|
bATgWUxV1lC+ycGttdTSRx583djPZ68a3IVIo01fmhIGioiYCpTyFHtlljOapO7Qe7wO7S
|
||||||
|
THnoT2bZDlSu2GIA9CXl8KHqOLGtF75tEfDW4yEg/LpyoMfNxjJqN0w6zMh6gI5N0F31BR
|
||||||
|
73BmjjJ1usQT8kJ3Kdaru+U9IxsQTyfk/cu1WiBrrB6FYQ7XMwKvXPyeijI9oHXDP2SZtV
|
||||||
|
ry7RwvMgEPLh5MjUX3y8nOWuqR0gkPVDtPiTQKogE07TXDyUVsIiNSlVEur5mpFWppC1nm
|
||||||
|
K1nPCC8Smr6d4pScg3UCLOR3ZJXyPtRSDn/n5q2eN2GymHcpvK6yx0RUVaXmw4iU9vNIOO
|
||||||
|
4s7fdEcpvDJog5TQHpWp3qP/S3Ug38xP56qN88+j01sa27PuNY7gF5UouewQjfvJ8xtCVF
|
||||||
|
vcVJ4XgxDAhdrptMX4MJPXpgS536f+8y03ahSJt+qzQacTfyzdJUDQ1jICD9UXrjSRw07F
|
||||||
|
/I4dvnm4rn9mJNGFUFDcnK4CD7/RwJRlAtJrUG7/eyxXLjrBcbP76CaERitVFgJuMwNSme
|
||||||
|
2QY2EXyUkQ+nwYJJt3Vjofo9UFWONWikQshnrKwebaCYwpK6U1A16cdPKFc8cUuUvMdEB3
|
||||||
|
Vb9Iq2LmxjbCYwGqTpUp2LE1VWIyuZgjI4lOTeegWlQ+7w3zEqxofO9fNy4KgrWUxBEh5V
|
||||||
|
z22mdB4/3So9Oq/zE1o4cpQwQFooa3OlC+A30JdRJZ9FAJDMQ49IOeki7xEKp5B+5omCcI
|
||||||
|
MlsmsEwj4/pSCjOBREos86rfNQMJ/N03kFl+dk0iE7NHxLo26nzAsCzV9kEK7R0SxtWZSC
|
||||||
|
HJbhMNoeNwQJPhEpKO5TKlnFDQ08VlSqIWTKc3yWqCZb68+c5AiMLfEqiI8pix/MKHkSKV
|
||||||
|
XRfgxwWVuli1UEhTRXm1ZSxVJaJLt0s6ePgEi7a8qRvep8uyh8/QgTUEW71CV9RpvklCm/
|
||||||
|
VSdgCrS/bwXo3YsAAAQBAN8ZoTRbrSIOXheb6KeZqNKJiiYjVYPHNoJYoVqtBNj6pPeNm9
|
||||||
|
iMeZhFs8TnHCYEcQ31+5LwCShFbKfaO+aBTHB57LpY+H8rG5OTdim+fhqH061pOTzDCZhQ
|
||||||
|
h7NUwIxK/hM0QBR/69cPOTFKSH0R997MAh/FougAgX5SwIp0rg6wFOG0sXla9oc3qZXuOE
|
||||||
|
7SGHIPe3ffQk70R8EdVTPuPATNe5a7qLUZEC6qf9nScpIyAEluCZF8VCf93oeqaTF3n/qG
|
||||||
|
gRnrj8S/zYdgFKONnZZL49NR4M1bQaOw0Ls2fR97rrWnqZFzevgZYSV7tgn+hf3JNo0nWm
|
||||||
|
LbubR8mLBUjx5u1nq1Od22TwUhS2eXXldPn0TLrofK1l0TzkwSsk4FypxNc/7uZVY0OJN+
|
||||||
|
FIQpeK4hVcbX/tIpC1gMXJVqybRRT+x7G2EC5CHLJFV+G0FwKawBqhnLD0wWPWY++fpQ5g
|
||||||
|
ZEWPu2cVZppC7MDSExum8+xQikprZdkOKBC5UGCqHfVXs60tcEHUB0VWWWVq0+WV8KH2XN
|
||||||
|
OONfl+dg5NdbgdLunAjD80oKh7rMRnJ686Sr3Jh5HSOnJ0QEZ+8G1Iz0vFWTHwAKtrzSBk
|
||||||
|
XsnnGbPZiFEybhNfae0VSgBgL6Ch/8yOsby3ZvGS1v1MY7XLM74o0IDHXPBzm4NoukM9dD
|
||||||
|
ELcLgYdDaX3RSq0U2FmyyU/lIaasOArSwf3bgAIFGQ8ux45LWW/vQ51aRCwiw6gwwCzHj5
|
||||||
|
bS4KfUTxX6E+tucHaq1S108E4UtkizFyyhNdeg49P1ljhGt0EfjIoHKJOevtH6sfvjt9U7
|
||||||
|
nx6vuAiyGOHb3Od2SzMYXM3evC102F2xGqsZpgAyQ/WVcj6kI4mCd0LoLsSqNUrcNaG93Z
|
||||||
|
ntt+lm3Wif11EZW37+Xe0TLg/Fi3VUaeju89grBiS4hvRBKz+yjiNwFJ8PvS47T91ORzTD
|
||||||
|
3SepgdMIi26umLMBqXfTKLnJvKmnYHdNfaaH5rW81EFh3xyPaoaatZ4vwJzj0IIQdeBIke
|
||||||
|
KiDfEjYAERQfXi/SG8cGONz/VWA97LroWuaNDa6YN1p6PzTUIEDTHdz5a1jnA7zApeHU7Q
|
||||||
|
EO0zINv0O0TXVDYPWjB6Wysnw7hIFg5HOAUz5DamkEWCifKQ0MH9dgKb9BiFPxwuvWrBIR
|
||||||
|
hqB5YPvUvyz8aYVm+RdbjMV0jCyUMxNe74/o02TrXbj5u50H2KfqD3qjFxu4BXzsw8sXBM
|
||||||
|
e0IdOmYjTAYEpHQRqNLhKdIcycTkXZ+K4SZwmK9VLI0ggn6NwSR8H1hZ7+GMnSTLtPm2u2
|
||||||
|
OthateBQGfnsGpTLxkpydh4jlYzTCp9bVRK8YXwsocglOcaRQH5ghfmgsAAAALbmljb2xh
|
||||||
|
QHAxNnM=
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
|
`)
|
||||||
|
// A 16384-bit modulus is the largest OpenSSH will generate and must be
|
||||||
|
// accepted.
|
||||||
|
if _, err := ParseRawPrivateKey(rsa16384); err != nil {
|
||||||
|
t.Fatalf("ParseRawPrivateKey rejected a 16384-bit modulus: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseECPrivateKey(t *testing.T) {
|
func TestParseECPrivateKey(t *testing.T) {
|
||||||
key := testPrivateKeys["ecdsa"]
|
key := testPrivateKeys["ecdsa"]
|
||||||
|
|
||||||
@@ -272,18 +489,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) {
|
||||||
@@ -296,6 +513,38 @@ func TestParseEncryptedPrivateKeysWithIncorrectPassphrase(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseEncryptedPrivateKeyExcessiveBcryptRounds(t *testing.T) {
|
||||||
|
// Craft a minimal openssh-key-v1 blob whose KdfOpts declares a bcrypt
|
||||||
|
// round count above the accepted maximum. The check must reject the file
|
||||||
|
// before bcrypt_pbkdf is invoked, so the rest of the blob (public key,
|
||||||
|
// encrypted body) can be empty.
|
||||||
|
kdfOpts := Marshal(struct {
|
||||||
|
Salt []byte
|
||||||
|
Rounds uint32
|
||||||
|
}{
|
||||||
|
Salt: []byte("salt-not-used"),
|
||||||
|
Rounds: (1 << 11) + 1,
|
||||||
|
})
|
||||||
|
header := Marshal(openSSHEncryptedPrivateKey{
|
||||||
|
CipherName: "aes256-ctr",
|
||||||
|
KdfName: "bcrypt",
|
||||||
|
KdfOpts: string(kdfOpts),
|
||||||
|
NumKeys: 1,
|
||||||
|
})
|
||||||
|
pemBytes := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "OPENSSH PRIVATE KEY",
|
||||||
|
Bytes: append([]byte(privateKeyAuthMagic), header...),
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := ParseRawPrivateKeyWithPassphrase(pemBytes, []byte("password"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for excessive bcrypt rounds, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "bcrypt KDF rounds") {
|
||||||
|
t.Errorf("got error %q, want substring %q", err.Error(), "bcrypt KDF rounds")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseDSA(t *testing.T) {
|
func TestParseDSA(t *testing.T) {
|
||||||
// We actually exercise the ParsePrivateKey codepath here, as opposed to
|
// We actually exercise the ParsePrivateKey codepath here, as opposed to
|
||||||
// using the ParseRawPrivateKey+NewSignerFromKey path that testdata_test.go
|
// using the ParseRawPrivateKey+NewSignerFromKey path that testdata_test.go
|
||||||
@@ -356,6 +605,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
|
||||||
@@ -540,6 +855,65 @@ func TestInvalidEntry(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthorizedKeyOptionWithoutKeyType(t *testing.T) {
|
||||||
|
_, pubSerialized := getTestKey()
|
||||||
|
for _, opt := range []string{"restrict", "no-pty", "no-port-forwarding", "no-agent-forwarding"} {
|
||||||
|
// The key type ("ssh-rsa") is intentionally omitted.
|
||||||
|
line := opt + " " + pubSerialized
|
||||||
|
testAuthorizedKeys(t, []byte(line), []testAuthResult{
|
||||||
|
{nil, nil, "", "", false},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorizedKeyTypeMismatch(t *testing.T) {
|
||||||
|
_, pubSerialized := getTestKey() // an ssh-rsa key
|
||||||
|
lines := []string{
|
||||||
|
"ssh-ed25519 " + pubSerialized + " user@host",
|
||||||
|
"restrict ssh-ed25519 " + pubSerialized + " user@host",
|
||||||
|
"rsa-sha2-512 " + pubSerialized + " user@host",
|
||||||
|
}
|
||||||
|
for _, line := range lines {
|
||||||
|
testAuthorizedKeys(t, []byte(line), []testAuthResult{
|
||||||
|
{nil, nil, "", "", false},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorizedKeyCertificate(t *testing.T) {
|
||||||
|
certLine := testdata.SSHCertificates["rsa-user-testcertificate"]
|
||||||
|
|
||||||
|
key, _, options, _, err := ParseAuthorizedKey(certLine)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseAuthorizedKey on certificate: %v", err)
|
||||||
|
}
|
||||||
|
if _, ok := key.(*Certificate); !ok {
|
||||||
|
t.Fatalf("got %T, want *Certificate", key)
|
||||||
|
}
|
||||||
|
if len(options) != 0 {
|
||||||
|
t.Errorf("got options %v, want none", options)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, _, options, _, err = ParseAuthorizedKey(append([]byte("cert-authority "), certLine...))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseAuthorizedKey on cert-authority certificate: %v", err)
|
||||||
|
}
|
||||||
|
if _, ok := key.(*Certificate); !ok {
|
||||||
|
t.Fatalf("got %T, want *Certificate", key)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(options, []string{"cert-authority"}) {
|
||||||
|
t.Errorf("got options %v, want [cert-authority]", options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorizedKeyOptionWithKeyType(t *testing.T) {
|
||||||
|
pub, pubSerialized := getTestKey()
|
||||||
|
line := "restrict ssh-rsa " + pubSerialized + " user@host"
|
||||||
|
testAuthorizedKeys(t, []byte(line), []testAuthResult{
|
||||||
|
{pub, []string{"restrict"}, "user@host", "", true},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var knownHostsParseTests = []struct {
|
var knownHostsParseTests = []struct {
|
||||||
input string
|
input string
|
||||||
err string
|
err string
|
||||||
@@ -619,6 +993,13 @@ var knownHostsParseTests = []struct {
|
|||||||
"@marker \tlocalhost,[host2:123]\tssh-rsa aabbccdd",
|
"@marker \tlocalhost,[host2:123]\tssh-rsa aabbccdd",
|
||||||
"short read",
|
"short read",
|
||||||
|
|
||||||
|
"", "", nil, "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Declared key type does not match the type embedded in the blob.
|
||||||
|
"localhost ssh-ed25519 {RSAPUB}",
|
||||||
|
"key type mismatch",
|
||||||
|
|
||||||
"", "", nil, "",
|
"", "", nil, "",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -633,7 +1014,7 @@ func TestKnownHostsParsing(t *testing.T) {
|
|||||||
input := test.input
|
input := test.input
|
||||||
if strings.Contains(input, rsaKeyToken) {
|
if strings.Contains(input, rsaKeyToken) {
|
||||||
expectedKey = rsaPub
|
expectedKey = rsaPub
|
||||||
input = strings.Replace(test.input, rsaKeyToken, rsaPubSerialized, -1)
|
input = strings.ReplaceAll(test.input, rsaKeyToken, rsaPubSerialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
marker, hosts, pubKey, comment, rest, err := ParseKnownHosts([]byte(input))
|
marker, hosts, pubKey, comment, rest, err := ParseKnownHosts([]byte(input))
|
||||||
@@ -760,6 +1141,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 {
|
||||||
@@ -863,3 +1534,39 @@ cLYUOHfQDw==
|
|||||||
t.Fatal("parsing an SSH certificate using another certificate as signature key succeeded; expected failure")
|
t.Fatal("parsing an SSH certificate using another certificate as signature key succeeded; expected failure")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseECDSAAlgorithmMismatch(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
keyName string // key fixture in testPublicKeys
|
||||||
|
nativeAlgo string // algorithm actually carried in the key blob
|
||||||
|
askedAlgo string // algorithm passed to parsePubKey
|
||||||
|
}{
|
||||||
|
{"ecdsap256", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"},
|
||||||
|
{"ecdsap256", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp521"},
|
||||||
|
{"ecdsap384", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp256"},
|
||||||
|
{"ecdsap384", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521"},
|
||||||
|
{"ecdsap521", "ecdsa-sha2-nistp521", "ecdsa-sha2-nistp256"},
|
||||||
|
{"ecdsap521", "ecdsa-sha2-nistp521", "ecdsa-sha2-nistp384"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.nativeAlgo+"_as_"+tc.askedAlgo, func(t *testing.T) {
|
||||||
|
pubKey := testPublicKeys[tc.keyName]
|
||||||
|
algo, in, ok := parseString(pubKey.Marshal())
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("unable to parse public key wire format")
|
||||||
|
}
|
||||||
|
if string(algo) != tc.nativeAlgo {
|
||||||
|
t.Fatalf("test setup failed: expected %q, got %q", tc.nativeAlgo, algo)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := parsePubKey(in, tc.askedAlgo)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error due to algorithm mismatch, but got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "algorithm type mismatch") {
|
||||||
|
t.Fatalf("unexpected error message: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -142,6 +142,15 @@ func keyEq(a, b ssh.PublicKey) bool {
|
|||||||
return bytes.Equal(a.Marshal(), b.Marshal())
|
return bytes.Equal(a.Marshal(), b.Marshal())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// plainKeyBlob returns the serialized public portion of key: for a
|
||||||
|
// certificate this is the certified key, otherwise the key itself.
|
||||||
|
func plainKeyBlob(key ssh.PublicKey) string {
|
||||||
|
if cert, ok := key.(*ssh.Certificate); ok {
|
||||||
|
return string(cert.Key.Marshal())
|
||||||
|
}
|
||||||
|
return string(key.Marshal())
|
||||||
|
}
|
||||||
|
|
||||||
// IsHostAuthority can be used as a callback in ssh.CertChecker
|
// IsHostAuthority can be used as a callback in ssh.CertChecker
|
||||||
func (db *hostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool {
|
func (db *hostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool {
|
||||||
h, p, err := net.SplitHostPort(address)
|
h, p, err := net.SplitHostPort(address)
|
||||||
@@ -160,8 +169,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[plainKeyBlob(key)]; ok {
|
||||||
return ok
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := db.revoked[plainKeyBlob(key.SignatureKey)]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const markerCert = "@cert-authority"
|
const markerCert = "@cert-authority"
|
||||||
@@ -173,7 +187,7 @@ func nextWord(line []byte) (string, []byte) {
|
|||||||
return string(line), nil
|
return string(line), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(line[:i]), bytes.TrimSpace(line[i:])
|
return string(line[:i]), trimSpace(line[i:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error) {
|
func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error) {
|
||||||
@@ -183,12 +197,17 @@ func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
host, line = nextWord(line)
|
host, line = nextWord(line)
|
||||||
|
// If the extracted 'host' starts with '@', it means we either encountered
|
||||||
|
// a second marker (e.g., "@cert-authority @revoked") or an unknown marker
|
||||||
|
// (e.g., "@unknown"). Both are invalid.
|
||||||
|
if len(host) > 0 && host[0] == '@' {
|
||||||
|
return "", "", nil, fmt.Errorf("knownhosts: unexpected marker: %q", host)
|
||||||
|
}
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
return "", "", nil, errors.New("knownhosts: missing host pattern")
|
return "", "", nil, errors.New("knownhosts: missing host pattern")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore the keytype as it's in the key blob anyway.
|
wantType, line := nextWord(line)
|
||||||
_, line = nextWord(line)
|
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
return "", "", nil, errors.New("knownhosts: missing key type pattern")
|
return "", "", nil, errors.New("knownhosts: missing key type pattern")
|
||||||
}
|
}
|
||||||
@@ -204,6 +223,10 @@ func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error)
|
|||||||
return "", "", nil, err
|
return "", "", nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if key.Type() != wantType {
|
||||||
|
return "", "", nil, fmt.Errorf("knownhosts: key type mismatch: found %q, want %q", key.Type(), wantType)
|
||||||
|
}
|
||||||
|
|
||||||
return marker, host, key, nil
|
return marker, host, key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +237,7 @@ func (db *hostKeyDB) parseLine(line []byte, filename string, linenum int) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
if marker == markerRevoked {
|
if marker == markerRevoked {
|
||||||
db.revoked[string(key.Marshal())] = &KnownKey{
|
db.revoked[plainKeyBlob(key)] = &KnownKey{
|
||||||
Key: key,
|
Key: key,
|
||||||
Filename: filename,
|
Filename: filename,
|
||||||
Line: linenum,
|
Line: linenum,
|
||||||
@@ -327,7 +350,7 @@ func (r *RevokedError) Error() string {
|
|||||||
// check checks a key against the host database. This should not be
|
// check checks a key against the host database. This should not be
|
||||||
// used for verifying certificates.
|
// used for verifying certificates.
|
||||||
func (db *hostKeyDB) check(address string, remote net.Addr, remoteKey ssh.PublicKey) error {
|
func (db *hostKeyDB) check(address string, remote net.Addr, remoteKey ssh.PublicKey) error {
|
||||||
if revoked := db.revoked[string(remoteKey.Marshal())]; revoked != nil {
|
if revoked := db.revoked[plainKeyBlob(remoteKey)]; revoked != nil {
|
||||||
return &RevokedError{Revoked: *revoked}
|
return &RevokedError{Revoked: *revoked}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,7 +405,7 @@ func (db *hostKeyDB) Read(r io.Reader, filename string) error {
|
|||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
lineNum++
|
lineNum++
|
||||||
line := scanner.Bytes()
|
line := scanner.Bytes()
|
||||||
line = bytes.TrimSpace(line)
|
line = trimSpace(line)
|
||||||
if len(line) == 0 || line[0] == '#' {
|
if len(line) == 0 || line[0] == '#' {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -530,3 +553,10 @@ func newHashedHost(encoded string) (*hashedHost, error) {
|
|||||||
func (h *hashedHost) match(a addr) bool {
|
func (h *hashedHost) match(a addr) bool {
|
||||||
return bytes.Equal(hashHost(Normalize(a.String()), h.salt), h.hash)
|
return bytes.Equal(hashHost(Normalize(a.String()), h.salt), h.hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trimSpace removes leading and trailing ASCII whitespace (space and tab). It
|
||||||
|
// is used instead of bytes.TrimSpace to match OpenSSH behavior, which strictly
|
||||||
|
// parses only ASCII space (0x20) and tab (0x09) as whitespace.
|
||||||
|
func trimSpace(in []byte) []byte {
|
||||||
|
return bytes.Trim(in, " \t")
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,9 +6,13 @@ package knownhosts
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
@@ -272,7 +276,107 @@ 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRevokedCertifiedKey(t *testing.T) {
|
||||||
|
_, caPriv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
caSigner, err := ssh.NewSignerFromKey(caPriv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, hostPriv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
hostKey, err := ssh.NewPublicKey(hostPriv.Public())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signCert := func(serial uint64, principal string) *ssh.Certificate {
|
||||||
|
cert := &ssh.Certificate{
|
||||||
|
CertType: ssh.HostCert,
|
||||||
|
Key: hostKey,
|
||||||
|
Serial: serial,
|
||||||
|
ValidBefore: ssh.CertTimeInfinity,
|
||||||
|
ValidPrincipals: []string{principal},
|
||||||
|
}
|
||||||
|
if err := cert.SignCert(rand.Reader, caSigner); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return cert
|
||||||
|
}
|
||||||
|
cert := signCert(1, "server.org")
|
||||||
|
|
||||||
|
// Revoking the host key revokes any certificate for it, even when
|
||||||
|
// the CA is trusted, matching OpenSSH's check_key_not_revoked.
|
||||||
|
db := testDB(t, "@revoked server.org "+serialize(hostKey)+"\n"+
|
||||||
|
"@cert-authority server.org "+serialize(caSigner.PublicKey())+"\n")
|
||||||
|
if !db.IsRevoked(cert) {
|
||||||
|
t.Error("IsRevoked returned false for certificate with revoked certified key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversely, a @revoked line containing a certificate revokes the
|
||||||
|
// plain key it certifies.
|
||||||
|
db = testDB(t, "@revoked server.org "+serialize(cert)+"\n")
|
||||||
|
if err := db.check("server.org:22", testAddr, hostKey); err == nil {
|
||||||
|
t.Error("no error for plain key revoked via certificate")
|
||||||
|
} else if _, ok := err.(*RevokedError); !ok {
|
||||||
|
t.Errorf("got %T (%v), want *RevokedError", err, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since only public portions are compared, revoking a certificate
|
||||||
|
// also revokes any sibling certificate certifying the same key,
|
||||||
|
// whatever its serial number and principals.
|
||||||
|
sibling := signCert(2, "other.org")
|
||||||
|
db = testDB(t, "@revoked * "+serialize(cert)+"\n"+
|
||||||
|
"@cert-authority * "+serialize(caSigner.PublicKey())+"\n")
|
||||||
|
if !db.IsRevoked(sibling) {
|
||||||
|
t.Error("IsRevoked returned false for sibling certificate of a revoked certificate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const testHostname = "hostname"
|
const testHostname = "hostname"
|
||||||
|
|
||||||
@@ -367,3 +471,94 @@ func TestIssue36126(t *testing.T) {
|
|||||||
t.Errorf("should have passed the check, got %v", err)
|
t.Errorf("should have passed the check, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnicodeSpace(t *testing.T) {
|
||||||
|
line := fmt.Sprintf("server.org %s %s", edKey.Type(), base64.StdEncoding.EncodeToString(edKey.Marshal()))
|
||||||
|
|
||||||
|
db := newHostKeyDB()
|
||||||
|
err := db.Read(bytes.NewBufferString(line), "testdb")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Read succeeded on line with Unicode space, expected error due to strict ASCII parsing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnicodeSpaceTrimming(t *testing.T) {
|
||||||
|
const unicodeSpace = " "
|
||||||
|
line := fmt.Sprintf("%sserver.org %s", unicodeSpace, edKeyStr)
|
||||||
|
|
||||||
|
db := newHostKeyDB()
|
||||||
|
err := db.Read(bytes.NewBufferString(line), "testdb")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Read failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := db.lines
|
||||||
|
if len(lines) != 1 {
|
||||||
|
t.Fatalf("Expected 1 line, got %d", len(lines))
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanAddr := addr{host: "server.org", port: "22"}
|
||||||
|
dirtyAddr := addr{host: unicodeSpace + "server.org", port: "22"}
|
||||||
|
|
||||||
|
if lines[0].match(cleanAddr) {
|
||||||
|
t.Errorf("Matched clean host 'server.org', implying Unicode space was trimmed")
|
||||||
|
}
|
||||||
|
if !lines[0].match(dirtyAddr) {
|
||||||
|
t.Errorf("Did not match dirty host, implying parsing issue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyTypeMismatch(t *testing.T) {
|
||||||
|
line := fmt.Sprintf("server.org ssh-rsa %s", base64.StdEncoding.EncodeToString(edKey.Marshal()))
|
||||||
|
|
||||||
|
db := newHostKeyDB()
|
||||||
|
err := db.Read(bytes.NewBufferString(line), "testdb")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Read succeeded on line with key type mismatch, expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedErr := `knownhosts: key type mismatch: found "ssh-ed25519", want "ssh-rsa"`
|
||||||
|
if !strings.Contains(err.Error(), expectedErr) {
|
||||||
|
t.Fatalf("got error %q, want to contain %q", err.Error(), expectedErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultipleMarkers(t *testing.T) {
|
||||||
|
lineDouble := fmt.Sprintf("@cert-authority @revoked server.org %s %s", edKey.Type(), base64.StdEncoding.EncodeToString(edKey.Marshal()))
|
||||||
|
db := newHostKeyDB()
|
||||||
|
err := db.Read(bytes.NewBufferString(lineDouble), "testdb")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Read succeeded on line with multiple markers, expected error")
|
||||||
|
}
|
||||||
|
expectedError := "unexpected marker"
|
||||||
|
if !strings.Contains(err.Error(), expectedError) {
|
||||||
|
t.Errorf("got error %q, want it to contain %q", err.Error(), expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
lineUnknown := fmt.Sprintf("@unknown-marker server.org %s %s", edKey.Type(), base64.StdEncoding.EncodeToString(edKey.Marshal()))
|
||||||
|
err = db.Read(bytes.NewBufferString(lineUnknown), "testdb")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Read succeeded on line with unknown marker, expected error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), expectedError) {
|
||||||
|
t.Errorf("got error %q, want it to contain %q", err.Error(), expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnknownMarker(t *testing.T) {
|
||||||
|
line := fmt.Sprintf("@unknown-marker server.org %s %s", edKey.Type(), base64.StdEncoding.EncodeToString(edKey.Marshal()))
|
||||||
|
|
||||||
|
db := newHostKeyDB()
|
||||||
|
err := db.Read(bytes.NewBufferString(line), "testdb")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Read succeeded on line with unknown marker, expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedError := "unexpected marker"
|
||||||
|
if !strings.Contains(err.Error(), expectedError) {
|
||||||
|
t.Errorf("got error %q, want it to contain %q", err.Error(), expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ type disconnectMsg struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *disconnectMsg) Error() string {
|
func (d *disconnectMsg) Error() string {
|
||||||
return fmt.Sprintf("ssh: disconnect, reason %d: %s", d.Reason, d.Message)
|
return fmt.Sprintf("ssh: disconnect, reason %d: %q", d.Reason, d.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// See RFC 4253, section 7.1.
|
// See RFC 4253, section 7.1.
|
||||||
|
|||||||
+28
-2
@@ -9,6 +9,7 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"testing/quick"
|
"testing/quick"
|
||||||
)
|
)
|
||||||
@@ -262,6 +263,31 @@ func TestDecode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDisconnectMsgSanitizesMessage(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
in string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"clean message", `"clean message"`},
|
||||||
|
{"line1\nline2\nline3", `"line1\nline2\nline3"`},
|
||||||
|
{"has\x00null\x00bytes", `"has\x00null\x00bytes"`},
|
||||||
|
{"\x1b[31mred\x1b[0m", `"\x1b[31mred\x1b[0m"`},
|
||||||
|
{"newline\r\nCRLF", `"newline\r\nCRLF"`},
|
||||||
|
{"tab\there", `"tab\there"`},
|
||||||
|
} {
|
||||||
|
t.Run(tc.in, func(t *testing.T) {
|
||||||
|
d := &disconnectMsg{Reason: 11, Message: tc.in}
|
||||||
|
got := d.Error()
|
||||||
|
if !strings.Contains(got, tc.want) {
|
||||||
|
t.Errorf("got %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
if bad := "\n\r\x00\x1b"; strings.ContainsAny(got, bad) {
|
||||||
|
t.Errorf("got %q, want none of %q", got, bad)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func randomBytes(out []byte, rand *rand.Rand) {
|
func randomBytes(out []byte, rand *rand.Rand) {
|
||||||
for i := 0; i < len(out); i++ {
|
for i := 0; i < len(out); i++ {
|
||||||
out[i] = byte(rand.Int31())
|
out[i] = byte(rand.Int31())
|
||||||
@@ -318,7 +344,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkMarshalKexInitMsg(b *testing.B) {
|
func BenchmarkMarshalKexInitMsg(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for b.Loop() {
|
||||||
Marshal(_kexInitMsg)
|
Marshal(_kexInitMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,7 +357,7 @@ func BenchmarkUnmarshalKexInitMsg(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkMarshalKexDHInitMsg(b *testing.B) {
|
func BenchmarkMarshalKexDHInitMsg(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for b.Loop() {
|
||||||
Marshal(_kexDHInitMsg)
|
Marshal(_kexDHInitMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user