diff --git a/acme/autocert/autocert_test.go b/acme/autocert/autocert_test.go index 269bc2a6c2..8ca8e2b3a7 100644 --- a/acme/autocert/autocert_test.go +++ b/acme/autocert/autocert_test.go @@ -561,9 +561,9 @@ func TestHTTPHandlerDefaultFallback(t *testing.T) { {"GET", "http://example.org/foo?a=b", 302, "https://example.org/foo?a=b"}, {"GET", "http://example.org/foo?a=b", 302, "https://example.org/foo?a=b"}, {"GET", "http://example.org/foo%20bar", 302, "https://example.org/foo%20bar"}, - {"GET", "http://[2602:d1:xxxx::c60a]:1234", 302, "https://[2602:d1:xxxx::c60a]:443/"}, - {"GET", "http://[2602:d1:xxxx::c60a]", 302, "https://[2602:d1:xxxx::c60a]/"}, - {"GET", "http://[2602:d1:xxxx::c60a]/foo?a=b", 302, "https://[2602:d1:xxxx::c60a]/foo?a=b"}, + {"GET", "http://[2602:d1:abcd::c60a]:1234/", 302, "https://[2602:d1:abcd::c60a]/"}, + {"GET", "http://[2602:d1:abcd::c60a]/", 302, "https://[2602:d1:abcd::c60a]/"}, + {"GET", "http://[2602:d1:abcd::c60a]/foo?a=b", 302, "https://[2602:d1:abcd::c60a]/foo?a=b"}, {"HEAD", "http://example.org/", 302, "https://example.org/"}, {"HEAD", "http://example.org/foo", 302, "https://example.org/foo"}, {"HEAD", "http://example.org/foo/bar/", 302, "https://example.org/foo/bar/"}, diff --git a/acme/pebble_test.go b/acme/pebble_test.go index b633435a71..bb4809faf7 100644 --- a/acme/pebble_test.go +++ b/acme/pebble_test.go @@ -382,7 +382,12 @@ func testIssuance(t *testing.T, env *environment, challSrv challengeServer) { // Wait for the order to become ready for finalization. order, err = client.WaitOrder(ctx, order.URI) if err != nil { - t.Fatalf("failed to wait for order %s: %s", orderURL, err) + var orderErr *acme.OrderError + if errors.Is(err, orderErr) { + t.Fatalf("failed to wait for order %s: %s: %s", orderURL, err, orderErr.Problem) + } else { + t.Fatalf("failed to wait for order %s: %s", orderURL, err) + } } if order.Status != acme.StatusReady { t.Fatalf("expected order %s status to be ready, got %v", diff --git a/acme/rfc8555.go b/acme/rfc8555.go index 3152e531b6..fc653f3f0b 100644 --- a/acme/rfc8555.go +++ b/acme/rfc8555.go @@ -272,7 +272,7 @@ func (c *Client) WaitOrder(ctx context.Context, url string) (*Order, error) { case err != nil: // Skip and retry. case o.Status == StatusInvalid: - return nil, &OrderError{OrderURL: o.URI, Status: o.Status} + return nil, &OrderError{OrderURL: o.URI, Status: o.Status, Problem: o.Error} case o.Status == StatusReady || o.Status == StatusValid: return o, nil } @@ -369,7 +369,7 @@ func (c *Client) CreateOrderCert(ctx context.Context, url string, csr []byte, bu } // The only acceptable status post finalize and WaitOrder is "valid". if o.Status != StatusValid { - return nil, "", &OrderError{OrderURL: o.URI, Status: o.Status} + return nil, "", &OrderError{OrderURL: o.URI, Status: o.Status, Problem: o.Error} } crt, err := c.fetchCertRFC(ctx, o.CertURL, bundle) return crt, o.CertURL, err diff --git a/acme/rfc8555_test.go b/acme/rfc8555_test.go index d65720a356..e9cedb5927 100644 --- a/acme/rfc8555_test.go +++ b/acme/rfc8555_test.go @@ -885,11 +885,17 @@ func TestRFC_WaitOrderError(t *testing.T) { s.handle("/orders/1", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", s.url("https://github.com/orders/1")) w.WriteHeader(http.StatusOK) - s := StatusPending if count > 0 { - s = StatusInvalid + // https://www.rfc-editor.org/rfc/rfc8555#section-7.3.3 + errorData := `{ + "type": "urn:ietf:params:acme:error:userActionRequired", + "detail": "Terms of service have changed", + "instance": "https://example.com/acme/agreement/?token=W8Ih3PswD-8" + }` + fmt.Fprintf(w, `{"status": %q, "error": %s}`, StatusInvalid, errorData) + } else { + fmt.Fprintf(w, `{"status": %q}`, StatusPending) } - fmt.Fprintf(w, `{"status": %q}`, s) count++ }) s.start() @@ -910,6 +916,13 @@ func TestRFC_WaitOrderError(t *testing.T) { if e.Status != StatusInvalid { t.Errorf("e.Status = %q; want %q", e.Status, StatusInvalid) } + if e.Problem == nil { + t.Errorf("e.Problem = nil") + } + expectedProbType := "urn:ietf:params:acme:error:userActionRequired" + if e.Problem.ProblemType != expectedProbType { + t.Errorf("e.Problem.ProblemType = %q; want %q", e.Problem.ProblemType, expectedProbType) + } } func TestRFC_CreateOrderCert(t *testing.T) { diff --git a/acme/types.go b/acme/types.go index c466645ca1..322640c453 100644 --- a/acme/types.go +++ b/acme/types.go @@ -154,13 +154,16 @@ func (a *AuthorizationError) Error() string { // OrderError is returned from Client's order related methods. // It indicates the order is unusable and the clients should start over with -// AuthorizeOrder. +// AuthorizeOrder. A Problem description may be provided with details on +// what caused the order to become unusable. // // The clients can still fetch the order object from CA using GetOrder // to inspect its state. type OrderError struct { OrderURL string Status string + // Problem is the error that occurred while processing the order. + Problem *Error } func (oe *OrderError) Error() string { diff --git a/curve25519/curve25519.go b/curve25519/curve25519.go index 21ca3b2ee4..048faef3a5 100644 --- a/curve25519/curve25519.go +++ b/curve25519/curve25519.go @@ -3,11 +3,14 @@ // license that can be found in the LICENSE file. // Package curve25519 provides an implementation of the X25519 function, which -// performs scalar multiplication on the elliptic curve known as Curve25519. -// See RFC 7748. +// performs scalar multiplication on the elliptic curve known as Curve25519 +// according to [RFC 7748]. // -// This package is a wrapper for the X25519 implementation -// in the crypto/ecdh package. +// The curve25519 package is a wrapper for the X25519 implementation in the +// crypto/ecdh package. It is [frozen] and is not accepting new features. +// +// [RFC 7748]: https://datatracker.ietf.org/doc/html/rfc7748 +// [frozen]: https://go.dev/wiki/Frozen package curve25519 import "crypto/ecdh" @@ -36,7 +39,7 @@ func ScalarBaseMult(dst, scalar *[32]byte) { curve := ecdh.X25519() priv, err := curve.NewPrivateKey(scalar[:]) if err != nil { - panic("curve25519: internal error: scalarBaseMult was not 32 bytes") + panic("curve25519: " + err.Error()) } copy(dst[:], priv.PublicKey().Bytes()) } diff --git a/ed25519/ed25519.go b/ed25519/ed25519.go index 59b3a95a7d..df453dcce0 100644 --- a/ed25519/ed25519.go +++ b/ed25519/ed25519.go @@ -2,16 +2,19 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package ed25519 implements the Ed25519 signature algorithm. See -// https://ed25519.cr.yp.to/. +// Package ed25519 implements the Ed25519 signature algorithm. // // These functions are also compatible with the “Ed25519” function defined in -// RFC 8032. However, unlike RFC 8032's formulation, this package's private key +// [RFC 8032]. However, unlike RFC 8032's formulation, this package's private key // representation includes a public key suffix to make multiple signing // operations with the same key more efficient. This package refers to the RFC // 8032 private key as the “seed”. // -// This package is a wrapper around the standard library crypto/ed25519 package. +// The ed25519 package is a wrapper for the Ed25519 implementation in the +// crypto/ed25519 package. It is [frozen] and is not accepting new features. +// +// [RFC 8032]: https://datatracker.ietf.org/doc/html/rfc8032 +// [frozen]: https://go.dev/wiki/Frozen package ed25519 import ( diff --git a/go.mod b/go.mod index 4ba08a1445..f331951a16 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ module golang.org/x/crypto -go 1.23.0 +go 1.24.0 require ( - golang.org/x/net v0.42.0 // tagx:ignore - golang.org/x/sys v0.35.0 - golang.org/x/term v0.34.0 + golang.org/x/net v0.45.0 // tagx:ignore + golang.org/x/sys v0.37.0 + golang.org/x/term v0.36.0 ) -require golang.org/x/text v0.28.0 // indirect +require golang.org/x/text v0.30.0 // indirect diff --git a/go.sum b/go.sum index b75af8566b..6d9f239e86 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= diff --git a/nacl/auth/auth.go b/nacl/auth/auth.go index 1d588d5c1c..136093841f 100644 --- a/nacl/auth/auth.go +++ b/nacl/auth/auth.go @@ -2,25 +2,16 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -/* -Package auth authenticates a message using a secret key. - -The Sum function, viewed as a function of the message for a uniform random -key, is designed to meet the standard notion of unforgeability. This means -that an attacker cannot find authenticators for any messages not authenticated -by the sender, even if the attacker has adaptively influenced the messages -authenticated by the sender. For a formal definition see, e.g., Section 2.4 -of Bellare, Kilian, and Rogaway, "The security of the cipher block chaining -message authentication code," Journal of Computer and System Sciences 61 (2000), -362–399; http://www-cse.ucsd.edu/~mihir/papers/cbc.html. - -auth does not make any promises regarding "strong" unforgeability; perhaps -one valid authenticator can be converted into another valid authenticator for -the same message. NaCl also does not make any promises regarding "truncated -unforgeability." - -This package is interoperable with NaCl: https://nacl.cr.yp.to/auth.html. -*/ +// Package auth authenticates a message using a secret key. +// +// This package is interoperable with [NaCl]. +// +// The auth package is essentially a wrapper for HMAC-SHA-512 (implemented by +// crypto/hmac and crypto/sha512), truncated to 32 bytes. It is [frozen] and is +// not accepting new features. +// +// [NaCl]: https://nacl.cr.yp.to/auth.html +// [frozen]: https://go.dev/wiki/Frozen package auth import ( diff --git a/nacl/sign/sign.go b/nacl/sign/sign.go index 109c08bb95..1cf2c4be2c 100644 --- a/nacl/sign/sign.go +++ b/nacl/sign/sign.go @@ -4,20 +4,15 @@ // Package sign signs small messages using public-key cryptography. // -// Sign uses Ed25519 to sign messages. The length of messages is not hidden. -// Messages should be small because: -// 1. The whole message needs to be held in memory to be processed. -// 2. Using large messages pressures implementations on small machines to process -// plaintext without verifying the signature. This is very dangerous, and this API -// discourages it, but a protocol that uses excessive message sizes might present -// some implementations with no other choice. -// 3. Performance may be improved by working with messages that fit into data caches. -// Thus large amounts of data should be chunked so that each message is small. +// This package is interoperable with [libsodium], as well as [TweetNaCl]. // -// This package is not interoperable with the current release of NaCl -// (https://nacl.cr.yp.to/sign.html), which does not support Ed25519 yet. However, -// it is compatible with the NaCl fork libsodium (https://www.libsodium.org), as well -// as TweetNaCl (https://tweetnacl.cr.yp.to/). +// The sign package is essentially a wrapper for the Ed25519 signature +// algorithm (implemented by crypto/ed25519). It is [frozen] and is not accepting +// new features. +// +// [libsodium]: https://libsodium.gitbook.io/doc/public-key_cryptography/public-key_signatures +// [TweetNaCl]: https://tweetnacl.cr.yp.to/ +// [frozen]: https://go.dev/wiki/Frozen package sign import ( diff --git a/otr/otr.go b/otr/otr.go index 6210c1ae61..a36f7ca245 100644 --- a/otr/otr.go +++ b/otr/otr.go @@ -8,6 +8,10 @@ // The version of OTR implemented by this package has been deprecated // (https://bugs.otr.im/lib/libotr/issues/140). An implementation of OTRv3 is // available at https://github.com/coyim/otr3. +// +// The otr package is [frozen] and is not accepting new features. +// +// [frozen]: https://go.dev/wiki/Frozen package otr import ( diff --git a/pkcs12/pkcs12.go b/pkcs12/pkcs12.go index 3a89bdb3e3..374d9facf8 100644 --- a/pkcs12/pkcs12.go +++ b/pkcs12/pkcs12.go @@ -4,12 +4,16 @@ // Package pkcs12 implements some of PKCS#12. // -// This implementation is distilled from https://tools.ietf.org/html/rfc7292 -// and referenced documents. It is intended for decoding P12/PFX-stored -// certificates and keys for use with the crypto/tls package. +// This implementation is distilled from [RFC 7292] and referenced documents. +// It is intended for decoding P12/PFX-stored certificates and keys for use +// with the crypto/tls package. // -// This package is frozen. If it's missing functionality you need, consider -// an alternative like software.sslmate.com/src/go-pkcs12. +// The pkcs12 package is [frozen] and is not accepting new features. +// If it's missing functionality you need, consider an alternative like +// software.sslmate.com/src/go-pkcs12. +// +// [RFC 7292]: https://datatracker.ietf.org/doc/html/rfc7292 +// [frozen]: https://go.dev/wiki/Frozen package pkcs12 import ( diff --git a/salsa20/salsa/hsalsa20.go b/salsa20/salsa/hsalsa20.go index 3685b34458..75df77406d 100644 --- a/salsa20/salsa/hsalsa20.go +++ b/salsa20/salsa/hsalsa20.go @@ -3,6 +3,10 @@ // license that can be found in the LICENSE file. // Package salsa provides low-level access to functions in the Salsa family. +// +// Deprecated: this package exposes unsafe low-level operations. New applications +// should consider using the AEAD construction in golang.org/x/crypto/chacha20poly1305 +// instead. Existing users should migrate to golang.org/x/crypto/salsa20. package salsa import "math/bits" diff --git a/ssh/agent/client.go b/ssh/agent/client.go index 37525e1a18..b357e18b0a 100644 --- a/ssh/agent/client.go +++ b/ssh/agent/client.go @@ -430,8 +430,9 @@ func (c *client) List() ([]*Key, error) { return keys, nil case *failureAgentMsg: return nil, errors.New("agent: failed to list keys") + default: + return nil, fmt.Errorf("agent: failed to list keys, unexpected message type %T", msg) } - panic("unreachable") } // Sign has the agent sign the data using a protocol 2 key as defined @@ -462,8 +463,9 @@ func (c *client) SignWithFlags(key ssh.PublicKey, data []byte, flags SignatureFl return &sig, nil case *failureAgentMsg: return nil, errors.New("agent: failed to sign challenge") + default: + return nil, fmt.Errorf("agent: failed to sign challenge, unexpected message type %T", msg) } - panic("unreachable") } // unmarshal parses an agent message in packet, returning the parsed diff --git a/ssh/agent/client_test.go b/ssh/agent/client_test.go index f0ffd59592..0fd284d786 100644 --- a/ssh/agent/client_test.go +++ b/ssh/agent/client_test.go @@ -7,6 +7,7 @@ package agent import ( "bytes" "crypto/rand" + "encoding/binary" "errors" "io" "net" @@ -347,6 +348,53 @@ func TestServerResponseTooLarge(t *testing.T) { } } +func TestInvalidResponses(t *testing.T) { + a, b, err := netPipe() + if err != nil { + t.Fatalf("netPipe: %v", err) + } + done := make(chan struct{}) + defer func() { <-done }() + + defer a.Close() + defer b.Close() + + agent := NewClient(a) + go func() { + defer close(done) + + resp := []byte{agentSuccess} + msg := make([]byte, 4+len(resp)) + binary.BigEndian.PutUint32(msg[:4], uint32(len(resp))) + copy(msg[4:], resp) + + if _, err := b.Write(msg); err != nil { + t.Errorf("unexpected error sending agent reply: %v", err) + b.Close() + return + } + + if _, err := b.Write(msg); err != nil { + t.Errorf("unexpected error sending agent reply: %v", err) + b.Close() + } + }() + _, err = agent.List() + if err == nil { + t.Fatal("error expected") + } + if !strings.Contains(err.Error(), "failed to list keys, unexpected message type") { + t.Fatalf("unexpected error: %v", err) + } + _, err = agent.Sign(testPublicKeys["rsa"], []byte("message")) + if err == nil { + t.Fatal("error expected") + } + if !strings.Contains(err.Error(), "failed to sign challenge, unexpected message type") { + t.Fatalf("unexpected error: %v", err) + } +} + func TestAuth(t *testing.T) { agent, _, cleanup := startOpenSSHAgent(t) defer cleanup() diff --git a/ssh/certs_test.go b/ssh/certs_test.go index e2a6fedc8b..ba435ab9cf 100644 --- a/ssh/certs_test.go +++ b/ssh/certs_test.go @@ -338,7 +338,9 @@ func TestCertTypes(t *testing.T) { CertType: UserCert, Key: priv.PublicKey(), } - cert.SignCert(rand.Reader, priv) + if err := cert.SignCert(rand.Reader, priv); err != nil { + t.Fatalf("error signing certificate: %v", err) + } certSigner, err := NewCertSigner(cert, priv) if err != nil { diff --git a/ssh/cipher.go b/ssh/cipher.go index 6a5b582aa9..7554ed57a9 100644 --- a/ssh/cipher.go +++ b/ssh/cipher.go @@ -8,6 +8,7 @@ import ( "crypto/aes" "crypto/cipher" "crypto/des" + "crypto/fips140" "crypto/rc4" "crypto/subtle" "encoding/binary" @@ -15,6 +16,7 @@ import ( "fmt" "hash" "io" + "slices" "golang.org/x/crypto/chacha20" "golang.org/x/crypto/internal/poly1305" @@ -93,41 +95,41 @@ func streamCipherMode(skip int, createFunc func(key, iv []byte) (cipher.Stream, } // cipherModes documents properties of supported ciphers. Ciphers not included -// are not supported and will not be negotiated, even if explicitly requested in -// ClientConfig.Crypto.Ciphers. -var cipherModes = map[string]*cipherMode{ - // Ciphers from RFC 4344, which introduced many CTR-based ciphers. Algorithms - // are defined in the order specified in the RFC. - CipherAES128CTR: {16, aes.BlockSize, streamCipherMode(0, newAESCTR)}, - CipherAES192CTR: {24, aes.BlockSize, streamCipherMode(0, newAESCTR)}, - CipherAES256CTR: {32, aes.BlockSize, streamCipherMode(0, newAESCTR)}, - - // Ciphers from RFC 4345, which introduces security-improved arcfour ciphers. - // They are defined in the order specified in the RFC. - InsecureCipherRC4128: {16, 0, streamCipherMode(1536, newRC4)}, - InsecureCipherRC4256: {32, 0, streamCipherMode(1536, newRC4)}, - - // Cipher defined in RFC 4253, which describes SSH Transport Layer Protocol. - // Note that this cipher is not safe, as stated in RFC 4253: "Arcfour (and - // RC4) has problems with weak keys, and should be used with caution." - // RFC 4345 introduces improved versions of Arcfour. - InsecureCipherRC4: {16, 0, streamCipherMode(0, newRC4)}, - - // AEAD ciphers - CipherAES128GCM: {16, 12, newGCMCipher}, - CipherAES256GCM: {32, 12, newGCMCipher}, - CipherChaCha20Poly1305: {64, 0, newChaCha20Cipher}, - +// are not supported and will not be negotiated, even if explicitly configured. +// When FIPS mode is enabled, only FIPS-approved algorithms are included. +var cipherModes = map[string]*cipherMode{} + +func init() { + cipherModes[CipherAES128CTR] = &cipherMode{16, aes.BlockSize, streamCipherMode(0, newAESCTR)} + cipherModes[CipherAES192CTR] = &cipherMode{24, aes.BlockSize, streamCipherMode(0, newAESCTR)} + cipherModes[CipherAES256CTR] = &cipherMode{32, aes.BlockSize, streamCipherMode(0, newAESCTR)} + // Use of GCM with arbitrary IVs is not allowed in FIPS 140-only mode, + // we'll wire it up to NewGCMForSSH in Go 1.26. + // + // For now it means we'll work with fips140=on but not fips140=only. + cipherModes[CipherAES128GCM] = &cipherMode{16, 12, newGCMCipher} + cipherModes[CipherAES256GCM] = &cipherMode{32, 12, newGCMCipher} + + if fips140.Enabled() { + defaultCiphers = slices.DeleteFunc(defaultCiphers, func(algo string) bool { + _, ok := cipherModes[algo] + return !ok + }) + return + } + + cipherModes[CipherChaCha20Poly1305] = &cipherMode{64, 0, newChaCha20Cipher} + // Insecure ciphers not included in the default configuration. + cipherModes[InsecureCipherRC4128] = &cipherMode{16, 0, streamCipherMode(1536, newRC4)} + cipherModes[InsecureCipherRC4256] = &cipherMode{32, 0, streamCipherMode(1536, newRC4)} + cipherModes[InsecureCipherRC4] = &cipherMode{16, 0, streamCipherMode(0, newRC4)} // CBC mode is insecure and so is not included in the default config. // (See https://www.ieee-security.org/TC/SP2013/papers/4977a526.pdf). If absolutely // needed, it's possible to specify a custom Config to enable it. // You should expect that an active attacker can recover plaintext if // you do. - InsecureCipherAES128CBC: {16, aes.BlockSize, newAESCBCCipher}, - - // 3des-cbc is insecure and is not included in the default - // config. - InsecureCipherTripleDESCBC: {24, des.BlockSize, newTripleDESCBCCipher}, + cipherModes[InsecureCipherAES128CBC] = &cipherMode{16, aes.BlockSize, newAESCBCCipher} + cipherModes[InsecureCipherTripleDESCBC] = &cipherMode{24, des.BlockSize, newTripleDESCBCCipher} } // prefixLen is the length of the packet prefix that contains the packet length diff --git a/ssh/client_auth.go b/ssh/client_auth.go index c12818fdc5..3127e49903 100644 --- a/ssh/client_auth.go +++ b/ssh/client_auth.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "slices" "strings" ) @@ -83,7 +84,7 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error { // success return nil } else if ok == authFailure { - if m := auth.method(); !contains(tried, m) { + if m := auth.method(); !slices.Contains(tried, m) { tried = append(tried, m) } } @@ -97,7 +98,7 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error { findNext: for _, a := range config.Auth { candidateMethod := a.method() - if contains(tried, candidateMethod) { + if slices.Contains(tried, candidateMethod) { continue } for _, meth := range methods { @@ -117,15 +118,6 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error { return fmt.Errorf("ssh: unable to authenticate, attempted methods %v, no supported methods remain", tried) } -func contains(list []string, e string) bool { - for _, s := range list { - if s == e { - return true - } - } - return false -} - // An AuthMethod represents an instance of an RFC 4252 authentication method. type AuthMethod interface { // auth authenticates user over transport t. @@ -255,7 +247,7 @@ func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (MultiA // Fallback to use if there is no "server-sig-algs" extension or a // common algorithm cannot be found. We use the public key format if the // MultiAlgorithmSigner supports it, otherwise we return an error. - if !contains(as.Algorithms(), underlyingAlgo(keyFormat)) { + if !slices.Contains(as.Algorithms(), underlyingAlgo(keyFormat)) { return "", fmt.Errorf("ssh: no common public key signature algorithm, server only supports %q for key type %q, signer only supports %v", underlyingAlgo(keyFormat), keyFormat, as.Algorithms()) } @@ -284,7 +276,7 @@ func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (MultiA // Filter algorithms based on those supported by MultiAlgorithmSigner. var keyAlgos []string for _, algo := range algorithmsForKeyFormat(keyFormat) { - if contains(as.Algorithms(), underlyingAlgo(algo)) { + if slices.Contains(as.Algorithms(), underlyingAlgo(algo)) { keyAlgos = append(keyAlgos, algo) } } @@ -334,7 +326,7 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand // the key try to use the obtained algorithm as if "server-sig-algs" had // not been implemented if supported from the algorithm signer. if !ok && idx < origSignersLen && isRSACert(algo) && algo != CertAlgoRSAv01 { - if contains(as.Algorithms(), KeyAlgoRSA) { + if slices.Contains(as.Algorithms(), KeyAlgoRSA) { // We retry using the compat algorithm after all signers have // been tried normally. signers = append(signers, &multiAlgorithmSigner{ @@ -385,7 +377,7 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand // contain the "publickey" method, do not attempt to authenticate with any // other keys. According to RFC 4252 Section 7, the latter can occur when // additional authentication methods are required. - if success == authSuccess || !contains(methods, cb.method()) { + if success == authSuccess || !slices.Contains(methods, cb.method()) { return success, methods, err } } @@ -434,7 +426,7 @@ func confirmKeyAck(key PublicKey, c packetConn) (bool, error) { // servers send the key type instead. OpenSSH allows any algorithm // that matches the public key, so we do the same. // https://github.com/openssh/openssh-portable/blob/86bdd385/sshconnect2.c#L709 - if !contains(algorithmsForKeyFormat(key.Type()), msg.Algo) { + if !slices.Contains(algorithmsForKeyFormat(key.Type()), msg.Algo) { return false, nil } if !bytes.Equal(msg.PubKey, pubKey) { diff --git a/ssh/client_auth_test.go b/ssh/client_auth_test.go index 8d1767cdb7..e398c4062c 100644 --- a/ssh/client_auth_test.go +++ b/ssh/client_auth_test.go @@ -14,6 +14,7 @@ import ( "net" "os" "runtime" + "slices" "strings" "testing" ) @@ -1214,7 +1215,7 @@ func (cb configurablePublicKeyCallback) auth(session []byte, user string, c pack if err != nil { return authFailure, nil, err } - if success == authSuccess || !contains(methods, cb.method()) { + if success == authSuccess || !slices.Contains(methods, cb.method()) { return success, methods, err } diff --git a/ssh/common.go b/ssh/common.go index f2ec0896c2..2e44e9c9ec 100644 --- a/ssh/common.go +++ b/ssh/common.go @@ -6,6 +6,7 @@ package ssh import ( "crypto" + "crypto/fips140" "crypto/rand" "fmt" "io" @@ -83,6 +84,7 @@ var ( // supportedKexAlgos specifies key-exchange algorithms implemented by this // package in preference order, excluding those with security issues. supportedKexAlgos = []string{ + KeyExchangeMLKEM768X25519, KeyExchangeCurve25519, KeyExchangeECDHP256, KeyExchangeECDHP384, @@ -94,6 +96,7 @@ var ( // defaultKexAlgos specifies the default preference for key-exchange // algorithms in preference order. defaultKexAlgos = []string{ + KeyExchangeMLKEM768X25519, KeyExchangeCurve25519, KeyExchangeECDHP256, KeyExchangeECDHP384, @@ -254,6 +257,40 @@ type Algorithms struct { PublicKeyAuths []string } +func init() { + if fips140.Enabled() { + defaultHostKeyAlgos = slices.DeleteFunc(defaultHostKeyAlgos, func(algo string) bool { + _, err := hashFunc(underlyingAlgo(algo)) + return err != nil + }) + defaultPubKeyAuthAlgos = slices.DeleteFunc(defaultPubKeyAuthAlgos, func(algo string) bool { + _, err := hashFunc(underlyingAlgo(algo)) + return err != nil + }) + } +} + +func hashFunc(format string) (crypto.Hash, error) { + switch format { + case KeyAlgoRSASHA256, KeyAlgoECDSA256, KeyAlgoSKED25519, KeyAlgoSKECDSA256: + return crypto.SHA256, nil + case KeyAlgoECDSA384: + return crypto.SHA384, nil + case KeyAlgoRSASHA512, KeyAlgoECDSA521: + return crypto.SHA512, nil + case KeyAlgoED25519: + // KeyAlgoED25519 doesn't pre-hash. + return 0, nil + case KeyAlgoRSA, InsecureKeyAlgoDSA: + if fips140.Enabled() { + return 0, fmt.Errorf("ssh: hash algorithm for format %q not allowed in FIPS 140 mode", format) + } + return crypto.SHA1, nil + default: + return 0, fmt.Errorf("ssh: hash algorithm for format %q not mapped", format) + } +} + // SupportedAlgorithms returns algorithms currently implemented by this package, // excluding those with security issues, which are returned by // InsecureAlgorithms. The algorithms listed here are in preference order. @@ -281,21 +318,6 @@ func InsecureAlgorithms() Algorithms { var supportedCompressions = []string{compressionNone} -// hashFuncs keeps the mapping of supported signature algorithms to their -// respective hashes needed for signing and verification. -var hashFuncs = map[string]crypto.Hash{ - KeyAlgoRSA: crypto.SHA1, - KeyAlgoRSASHA256: crypto.SHA256, - KeyAlgoRSASHA512: crypto.SHA512, - InsecureKeyAlgoDSA: crypto.SHA1, - KeyAlgoECDSA256: crypto.SHA256, - KeyAlgoECDSA384: crypto.SHA384, - KeyAlgoECDSA521: crypto.SHA512, - // KeyAlgoED25519 doesn't pre-hash. - KeyAlgoSKECDSA256: crypto.SHA256, - KeyAlgoSKED25519: crypto.SHA256, -} - // algorithmsForKeyFormat returns the supported signature algorithms for a given // public key format (PublicKey.Type), in order of preference. See RFC 8332, // Section 2. See also the note in sendKexInit on backwards compatibility. @@ -310,11 +332,40 @@ func algorithmsForKeyFormat(keyFormat string) []string { } } +// keyFormatForAlgorithm returns the key format corresponding to the given +// signature algorithm. It returns an empty string if the signature algorithm is +// invalid or unsupported. +func keyFormatForAlgorithm(sigAlgo string) string { + switch sigAlgo { + case KeyAlgoRSA, KeyAlgoRSASHA256, KeyAlgoRSASHA512: + return KeyAlgoRSA + case CertAlgoRSAv01, CertAlgoRSASHA256v01, CertAlgoRSASHA512v01: + return CertAlgoRSAv01 + case KeyAlgoED25519, + KeyAlgoSKED25519, + KeyAlgoSKECDSA256, + KeyAlgoECDSA256, + KeyAlgoECDSA384, + KeyAlgoECDSA521, + InsecureKeyAlgoDSA, + InsecureCertAlgoDSAv01, + CertAlgoECDSA256v01, + CertAlgoECDSA384v01, + CertAlgoECDSA521v01, + CertAlgoSKECDSA256v01, + CertAlgoED25519v01, + CertAlgoSKED25519v01: + return sigAlgo + default: + return "" + } +} + // isRSA returns whether algo is a supported RSA algorithm, including certificate // algorithms. func isRSA(algo string) bool { algos := algorithmsForKeyFormat(KeyAlgoRSA) - return contains(algos, underlyingAlgo(algo)) + return slices.Contains(algos, underlyingAlgo(algo)) } func isRSACert(algo string) bool { @@ -513,7 +564,7 @@ func (c *Config) SetDefaults() { if kexAlgoMap[k] != nil { // Ignore the KEX if we have no kexAlgoMap definition. kexs = append(kexs, k) - if k == KeyExchangeCurve25519 && !contains(c.KeyExchanges, keyExchangeCurve25519LibSSH) { + if k == KeyExchangeCurve25519 && !slices.Contains(c.KeyExchanges, keyExchangeCurve25519LibSSH) { kexs = append(kexs, keyExchangeCurve25519LibSSH) } } diff --git a/ssh/common_test.go b/ssh/common_test.go index 67cf1f4269..80aa2df73b 100644 --- a/ssh/common_test.go +++ b/ssh/common_test.go @@ -5,7 +5,9 @@ package ssh import ( + "maps" "reflect" + "slices" "testing" ) @@ -174,3 +176,21 @@ func TestFindAgreedAlgorithms(t *testing.T) { }) } } + +func TestKeyFormatAlgorithms(t *testing.T) { + supportedAlgos := SupportedAlgorithms() + insecureAlgos := InsecureAlgorithms() + algoritms := append(supportedAlgos.PublicKeyAuths, insecureAlgos.PublicKeyAuths...) + algoritms = append(algoritms, slices.Collect(maps.Keys(certKeyAlgoNames))...) + + for _, algo := range algoritms { + keyFormat := keyFormatForAlgorithm(algo) + if keyFormat == "" { + t.Errorf("got empty key format for algorithm %q", algo) + } + if !slices.Contains(algorithmsForKeyFormat(keyFormat), algo) { + t.Errorf("algorithms for key format %q, does not contain %q", keyFormat, algo) + } + + } +} diff --git a/ssh/doc.go b/ssh/doc.go index 04ccce3461..5b4de9effc 100644 --- a/ssh/doc.go +++ b/ssh/doc.go @@ -17,8 +17,18 @@ References: [PROTOCOL.certkeys]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?rev=HEAD [SSH-PARAMETERS]: http://www.iana.org/assignments/ssh-parameters/ssh-parameters.xml#ssh-parameters-1 [SSH-CERTS]: https://datatracker.ietf.org/doc/html/draft-miller-ssh-cert-01 + [FIPS 140-3 mode]: https://go.dev/doc/security/fips140 This package does not fall under the stability promise of the Go language itself, so its API may be changed when pressing needs arise. + +# FIPS 140-3 mode + +When the program is in [FIPS 140-3 mode], this package behaves as if only SP +800-140C and SP 800-140D approved cipher suites, signature algorithms, +certificate public key types and sizes, and key exchange and derivation +algorithms were implemented. Others are silently ignored and not negotiated, or +rejected. This set may depend on the algorithms supported by the FIPS 140-3 Go +Cryptographic Module selected with GOFIPS140, and may change across Go versions. */ package ssh diff --git a/ssh/handshake.go b/ssh/handshake.go index a90bfe331c..4be3cbb6de 100644 --- a/ssh/handshake.go +++ b/ssh/handshake.go @@ -10,6 +10,7 @@ import ( "io" "log" "net" + "slices" "strings" "sync" ) @@ -527,7 +528,7 @@ func (t *handshakeTransport) sendKexInit() error { switch s := k.(type) { case MultiAlgorithmSigner: for _, algo := range algorithmsForKeyFormat(keyFormat) { - if contains(s.Algorithms(), underlyingAlgo(algo)) { + if slices.Contains(s.Algorithms(), underlyingAlgo(algo)) { msg.ServerHostKeyAlgos = append(msg.ServerHostKeyAlgos, algo) } } @@ -679,7 +680,7 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error { return err } - if t.sessionID == nil && ((isClient && contains(serverInit.KexAlgos, kexStrictServer)) || (!isClient && contains(clientInit.KexAlgos, kexStrictClient))) { + if t.sessionID == nil && ((isClient && slices.Contains(serverInit.KexAlgos, kexStrictServer)) || (!isClient && slices.Contains(clientInit.KexAlgos, kexStrictClient))) { t.strictMode = true if err := t.conn.setStrictMode(); err != nil { return err @@ -736,7 +737,7 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error { // On the server side, after the first SSH_MSG_NEWKEYS, send a SSH_MSG_EXT_INFO // message with the server-sig-algs extension if the client supports it. See // RFC 8308, Sections 2.4 and 3.1, and [PROTOCOL], Section 1.9. - if !isClient && firstKeyExchange && contains(clientInit.KexAlgos, "ext-info-c") { + if !isClient && firstKeyExchange && slices.Contains(clientInit.KexAlgos, "ext-info-c") { supportedPubKeyAuthAlgosList := strings.Join(t.publicKeyAuthAlgorithms, ",") extInfo := &extInfoMsg{ NumExtensions: 2, @@ -790,7 +791,7 @@ func (a algorithmSignerWrapper) SignWithAlgorithm(rand io.Reader, data []byte, a func pickHostKey(hostKeys []Signer, algo string) AlgorithmSigner { for _, k := range hostKeys { if s, ok := k.(MultiAlgorithmSigner); ok { - if !contains(s.Algorithms(), underlyingAlgo(algo)) { + if !slices.Contains(s.Algorithms(), underlyingAlgo(algo)) { continue } } diff --git a/ssh/kex.go b/ssh/kex.go index cf388a92aa..5f7fdd8514 100644 --- a/ssh/kex.go +++ b/ssh/kex.go @@ -8,13 +8,14 @@ import ( "crypto" "crypto/ecdsa" "crypto/elliptic" + "crypto/fips140" "crypto/rand" - "crypto/subtle" "encoding/binary" "errors" "fmt" "io" "math/big" + "slices" "golang.org/x/crypto/curve25519" ) @@ -396,9 +397,27 @@ func ecHash(curve elliptic.Curve) crypto.Hash { return crypto.SHA512 } +// kexAlgoMap defines the supported KEXs. KEXs not included are not supported +// and will not be negotiated, even if explicitly configured. When FIPS mode is +// enabled, only FIPS-approved algorithms are included. var kexAlgoMap = map[string]kexAlgorithm{} func init() { + // mlkem768x25519-sha256 we'll work with fips140=on but not fips140=only + // until Go 1.26. + kexAlgoMap[KeyExchangeMLKEM768X25519] = &mlkem768WithCurve25519sha256{} + kexAlgoMap[KeyExchangeECDHP521] = &ecdh{elliptic.P521()} + kexAlgoMap[KeyExchangeECDHP384] = &ecdh{elliptic.P384()} + kexAlgoMap[KeyExchangeECDHP256] = &ecdh{elliptic.P256()} + + if fips140.Enabled() { + defaultKexAlgos = slices.DeleteFunc(defaultKexAlgos, func(algo string) bool { + _, ok := kexAlgoMap[algo] + return !ok + }) + return + } + p, _ := new(big.Int).SetString(oakleyGroup2, 16) kexAlgoMap[InsecureKeyExchangeDH1SHA1] = &dhGroup{ g: new(big.Int).SetInt64(2), @@ -432,9 +451,6 @@ func init() { hashFunc: crypto.SHA512, } - kexAlgoMap[KeyExchangeECDHP521] = &ecdh{elliptic.P521()} - kexAlgoMap[KeyExchangeECDHP384] = &ecdh{elliptic.P384()} - kexAlgoMap[KeyExchangeECDHP256] = &ecdh{elliptic.P256()} kexAlgoMap[KeyExchangeCurve25519] = &curve25519sha256{} kexAlgoMap[keyExchangeCurve25519LibSSH] = &curve25519sha256{} kexAlgoMap[InsecureKeyExchangeDHGEXSHA1] = &dhGEXSHA{hashFunc: crypto.SHA1} @@ -454,15 +470,17 @@ func (kp *curve25519KeyPair) generate(rand io.Reader) error { if _, err := io.ReadFull(rand, kp.priv[:]); err != nil { return err } - curve25519.ScalarBaseMult(&kp.pub, &kp.priv) + p, err := curve25519.X25519(kp.priv[:], curve25519.Basepoint) + if err != nil { + return fmt.Errorf("curve25519: %w", err) + } + if len(p) != 32 { + return fmt.Errorf("curve25519: internal error: X25519 returned %d bytes, expected 32", len(p)) + } + copy(kp.pub[:], p) return nil } -// curve25519Zeros is just an array of 32 zero bytes so that we have something -// convenient to compare against in order to reject curve25519 points with the -// wrong order. -var curve25519Zeros [32]byte - func (kex *curve25519sha256) Client(c packetConn, rand io.Reader, magics *handshakeMagics) (*kexResult, error) { var kp curve25519KeyPair if err := kp.generate(rand); err != nil { @@ -485,11 +503,9 @@ func (kex *curve25519sha256) Client(c packetConn, rand io.Reader, magics *handsh return nil, errors.New("ssh: peer's curve25519 public value has wrong length") } - var servPub, secret [32]byte - copy(servPub[:], reply.EphemeralPubKey) - curve25519.ScalarMult(&secret, &kp.priv, &servPub) - if subtle.ConstantTimeCompare(secret[:], curve25519Zeros[:]) == 1 { - return nil, errors.New("ssh: peer's curve25519 public value has wrong order") + secret, err := curve25519.X25519(kp.priv[:], reply.EphemeralPubKey) + if err != nil { + return nil, fmt.Errorf("ssh: peer's curve25519 public value is not valid: %w", err) } h := crypto.SHA256.New() @@ -531,11 +547,9 @@ func (kex *curve25519sha256) Server(c packetConn, rand io.Reader, magics *handsh return nil, err } - var clientPub, secret [32]byte - copy(clientPub[:], kexInit.ClientPubKey) - curve25519.ScalarMult(&secret, &kp.priv, &clientPub) - if subtle.ConstantTimeCompare(secret[:], curve25519Zeros[:]) == 1 { - return nil, errors.New("ssh: peer's curve25519 public value has wrong order") + secret, err := curve25519.X25519(kp.priv[:], kexInit.ClientPubKey) + if err != nil { + return nil, fmt.Errorf("ssh: peer's curve25519 public value is not valid: %w", err) } hostKeyBytes := priv.PublicKey().Marshal() diff --git a/ssh/keys.go b/ssh/keys.go index a28c0de503..a035956fcc 100644 --- a/ssh/keys.go +++ b/ssh/keys.go @@ -27,6 +27,7 @@ import ( "fmt" "io" "math/big" + "slices" "strings" "golang.org/x/crypto/ssh/internal/bcrypt_pbkdf" @@ -89,6 +90,11 @@ func parsePubKey(in []byte, algo string) (pubKey PublicKey, rest []byte, err err } return cert, nil, nil } + if keyFormat := keyFormatForAlgorithm(algo); keyFormat != "" { + return nil, nil, fmt.Errorf("ssh: signature algorithm %q isn't a key format; key is malformed and should be re-encoded with type %q", + algo, keyFormat) + } + return nil, nil, fmt.Errorf("ssh: unknown key algorithm: %v", algo) } @@ -191,9 +197,10 @@ func ParseKnownHosts(in []byte) (marker string, hosts []string, pubKey PublicKey return "", nil, nil, "", nil, io.EOF } -// ParseAuthorizedKey parses a public key from an authorized_keys -// file used in OpenSSH according to the sshd(8) manual page. +// ParseAuthorizedKey parses a public key from an authorized_keys file used in +// OpenSSH according to the sshd(8) manual page. Invalid lines are ignored. func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, err error) { + var lastErr error for len(in) > 0 { end := bytes.IndexByte(in, '\n') if end != -1 { @@ -222,6 +229,8 @@ func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []str if out, comment, err = parseAuthorizedKey(in[i:]); err == nil { return out, comment, options, rest, nil + } else { + lastErr = err } // No key type recognised. Maybe there's an options field at @@ -264,12 +273,18 @@ func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []str if out, comment, err = parseAuthorizedKey(in[i:]); err == nil { options = candidateOptions return out, comment, options, rest, nil + } else { + lastErr = err } in = rest continue } + if lastErr != nil { + return nil, "", nil, nil, fmt.Errorf("ssh: no key found; last parsing error for ignored line: %w", lastErr) + } + return nil, "", nil, nil, errors.New("ssh: no key found") } @@ -395,11 +410,11 @@ func NewSignerWithAlgorithms(signer AlgorithmSigner, algorithms []string) (Multi } for _, algo := range algorithms { - if !contains(supportedAlgos, algo) { + if !slices.Contains(supportedAlgos, algo) { return nil, fmt.Errorf("ssh: algorithm %q is not supported for key type %q", algo, signer.PublicKey().Type()) } - if !contains(signerAlgos, algo) { + if !slices.Contains(signerAlgos, algo) { return nil, fmt.Errorf("ssh: algorithm %q is restricted for the provided signer", algo) } } @@ -486,10 +501,13 @@ func (r *rsaPublicKey) Marshal() []byte { func (r *rsaPublicKey) Verify(data []byte, sig *Signature) error { supportedAlgos := algorithmsForKeyFormat(r.Type()) - if !contains(supportedAlgos, sig.Format) { + if !slices.Contains(supportedAlgos, sig.Format) { return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, r.Type()) } - hash := hashFuncs[sig.Format] + hash, err := hashFunc(sig.Format) + if err != nil { + return err + } h := hash.New() h.Write(data) digest := h.Sum(nil) @@ -606,7 +624,11 @@ func (k *dsaPublicKey) Verify(data []byte, sig *Signature) error { if sig.Format != k.Type() { return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, k.Type()) } - h := hashFuncs[sig.Format].New() + hash, err := hashFunc(sig.Format) + if err != nil { + return err + } + h := hash.New() h.Write(data) digest := h.Sum(nil) @@ -651,7 +673,11 @@ func (k *dsaPrivateKey) SignWithAlgorithm(rand io.Reader, data []byte, algorithm return nil, fmt.Errorf("ssh: unsupported signature algorithm %s", algorithm) } - h := hashFuncs[k.PublicKey().Type()].New() + hash, err := hashFunc(k.PublicKey().Type()) + if err != nil { + return nil, err + } + h := hash.New() h.Write(data) digest := h.Sum(nil) r, s, err := dsa.Sign(rand, k.PrivateKey, digest) @@ -801,8 +827,11 @@ func (k *ecdsaPublicKey) Verify(data []byte, sig *Signature) error { if sig.Format != k.Type() { return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, k.Type()) } - - h := hashFuncs[sig.Format].New() + hash, err := hashFunc(sig.Format) + if err != nil { + return err + } + h := hash.New() h.Write(data) digest := h.Sum(nil) @@ -905,8 +934,11 @@ func (k *skECDSAPublicKey) Verify(data []byte, sig *Signature) error { if sig.Format != k.Type() { return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, k.Type()) } - - h := hashFuncs[sig.Format].New() + hash, err := hashFunc(sig.Format) + if err != nil { + return err + } + h := hash.New() h.Write([]byte(k.application)) appDigest := h.Sum(nil) @@ -1009,7 +1041,11 @@ func (k *skEd25519PublicKey) Verify(data []byte, sig *Signature) error { return fmt.Errorf("invalid size %d for Ed25519 public key", l) } - h := hashFuncs[sig.Format].New() + hash, err := hashFunc(sig.Format) + if err != nil { + return err + } + h := hash.New() h.Write([]byte(k.application)) appDigest := h.Sum(nil) @@ -1112,11 +1148,14 @@ func (s *wrappedSigner) SignWithAlgorithm(rand io.Reader, data []byte, algorithm algorithm = s.pubKey.Type() } - if !contains(s.Algorithms(), algorithm) { + if !slices.Contains(s.Algorithms(), algorithm) { return nil, fmt.Errorf("ssh: unsupported signature algorithm %q for key format %q", algorithm, s.pubKey.Type()) } - hashFunc := hashFuncs[algorithm] + hashFunc, err := hashFunc(algorithm) + if err != nil { + return nil, err + } var digest []byte if hashFunc != 0 { h := hashFunc.New() diff --git a/ssh/keys_test.go b/ssh/keys_test.go index f3eb223a9c..661e3cb31c 100644 --- a/ssh/keys_test.go +++ b/ssh/keys_test.go @@ -59,6 +59,17 @@ func TestKeyMarshalParse(t *testing.T) { } } +func TestParsePublicKeyWithSigningAlgoAsKeyFormat(t *testing.T) { + key := []byte(`rsa-sha2-256 AAAADHJzYS1zaGEyLTI1NgAAAAMBAAEAAAEBAJ7qMyjLXEJCCJmRknuCLo0uPi5GrPY5pQYr84lhlN8Gor5KVL2LKYCW4e70r5xzj7SrHHSCft1FMlYg1KDO9xrprJh733kQqAPWETmSuH0EfRtGtcH6EarKyVxk6As076/yNiiMKVBtG0RPa1L7FviTfcYK4vnCCVrbv3RmA5CCzuG5BSMbRLxzVb4Ri3p8jhxYT8N4QGe/2yqvJLys5vQ9szpZR3tcFp3DJIVZhBRfR6LnoY23XZniAAMQaUVBX86dXQ++dNwAwZSXSt9Og+AniOCiBYqhNVa5n3DID/H7YtEtG+CbZr3r2KD3fv8AfSLRar4XOp8rsRdD31h/kr8=`) + _, _, _, _, err := ParseAuthorizedKey(key) + if err == nil { + t.Fatal("parsing a public key using a signature algorithm as the key format succeeded unexpectedly") + } + if !strings.Contains(err.Error(), `signature algorithm "rsa-sha2-256" isn't a key format`) { + t.Errorf(`got %v, expected 'signature algorithm "rsa-sha2-256" isn't a key format'`, err) + } +} + func TestUnsupportedCurves(t *testing.T) { raw, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) if err != nil { diff --git a/ssh/knownhosts/knownhosts.go b/ssh/knownhosts/knownhosts.go index c022e411f0..1ebd7e6da2 100644 --- a/ssh/knownhosts/knownhosts.go +++ b/ssh/knownhosts/knownhosts.go @@ -421,20 +421,26 @@ func New(files ...string) (ssh.HostKeyCallback, error) { return certChecker.CheckHostKey, nil } -// Normalize normalizes an address into the form used in known_hosts +// Normalize normalizes an address into the form used in known_hosts. Supports +// IPv4, hostnames, bracketed IPv6. Any other non-standard formats are returned +// with minimal transformation. func Normalize(address string) string { + const defaultSSHPort = "22" + host, port, err := net.SplitHostPort(address) if err != nil { host = address - port = "22" + port = defaultSSHPort + } + + if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { + host = host[1 : len(host)-1] } - entry := host - if port != "22" { - entry = "[" + entry + "]:" + port - } else if strings.Contains(host, ":") && !strings.HasPrefix(host, "[") { - entry = "[" + entry + "]" + + if port == defaultSSHPort { + return host } - return entry + return "[" + host + "]:" + port } // Line returns a line to add append to the known_hosts files. diff --git a/ssh/knownhosts/knownhosts_test.go b/ssh/knownhosts/knownhosts_test.go index 156576ad07..552a556110 100644 --- a/ssh/knownhosts/knownhosts_test.go +++ b/ssh/knownhosts/knownhosts_test.go @@ -236,7 +236,7 @@ func TestLine(t *testing.T) { "server.org": "server.org " + edKeyStr, "server.org:22": "server.org " + edKeyStr, "server.org:23": "[server.org]:23 " + edKeyStr, - "[c629:1ec4:102:304:102:304:102:304]:22": "[c629:1ec4:102:304:102:304:102:304] " + edKeyStr, + "[c629:1ec4:102:304:102:304:102:304]:22": "c629:1ec4:102:304:102:304:102:304 " + edKeyStr, "[c629:1ec4:102:304:102:304:102:304]:23": "[c629:1ec4:102:304:102:304:102:304]:23 " + edKeyStr, } { if got := Line([]string{in}, edKey); got != want { @@ -310,14 +310,25 @@ func testHostHash(t *testing.T, hostname, encoded string) { func TestNormalize(t *testing.T) { for in, want := range map[string]string{ - "127.0.0.1:22": "127.0.0.1", - "[127.0.0.1]:22": "127.0.0.1", - "[127.0.0.1]:23": "[127.0.0.1]:23", - "127.0.0.1:23": "[127.0.0.1]:23", - "[a.b.c]:22": "a.b.c", - "[abcd:abcd:abcd:abcd]": "[abcd:abcd:abcd:abcd]", - "[abcd:abcd:abcd:abcd]:22": "[abcd:abcd:abcd:abcd]", - "[abcd:abcd:abcd:abcd]:23": "[abcd:abcd:abcd:abcd]:23", + "127.0.0.1": "127.0.0.1", + "127.0.0.1:22": "127.0.0.1", + "[127.0.0.1]:22": "127.0.0.1", + "[127.0.0.1]:23": "[127.0.0.1]:23", + "127.0.0.1:23": "[127.0.0.1]:23", + "[a.b.c]:22": "a.b.c", + "[a.b.c]:23": "[a.b.c]:23", + "abcd::abcd:abcd:abcd": "abcd::abcd:abcd:abcd", + "[abcd::abcd:abcd:abcd]": "abcd::abcd:abcd:abcd", + "[abcd::abcd:abcd:abcd]:22": "abcd::abcd:abcd:abcd", + "[abcd::abcd:abcd:abcd]:23": "[abcd::abcd:abcd:abcd]:23", + "2001:db8::1": "2001:db8::1", + "2001:db8::1:22": "2001:db8::1:22", + "[2001:db8::1]:22": "2001:db8::1", + "2001:db8::1:2200": "2001:db8::1:2200", + "a.b.c.d.com:2200": "[a.b.c.d.com]:2200", + "2001::db8:1": "2001::db8:1", + "2001::db8:1:22": "2001::db8:1:22", + "2001::db8:1:2200": "2001::db8:1:2200", } { got := Normalize(in) if got != want { diff --git a/ssh/mac.go b/ssh/mac.go index de2639d57f..87d626fbbf 100644 --- a/ssh/mac.go +++ b/ssh/mac.go @@ -7,11 +7,13 @@ package ssh // Message authentication support import ( + "crypto/fips140" "crypto/hmac" "crypto/sha1" "crypto/sha256" "crypto/sha512" "hash" + "slices" ) type macMode struct { @@ -46,23 +48,37 @@ func (t truncatingMAC) Size() int { func (t truncatingMAC) BlockSize() int { return t.hmac.BlockSize() } -var macModes = map[string]*macMode{ - HMACSHA512ETM: {64, true, func(key []byte) hash.Hash { +// macModes defines the supported MACs. MACs not included are not supported +// and will not be negotiated, even if explicitly configured. When FIPS mode is +// enabled, only FIPS-approved algorithms are included. +var macModes = map[string]*macMode{} + +func init() { + macModes[HMACSHA512ETM] = &macMode{64, true, func(key []byte) hash.Hash { return hmac.New(sha512.New, key) - }}, - HMACSHA256ETM: {32, true, func(key []byte) hash.Hash { + }} + macModes[HMACSHA256ETM] = &macMode{32, true, func(key []byte) hash.Hash { return hmac.New(sha256.New, key) - }}, - HMACSHA512: {64, false, func(key []byte) hash.Hash { + }} + macModes[HMACSHA512] = &macMode{64, false, func(key []byte) hash.Hash { return hmac.New(sha512.New, key) - }}, - HMACSHA256: {32, false, func(key []byte) hash.Hash { + }} + macModes[HMACSHA256] = &macMode{32, false, func(key []byte) hash.Hash { return hmac.New(sha256.New, key) - }}, - HMACSHA1: {20, false, func(key []byte) hash.Hash { + }} + + if fips140.Enabled() { + defaultMACs = slices.DeleteFunc(defaultMACs, func(algo string) bool { + _, ok := macModes[algo] + return !ok + }) + return + } + + macModes[HMACSHA1] = &macMode{20, false, func(key []byte) hash.Hash { return hmac.New(sha1.New, key) - }}, - InsecureHMACSHA196: {20, false, func(key []byte) hash.Hash { + }} + macModes[InsecureHMACSHA196] = &macMode{20, false, func(key []byte) hash.Hash { return truncatingMAC{12, hmac.New(sha1.New, key)} - }}, + }} } diff --git a/ssh/mlkem.go b/ssh/mlkem.go index 657e1079d4..ddc0ed1fc0 100644 --- a/ssh/mlkem.go +++ b/ssh/mlkem.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.24 - package ssh import ( @@ -13,23 +11,10 @@ import ( "errors" "fmt" "io" - "runtime" - "slices" "golang.org/x/crypto/curve25519" ) -func init() { - // After Go 1.24rc1 mlkem swapped the order of return values of Encapsulate. - // See #70950. - if runtime.Version() == "go1.24rc1" { - return - } - supportedKexAlgos = slices.Insert(supportedKexAlgos, 0, KeyExchangeMLKEM768X25519) - defaultKexAlgos = slices.Insert(defaultKexAlgos, 0, KeyExchangeMLKEM768X25519) - kexAlgoMap[KeyExchangeMLKEM768X25519] = &mlkem768WithCurve25519sha256{} -} - // mlkem768WithCurve25519sha256 implements the hybrid ML-KEM768 with // curve25519-sha256 key exchange method, as described by // draft-kampanakis-curdle-ssh-pq-ke-05 section 2.3.3. diff --git a/ssh/server.go b/ssh/server.go index 98679ba5b6..064dcbaf5a 100644 --- a/ssh/server.go +++ b/ssh/server.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "net" + "slices" "strings" ) @@ -43,6 +44,9 @@ type Permissions struct { // pass data from the authentication callbacks to the server // application layer. Extensions map[string]string + + // ExtraData allows to store user defined data. + ExtraData map[any]any } type GSSAPIWithMICConfig struct { @@ -126,6 +130,21 @@ type ServerConfig struct { // Permissions.Extensions entry. PublicKeyCallback func(conn ConnMetadata, key PublicKey) (*Permissions, error) + // VerifiedPublicKeyCallback, if non-nil, is called after a client + // successfully confirms having control over a key that was previously + // approved by PublicKeyCallback. The permissions object passed to the + // callback is the one returned by PublicKeyCallback for the given public + // key and its ownership is transferred to the callback. The returned + // Permissions object can be the same object, optionally modified, or a + // completely new object. If VerifiedPublicKeyCallback is non-nil, + // PublicKeyCallback is not allowed to return a PartialSuccessError, which + // can instead be returned by VerifiedPublicKeyCallback. + // + // VerifiedPublicKeyCallback does not affect which authentication methods + // are included in the list of methods that can be attempted by the client. + VerifiedPublicKeyCallback func(conn ConnMetadata, key PublicKey, permissions *Permissions, + signatureAlgorithm string) (*Permissions, error) + // KeyboardInteractiveCallback, if non-nil, is called when // keyboard-interactive authentication is selected (RFC // 4256). The client object's Challenge function should be @@ -246,7 +265,7 @@ func NewServerConn(c net.Conn, config *ServerConfig) (*ServerConn, <-chan NewCha fullConf.PublicKeyAuthAlgorithms = defaultPubKeyAuthAlgos } else { for _, algo := range fullConf.PublicKeyAuthAlgorithms { - if !contains(SupportedAlgorithms().PublicKeyAuths, algo) && !contains(InsecureAlgorithms().PublicKeyAuths, algo) { + if !slices.Contains(SupportedAlgorithms().PublicKeyAuths, algo) && !slices.Contains(InsecureAlgorithms().PublicKeyAuths, algo) { c.Close() return nil, nil, nil, fmt.Errorf("ssh: unsupported public key authentication algorithm %s", algo) } @@ -631,7 +650,7 @@ userAuthLoop: return nil, parseError(msgUserAuthRequest) } algo := string(algoBytes) - if !contains(config.PublicKeyAuthAlgorithms, underlyingAlgo(algo)) { + if !slices.Contains(config.PublicKeyAuthAlgorithms, underlyingAlgo(algo)) { authErr = fmt.Errorf("ssh: algorithm %q not accepted", algo) break } @@ -652,6 +671,9 @@ userAuthLoop: candidate.pubKeyData = pubKeyData candidate.perms, candidate.result = authConfig.PublicKeyCallback(s, pubKey) _, isPartialSuccessError := candidate.result.(*PartialSuccessError) + if isPartialSuccessError && config.VerifiedPublicKeyCallback != nil { + return nil, errors.New("ssh: invalid library usage: PublicKeyCallback must not return partial success when VerifiedPublicKeyCallback is defined") + } if (candidate.result == nil || isPartialSuccessError) && candidate.perms != nil && @@ -695,7 +717,7 @@ userAuthLoop: // ssh-rsa-cert-v01@openssh.com algorithm with ssh-rsa public // key type. The algorithm and public key type must be // consistent: both must be certificate algorithms, or neither. - if !contains(algorithmsForKeyFormat(pubKey.Type()), algo) { + if !slices.Contains(algorithmsForKeyFormat(pubKey.Type()), algo) { authErr = fmt.Errorf("ssh: public key type %q not compatible with selected algorithm %q", pubKey.Type(), algo) break @@ -705,7 +727,7 @@ userAuthLoop: // algorithm name that corresponds to algo with // sig.Format. This is usually the same, but // for certs, the names differ. - if !contains(config.PublicKeyAuthAlgorithms, sig.Format) { + if !slices.Contains(config.PublicKeyAuthAlgorithms, sig.Format) { authErr = fmt.Errorf("ssh: algorithm %q not accepted", sig.Format) break } @@ -722,6 +744,12 @@ userAuthLoop: authErr = candidate.result perms = candidate.perms + if authErr == nil && config.VerifiedPublicKeyCallback != nil { + // Only call VerifiedPublicKeyCallback after the key has been accepted + // and successfully verified. If authErr is non-nil, the key is not + // considered verified and the callback must not run. + perms, authErr = config.VerifiedPublicKeyCallback(s, pubKey, perms, algo) + } } case "gssapi-with-mic": if authConfig.GSSAPIWithMICConfig == nil { diff --git a/ssh/server_test.go b/ssh/server_test.go index 5bd18db5bb..e48f7e3059 100644 --- a/ssh/server_test.go +++ b/ssh/server_test.go @@ -434,6 +434,380 @@ func TestPreAuthConnAndBanners(t *testing.T) { } } +func TestVerifiedPublicKeyCallback(t *testing.T) { + c1, c2, err := netPipe() + if err != nil { + t.Fatalf("netPipe: %v", err) + } + defer c1.Close() + defer c2.Close() + + extraKey := "extra" + extraDataString := "just a string" + + serverConf := &ServerConfig{ + VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { + if permissions != nil && permissions.ExtraData != nil { + if !reflect.DeepEqual(map[any]any{extraKey: extraDataString}, permissions.ExtraData) { + t.Errorf("expected extra data: %v; got: %v", extraDataString, permissions.ExtraData) + } + } else { + t.Error("expected extra data is missing") + } + if signatureAlgorithm != KeyAlgoRSASHA256 { + t.Errorf("expected signature algorithm: %q; got: %q", KeyAlgoRSASHA256, signatureAlgorithm) + } + return permissions, nil + }, + PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { + return &Permissions{ExtraData: map[any]any{extraKey: extraDataString}}, nil + }, + } + serverConf.AddHostKey(testSigners["rsa"]) + + done := make(chan struct{}) + go func() { + defer close(done) + conn, _, _, err := NewServerConn(c1, serverConf) + if err != nil { + t.Errorf("unexpected server error: %v", err) + } + if !reflect.DeepEqual(map[any]any{extraKey: extraDataString}, conn.Permissions.ExtraData) { + t.Errorf("expected extra data: %v; got: %v", extraDataString, conn.Permissions.ExtraData) + } + }() + + clientConf := ClientConfig{ + User: "user", + Auth: []AuthMethod{ + PublicKeys(testSigners["rsa"]), + }, + HostKeyCallback: InsecureIgnoreHostKey(), + } + + _, _, _, err = NewClientConn(c2, "", &clientConf) + if err != nil { + t.Fatal(err) + } + <-done +} + +func TestVerifiedPublicCallbackPartialSuccess(t *testing.T) { + c1, c2, err := netPipe() + if err != nil { + t.Fatalf("netPipe: %v", err) + } + defer c1.Close() + defer c2.Close() + + serverConf := &ServerConfig{ + PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { + if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) { + return nil, nil + } + return nil, errors.New("invalid credentials") + }, + VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { + if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) { + return nil, &PartialSuccessError{ + Next: ServerAuthCallbacks{ + PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { + if string(password) == clientPassword { + return nil, nil + } + return nil, nil + }, + }, + } + } + return nil, errors.New("invalid credentials") + }, + } + serverConf.AddHostKey(testSigners["rsa"]) + + clientConf := ClientConfig{ + User: "user", + Auth: []AuthMethod{ + PublicKeys(testSigners["rsa"]), + Password(clientPassword), + }, + HostKeyCallback: InsecureIgnoreHostKey(), + } + + go NewServerConn(c1, serverConf) + + _, _, _, err = NewClientConn(c2, "", &clientConf) + if err != nil { + t.Fatalf("client login error: %s", err) + } +} + +func TestVerifiedPublicKeyCallbackPwdAndKey(t *testing.T) { + c1, c2, err := netPipe() + if err != nil { + t.Fatalf("netPipe: %v", err) + } + defer c1.Close() + defer c2.Close() + + serverConf := &ServerConfig{ + PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { + if string(password) == clientPassword { + 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") + }, + }, + } + } + 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, nil + } + return nil, errors.New("invalid credentials") + }, + } + serverConf.AddHostKey(testSigners["rsa"]) + + clientConf := ClientConfig{ + User: "user", + Auth: []AuthMethod{ + Password(clientPassword), + PublicKeys(testSigners["rsa"]), + }, + HostKeyCallback: InsecureIgnoreHostKey(), + } + + go NewServerConn(c1, serverConf) + + _, _, _, err = NewClientConn(c2, "", &clientConf) + if err != nil { + t.Fatalf("client login error: %s", err) + } +} + +func TestVerifiedPubKeyCallbackAuthMethods(t *testing.T) { + c1, c2, err := netPipe() + if err != nil { + t.Fatalf("netPipe: %v", err) + } + defer c1.Close() + defer c2.Close() + + serverConf := &ServerConfig{ + PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { + return nil, nil + }, + VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { + if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) { + return nil, nil + } + return nil, errors.New("invalid credentials") + }, + } + serverConf.AddHostKey(testSigners["rsa"]) + + clientConf := ClientConfig{ + User: "user", + Auth: []AuthMethod{ + PublicKeys(testSigners["rsa"]), + }, + HostKeyCallback: InsecureIgnoreHostKey(), + } + + go NewServerConn(c1, serverConf) + + _, _, _, err = NewClientConn(c2, "", &clientConf) + if err == nil { + t.Fatal("client login succeed with only VerifiedPublicKeyCallback defined") + } +} + +func TestVerifiedPubKeyCallbackError(t *testing.T) { + c1, c2, err := netPipe() + if err != nil { + t.Fatalf("netPipe: %v", err) + } + defer c1.Close() + defer c2.Close() + + serverConf := &ServerConfig{ + PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { + return nil, nil + }, + VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { + return nil, errors.New("invalid credentials") + }, + } + serverConf.AddHostKey(testSigners["rsa"]) + + clientConf := ClientConfig{ + User: "user", + Auth: []AuthMethod{ + PublicKeys(testSigners["rsa"]), + }, + HostKeyCallback: InsecureIgnoreHostKey(), + } + + go NewServerConn(c1, serverConf) + + _, _, _, err = NewClientConn(c2, "", &clientConf) + if err == nil { + t.Fatal("client login succeed with VerifiedPublicKeyCallback returning an error") + } +} + +func TestVerifiedPublicCallbackPartialSuccessBadUsage(t *testing.T) { + c1, c2, err := netPipe() + if err != nil { + t.Fatalf("netPipe: %v", err) + } + defer c1.Close() + defer c2.Close() + + serverConf := &ServerConfig{ + PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { + if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) { + // Returning PartialSuccessError is not permitted when + // VerifiedPublicKeyCallback is defined. This callback is + // invoked for both query requests and real authentications, + // while VerifiedPublicKeyCallback is only triggered if the + // client has proven control of the key. + return nil, &PartialSuccessError{ + Next: ServerAuthCallbacks{ + PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { + if string(password) == clientPassword { + return nil, nil + } + return nil, nil + }, + }, + } + } + return nil, errors.New("invalid credentials") + }, + 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{ + PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { + if string(password) == clientPassword { + return nil, nil + } + return nil, nil + }, + }, + } + } + return nil, errors.New("invalid credentials") + }, + } + serverConf.AddHostKey(testSigners["rsa"]) + + clientConf := ClientConfig{ + User: "user", + Auth: []AuthMethod{ + PublicKeys(testSigners["rsa"]), + Password(clientPassword), + }, + HostKeyCallback: InsecureIgnoreHostKey(), + } + + go NewServerConn(c1, serverConf) + + _, _, _, err = NewClientConn(c2, "", &clientConf) + if err == nil { + t.Fatal("authentication suceeded with PartialSuccess returned from PublicKeyCallback and VerifiedPublicKeyCallback defined") + } +} + +func TestVerifiedPublicKeyCallbackOnError(t *testing.T) { + c1, c2, err := netPipe() + if err != nil { + t.Fatalf("netPipe: %v", err) + } + defer c1.Close() + defer c2.Close() + + var verifiedCallbackCalled bool + + serverConf := &ServerConfig{ + VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { + verifiedCallbackCalled = true + return nil, nil + }, + PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { + return nil, errors.New("invalid key") + }, + } + serverConf.AddHostKey(testSigners["rsa"]) + + done := make(chan struct{}) + go func() { + defer close(done) + NewServerConn(c1, serverConf) + }() + + clientConf := ClientConfig{ + User: "user", + Auth: []AuthMethod{ + PublicKeys(testSigners["rsa"]), + }, + HostKeyCallback: InsecureIgnoreHostKey(), + } + + _, _, _, err = NewClientConn(c2, "", &clientConf) + if err == nil { + t.Fatal("authentication should fail") + } + <-done + if verifiedCallbackCalled { + t.Error("VerifiedPublicKeyCallback called after PublicKeyCallback returned an error") + } +} + +func TestVerifiedPublicKeyCallbackOnly(t *testing.T) { + c1, c2, err := netPipe() + if err != nil { + t.Fatalf("netPipe: %v", err) + } + defer c1.Close() + defer c2.Close() + + serverConf := &ServerConfig{ + VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { + return nil, nil + }, + } + serverConf.AddHostKey(testSigners["rsa"]) + done := make(chan struct{}) + go func() { + defer close(done) + NewServerConn(c1, serverConf) + }() + + clientConf := ClientConfig{ + User: "user", + Auth: []AuthMethod{ + PublicKeys(testSigners["rsa"]), + }, + HostKeyCallback: InsecureIgnoreHostKey(), + } + + _, _, _, err = NewClientConn(c2, "", &clientConf) + if err == nil { + t.Fatal("authentication suceeded with only VerifiedPublicKeyCallback defined") + } + <-done +} + type markerConn struct { closed uint32 used uint32 diff --git a/ssh/test/doc.go b/ssh/test/doc.go index 444b29966b..865781c819 100644 --- a/ssh/test/doc.go +++ b/ssh/test/doc.go @@ -4,4 +4,6 @@ // Package test contains integration tests for the // golang.org/x/crypto/ssh package. +// +// Deprecated: this package is for internal use only. package test diff --git a/ssh/transport.go b/ssh/transport.go index 663619845c..fa3dd6a429 100644 --- a/ssh/transport.go +++ b/ssh/transport.go @@ -8,6 +8,7 @@ import ( "bufio" "bytes" "errors" + "fmt" "io" "log" ) @@ -254,6 +255,9 @@ var ( // (to setup server->client keys) or clientKeys (for client->server keys). func newPacketCipher(d direction, algs DirectionAlgorithms, kex *kexResult) (packetCipher, error) { cipherMode := cipherModes[algs.Cipher] + if cipherMode == nil { + return nil, fmt.Errorf("ssh: unsupported cipher %v", algs.Cipher) + } iv := make([]byte, cipherMode.ivSize) key := make([]byte, cipherMode.keySize) diff --git a/x509roots/fallback/bundle.der b/x509roots/fallback/bundle/bundle.der similarity index 98% rename from x509roots/fallback/bundle.der rename to x509roots/fallback/bundle/bundle.der index 1abf12f598..7c75ce5e07 100644 Binary files a/x509roots/fallback/bundle.der and b/x509roots/fallback/bundle/bundle.der differ diff --git a/x509roots/fallback/bundle.go b/x509roots/fallback/bundle/bundle.go similarity index 94% rename from x509roots/fallback/bundle.go rename to x509roots/fallback/bundle/bundle.go index ee99a40a28..8b8f20a470 100644 --- a/x509roots/fallback/bundle.go +++ b/x509roots/fallback/bundle/bundle.go @@ -1,6 +1,6 @@ // Code generated by gen_fallback_bundle.go; DO NOT EDIT. -package fallback +package bundle var unparsedCertificates = []unparsedCertificate{ { @@ -521,365 +521,377 @@ var unparsedCertificates = []unparsedCertificate{ certStartOff: 88949, certLength: 1049, }, + { + cn: "CN=OISTE Server Root ECC G1,O=OISTE Foundation,C=CH", + sha256Hash: "eec997c0c30f216f7e3b8b307d2bae42412d753fc8219dafd1520b2572850f49", + certStartOff: 89998, + certLength: 569, + }, + { + cn: "CN=OISTE Server Root RSA G1,O=OISTE Foundation,C=CH", + sha256Hash: "9ae36232a5189ffddb353dfd26520c015395d22777dac59db57b98c089a651e6", + certStartOff: 90567, + certLength: 1415, + }, { cn: "CN=OISTE WISeKey Global Root GB CA,OU=OISTE Foundation Endorsed,O=WISeKey,C=CH", sha256Hash: "6b9c08e86eb0f767cfad65cd98b62149e5494a67f5845e7bd1ed019f27b86bd6", - certStartOff: 89998, + certStartOff: 91982, certLength: 953, }, { cn: "CN=OISTE WISeKey Global Root GC CA,OU=OISTE Foundation Endorsed,O=WISeKey,C=CH", sha256Hash: "8560f91c3624daba9570b5fea0dbe36ff11a8323be9486854fb3f34a5571198d", - certStartOff: 90951, + certStartOff: 92935, certLength: 621, }, { cn: "CN=QuoVadis Root CA 1 G3,O=QuoVadis Limited,C=BM", sha256Hash: "8a866fd1b276b57e578e921c65828a2bed58e9f2f288054134b7f1f4bfc9cc74", - certStartOff: 91572, + certStartOff: 93556, certLength: 1380, }, { cn: "CN=QuoVadis Root CA 2 G3,O=QuoVadis Limited,C=BM", sha256Hash: "8fe4fb0af93a4d0d67db0bebb23e37c71bf325dcbcdd240ea04daf58b47e1840", - certStartOff: 92952, + certStartOff: 94936, certLength: 1380, }, { cn: "CN=QuoVadis Root CA 2,O=QuoVadis Limited,C=BM", sha256Hash: "85a0dd7dd720adb7ff05f83d542b209dc7ff4528f7d677b18389fea5e5c49e86", - certStartOff: 94332, + certStartOff: 96316, certLength: 1467, }, { cn: "CN=QuoVadis Root CA 3 G3,O=QuoVadis Limited,C=BM", sha256Hash: "88ef81de202eb018452e43f864725cea5fbd1fc2d9d205730709c5d8b8690f46", - certStartOff: 95799, + certStartOff: 97783, certLength: 1380, }, { cn: "CN=QuoVadis Root CA 3,O=QuoVadis Limited,C=BM", sha256Hash: "18f1fc7f205df8adddeb7fe007dd57e3af375a9c4d8d73546bf4f1fed1e18d35", - certStartOff: 97179, + certStartOff: 99163, certLength: 1697, }, { cn: "CN=SSL.com EV Root Certification Authority ECC,O=SSL Corporation,L=Houston,ST=Texas,C=US", sha256Hash: "22a2c1f7bded704cc1e701b5f408c310880fe956b5de2a4a44f99c873a25a7c8", - certStartOff: 98876, + certStartOff: 100860, certLength: 664, }, { cn: "CN=SSL.com EV Root Certification Authority RSA R2,O=SSL Corporation,L=Houston,ST=Texas,C=US", sha256Hash: "2e7bf16cc22485a7bbe2aa8696750761b0ae39be3b2fe9d0cc6d4ef73491425c", - certStartOff: 99540, + certStartOff: 101524, certLength: 1519, }, { cn: "CN=SSL.com Root Certification Authority ECC,O=SSL Corporation,L=Houston,ST=Texas,C=US", sha256Hash: "3417bb06cc6007da1b961c920b8ab4ce3fad820e4aa30b9acbc4a74ebdcebc65", - certStartOff: 101059, + certStartOff: 103043, certLength: 657, }, { cn: "CN=SSL.com Root Certification Authority RSA,O=SSL Corporation,L=Houston,ST=Texas,C=US", sha256Hash: "85666a562ee0be5ce925c1d8890a6f76a87ec16d4d7d5f29ea7419cf20123b69", - certStartOff: 101716, + certStartOff: 103700, certLength: 1505, }, { cn: "CN=SSL.com TLS ECC Root CA 2022,O=SSL Corporation,C=US", sha256Hash: "c32ffd9f46f936d16c3673990959434b9ad60aafbb9e7cf33654f144cc1ba143", - certStartOff: 103221, + certStartOff: 105205, certLength: 574, }, { cn: "CN=SSL.com TLS RSA Root CA 2022,O=SSL Corporation,C=US", sha256Hash: "8faf7d2e2cb4709bb8e0b33666bf75a5dd45b5de480f8ea8d4bfe6bebc17f2ed", - certStartOff: 103795, + certStartOff: 105779, certLength: 1421, }, { cn: "CN=SZAFIR ROOT CA2,O=Krajowa Izba Rozliczeniowa S.A.,C=PL", sha256Hash: "a1339d33281a0b56e557d3d32b1ce7f9367eb094bd5fa72a7e5004c8ded7cafe", - certStartOff: 105216, + certStartOff: 107200, certLength: 886, }, { cn: "CN=Sectigo Public Server Authentication Root E46,O=Sectigo Limited,C=GB", sha256Hash: "c90f26f0fb1b4018b22227519b5ca2b53e2ca5b3be5cf18efe1bef47380c5383", - certStartOff: 106102, + certStartOff: 108086, certLength: 574, }, { cn: "CN=Sectigo Public Server Authentication Root R46,O=Sectigo Limited,C=GB", sha256Hash: "7bb647a62aeeac88bf257aa522d01ffea395e0ab45c73f93f65654ec38f25a06", - certStartOff: 106676, + certStartOff: 108660, certLength: 1422, }, { cn: "CN=Secure Global CA,O=SecureTrust Corporation,C=US", sha256Hash: "4200f5043ac8590ebb527d209ed1503029fbcbd41ca1b506ec27f15ade7dac69", - certStartOff: 108098, + certStartOff: 110082, certLength: 960, }, { cn: "CN=SecureSign Root CA12,O=Cybertrust Japan Co.\\, Ltd.,C=JP", sha256Hash: "3f034bb5704d44b2d08545a02057de93ebf3905fce721acbc730c06ddaee904e", - certStartOff: 109058, + certStartOff: 111042, certLength: 886, }, { cn: "CN=SecureSign Root CA14,O=Cybertrust Japan Co.\\, Ltd.,C=JP", sha256Hash: "4b009c1034494f9ab56bba3ba1d62731fc4d20d8955adcec10a925607261e338", - certStartOff: 109944, + certStartOff: 111928, certLength: 1398, }, { cn: "CN=SecureSign Root CA15,O=Cybertrust Japan Co.\\, Ltd.,C=JP", sha256Hash: "e778f0f095fe843729cd1a0082179e5314a9c291442805e1fb1d8fb6b8886c3a", - certStartOff: 111342, + certStartOff: 113326, certLength: 551, }, { cn: "CN=SecureTrust CA,O=SecureTrust Corporation,C=US", sha256Hash: "f1c1b50ae5a20dd8030ec9f6bc24823dd367b5255759b4e71b61fce9f7375d73", - certStartOff: 111893, + certStartOff: 113877, certLength: 956, }, { cn: "CN=Security Communication ECC RootCA1,O=SECOM Trust Systems CO.\\,LTD.,C=JP", sha256Hash: "e74fbda55bd564c473a36b441aa799c8a68e077440e8288b9fa1e50e4bbaca11", - certStartOff: 112849, + certStartOff: 114833, certLength: 572, }, { cn: "CN=Starfield Root Certificate Authority - G2,O=Starfield Technologies\\, Inc.,L=Scottsdale,ST=Arizona,C=US", sha256Hash: "2ce1cb0bf9d2f9e102993fbe215152c3b2dd0cabde1c68e5319b839154dbb7f5", - certStartOff: 113421, + certStartOff: 115405, certLength: 993, }, { cn: "CN=Starfield Services Root Certificate Authority - G2,O=Starfield Technologies\\, Inc.,L=Scottsdale,ST=Arizona,C=US", sha256Hash: "568d6905a2c88708a4b3025190edcfedb1974a606a13c6e5290fcb2ae63edab5", - certStartOff: 114414, + certStartOff: 116398, certLength: 1011, }, { cn: "CN=SwissSign Gold CA - G2,O=SwissSign AG,C=CH", sha256Hash: "62dd0be9b9f50a163ea0f8e75c053b1eca57ea55c8688f647c6881f2c8357b95", - certStartOff: 115425, + certStartOff: 117409, certLength: 1470, }, { cn: "CN=SwissSign RSA TLS Root CA 2022 - 1,O=SwissSign AG,C=CH", sha256Hash: "193144f431e0fddb740717d4de926a571133884b4360d30e272913cbe660ce41", - certStartOff: 116895, + certStartOff: 118879, certLength: 1431, }, { cn: "CN=T-TeleSec GlobalRoot Class 2,OU=T-Systems Trust Center,O=T-Systems Enterprise Services GmbH,C=DE", sha256Hash: "91e2f5788d5810eba7ba58737de1548a8ecacd014598bc0b143e041b17052552", - certStartOff: 118326, + certStartOff: 120310, certLength: 967, }, { cn: "CN=T-TeleSec GlobalRoot Class 3,OU=T-Systems Trust Center,O=T-Systems Enterprise Services GmbH,C=DE", sha256Hash: "fd73dad31c644ff1b43bef0ccdda96710b9cd9875eca7e31707af3e96d522bbd", - certStartOff: 119293, + certStartOff: 121277, certLength: 967, }, { cn: "CN=TWCA CYBER Root CA,OU=Root CA,O=TAIWAN-CA,C=TW", sha256Hash: "3f63bb2814be174ec8b6439cf08d6d56f0b7c405883a5648a334424d6b3ec558", - certStartOff: 120260, + certStartOff: 122244, certLength: 1425, }, { cn: "CN=TWCA Global Root CA,OU=Root CA,O=TAIWAN-CA,C=TW", sha256Hash: "59769007f7685d0fcd50872f9f95d5755a5b2b457d81f3692b610a98672f0e1b", - certStartOff: 121685, + certStartOff: 123669, certLength: 1349, }, { cn: "CN=TWCA Root Certification Authority,OU=Root CA,O=TAIWAN-CA,C=TW", sha256Hash: "bfd88fe1101c41ae3e801bf8be56350ee9bad1a6b9bd515edc5c6d5b8711ac44", - certStartOff: 123034, + certStartOff: 125018, certLength: 895, }, { cn: "CN=Telekom Security TLS ECC Root 2020,O=Deutsche Telekom Security GmbH,C=DE", sha256Hash: "578af4ded0853f4e5998db4aeaf9cbea8d945f60b620a38d1a3c13b2bc7ba8e1", - certStartOff: 123929, + certStartOff: 125913, certLength: 582, }, { cn: "CN=Telekom Security TLS RSA Root 2023,O=Deutsche Telekom Security GmbH,C=DE", sha256Hash: "efc65cadbb59adb6efe84da22311b35624b71b3b1ea0da8b6655174ec8978646", - certStartOff: 124511, + certStartOff: 126495, certLength: 1463, }, { cn: "CN=Telia Root CA v2,O=Telia Finland Oyj,C=FI", sha256Hash: "242b69742fcb1e5b2abf98898b94572187544e5b4d9911786573621f6a74b82c", - certStartOff: 125974, + certStartOff: 127958, certLength: 1400, }, { cn: "CN=TeliaSonera Root CA v1,O=TeliaSonera", sha256Hash: "dd6936fe21f8f077c123a1a521c12224f72255b73e03a7260693e8a24b0fa389", - certStartOff: 127374, + certStartOff: 129358, certLength: 1340, }, { cn: "CN=TrustAsia Global Root CA G3,O=TrustAsia Technologies\\, Inc.,C=CN", sha256Hash: "e0d3226aeb1163c2e48ff9be3b50b4c6431be7bb1eacc5c36b5d5ec509039a08", - certStartOff: 128714, + certStartOff: 130698, certLength: 1449, }, { cn: "CN=TrustAsia Global Root CA G4,O=TrustAsia Technologies\\, Inc.,C=CN", sha256Hash: "be4b56cb5056c0136a526df444508daa36a0b54f42e4ac38f72af470e479654c", - certStartOff: 130163, + certStartOff: 132147, certLength: 601, }, { cn: "CN=TrustAsia TLS ECC Root CA,O=TrustAsia Technologies\\, Inc.,C=CN", sha256Hash: "c0076b9ef0531fb1a656d67c4ebe97cd5dbaa41ef44598acc2489878c92d8711", - certStartOff: 130764, + certStartOff: 132748, certLength: 565, }, { cn: "CN=TrustAsia TLS RSA Root CA,O=TrustAsia Technologies\\, Inc.,C=CN", sha256Hash: "06c08d7dafd876971eb1124fe67f847ec0c7a158d3ea53cbe940e2ea9791f4c3", - certStartOff: 131329, + certStartOff: 133313, certLength: 1412, }, { cn: "CN=Trustwave Global Certification Authority,O=Trustwave Holdings\\, Inc.,L=Chicago,ST=Illinois,C=US", sha256Hash: "97552015f5ddfc3c8788c006944555408894450084f100867086bc1a2bb58dc8", - certStartOff: 132741, + certStartOff: 134725, certLength: 1502, }, { cn: "CN=Trustwave Global ECC P256 Certification Authority,O=Trustwave Holdings\\, Inc.,L=Chicago,ST=Illinois,C=US", sha256Hash: "945bbc825ea554f489d1fd51a73ddf2ea624ac7019a05205225c22a78ccfa8b4", - certStartOff: 134243, + certStartOff: 136227, certLength: 612, }, { cn: "CN=Trustwave Global ECC P384 Certification Authority,O=Trustwave Holdings\\, Inc.,L=Chicago,ST=Illinois,C=US", sha256Hash: "55903859c8c0c3ebb8759ece4e2557225ff5758bbd38ebd48276601e1bd58097", - certStartOff: 134855, + certStartOff: 136839, certLength: 673, }, { cn: "CN=TunTrust Root CA,O=Agence Nationale de Certification Electronique,C=TN", sha256Hash: "2e44102ab58cb85419451c8e19d9acf3662cafbc614b6a53960a30f7d0e2eb41", - certStartOff: 135528, + certStartOff: 137512, certLength: 1463, }, { cn: "CN=UCA Extended Validation Root,O=UniTrust,C=CN", sha256Hash: "d43af9b35473755c9684fc06d7d8cb70ee5c28e773fb294eb41ee71722924d24", - certStartOff: 136991, + certStartOff: 138975, certLength: 1374, }, { cn: "CN=UCA Global G2 Root,O=UniTrust,C=CN", sha256Hash: "9bea11c976fe014764c1be56a6f914b5a560317abd9988393382e5161aa0493c", - certStartOff: 138365, + certStartOff: 140349, certLength: 1354, }, { cn: "CN=USERTrust ECC Certification Authority,O=The USERTRUST Network,L=Jersey City,ST=New Jersey,C=US", sha256Hash: "4ff460d54b9c86dabfbcfc5712e0400d2bed3fbc4d4fbdaa86e06adcd2a9ad7a", - certStartOff: 139719, + certStartOff: 141703, certLength: 659, }, { cn: "CN=USERTrust RSA Certification Authority,O=The USERTRUST Network,L=Jersey City,ST=New Jersey,C=US", sha256Hash: "e793c9b02fd8aa13e21c31228accb08119643b749c898964b1746d46c3d4cbd2", - certStartOff: 140378, + certStartOff: 142362, certLength: 1506, }, { cn: "CN=e-Szigno Root CA 2017,O=Microsec Ltd.,L=Budapest,C=HU,2.5.4.97=#130e56415448552d3233353834343937", sha256Hash: "beb00b30839b9bc32c32e4447905950641f26421b15ed089198b518ae2ea1b99", - certStartOff: 141884, + certStartOff: 143868, certLength: 580, }, { cn: "CN=emSign ECC Root CA - C3,OU=emSign PKI,O=eMudhra Inc,C=US", sha256Hash: "bc4d809b15189d78db3e1d8cf4f9726a795da1643ca5f1358e1ddb0edc0d7eb3", - certStartOff: 142464, + certStartOff: 144448, certLength: 559, }, { cn: "CN=emSign ECC Root CA - G3,OU=emSign PKI,O=eMudhra Technologies Limited,C=IN", sha256Hash: "86a1ecba089c4a8d3bbe2734c612ba341d813e043cf9e8a862cd5c57a36bbe6b", - certStartOff: 143023, + certStartOff: 145007, certLength: 594, }, { cn: "CN=emSign Root CA - C1,OU=emSign PKI,O=eMudhra Inc,C=US", sha256Hash: "125609aa301da0a249b97a8239cb6a34216f44dcac9f3954b14292f2e8c8608f", - certStartOff: 143617, + certStartOff: 145601, certLength: 887, }, { cn: "CN=emSign Root CA - G1,OU=emSign PKI,O=eMudhra Technologies Limited,C=IN", sha256Hash: "40f6af0346a99aa1cd1d555a4e9cce62c7f9634603ee406615833dc8c8d00367", - certStartOff: 144504, + certStartOff: 146488, certLength: 920, }, { cn: "CN=vTrus ECC Root CA,O=iTrusChina Co.\\,Ltd.,C=CN", sha256Hash: "30fbba2c32238e2a98547af97931e550428b9b3f1c8eeb6633dcfa86c5b27dd3", - certStartOff: 145424, + certStartOff: 147408, certLength: 531, }, { cn: "CN=vTrus Root CA,O=iTrusChina Co.\\,Ltd.,C=CN", sha256Hash: "8a71de6559336f426c26e53880d00d88a18da4c6a91f0dcb6194e206c5c96387", - certStartOff: 145955, + certStartOff: 147939, certLength: 1370, }, { cn: "OU=AC RAIZ FNMT-RCM,O=FNMT-RCM,C=ES", sha256Hash: "ebc5570c29018c4d67b1aa127baf12f703b4611ebc17b7dab5573894179b93fa", - certStartOff: 147325, + certStartOff: 149309, certLength: 1415, }, { cn: "OU=Security Communication RootCA2,O=SECOM Trust Systems CO.\\,LTD.,C=JP", sha256Hash: "513b2cecb810d4cde5dd85391adfc6c2dd60d87bb736d2b521484aa47a0ebef6", - certStartOff: 148740, + certStartOff: 150724, certLength: 891, }, { cn: "OU=certSIGN ROOT CA G2,O=CERTSIGN SA,C=RO", sha256Hash: "657cfe2fa73faa38462571f332a2363a46fce7020951710702cdfbb6eeda3305", - certStartOff: 149631, + certStartOff: 151615, certLength: 1355, }, { cn: "OU=certSIGN ROOT CA,O=certSIGN,C=RO", sha256Hash: "eaa962c4fa4a6bafebe415196d351ccd888d4f53f3fa8ae6d7c466a94e6042bb", - certStartOff: 150986, + certStartOff: 152970, certLength: 828, }, { cn: "OU=ePKI Root Certification Authority,O=Chunghwa Telecom Co.\\, Ltd.,C=TW", sha256Hash: "c0a6f4dc63a24bfdcf54ef2a6a082a0a72de35803e2ff5ff527ae5d87206dfd5", - certStartOff: 151814, + certStartOff: 153798, certLength: 1460, distrustAfter: "2025-04-15T23:59:59Z", }, { cn: "SERIALNUMBER=G63287510,CN=ANF Secure Server Root CA,OU=ANF CA Raiz,O=ANF Autoridad de Certificacion,C=ES", sha256Hash: "fb8fec759169b9106b1e511644c618c51304373f6c0643088d8beffd1b997599", - certStartOff: 153274, + certStartOff: 155258, certLength: 1523, }, } diff --git a/x509roots/fallback/bundle_test.go b/x509roots/fallback/bundle/bundle_test.go similarity index 98% rename from x509roots/fallback/bundle_test.go rename to x509roots/fallback/bundle/bundle_test.go index a8922cc93d..3eafe153af 100644 --- a/x509roots/fallback/bundle_test.go +++ b/x509roots/fallback/bundle/bundle_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package fallback +package bundle import ( "crypto/sha256" diff --git a/x509roots/fallback/bundle/roots.go b/x509roots/fallback/bundle/roots.go new file mode 100644 index 0000000000..38a1b3d671 --- /dev/null +++ b/x509roots/fallback/bundle/roots.go @@ -0,0 +1,73 @@ +// 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 bundle contains the bundle of root certificates parsed from the NSS +// trust store, using x509roots/nss. +package bundle + +import ( + "crypto/x509" + _ "embed" + "fmt" + "iter" + "time" +) + +//go:embed bundle.der +var rawCerts []byte + +// Root represents a root certificate parsed from the NSS trust store. +type Root struct { + // Certificate is the DER-encoded certificate (read-only; do not modify!). + Certificate []byte + + // Constraint is nil if the root is unconstrained. If Constraint is non-nil, + // the certificate has additional constraints that cannot be encoded in + // X.509, and when building a certificate chain anchored with this root the + // chain should be passed to this function to check its validity. If using a + // [crypto/x509.CertPool] the root should be added using + // [crypto/x509.CertPool.AddCertWithConstraint]. + Constraint func([]*x509.Certificate) error +} + +// Roots returns the bundle of root certificates from the NSS trust store. The +// [Root.Certificate] slice must be treated as read-only and should not be +// modified. +func Roots() iter.Seq[Root] { + return func(yield func(Root) bool) { + for _, unparsed := range unparsedCertificates { + root := Root{ + Certificate: rawCerts[unparsed.certStartOff : unparsed.certStartOff+unparsed.certLength], + } + // parse possible constraints, this should check all fields of unparsedCertificate. + if unparsed.distrustAfter != "" { + distrustAfter, err := time.Parse(time.RFC3339, unparsed.distrustAfter) + if err != nil { + panic(fmt.Sprintf("failed to parse distrustAfter %q: %s", unparsed.distrustAfter, err)) + } + root.Constraint = func(chain []*x509.Certificate) error { + for _, c := range chain { + if c.NotBefore.After(distrustAfter) { + return fmt.Errorf("certificate issued after distrust-after date %q", distrustAfter) + } + } + return nil + } + } + if !yield(root) { + return + } + } + } +} + +type unparsedCertificate struct { + cn string + sha256Hash string + certStartOff int + certLength int + + // possible constraints + distrustAfter string +} diff --git a/x509roots/fallback/bundle/roots_test.go b/x509roots/fallback/bundle/roots_test.go new file mode 100644 index 0000000000..04ba9db299 --- /dev/null +++ b/x509roots/fallback/bundle/roots_test.go @@ -0,0 +1,18 @@ +// 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 bundle + +import ( + "crypto/x509" + "testing" +) + +func TestRootsCanBeParsed(t *testing.T) { + for root := range Roots() { + if _, err := x509.ParseCertificate(root.Certificate); err != nil { + t.Fatalf("Could not parse root certificate: %v", err) + } + } +} diff --git a/x509roots/fallback/fallback.go b/x509roots/fallback/fallback.go index a0dad330ea..79e18704a8 100644 --- a/x509roots/fallback/fallback.go +++ b/x509roots/fallback/fallback.go @@ -20,13 +20,9 @@ package fallback import ( "crypto/x509" - _ "embed" - "fmt" - "time" -) -//go:embed bundle.der -var rawCerts []byte + "golang.org/x/crypto/x509roots/fallback/bundle" +) func init() { x509.SetFallbackRoots(newFallbackCertPool()) @@ -34,62 +30,16 @@ func init() { func newFallbackCertPool() *x509.CertPool { p := x509.NewCertPool() - for _, c := range mustParse(unparsedCertificates) { - if len(c.constraints) == 0 { - p.AddCert(c.cert) - } else { - p.AddCertWithConstraint(c.cert, func(chain []*x509.Certificate) error { - for _, constraint := range c.constraints { - if err := constraint(chain); err != nil { - return err - } - } - return nil - }) - } - } - return p -} - -type unparsedCertificate struct { - cn string - sha256Hash string - certStartOff int - certLength int - - // possible constraints - distrustAfter string -} - -type parsedCertificate struct { - cert *x509.Certificate - constraints []func([]*x509.Certificate) error -} - -func mustParse(unparsedCerts []unparsedCertificate) []parsedCertificate { - b := make([]parsedCertificate, 0, len(unparsedCerts)) - for _, unparsed := range unparsedCerts { - cert, err := x509.ParseCertificate(rawCerts[unparsed.certStartOff : unparsed.certStartOff+unparsed.certLength]) + for c := range bundle.Roots() { + cert, err := x509.ParseCertificate(c.Certificate) if err != nil { panic(err) } - parsed := parsedCertificate{cert: cert} - // parse possible constraints, this should check all fields of unparsedCertificate. - if unparsed.distrustAfter != "" { - distrustAfter, err := time.Parse(time.RFC3339, unparsed.distrustAfter) - if err != nil { - panic(fmt.Sprintf("failed to parse distrustAfter %q: %s", unparsed.distrustAfter, err)) - } - parsed.constraints = append(parsed.constraints, func(chain []*x509.Certificate) error { - for _, c := range chain { - if c.NotBefore.After(distrustAfter) { - return fmt.Errorf("certificate issued after distrust-after date %q", distrustAfter) - } - } - return nil - }) + if c.Constraint == nil { + p.AddCert(cert) + } else { + p.AddCertWithConstraint(cert, c.Constraint) } - b = append(b, parsed) } - return b + return p } diff --git a/x509roots/fallback/go.mod b/x509roots/fallback/go.mod index 6ffde44ff8..5d4b07eee5 100644 --- a/x509roots/fallback/go.mod +++ b/x509roots/fallback/go.mod @@ -1,3 +1,3 @@ module golang.org/x/crypto/x509roots/fallback -go 1.23.0 +go 1.24.0 diff --git a/x509roots/gen_fallback_bundle.go b/x509roots/gen_fallback_bundle.go index ed2f9f84fb..810996cbac 100644 --- a/x509roots/gen_fallback_bundle.go +++ b/x509roots/gen_fallback_bundle.go @@ -27,7 +27,7 @@ import ( const tmpl = `// Code generated by gen_fallback_bundle.go; DO NOT EDIT. -package fallback +package bundle var unparsedCertificates = []unparsedCertificate{ ` @@ -35,8 +35,8 @@ var unparsedCertificates = []unparsedCertificate{ var ( certDataURL = flag.String("certdata-url", "https://hg.mozilla.org/mozilla-central/raw-file/tip/security/nss/lib/ckfw/builtins/certdata.txt", "URL to the raw certdata.txt file to parse (certdata-path overrides this, if provided)") certDataPath = flag.String("certdata-path", "", "Path to the NSS certdata.txt file to parse (this overrides certdata-url, if provided)") - output = flag.String("output", "fallback/bundle.go", "Path to file to write output to") - derOutput = flag.String("deroutput", "fallback/bundle.der", "Path to file to write output to (DER certificate bundle)") + output = flag.String("output", "fallback/bundle/bundle.go", "Path to file to write output to") + derOutput = flag.String("deroutput", "fallback/bundle/bundle.der", "Path to file to write output to (DER certificate bundle)") ) func main() { diff --git a/xts/xts.go b/xts/xts.go index d64f536f9d..6a73020207 100644 --- a/xts/xts.go +++ b/xts/xts.go @@ -21,6 +21,10 @@ // // Note that XTS is usually not appropriate for any use besides disk encryption. // Most users should use an AEAD mode like GCM (from crypto/cipher.NewGCM) instead. +// +// The xts package is [frozen] and is not accepting new features. +// +// [frozen]: https://go.dev/wiki/Frozen package xts import (