Compare commits

...
31 Commits
Author SHA1 Message Date
Gopher Robot cdce021fa6 go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: I431b878ec8830bf1894ae19176b5984b00954557
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/798280
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
2026-07-08 11:22:26 -07:00
Roland Shoemaker d9474cc485 openpgp: make the deprecation message more explicit
The package is unmaintained, unsafe at any speed, and does not receive
security fixes. Document that, and point at the ProtonMail fork for
those that _must_ still use OpenPGP.

Change-Id: Ia602eaaf9e08bd25389400ebe7f346b60bdb9d99
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/794200
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
2026-07-07 10:06:58 -07:00
Nicola Murino 7626c50256 ssh: verify declared key type matches decoded key in authorized_keys
ParseAuthorizedKey and ParseKnownHosts previously ignored the key type
field (e.g. "ssh-rsa") in each entry, relying solely on the type
information embedded within the base64-encoded key blob.

For ParseAuthorizedKey this also caused a single-token option to be
silently dropped: a line such as "restrict <key>" (with the key type
omitted) was parsed as an unrestricted key, because the option token
landed in the key type position and was discarded together with its
meaning. The same happens for no-pty, no-port-forwarding and the other
single-word options. OpenSSH's sshkey_read rejects such lines outright,
as the option token cannot be read as a key type.

OpenSSH's sshkey_read also explicitly verifies that the key type
declared in the text matches the type of the parsed key, returning
SSH_ERR_KEY_TYPE_MISMATCH if they differ.

This change adds, in both functions, a check that the declared key type
matches the decoded key's type, returning an error for malformed lines
where they diverge. This mirrors the fix already applied to the
ssh/knownhosts package in CL 782427.

Change-Id: I9163108d964de4be8a415fe0efd68e6b71f77990
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/792840
Reviewed-by: Filippo Valsorda <filippo@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Nicola Murino <nicola.murino@gmail.com>
Reviewed-by: Neal Patel <nealpatel@google.com>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
2026-06-30 10:24:32 -07:00
Nicola Murino 0471e7969e ssh/agent: enforce strict limits on DSA key parameters
The parseDSAKey function constructed a *dsa.PrivateKey directly from the
add-identity request without validating the key parameters. Unlike DSA
certificates, whose parameters are checked by the ssh package when the
certificate's public key is parsed, raw DSA keys added to the agent were
not validated at all.

Align the raw DSA key parsing with the validation already performed by
the main ssh package.

Fixes golang/go#79725

Change-Id: I537cc2175d35c19848c90c68739cf94ba7b50e10
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/795422
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-06-30 10:06:28 -07:00
Neal Patel 6435c37ab6 ssh: sanitize client disconnect messages
A malicious peer can send control characters
and escape sequences in SSH_MSG_DISCONNECT;
strip these characters and %q escape it.

Change-Id: I266f78630a7a816a79ce4dda568ce57d7c3f6fb0
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/780180
Commit-Queue: Nicola Murino <nicola.murino@gmail.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Neal Patel <nealpatel@google.com>
Reviewed-by: Nicola Murino <nicola.murino@gmail.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Nicola Murino <nicola.murino@gmail.com>
2026-06-29 10:23:17 -07:00
Nicola Murino 7d695da948 ssh/agent: drain channel stderr in agent forwarders
ForwardToAgent and ForwardToRemote only read the main stream of the
auth-agent@openssh.com channels they accept. If a peer sends data on
the channel's extended (stderr) stream the bytes accumulate in the
client-side extPending buffer and the receive window is never
replenished, because the window is only adjusted as a side effect of
ReadExtended. That can pin up to channelWindowSize (2 MiB) of memory
per channel and silently stalls any stderr traffic once the window is
exhausted.

The auth-agent protocol does not use stderr, so a well-behaved peer
never sends anything on it. To stay tolerant of misbehaving peers
without leaving the channel half-stuck, drain the stderr stream into
io.Discard, mirroring the existing DiscardRequests pattern. The
goroutine exits when the channel is closed because Stderr().Read
returns io.EOF.

Add a regression test that opens an agent-forwarding channel and
writes more than the default window on the stderr stream from the
server side. Without the fix the write blocks once the remote window
is exhausted; with the fix the bytes are drained and the agent stream
remains usable.

Change-Id: Iadf8ea6ca726c058421bbc39f92e0100579fda17
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/783720
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
2026-06-29 06:36:26 -07:00
Daniel McCarney 5b7f841599 acme/autocert: fix data race in Manager.createCert
Previously Manager.createCert() read state.locked without holding any
lock, while the owner goroutine wrote state.locked = false under the
state's write lock. Concurrent goroutines for the same domain raced on
that field.

We fix the issue by removing the certState.locked field and instead
having Manager.certState() return an additional owner bool computed
under the m.stateMu lock. This allows the Manager.createCert() caller to
know whether it must block on the read lock and return the state handled
by another goroutine, or instead do the work itself and release the
write lock.

A regression unit test is included that fails under -race without the
fix, and passes with the fix in-place.

Fixes golang/go#80119

Change-Id: I0f4c5c98f7e6bcf1ab71a3c0707bb0c7ce73415b
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/793840
Reviewed-by: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Auto-Submit: Daniel McCarney <daniel@binaryparadox.net>
2026-06-26 08:59:20 -07:00
harjoth 0b316e7ee4 argon2: update RFC 9106 parameter recommendations
Update the IDKey example to use RFC 9106's first recommended Argon2id
option and clarify the relationship between the first and second
recommended options.

Fixes golang/go#79823

Change-Id: If44a86a5a8112b02fab97b4d5e325fe91d6049ee
GitHub-Last-Rev: a764ae65875f12d7422865c4e83f345a8a4f246e
GitHub-Pull-Request: golang/crypto#357
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/789020
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-06-18 09:22:19 -07:00
Gopher Robot 55aec0a86b x509roots/fallback: update bundle
This is an automated CL which updates the NSS root bundle.

[git-generate]
go generate ./x509roots

Change-Id: I448f9962a67bf042b04a2e34f16ae02a004d6edd
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/791040
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
2026-06-15 14:38:55 -07:00
Daniel McCarney 5f2de1a9f1 internal: remove wycheproof tests
Equivalent coverage using newer Wycheproof vectors has been added to the
standard library, alongside the implementations under test.

In the case where the algorithm doesn't exist in the stdlib, but is
intended to be supported there one day (e.g. chacha20poly1305) we flip
the relationship and run the Wycheproof tests from the stdlib on the
vendored /x/crypto. As a result the legacy Wycheproof coverage in this
repo can be removed.

Change-Id: Ic07d559e42599b715906f5bcc39be10944a8dde4
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/758420
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Auto-Submit: Daniel McCarney <daniel@binaryparadox.net>
Reviewed-by: Roland Shoemaker <roland@golang.org>
2026-06-09 11:23:32 -07:00
Gopher Robot 45460e0797 go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: I1dc78ceffd32c80457b284aa1bcefb6dfc2757ba
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/788222
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
2026-06-08 08:52:49 -07:00
mohammadmseet-hue d37c95e27d pkcs12: limit PBKDF iteration count to prevent CPU exhaustion
The PKCS#12 PBKDF iteration count is read directly from
the input file with no upper bound. A crafted 83-byte .p12
file can set iterations to 2^31-1 (2147483647), causing
Decode() to block a CPU core permanently.

This change adds a maximum iteration limit of 1000000 in
both verifyMac and pbDecrypterFor. Any file that specifies
more iterations than this cap is rejected with an error.

For reference, OpenSSL caps PBKDF2 at 10000000 iterations,
and scrypt is bounded by its memory-hardness parameters.
The 1000000 limit is generous for legitimate PKCS#12 files
while still preventing denial of service.

Fixes golang/go#78524

Change-Id: I1d12d6f5d90e347ca0dc113678abc51b0cff8d85
GitHub-Last-Rev: a0370d492dd4735cc7c1bb082b359ad79863fdae
GitHub-Pull-Request: golang/crypto#343
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/759900
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-06-04 06:58:05 -07:00
povcfe e2ffffe738 ssh: reject incomplete gssapi-with-mic configurations
Make the runtime gssapi-with-mic guard match the existing
configuration and method advertisement checks.

An incomplete GSSAPIWithMICConfig can be treated as unavailable when
building the advertised auth method list, while still remaining
reachable from the runtime auth dispatcher. Treat incomplete
configurations as not configured.

This change introduces a single internal completeness check for
GSSAPIWithMICConfig and uses it for the startup authentication
validation, the runtime gssapi-with-mic dispatch guard, and the
advertised authentication method list.

The change also adds a regression test. The test configures a server
with a normal PasswordCallback, a GSSAPIWithMICConfig with Server set,
and AllowLogin intentionally unset. It then uses a custom client auth
method that explicitly sends a USERAUTH_REQUEST with Method set to
gssapi-with-mic even though the server does not advertise that method,
and verifies that authentication fails cleanly with
"ssh: gssapi-with-mic auth not configured".

No golang/go issue reference is available yet.

Change-Id: I9a0c965d3a56192bd68309aa41e2c1f91952036c
GitHub-Last-Rev: 0267bda8e15e7c258ba3b92cd54f0941534c5fc9
GitHub-Pull-Request: golang/crypto#345
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/773460
Reviewed-by: Mark Freeman <markfreeman@google.com>
Reviewed-by: Nicola Murino <nicola.murino@gmail.com>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-06-02 00:25:39 -07:00
Mike Nolta 60e158ad30 ssh/test: isolate CLI tests from user SSH config and agent
Pass -F none and -o IdentityAgent=none to ssh(1) invocations in the
CLI tests. Without these flags, a user's ~/.ssh/config can enable
Control{Master,Persist}, which causes the ssh process to fork a
background daemon that holds stdout/stderr pipes open, preventing
CombinedOutput from ever returning. The agent flag prevents keys loaded
in the user's SSH agent from being offered to the server before the test
key, which would exhaust MaxAuthTries and cause a disconnect.

Fixes golang/go#79411

Change-Id: Id5b073e6df661006183d000928d7ef6e6096b933
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/777900
Reviewed-by: Mark Freeman <markfreeman@google.com>
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
Reviewed-by: David Chase <drchase@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-06-02 00:25:33 -07:00
Nicola Murino 1b77d23dc8 ssh/knownhosts: reject lines with multiple or unknown markers
Previously, parseLine would accept lines with multiple markers (e.g.,
"@cert-authority @revoked ...") or unknown markers (e.g., "@unknown ...").
In these cases, the second or unknown marker was incorrectly parsed as
the hostname. This often resulted in confusing downstream errors (like
"illegal base64 data") because field positions were shifted.

OpenSSH's hostfile.c strictly enforces a limit of one marker per line
and validates that markers must be exactly "@cert-authority" or
"@revoked".

This change adds a check to ensure that the parsed hostname does not
start with '@'. This effectively catches both multiple markers (where
the second marker becomes the host) and unknown markers (which are not
consumed as markers and thus become the host), enforcing strict
compliance with the OpenSSH format.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport, and was assessed and is being fixed as
a non-security bug.

Change-Id: I13bb04d1f2610483ad9c6d1020e5100b6feded90
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/782428
Reviewed-by: David Chase <drchase@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Mark Freeman <markfreeman@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
2026-06-02 00:24:36 -07:00
Nicola Murino 3872a2bc69 ssh/knownhosts: verify declared key type matches decoded key
The parseLine function previously ignored the key type field (e.g.,
"ssh-rsa") in known_hosts entries, relying solely on the type
information embedded within the base64-encoded key blob.

OpenSSH's sshkey_read implementation explicitly verifies that the key
type declared in the text matches the type of the parsed key, returning
SSH_ERR_KEY_TYPE_MISMATCH if they differ.

This change adds a check to ensure the declared key type matches
key.Type(), returning an error for malformed lines where they diverge.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport, and was assessed and is being fixed as
a non-security bug.

Change-Id: Id4f35c74055f5691088273630b50cdd02c81bfe9
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/782427
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
2026-06-02 00:24:24 -07:00
Nicola Murino 9f72eccdf6 ssh/knownhosts: treat only ASCII space and tab as whitespace
The previous implementation used bytes.TrimSpace, which strips all
Unicode whitespace categories (e.g., non-breaking spaces). However,
OpenSSH's known_hosts parser (hostfile.c) strictly treats only ASCII
space (0x20) and horizontal tab (0x09) as separators.

This discrepancy meant the Go parser might interpret fields differently
than OpenSSH, potentially treating parts of a key or hostname as
separators if they contained Unicode whitespace.

This change replaces bytes.TrimSpace with a local trimSpace helper that
only trims " \t", ensuring parsing behavior consistent with the
reference implementation.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport, and was assessed and is being fixed as
a non-security bug.

Change-Id: Ia536889636de2c167d2507c01e3f1b7c033c9a8f
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/782426
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
2026-06-02 00:24:19 -07:00
Nicola Murino 8f405a4109 ssh: validate ECDSA curve matches expected algorithm
Previously, parseECDSA determined the curve purely based on the key
blob content, ignoring the algorithm identifier passed to parsePubKey.

This allowed a mismatch where a key could be encoded with an algorithm
type of "ecdsa-sha2-nistp256" but contain a NIST P-384 or P-521 curve.
The parser would succeed, returning a key with a type different from
the one indicated by the caller/wire format.

This change updates parseECDSA to accept the expected algorithm type
and verify that it matches the curve specified in the key data. This
matches the behavior of OpenSSH's ssh_ecdsa_deserialize_public in
ssh-ecdsa.c, which rejects a curve identifier that does not correspond
to the key algorithm name.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport, and was assessed and is being fixed as
a non-security bug.

Change-Id: I9c748be948cca65e2f41089bb7510466d3bb316a
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/782425
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
2026-06-02 00:24:14 -07:00
Nicola Murino bb41b3dbe6 ssh: improve DH GEX group selection using PreferredBits
Previously, the server selected the Diffie-Hellman group based solely on
the MaxBits value provided by the client. This resulted in suboptimal
modulus selection, often ignoring the client's PreferredBits or selecting
a larger-than-necessary group.

This change implements a "best fit" selection algorithm similar to
OpenSSH's choose_dh logic. It attempts to find the smallest available
group larger than or equal to the client's PreferredBits, falling back to
the largest available group within the accepted range if no group above
the preference is available.

Additionally, this commit caches the parsed Oakley groups using
sync.OnceValue, avoiding repeated big.Int parsing on every handshake
while keeping the cost out of package initialization.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport, and was assessed and is being fixed as
a non-security bug.

Change-Id: Idfa81bbcf354a7fb7b541cb4bbeb6e4a0181398a
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/782424
Auto-Submit: Nicola Murino <nicola.murino@gmail.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Mark Freeman <markfreeman@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: David Chase <drchase@google.com>
2026-06-02 00:23:03 -07:00
Nicola Murino e04e721997 ssh/agent: validate ed25519 private key length in Add
insertKey and insertCert assume an ed25519.PrivateKey is exactly
64 bytes long: a 32-byte seed followed by a 32-byte public key.
The expression []byte(k)[32:] panics with "slice bounds out of
range" when a shorter slice is passed, which is reachable through
the exported Add API and crashes the caller process.

Reject ed25519 keys whose length is not ed25519.PrivateKeySize in
all four branches (value and pointer variants of insertKey and
insertCert), using the same error wording already used by
parseEd25519Cert in server.go.

This issue was found during a security audit by NCC Group
Cryptography Services, sponsored by Teleport, and was assessed
and is being fixed as a non-security bug.

Change-Id: I4a053835588aad4c3482fe1ca8045542cd960500
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/782423
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-06-02 00:22:54 -07:00
Nicola Murino b315afd69a ssh: limit bcrypt KDF rounds in OpenSSH private key decryption
passphraseProtectedOpenSSHKey decodes the bcrypt round count from
the key file and passes it directly to bcrypt_pbkdf.Key, whose
running time is linear in that count. Files specifying very large
round values cause the decryption to consume excessive CPU time:
a uint32 maximum is several months of work on commodity hardware.

Cap the accepted round count at 2048 (128x the default of 16, a
few seconds of CPU on a modern core). OpenSSH itself does not
impose an upper bound, but accepting arbitrary values turns key
loading into a resource-exhaustion footgun for any code that
processes files supplied by end users.

This issue was found during a security audit by NCC Group
Cryptography Services, sponsored by Teleport, and was assessed
and is being fixed as a non-security bug.

Change-Id: I01112bdf1b484ae4fab5dc8841d1a272f112df74
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/782422
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-06-02 00:22:48 -07:00
Nicola Murino d4a85f4749 ssh/agent: limit RSA key size on Add requests
parseRSAKey and parseRSACert build an rsa.PrivateKey directly from
the Add request body and then call priv.Precompute(). The CRT
coefficient recomputation in Precompute() is cubic in the size of
the prime factors, so accepting RSA keys with arbitrary modulus
and prime sizes can consume excessive CPU resources during a
single Add operation.

Add a checkRSAKeyParams helper that enforces the same bounds as
parseRSA in the ssh package (modulus <= 8192 bits, exponent <= 24
bits, exponent >= 3 and odd) plus a cap of 4096 bits on each prime
factor, and call it from both parseRSAKey and parseRSACert before
priv.Precompute().

The previous inline check on the exponent (BitLen > 30) is
subsumed by the new helper, which tightens it to BitLen > 24 for
consistency with parseRSA.

Change-Id: I6f0bbbfdc1db13d62a1f85e1d0e6ae517f000443
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/782421
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: David Chase <drchase@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
2026-06-02 00:22:42 -07:00
Nicola Murino 6f39c5226a ssh: limit RSA key size in OpenSSH private key parsing
parseOpenSSHPrivateKey builds an rsa.PrivateKey directly from the
unmarshalled OpenSSH key blob, bypassing parseRSA and its modulus
limit. Validate() and Precompute() then perform several modular
exponentiations whose cost grows with the size of the prime factors;
in particular Precompute() recomputes the CRT coefficient as
q^(p-2) mod p, which is cubic in |p|. A maliciously crafted key with
oversized N and P can keep the CPU busy for hours or days during a
single load.

Mirror the parseRSA validation here: cap the modulus at 8192 bits
(the same limit enforced by crypto/tls), reject exponents larger than
24 bits, and reject invalid exponent values (< 3 or even). In addition,
bound each prime factor at 4096 bits to prevent the CRT computation
from becoming the dominant cost.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport, and was assessed and is being fixed as
a non-security bug.

Change-Id: Ia5991f25dd41a22eddd7cf63a8fc5106de9e9663
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/782420
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
Reviewed-by: David Chase <drchase@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-06-02 00:22:37 -07:00
Nicola Murino 4c4d20b72c ssh: fix spinloop in mux SendRequest drain on closed channel
The drain loop added in 4e7a738 ("ssh: fix deadlock on unexpected
global responses") receives from m.globalResponses without checking
the comma-ok flag. Once mux.loop closes globalResponses on connection
teardown, the loop spins forever on the zero value, pinning a CPU
core for every concurrent SendRequest caller.

Use the comma-ok idiom to detect the closed channel and break out.

Change-Id: Icf0a7419fe5b206b7a4f70429fa9f25d0b5af731
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/783681
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Neal Patel <nealpatel@google.com>
Auto-Submit: Nicola Murino <nicola.murino@gmail.com>
Reviewed-by: Neal Patel <neal@golang.org>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
2026-05-28 10:16:30 -07:00
Nicola Murino e3e62d9601 ssh: fix spinloop in channel SendRequest drain on closed channel
The drain loop added in 3c7c869 ("ssh: fix deadlock on unexpected
channel responses") receives from ch.msg without checking the comma-ok
flag. Once ch.msg is closed by channel.close(), receives succeed
immediately with the zero value, so the default arm is never taken
and the loop spins forever, pinning a CPU core for every concurrent
SendRequest caller on a torn-down channel.

Use the comma-ok idiom to detect the closed channel and break out.

Fixes golang/go#79658

Change-Id: Id80aa5d8429550c7a68e8759aed9ffba3aabb006
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/783680
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
Reviewed-by: Neal Patel <neal@golang.org>
Auto-Submit: Nicola Murino <nicola.murino@gmail.com>
Reviewed-by: Neal Patel <nealpatel@google.com>
2026-05-28 10:16:26 -07:00
Nicola Murino 5adb68b3e6 ssh: cap total userauth attempts per server connection
serverAuthenticate only bounded real failures via MaxAuthTries.
PartialSuccessError responses and the publickey isQuery short-circuit
both kept the loop running without incrementing authFailures, so a
client could keep the server processing SSH_MSG_USERAUTH_REQUEST
messages indefinitely on a single connection.

Add an unconditional cap, maxAuthServerAttempts = 128, on the total
number of userauth requests handled per connection. The counter is
incremented at the top of the loop before method dispatch, so every
method and every isQuery / partial-success path counts. When the cap
is exceeded the server sends SSH_MSG_DISCONNECT with reason 2 ("too
many authentication attempts"), mirroring the MaxAuthTries handling.
The bound is well below OpenSSH's hard cap of 1024 but above any
realistic multi-step auth flow.

Change-Id: I56779fc55cd00ddfd32ec938f8de3a49be0145dc
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781903
Reviewed-by: Junyang Shao <shaojunyang@google.com>
Reviewed-by: David Chase <drchase@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
2026-05-27 09:40:09 -07:00
Neal Patel 9beb694f97 ssh: prevent malformed exit-status panic
A malicious server can crash any client by sending
a truncated exit-status payload, which triggers a
panic in binary.BigEndian.Uint32.

Change-Id: Iff4fe182b40210c99d46562f6c3b5ecf38bb98b4
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/780181
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Nicola Murino <nicola.murino@gmail.com>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
Reviewed-by: David Chase <drchase@google.com>
2026-05-25 19:49:21 -07:00
Nicola Murino e5306b2f3e ssh/agent: support parallel signing using request pipelining
Make NewClient automatically pipeline concurrent requests over its
connection when the supplied io.ReadWriter also implements io.Closer.
In this mode the client writes requests to the wire as soon as the
write path is available and dispatches responses back to callers in
FIFO order via a dedicated reader goroutine, instead of fully
serializing each call. Up to 32 requests may be in flight on a single
connection.

This lets an agent that load-balances signing across multiple backend
devices keep several of them busy concurrently without forcing callers
to maintain their own connection pool. Aggregate throughput scales
roughly linearly with the number of backend devices up to the
in-flight cap; the protocol's in-order response requirement means
slow requests still delay subsequent ones on the same connection,
which is unchanged.

The pipelined path requires io.Closer because, on a Write error, the
background reader goroutine must be unblocked by closing the
underlying connection; otherwise it would remain parked forever
waiting for a response that will never arrive, leaking the goroutine
and desynchronising the FIFO routing of responses for subsequent
successful writes. When the supplied transport does not implement
io.Closer, NewClient falls back to the previous fully-serialized
behavior: a single in-flight call at a time, with no background
goroutine.

Fixes golang/go#78473

Change-Id: Icf14e5e8ca897506d68fb32c14fc72c774cd97b2
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/768483
Reviewed-by: Cherry Mui <cherryyz@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-05-21 19:39:50 -07:00
dingandCyrus Katrak d2fe592df1 ssh: add openssh controlmaster socket support
Adds support for establishing SSH sessions over an existing "ControlMaster" [unix domain] socket in proxy mode.

Details of the protocol can be found here: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.mux
More details about ControlMaster sockets can be found here: https://linux.die.net/man/5/ssh_config

Fixes golang/go#32958

Co-authored-by: Cyrus Katrak <ckatrak@slack-corp.com>
Change-Id: Ia3ae8893262f5060ed3fadfcbe97619c9659145b
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/733040
Reviewed-by: Cherry Mui <cherryyz@google.com>
Reviewed-by: Nicola Murino <nicola.murino@gmail.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
2026-05-21 19:39:36 -07:00
Nicola Murino d59570d1a3 ssh: add AuthCallback to ClientConfig
Add ClientAuthCallback, a hook invoked before each authentication
attempt that allows the client to dynamically select an auth method
based on server capabilities, partial successes, or previous failures.

clientAuthenticate tracks partial successes in a slice and passes
them to AuthCallback via ClientAuthContext.PartialSuccessMethods,
along with the list of failed methods and currently allowed methods.
When AuthCallback returns a non-nil AuthMethod, it is used for the
next attempt, bypassing the static findNext selection. When it
returns (nil, nil), the static config.Auth selection proceeds as
before. If AuthCallback returns (nil, error) the handshake aborts
immediately with that error.

To bound resource use when AuthCallback keeps supplying methods, the
client caps the total number of authentication attempts (failures and
partial successes combined) at 64; exceeding the cap aborts the
handshake with an error.

Fixes golang/go#76146

Change-Id: I0d02bea7b9dd724e95e5d9d49d85306666c0df7a
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/717140
Reviewed-by: Cherry Mui <cherryyz@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-05-21 19:36:57 -07:00
Nicola Murino 4aab0d9d6a ssh: return partial success immediately from publickey auth
Change publicKeyCallback to return authPartialSuccess to the caller
immediately when a signer receives partial success, instead of
continuing to try remaining signers internally.

The caller (clientAuthenticate) already handles authPartialSuccess
by not adding the method to "tried", so findNext can re-select
publickey on the next iteration. When it does, all signers are
tried again from the beginning.

Change-Id: Icc87721aa56ef59a357e409aa046640a27e5fc46
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/757900
Reviewed-by: Filippo Valsorda <filippo@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
2026-05-21 19:36:47 -07:00
62 changed files with 3751 additions and 2039 deletions
+21 -23
View File
@@ -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.
// Either way, createCert blocks for the duration of the whole process.
func (m *Manager) createCert(ctx context.Context, ck certKey) (*tls.Certificate, error) {
// TODO: maybe rewrite this whole piece using sync.Once
state, err := m.certState(ck)
state, owner, err := m.certState(ck)
if err != nil {
return nil, err
}
// state may exist if another goroutine is already working on it
// in which case just wait for it to finish
if !state.locked {
// If another goroutine is already working on this state, wait for it
// to finish by taking the read lock
if !owner {
state.RLock()
defer state.RUnlock()
return state.tlscert()
}
// We are the first; state is locked.
// Unblock the readers when domain ownership is verified
// and we got the cert or the process failed.
// We are the first to work on this certKey, so state is write-locked.
// Unblock the readers when our work is complete.
defer state.Unlock()
state.locked = false
der, leaf, err := m.authorizedCert(ctx, state.key, ck)
if err != nil {
@@ -611,10 +608,15 @@ func (m *Manager) createCert(ctx context.Context, ck certKey) (*tls.Certificate,
return state.tlscert()
}
// certState returns a new or existing certState.
// If a new certState is returned, state.exist is false and the state is locked.
// certState returns a new or existing certState along with a boolean
// 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.
func (m *Manager) certState(ck certKey) (*certState, error) {
func (m *Manager) certState(ck certKey) (*certState, bool, error) {
m.stateMu.Lock()
defer m.stateMu.Unlock()
if m.state == nil {
@@ -622,7 +624,7 @@ func (m *Manager) certState(ck certKey) (*certState, error) {
}
// existing state
if state, ok := m.state[ck]; ok {
return state, nil
return state, false, nil
}
// new locked state
@@ -636,16 +638,13 @@ func (m *Manager) certState(ck certKey) (*certState, error) {
key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}
if err != nil {
return nil, err
return nil, false, err
}
state := &certState{
key: key,
locked: true,
}
state := &certState{key: key}
state.Lock() // will be unlocked by m.certState caller
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.
@@ -1035,10 +1034,9 @@ func (m *Manager) now() time.Time {
// certState is ready when its mutex is unlocked for reading.
type certState struct {
sync.RWMutex
locked bool // locked for read/write
key crypto.Signer // private key for cert
cert [][]byte // DER encoding
leaf *x509.Certificate // parsed cert[0]; always non-nil if cert != nil
key crypto.Signer // private key for cert
cert [][]byte // DER encoding
leaf *x509.Certificate // parsed cert[0]; always non-nil if cert != nil
}
// tlscert creates a tls.Certificate from s.key and s.cert.
+27
View File
@@ -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) {
ca := acmetest.NewCAServer(t)
// Make the authz unfulfillable on the client side, so it will be left
+18 -17
View File
@@ -17,8 +17,8 @@
// It uses data-independent memory access, which is preferred for password
// hashing and password-based key derivation. Argon2i requires more passes over
// 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
// use the maximum available memory.
// parameters (taken from [RFC 9106 Section 7.3]) for non-interactive
// operations are time=3 and to use the maximum available memory.
//
// # Argon2id
//
@@ -26,11 +26,14 @@
// 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
// for the rest. Argon2id is side-channel resistant and provides better brute-
// force cost savings due to time-memory tradeoffs than Argon2i. The recommended
// parameters for non-interactive operations (taken from [RFC 9106 Section 7.3]) are time=1 and to
// use the maximum available memory.
// force cost savings due to time-memory tradeoffs than Argon2i. [RFC 9106
// Section 4] recommends time=1, memory=2*1024*1024 KiB (2 GiB), and threads=4
// 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
// [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
package argon2
@@ -59,9 +62,9 @@ const (
//
// 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.
// If using that amount of memory (32 MB) is not possible in some contexts then
// the time parameter can be increased to compensate.
// The example above uses time=3 and memory=32*1024. Argon2i generally
// requires more passes over memory than Argon2id. If in doubt, prefer IDKey
// and its Argon2id parameter recommendations.
//
// 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
@@ -69,8 +72,6 @@ const (
// adjusted to the number of available CPUs. The cost parameters should be
// increased as memory latency and CPU parallelism increases. Remember to get a
// 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 {
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
// 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.
// If using that amount of memory (64 MB) is not possible in some contexts then
// the time parameter can be increased to compensate.
// The example above uses the first [RFC 9106 Section 4] recommended option.
// If much less memory is available, the second recommended option is time=3,
// memory=64*1024 KiB (64 MiB), and threads=4.
//
// 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=64*1024 sets the memory cost to ~64 MB. The number of threads can be
// adjusted to the numbers of available CPUs. The cost parameters should be
// memory=2*1024*1024 sets the memory cost to ~2 GiB. The number of threads can
// 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
// 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 {
return deriveKey(argon2id, password, salt, nil, nil, time, memory, threads, keyLen)
}
+4 -4
View File
@@ -3,9 +3,9 @@ module golang.org/x/crypto
go 1.25.0
require (
golang.org/x/net v0.54.0 // tagx:ignore
golang.org/x/sys v0.45.0
golang.org/x/term v0.43.0
golang.org/x/net v0.56.0 // tagx:ignore
golang.org/x/sys v0.47.0
golang.org/x/term v0.45.0
)
require golang.org/x/text v0.37.0 // indirect
require golang.org/x/text v0.40.0 // indirect
+8 -8
View File
@@ -1,8 +1,8 @@
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
-12
View File
@@ -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.
-176
View File
@@ -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")
}
}
})
})
}
}
}
}
-127
View File
@@ -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)
}
}
}
}
-9
View File
@@ -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
-123
View File
@@ -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)
}
}
}
}
-140
View File
@@ -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)
}
}
})
}
}
}
}
-163
View File
@@ -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
}
-105
View File
@@ -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)
}
}
}
}
-97
View File
@@ -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)
}
}
}
}
-111
View File
@@ -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)
}
}
}
}
}
-105
View File
@@ -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)
}
}
}
}
}
-33
View File
@@ -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)
}
-9
View File
@@ -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)
}
})
}
}
}
}
-169
View File
@@ -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
}
}
}
}
-123
View File
@@ -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)
}
}
}
}
-141
View File
@@ -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))
}
}
+6 -5
View File
@@ -5,11 +5,12 @@
// Package armor implements OpenPGP ASCII Armor, see RFC 4880. OpenPGP Armor is
// very similar to PEM except that it has an additional CRC checksum.
//
// Deprecated: this package is unmaintained except for security fixes. New
// applications should consider a more focused, modern alternative to OpenPGP
// for their specific task. If you are required to interoperate with OpenPGP
// systems and need a maintained package, consider a community fork.
// See https://golang.org/issue/44226.
// Deprecated: this package is unsafe by design, and has numerous known security
// issues. It is not maintained, and should not be used. New applications should
// consider a more focused, modern alternative to OpenPGP for their specific
// task. If you are required to interoperate with OpenPGP systems and need a
// 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
import (
+6 -5
View File
@@ -8,11 +8,12 @@
// Clearsigned messages are cryptographically signed, but the contents of the
// message are kept in plaintext so that it can be read without special tools.
//
// Deprecated: this package is unmaintained except for security fixes. New
// applications should consider a more focused, modern alternative to OpenPGP
// for their specific task. If you are required to interoperate with OpenPGP
// systems and need a maintained package, consider a community fork.
// See https://golang.org/issue/44226.
// Deprecated: this package is unsafe by design, and has numerous known security
// issues. It is not maintained, and should not be used. New applications should
// consider a more focused, modern alternative to OpenPGP for their specific
// task. If you are required to interoperate with OpenPGP systems and need a
// 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
import (
+6 -5
View File
@@ -4,11 +4,12 @@
// Package errors contains common error types for the OpenPGP packages.
//
// Deprecated: this package is unmaintained except for security fixes. New
// applications should consider a more focused, modern alternative to OpenPGP
// for their specific task. If you are required to interoperate with OpenPGP
// systems and need a maintained package, consider a community fork.
// See https://golang.org/issue/44226.
// Deprecated: this package is unsafe by design, and has numerous known security
// issues. It is not maintained, and should not be used. New applications should
// consider a more focused, modern alternative to OpenPGP for their specific
// task. If you are required to interoperate with OpenPGP systems and need a
// 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
import (
+6 -5
View File
@@ -5,11 +5,12 @@
// Package packet implements parsing and serialization of OpenPGP packets, as
// specified in RFC 4880.
//
// Deprecated: this package is unmaintained except for security fixes. New
// applications should consider a more focused, modern alternative to OpenPGP
// for their specific task. If you are required to interoperate with OpenPGP
// systems and need a maintained package, consider a community fork.
// See https://golang.org/issue/44226.
// Deprecated: this package is unsafe by design, and has numerous known security
// issues. It is not maintained, and should not be used. New applications should
// consider a more focused, modern alternative to OpenPGP for their specific
// task. If you are required to interoperate with OpenPGP systems and need a
// 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
import (
+6 -5
View File
@@ -4,11 +4,12 @@
// Package openpgp implements high level operations on OpenPGP messages.
//
// Deprecated: this package is unmaintained except for security fixes. New
// applications should consider a more focused, modern alternative to OpenPGP
// for their specific task. If you are required to interoperate with OpenPGP
// systems and need a maintained package, consider a community fork.
// See https://golang.org/issue/44226.
// Deprecated: this package is unsafe by design, and has numerous known security
// issues. It is not maintained, and should not be used. New applications should
// consider a more focused, modern alternative to OpenPGP for their specific
// task. If you are required to interoperate with OpenPGP systems and need a
// 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
import (
+6 -5
View File
@@ -5,11 +5,12 @@
// Package s2k implements the various OpenPGP string-to-key transforms as
// specified in RFC 4800 section 3.7.1.
//
// Deprecated: this package is unmaintained except for security fixes. New
// applications should consider a more focused, modern alternative to OpenPGP
// for their specific task. If you are required to interoperate with OpenPGP
// systems and need a maintained package, consider a community fork.
// See https://golang.org/issue/44226.
// Deprecated: this package is unsafe by design, and has numerous known security
// issues. It is not maintained, and should not be used. New applications should
// consider a more focused, modern alternative to OpenPGP for their specific
// task. If you are required to interoperate with OpenPGP systems and need a
// 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
import (
+4
View File
@@ -80,6 +80,10 @@ func pbDecrypterFor(algorithm pkix.AlgorithmIdentifier, password []byte) (cipher
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)
iv := cipherType.deriveIV(params.Salt, password, params.Iterations)
+40
View File
@@ -13,6 +13,46 @@ import (
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) {
params, _ := asn1.Marshal(pbeParams{
Salt: []byte{1, 2, 3, 4, 5, 6, 7, 8},
+8
View File
@@ -27,11 +27,19 @@ var (
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 {
if !macData.Mac.Algorithm.Algorithm.Equal(oidSHA1) {
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)
mac := hmac.New(sha1.New, key)
+42
View File
@@ -5,10 +5,52 @@
package pkcs12
import (
"crypto/x509/pkix"
"encoding/asn1"
"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) {
td := macData{
Mac: digestInfo{
+226 -3
View File
@@ -26,6 +26,7 @@ import (
"io"
"math/big"
"sync"
"sync/atomic"
"golang.org/x/crypto/ssh"
)
@@ -307,17 +308,50 @@ func parseKey(in []byte) (out *Key, rest []byte, err error) {
}, 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.
//
// Exactly one of pipeline / (mu, conn) is set, chosen by NewClient
// based on whether the underlying transport implements io.Closer.
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
// 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
// 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 {
if rwc, ok := rw.(io.ReadWriteCloser); ok {
return &client{pipeline: newPipeline(rwc)}
}
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
// performed on the response.
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()
defer c.mu.Unlock()
@@ -577,6 +621,9 @@ func (c *client) insertKey(s interface{}, comment string, constraints []byte) er
Constraints: constraints,
})
case ed25519.PrivateKey:
if len(k) != ed25519.PrivateKeySize {
return fmt.Errorf("agent: bad ED25519 key size: %d", len(k))
}
req = ssh.Marshal(ed25519KeyMsg{
Type: ssh.KeyAlgoED25519,
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.
// We still support the pointer variant for backwards compatibility.
case *ed25519.PrivateKey:
if len(*k) != ed25519.PrivateKeySize {
return fmt.Errorf("agent: bad ED25519 key size: %d", len(*k))
}
req = ssh.Marshal(ed25519KeyMsg{
Type: ssh.KeyAlgoED25519,
Pub: []byte(*k)[32:],
@@ -712,6 +762,9 @@ func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string
Constraints: constraints,
})
case ed25519.PrivateKey:
if len(k) != ed25519.PrivateKeySize {
return fmt.Errorf("agent: bad ED25519 key size: %d", len(k))
}
req = ssh.Marshal(ed25519CertMsg{
Type: cert.Type(),
CertBytes: cert.Marshal(),
@@ -724,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.
// We still support the pointer variant for backwards compatibility.
case *ed25519.PrivateKey:
if len(*k) != ed25519.PrivateKeySize {
return fmt.Errorf("agent: bad ED25519 key size: %d", len(*k))
}
req = ssh.Marshal(ed25519CertMsg{
Type: cert.Type(),
CertBytes: cert.Marshal(),
@@ -861,3 +917,170 @@ func (c *client) Extension(extensionType string, contents []byte) ([]byte, error
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
}
+649
View File
@@ -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
})
}
}
+34
View File
@@ -6,6 +6,7 @@ package agent
import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"encoding/binary"
"errors"
@@ -654,3 +655,36 @@ func TestAddConstraintExtensionsWireFormat(t *testing.T) {
}
}
}
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")
}
})
}
}
+2
View File
@@ -41,6 +41,7 @@ func ForwardToAgent(client *ssh.Client, keyring Agent) error {
continue
}
go ssh.DiscardRequests(reqs)
go io.Copy(io.Discard, channel.Stderr())
go func() {
ServeAgent(keyring, channel)
channel.Close()
@@ -72,6 +73,7 @@ func ForwardToRemote(client *ssh.Client, addr string) error {
continue
}
go ssh.DiscardRequests(reqs)
go io.Copy(io.Discard, channel.Stderr())
go forwardUnixSocket(channel, addr)
}
}()
+73 -10
View File
@@ -240,13 +240,35 @@ func setConstraints(key *AddedKey, constraintBytes []byte) error {
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() > 8192 {
return errors.New("agent: RSA modulus too large")
}
if P.BitLen() > 4096 || Q.BitLen() > 4096 {
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) {
var k rsaKeyMsg
if err := ssh.Unmarshal(req, &k); err != nil {
return nil, err
}
if k.E.BitLen() > 30 {
return nil, errors.New("agent: RSA public exponent too large")
if err := checkRSAKeyParams(k.N, k.E, k.P, k.Q); err != nil {
return nil, err
}
priv := &rsa.PrivateKey{
PublicKey: rsa.PublicKey{
@@ -282,19 +304,60 @@ func parseEd25519Key(req []byte) (*AddedKey, error) {
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) {
var k dsaKeyMsg
if err := ssh.Unmarshal(req, &k); err != nil {
return nil, err
}
params := dsa.Parameters{
P: k.P,
Q: k.Q,
G: k.G,
}
if err := checkDSAParams(&params); 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{
PublicKey: dsa.PublicKey{
Parameters: dsa.Parameters{
P: k.P,
Q: k.Q,
G: k.G,
},
Y: k.Y,
Parameters: params,
Y: k.Y,
},
X: k.X,
}
@@ -399,8 +462,8 @@ func parseRSACert(req []byte) (*AddedKey, error) {
return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err)
}
if rsaPub.E.BitLen() > 30 {
return nil, errors.New("agent: RSA public exponent too large")
if err := checkRSAKeyParams(rsaPub.N, rsaPub.E, k.P, k.Q); err != nil {
return nil, err
}
priv := rsa.PrivateKey{
+161
View File
@@ -9,10 +9,12 @@ import (
"crypto/rand"
"fmt"
"io"
"math/big"
pseudorand "math/rand"
"reflect"
"strings"
"testing"
"time"
"golang.org/x/crypto/ssh"
)
@@ -88,6 +90,96 @@ func TestSetupForwardAgent(t *testing.T) {
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) {
c1, c2, err := netPipe()
if err != nil {
@@ -154,6 +246,75 @@ func addKeyToAgent(key crypto.PrivateKey) error {
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) {
for k, v := range testPrivateKeys {
if err := addKeyToAgent(v); err != nil {
+4 -1
View File
@@ -634,7 +634,10 @@ func (ch *channel) SendRequest(name string, wantReply bool, payload []byte) (boo
drain:
for {
select {
case <-ch.msg:
case _, ok := <-ch.msg:
if !ok {
break drain
}
default:
break drain
}
+85
View File
@@ -88,6 +88,32 @@ func NewClientConn(c net.Conn, addr string, config *ClientConfig) (Conn, <-chan
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
// 7.
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.
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
// modified after having been passed to an SSH function.
type ClientConfig struct {
@@ -210,6 +289,9 @@ type ClientConfig struct {
// Auth contains possible authentication methods to use with the
// server. Only the first instance of a particular RFC 4252 method will
// be used during authentication.
//
// If AuthCallback is set, these AuthMethod are only used if the
// callback returns nil.
Auth []AuthMethod
// HostKeyCallback is called during the cryptographic
@@ -240,6 +322,9 @@ type ClientConfig struct {
//
// A Timeout of zero means no timeout.
Timeout time.Duration
// AuthCallback, if non-nil, is invoked before each authentication attempt.
AuthCallback ClientAuthCallback
}
// InsecureIgnoreHostKey returns a function that can be used for
+50 -14
View File
@@ -21,6 +21,12 @@ const (
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.
func (c *connection) clientAuthenticate(config *ClientConfig) error {
// initiate user auth session
@@ -67,32 +73,62 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
// then any untried methods suggested by the server.
var tried []string
var lastMethods []string
var partialSuccess []string
sessionID := c.transport.getSessionID()
for auth := AuthMethod(new(noneAuth)); auth != nil; {
ok, methods, err := auth.auth(sessionID, config.User, c.transport, config.Rand, extensions)
if err != nil {
// On disconnect, return error immediately
if _, ok := err.(*disconnectMsg); ok {
if _, isDisconnect := err.(*disconnectMsg); isDisconnect {
return err
}
// We return the error later if there is no other method left to
// try.
// We return the error later if there is no other method
// left to try.
ok = authFailure
}
if ok == authSuccess {
// success
switch ok {
case authSuccess:
return nil
} else if ok == authFailure {
if m := auth.method(); !slices.Contains(tried, m) {
tried = append(tried, m)
}
case authPartialSuccess:
partialSuccess = append(partialSuccess, auth.method())
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 {
methods = lastMethods
}
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
findNext:
@@ -377,11 +413,11 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
return authFailure, nil, err
}
// If authentication succeeds or the list of available methods does not
// 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 || !slices.Contains(methods, cb.method()) {
// If authentication succeeds or partially succeeds, return immediately
// so the caller can select the next auth method. According to RFC 4252
// Section 7, if the server no longer lists "publickey" among its
// allowed methods, do not attempt to authenticate with any other keys.
if success == authSuccess || success == authPartialSuccess || !slices.Contains(methods, cb.method()) {
return success, methods, err
}
}
+751
View File
@@ -169,6 +169,689 @@ func TestClientAuthThirdKey(t *testing.T) {
}
}
// 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) {
config := &ClientConfig{
User: "testuser",
@@ -971,6 +1654,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) {
type testcase struct {
algo string
+9 -1
View File
@@ -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.
type connection struct {
transport *handshakeTransport
transport connTransport
sshConn
// The connection protocol.
+155
View File
@@ -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
}
+275
View File
@@ -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)
}
})
}
}
+66 -9
View File
@@ -16,6 +16,7 @@ import (
"io"
"math/big"
"slices"
"sync"
"golang.org/x/crypto/curve25519"
)
@@ -718,15 +719,9 @@ func (gex *dhGEXSHA) Server(c packetConn, randSource io.Reader, magics *handshak
kexDHGexRequest.MaxBits, kexDHGexRequest.PreferredBits)
}
var p *big.Int
// We hardcode sending Oakley Group 14 (2048 bits), Oakley Group 15 (3072
// bits) or Oakley Group 16 (4096 bits), based on the requested max size.
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)
p, err := chooseDH(kexDHGexRequest)
if err != nil {
return nil, err
}
g := big.NewInt(2)
@@ -805,3 +800,65 @@ func (gex *dhGEXSHA) Server(c packetConn, randSource io.Reader, magics *handshak
Hash: gex.hashFunc,
}, 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
}
+83
View File
@@ -9,6 +9,7 @@ package ssh
import (
"crypto/rand"
"fmt"
"math/big"
"reflect"
"sync"
"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) {
type kexResultErr struct {
result *kexResult
+64 -13
View File
@@ -76,7 +76,7 @@ func parsePubKey(in []byte, algo string) (pubKey PublicKey, rest []byte, err err
case InsecureKeyAlgoDSA:
return parseDSA(in)
case KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521:
return parseECDSA(in)
return parseECDSA(in, algo)
case KeyAlgoSKECDSA256:
return parseSKECDSA(in)
case KeyAlgoED25519:
@@ -182,14 +182,19 @@ func ParseKnownHosts(in []byte) (marker string, hosts []string, pubKey PublicKey
}
hosts := string(keyFields[0])
// keyFields[1] contains the key type (e.g. “ssh-rsa”).
// However, that information is duplicated inside the
// base64-encoded key and so is ignored here.
// keyFields[1] contains the key type (e.g. "ssh-rsa"). This information
// is duplicated within the base64-encoded key blob. As OpenSSH's
// 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(" "))
if pubKey, comment, err = parseAuthorizedKey(key); err != nil {
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
}
@@ -228,10 +233,17 @@ 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
// The first field contains the declared key type. As OpenSSH's
// sshkey_read does, we verify that it matches the type embedded in
// 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
// 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 {
options = candidateOptions
return out, comment, options, rest, nil
} else {
lastErr = err
// As above, the declared key type (here following the options
// field) must match the type embedded in the key blob.
if string(in[:i]) == out.Type() {
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
continue
@@ -806,7 +822,7 @@ func supportedEllipticCurve(curve elliptic.Curve) bool {
}
// 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 {
Curve string
KeyBytes []byte
@@ -817,6 +833,12 @@ func parseECDSA(in []byte) (out PublicKey, rest []byte, err error) {
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)
switch w.Curve {
@@ -1466,6 +1488,17 @@ func passphraseProtectedOpenSSHKey(passphrase []byte) openSSHDecryptFunc {
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)
if err != nil {
return nil, err
@@ -1635,10 +1668,28 @@ func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.Priv
return nil, err
}
// Mirror the validation done in parseRSA for public keys: cap the
// modulus at the same limit enforced by crypto/tls, 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() > 8192 {
return nil, errors.New("ssh: rsa modulus too large")
}
if key.P.BitLen() > 4096 || key.Q.BitLen() > 4096 {
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{
PublicKey: rsa.PublicKey{
N: key.N,
E: int(key.E.Int64()),
E: int(e),
},
D: key.D,
Primes: []*big.Int{key.P, key.Q},
+328
View File
@@ -232,6 +232,200 @@ func TestParseRSAModulusTooLarge(t *testing.T) {
}
}
func TestParsePrivateKeyRSAModulusTooLarge(t *testing.T) {
rsa16384 := []byte(`-----BEGIN OPENSSH PRIVATE KEY-----
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
QHAxNnM=
-----END OPENSSH PRIVATE KEY-----
`)
_, err := ParseRawPrivateKey(rsa16384)
if err == nil {
t.Fatal("ParseRawPrivateKey accepted a 16384-bit modulus; expected it to be rejected")
}
expectedError := "rsa modulus too large"
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("unexpected error message: got %q, want substring %q", err.Error(), expectedError)
}
}
func TestParseECPrivateKey(t *testing.T) {
key := testPrivateKeys["ecdsa"]
@@ -312,6 +506,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) {
// We actually exercise the ParsePrivateKey codepath here, as opposed to
// using the ParseRawPrivateKey+NewSignerFromKey path that testdata_test.go
@@ -622,6 +848,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 {
input string
err string
@@ -701,6 +986,13 @@ var knownHostsParseTests = []struct {
"@marker \tlocalhost,[host2:123]\tssh-rsa aabbccdd",
"short read",
"", "", nil, "",
},
{
// Declared key type does not match the type embedded in the blob.
"localhost ssh-ed25519 {RSAPUB}",
"key type mismatch",
"", "", nil, "",
},
}
@@ -1235,3 +1527,39 @@ cLYUOHfQDw==
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)
}
})
}
}
+20 -4
View File
@@ -178,7 +178,7 @@ func nextWord(line []byte) (string, []byte) {
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) {
@@ -188,12 +188,17 @@ func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error)
}
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 {
return "", "", nil, errors.New("knownhosts: missing host pattern")
}
// ignore the keytype as it's in the key blob anyway.
_, line = nextWord(line)
wantType, line := nextWord(line)
if len(line) == 0 {
return "", "", nil, errors.New("knownhosts: missing key type pattern")
}
@@ -209,6 +214,10 @@ func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error)
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
}
@@ -387,7 +396,7 @@ func (db *hostKeyDB) Read(r io.Reader, filename string) error {
for scanner.Scan() {
lineNum++
line := scanner.Bytes()
line = bytes.TrimSpace(line)
line = trimSpace(line)
if len(line) == 0 || line[0] == '#' {
continue
}
@@ -535,3 +544,10 @@ func newHashedHost(encoded string) (*hashedHost, error) {
func (h *hashedHost) match(a addr) bool {
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")
}
+93
View File
@@ -8,9 +8,11 @@ import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"fmt"
"net"
"reflect"
"strings"
"testing"
"golang.org/x/crypto/ssh"
@@ -407,3 +409,94 @@ func TestIssue36126(t *testing.T) {
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)
}
}
+10 -1
View File
@@ -44,7 +44,16 @@ type disconnectMsg struct {
}
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, sanitizeString(d.Message))
}
func sanitizeString(s string) string {
return strings.Map(func(r rune) rune {
if r == '\t' || (r >= ' ' && r < 0x7f) {
return r
}
return -1
}, s)
}
// See RFC 4253, section 7.1.
+28 -2
View File
@@ -9,6 +9,7 @@ import (
"math/big"
"math/rand"
"reflect"
"strings"
"testing"
"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", `"line1line2line3"`},
{"has\x00null\x00bytes", `"hasnullbytes"`},
{"\x1b[31mred\x1b[0m", `"[31mred[0m"`},
{"newline\r\nCRLF", `"newlineCRLF"`},
{"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) {
for i := 0; i < len(out); i++ {
out[i] = byte(rand.Int31())
@@ -318,7 +344,7 @@ var (
)
func BenchmarkMarshalKexInitMsg(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
Marshal(_kexInitMsg)
}
}
@@ -331,7 +357,7 @@ func BenchmarkUnmarshalKexInitMsg(b *testing.B) {
}
func BenchmarkMarshalKexDHInitMsg(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
Marshal(_kexDHInitMsg)
}
}
+4 -1
View File
@@ -155,7 +155,10 @@ func (m *mux) SendRequest(name string, wantReply bool, payload []byte) (bool, []
drain:
for {
select {
case <-m.globalResponses:
case _, ok := <-m.globalResponses:
if !ok {
break drain
}
default:
break drain
}
+77
View File
@@ -10,6 +10,7 @@ import (
"io"
"sync"
"testing"
"time"
)
func muxPair() (*mux, *mux) {
@@ -1366,3 +1367,79 @@ func TestChannelResponseAcceptedWhilePending(t *testing.T) {
writer.Close()
<-serverDone
}
func TestChannelSendRequestAfterCloseNoSpinloop(t *testing.T) {
writer, reader, mux := channelPair(t)
defer writer.Close()
defer mux.Close()
// Close the channel, this closes reader.msg.
if err := reader.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
// Wait for reader.msg to actually be closed by the mux loop.
for {
select {
case _, ok := <-reader.msg:
if !ok {
goto closed
}
case <-time.After(2 * time.Second):
t.Fatal("reader.msg was not closed")
}
}
closed:
done := make(chan error, 1)
go func() {
_, err := reader.SendRequest("ping", true, nil)
done <- err
}()
select {
case err := <-done:
if !errors.Is(err, io.EOF) {
t.Fatalf("SendRequest after close: got %v, want io.EOF", err)
}
case <-time.After(2 * time.Second):
t.Fatal("SendRequest spinloop on closed ch.msg")
}
}
func TestMuxSendRequestAfterCloseNoSpinloop(t *testing.T) {
a, b := muxPair()
defer a.Close()
// Closing the underlying conn causes mux.loop to exit and close
// b.globalResponses.
if err := b.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
// Wait for globalResponses to actually be closed by loop().
for {
select {
case _, ok := <-b.globalResponses:
if !ok {
goto closed
}
case <-time.After(2 * time.Second):
t.Fatal("globalResponses was not closed")
}
}
closed:
done := make(chan error, 1)
go func() {
_, _, err := b.SendRequest("ping", true, nil)
done <- err
}()
select {
case err := <-done:
if err == nil {
t.Fatal("SendRequest after close: got nil error")
}
case <-time.After(2 * time.Second):
t.Fatal("SendRequest spinloop on closed globalResponses")
}
}
+33 -5
View File
@@ -54,6 +54,9 @@ type Permissions struct {
ExtraData map[any]any
}
// GSSAPIWithMICConfig includes the server callbacks for gssapi-with-mic
// authentication. If either field is nil, gssapi-with-mic is considered not
// configured.
type GSSAPIWithMICConfig struct {
// AllowLogin, must be set, is called when gssapi-with-mic
// authentication is selected (RFC 4462 section 3). The srcName is from the
@@ -68,6 +71,10 @@ type GSSAPIWithMICConfig struct {
Server GSSAPIServer
}
func gssapiWithMICConfigured(config *GSSAPIWithMICConfig) bool {
return config != nil && config.AllowLogin != nil && config.Server != nil
}
// SendAuthBanner implements [ServerPreAuthConn].
func (s *connection) SendAuthBanner(msg string) error {
return s.transport.writePacket(Marshal(&userAuthBannerMsg{
@@ -382,8 +389,7 @@ func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error)
}
if !config.NoClientAuth && config.PasswordCallback == nil && config.PublicKeyCallback == nil &&
config.KeyboardInteractiveCallback == nil && (config.GSSAPIWithMICConfig == nil ||
config.GSSAPIWithMICConfig.AllowLogin == nil || config.GSSAPIWithMICConfig.Server == nil) {
config.KeyboardInteractiveCallback == nil && !gssapiWithMICConfigured(config.GSSAPIWithMICConfig) {
return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false")
}
@@ -607,6 +613,15 @@ func (b *BannerError) Error() string {
return b.Err.Error()
}
// maxAuthServerAttempts caps the total number of SSH_MSG_USERAUTH_REQUEST
// messages the server will process on a single connection, regardless of
// outcome (failure, partial success, public key query, or none). It is a
// backstop against clients that drive the authentication loop indefinitely
// without ever incurring a real failure — for example by repeatedly
// triggering PartialSuccessError or by spamming public key offer queries —
// neither of which increment the MaxAuthTries failure counter.
const maxAuthServerAttempts = 128
func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, error) {
if config.PreAuthConnCallback != nil {
config.PreAuthConnCallback(s)
@@ -617,6 +632,7 @@ func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, err
var perms *Permissions
authFailures := 0
authAttempts := 0
noneAuthCount := 0
var authErrs []error
var calledBannerCallback bool
@@ -645,6 +661,19 @@ userAuthLoop:
return nil, &ServerAuthError{Errors: authErrs}
}
if authAttempts >= maxAuthServerAttempts {
discMsg := &disconnectMsg{
Reason: 2,
Message: "too many authentication attempts",
}
if err := s.transport.writePacket(Marshal(discMsg)); err != nil {
return nil, err
}
authErrs = append(authErrs, discMsg)
return nil, &ServerAuthError{Errors: authErrs}
}
authAttempts++
var userAuthReq userAuthRequestMsg
if packet, err := s.transport.readPacket(); err != nil {
if err == io.EOF {
@@ -846,7 +875,7 @@ userAuthLoop:
}
}
case "gssapi-with-mic":
if authConfig.GSSAPIWithMICConfig == nil {
if !gssapiWithMICConfigured(authConfig.GSSAPIWithMICConfig) {
authErr = errors.New("ssh: gssapi-with-mic auth not configured")
break
}
@@ -979,8 +1008,7 @@ userAuthLoop:
if authConfig.KeyboardInteractiveCallback != nil {
failureMsg.Methods = append(failureMsg.Methods, "keyboard-interactive")
}
if authConfig.GSSAPIWithMICConfig != nil && authConfig.GSSAPIWithMICConfig.Server != nil &&
authConfig.GSSAPIWithMICConfig.AllowLogin != nil {
if gssapiWithMICConfigured(authConfig.GSSAPIWithMICConfig) {
failureMsg.Methods = append(failureMsg.Methods, "gssapi-with-mic")
}
+58 -5
View File
@@ -16,6 +16,7 @@ import (
"math/big"
"net"
"reflect"
"slices"
"strings"
"sync/atomic"
"testing"
@@ -165,6 +166,52 @@ func TestMaxAuthTriesNoneMethod(t *testing.T) {
}
}
func TestMaxAuthServerAttempts(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 server to disconnect after exceeding the attempts cap")
}
// The error must be the server's disconnect, not the client-side
// "too many authentication attempts (N), aborting" bound. The
// disconnect format is set by disconnectMsg.Error().
if !strings.Contains(err.Error(), "ssh: disconnect") ||
!strings.Contains(err.Error(), "too many authentication attempts") {
t.Fatalf("expected server disconnect, got: %v", err)
}
// Server-side request sequence per AuthCallback iteration:
// 1 "none" (initial, before any AuthCallback)
// 2 pubkey query (AuthCallback #k, server returns OK)
// 3 pubkey signed (AuthCallback #k, server returns partial)
// So after k complete callbacks the server has processed 1+2k requests.
// The (k+1)-th callback's query is processed as request number 2(k+1);
// the subsequent signed request is rejected by the cap check
// (authAttempts >= maxAuthServerAttempts), so 2(k+1) = maxAuthServerAttempts
// and invocations = maxAuthServerAttempts / 2.
expected := maxAuthServerAttempts / 2
if invocations != expected {
t.Errorf("AuthCallback invoked %d times; want %d", invocations, expected)
}
}
func TestMaxAuthTriesFirstNoneAuthErrorIgnored(t *testing.T) {
username := "testuser"
serverConfig := &ServerConfig{
@@ -531,12 +578,18 @@ func TestVerifiedPublicCallbackPartialSuccess(t *testing.T) {
serverConf.AddHostKey(testSigners["rsa"])
clientConf := ClientConfig{
User: "user",
Auth: []AuthMethod{
PublicKeys(testSigners["rsa"]),
Password(clientPassword),
},
User: "user",
Auth: nil,
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.AllowedMethods, "password") && slices.Contains(ctx.PartialSuccessMethods, "publickey") {
return Password(clientPassword), nil
}
return nil, nil
},
}
go NewServerConn(c1, serverConf)
+3
View File
@@ -423,6 +423,9 @@ func (s *Session) wait(reqs <-chan *Request) error {
for msg := range reqs {
switch msg.Type {
case "exit-status":
if len(msg.Payload) < 4 {
return errors.New("ssh: malformed exit-status request")
}
wm.status = int(binary.BigEndian.Uint32(msg.Payload))
case "exit-signal":
var sigval struct {
+30
View File
@@ -334,6 +334,25 @@ func TestExitWithoutStatusOrSignal(t *testing.T) {
}
}
func TestExitStatusShortPayload(t *testing.T) {
for _, payload := range [][]byte{nil, {0}, {0, 0}, {0, 0, 0}} {
conn := dial(exitStatusShortPayloadHandler(payload), t)
session, err := conn.NewSession()
if err != nil {
t.Fatalf("unable to request new session: %v", err)
}
if err := session.Shell(); err != nil {
t.Fatalf("unable to execute command: %v", err)
}
err = session.Wait()
if err == nil {
t.Fatalf("expected error for payload length %d", len(payload))
}
session.Close()
conn.Close()
}
}
// windowTestBytes is the number of bytes that we'll send to the SSH server.
const windowTestBytes = 16000 * 200
@@ -482,6 +501,17 @@ func exitWithoutSignalOrStatus(ch Channel, in <-chan *Request, t *testing.T) {
readLine(shell, t)
}
func exitStatusShortPayloadHandler(payload []byte) serverType {
return func(ch Channel, in <-chan *Request, t *testing.T) {
defer ch.Close()
shell := newServerShell(ch, in, "> ")
readLine(shell, t)
if _, err := ch.SendRequest("exit-status", false, payload); err != nil {
t.Errorf("unable to send status: %v", err)
}
}
}
func shellHandler(ch Channel, in <-chan *Request, t *testing.T) {
defer ch.Close()
// this string is returned to stdout
+93 -4
View File
@@ -7,11 +7,13 @@ package test
import (
"bytes"
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
"time"
"golang.org/x/crypto/internal/testenv"
"golang.org/x/crypto/ssh"
@@ -88,7 +90,8 @@ func TestSSHCLIAuth(t *testing.T) {
}
// test public key authentication.
cmd := testenv.Command(t, sshCLI, "-vvv", "-i", keyPrivPath, "-o", "StrictHostKeyChecking=no",
cmd := testenv.Command(t, sshCLI, "-F", "none", "-vvv", "-i", keyPrivPath,
"-o", "StrictHostKeyChecking=no", "-o", "IdentityAgent=none",
"-p", port, "testpubkey@127.0.0.1", "true")
out, err := cmd.CombinedOutput()
if err != nil {
@@ -97,7 +100,8 @@ func TestSSHCLIAuth(t *testing.T) {
// Test SSH user certificate authentication.
// The username must match one of the principals included in the certificate.
// The certificate "rsa-user-testcertificate" has "testcertificate" as principal.
cmd = testenv.Command(t, sshCLI, "-vvv", "-i", keyPrivPath, "-o", "StrictHostKeyChecking=no",
cmd = testenv.Command(t, sshCLI, "-F", "none", "-vvv", "-i", keyPrivPath,
"-o", "StrictHostKeyChecking=no", "-o", "IdentityAgent=none",
"-p", port, "testcertificate@127.0.0.1", "true")
out, err = cmd.CombinedOutput()
if err != nil {
@@ -119,7 +123,7 @@ func TestSSHCLIKeyExchanges(t *testing.T) {
keyExchanges := append(ssh.SupportedAlgorithms().KeyExchanges, ssh.InsecureAlgorithms().KeyExchanges...)
for _, kex := range keyExchanges {
t.Run(kex, func(t *testing.T) {
cmd := testenv.Command(t, sshCLI, "-Q", "kex")
cmd := testenv.Command(t, sshCLI, "-F", "none", "-Q", "kex")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("%s failed to check if the KEX is supported, error: %v, command output %q", kex, err, string(out))
@@ -152,7 +156,8 @@ func TestSSHCLIKeyExchanges(t *testing.T) {
t.Fatalf("unable to get server port: %v", err)
}
cmd = testenv.Command(t, sshCLI, "-vvv", "-i", keyPrivPath, "-o", "StrictHostKeyChecking=no",
cmd = testenv.Command(t, sshCLI, "-F", "none", "-vvv", "-i", keyPrivPath,
"-o", "StrictHostKeyChecking=no", "-o", "IdentityAgent=none",
"-o", fmt.Sprintf("KexAlgorithms=%s", kex), "-p", port, "testpubkey@127.0.0.1", "true")
out, err = cmd.CombinedOutput()
if err != nil {
@@ -161,3 +166,87 @@ func TestSSHCLIKeyExchanges(t *testing.T) {
})
}
}
func TestSSHCLIControlClientConn(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skipf("always fails on Windows, see #64403")
}
sshCLI := sshClient(t)
keyFiles := map[string][]byte{
"rsa": testdata.PEMBytes["rsa"],
"rsa.pub": ssh.MarshalAuthorizedKey(testPublicKeys["rsa"]),
}
keyPrivPath := setupSSHCLIKeys(t, keyFiles, "rsa")
config := &ssh.ServerConfig{
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
if conn.User() == "testcontrolproxy" && bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
return nil, nil
}
return nil, fmt.Errorf("pubkey for %q not acceptable", conn.User())
},
}
config.AddHostKey(testSigners["rsa"])
server, err := newTestServer(config)
if err != nil {
t.Fatalf("unable to start test server: %v", err)
}
defer server.Close()
port, err := server.port()
if err != nil {
t.Fatalf("unable to get server port: %v", err)
}
dir, err := os.MkdirTemp("", "controlSocket")
if err != nil {
t.Fatalf("unable to create temp dir for control socket: %v", err)
}
defer os.RemoveAll(dir)
csPath := filepath.Join(dir, "c")
cmd := testenv.Command(t, sshCLI, "-vvv", "-i", keyPrivPath, "-o", "StrictHostKeyChecking=no",
"-p", port, "-o", "ControlPath="+csPath, "-o", "ControlMaster=yes", "-N", "testcontrolproxy@127.0.0.1")
var output bytes.Buffer
cmd.Stdout = &output
cmd.Stderr = &output
if err := cmd.Start(); err != nil {
t.Fatalf("control socket master start failed, error: %v", err)
}
defer func() {
cmd.Process.Kill()
cmd.Wait()
if t.Failed() {
t.Logf("OpenSSH output:\n\n%s", cmd.Stdout)
}
}()
for i := range 10 {
if _, err := os.Stat(csPath); err == nil {
break
} else if !os.IsNotExist(err) {
t.Fatalf("unable to stat control socket: %v", err)
}
time.Sleep((1 << i) * 5 * time.Millisecond)
}
conn, err := net.Dial("unix", csPath)
if err != nil {
t.Fatalf("unable to open control socket: %v", err)
}
defer conn.Close()
cc, chans, reqs, err := ssh.NewControlClientConn(conn)
if err != nil {
t.Fatalf("unable to create client: %v", err)
}
client := ssh.NewClient(cc, chans, reqs)
defer client.Close()
session, err := client.NewSession()
if err != nil {
t.Fatalf("unable to create session: %v", err)
}
defer session.Close()
out, err := session.CombinedOutput("true")
if err != nil {
t.Fatalf("command execution failed, error: %v, command output %q", err, string(out))
}
}
Binary file not shown.
+79 -92
View File
@@ -237,492 +237,479 @@ var unparsedCertificates = []unparsedCertificate{
certStartOff: 40789,
certLength: 1428,
},
{
cn: "CN=Entrust Root Certification Authority,OU=www.entrust.net/CPS is incorporated by reference+OU=(c) 2006 Entrust\\, Inc.,O=Entrust\\, Inc.,C=US",
sha256Hash: "73c176434f1bc6d5adf45b0e76e727287c8de57616c1e6e6141a2b2cbc7d8e4c",
certStartOff: 42217,
certLength: 1173,
distrustAfter: "2024-11-30T23:59:59Z",
},
{
cn: "CN=GDCA TrustAUTH R5 ROOT,O=GUANG DONG CERTIFICATE AUTHORITY CO.\\,LTD.,C=CN",
sha256Hash: "bfff8fd04433487d6a8aa60c1a29767a9fc2bbb05e420f713a13b992891d3893",
certStartOff: 43390,
certStartOff: 42217,
certLength: 1420,
},
{
cn: "CN=GTS Root R1,O=Google Trust Services LLC,C=US",
sha256Hash: "d947432abde7b7fa90fc2e6b59101b1280e0e1c7e4e40fa3c6887fff57a7f4cf",
certStartOff: 44810,
certStartOff: 43637,
certLength: 1371,
},
{
cn: "CN=GTS Root R3,O=Google Trust Services LLC,C=US",
sha256Hash: "34d8a73ee208d9bcdb0d956520934b4e40e69482596e8b6f73c8426b010a6f48",
certStartOff: 46181,
certStartOff: 45008,
certLength: 525,
},
{
cn: "CN=GTS Root R4,O=Google Trust Services LLC,C=US",
sha256Hash: "349dfa4058c5e263123b398ae795573c4e1313c83fe68f93556cd5e8031b3c7d",
certStartOff: 46706,
certStartOff: 45533,
certLength: 525,
},
{
cn: "CN=GlobalSign Root E46,O=GlobalSign nv-sa,C=BE",
sha256Hash: "cbb9c44d84b8043e1050ea31a69f514955d7bfd2e2c6b49301019ad61d9f5058",
certStartOff: 47231,
certStartOff: 46058,
certLength: 527,
},
{
cn: "CN=GlobalSign Root R46,O=GlobalSign nv-sa,C=BE",
sha256Hash: "4fa3126d8d3a11d1c4855a4f807cbad6cf919d3a5a88b03bea2c6372d93c40c9",
certStartOff: 47758,
certStartOff: 46585,
certLength: 1374,
},
{
cn: "CN=GlobalSign,OU=GlobalSign ECC Root CA - R4,O=GlobalSign",
sha256Hash: "b085d70b964f191a73e4af0d54ae7a0e07aafdaf9b71dd0862138ab7325a24a2",
certStartOff: 49132,
certStartOff: 47959,
certLength: 480,
},
{
cn: "CN=GlobalSign,OU=GlobalSign ECC Root CA - R5,O=GlobalSign",
sha256Hash: "179fbc148a3dd00fd24ea13458cc43bfa7f59c8182d783a513f6ebec100c8924",
certStartOff: 49612,
certStartOff: 48439,
certLength: 546,
},
{
cn: "CN=GlobalSign,OU=GlobalSign Root CA - R3,O=GlobalSign",
sha256Hash: "cbb522d7b7f127ad6a0113865bdf1cd4102e7d0759af635a7cf4720dc963c53b",
certStartOff: 50158,
certStartOff: 48985,
certLength: 867,
},
{
cn: "CN=GlobalSign,OU=GlobalSign Root CA - R6,O=GlobalSign",
sha256Hash: "2cabeafe37d06ca22aba7391c0033d25982952c453647349763a3ab5ad6ccf69",
certStartOff: 51025,
certStartOff: 49852,
certLength: 1415,
},
{
cn: "CN=Go Daddy Root Certificate Authority - G2,O=GoDaddy.com\\, Inc.,L=Scottsdale,ST=Arizona,C=US",
sha256Hash: "45140b3247eb9cc8c5b4f0d7b53091f73292089e6e5a63e2749dd3aca9198eda",
certStartOff: 52440,
certStartOff: 51267,
certLength: 969,
},
{
cn: "CN=HARICA TLS ECC Root CA 2021,O=Hellenic Academic and Research Institutions CA,C=GR",
sha256Hash: "3f99cc474acfce4dfed58794665e478d1547739f2e780f1bb4ca9b133097d401",
certStartOff: 53409,
certStartOff: 52236,
certLength: 600,
},
{
cn: "CN=HARICA TLS RSA Root CA 2021,O=Hellenic Academic and Research Institutions CA,C=GR",
sha256Hash: "d95d0e8eda79525bf9beb11b14d2100d3294985f0c62d9fabd9cd999eccb7b1d",
certStartOff: 54009,
certStartOff: 52836,
certLength: 1448,
},
{
cn: "CN=Hellenic Academic and Research Institutions ECC RootCA 2015,O=Hellenic Academic and Research Institutions Cert. Authority,L=Athens,C=GR",
sha256Hash: "44b545aa8a25e65a73ca15dc27fc36d24c1cb9953a066539b11582dc487b4833",
certStartOff: 55457,
certStartOff: 54284,
certLength: 711,
},
{
cn: "CN=Hellenic Academic and Research Institutions RootCA 2015,O=Hellenic Academic and Research Institutions Cert. Authority,L=Athens,C=GR",
sha256Hash: "a040929a02ce53b4acf4f2ffc6981ce4496f755e6d45fe0b2a692bcd52523f36",
certStartOff: 56168,
certStartOff: 54995,
certLength: 1551,
},
{
cn: "CN=HiPKI Root CA - G1,O=Chunghwa Telecom Co.\\, Ltd.,C=TW",
sha256Hash: "f015ce3cc239bfef064be9f1d2c417e1a0264a0a94be1f0c8d121864eb6949cc",
certStartOff: 57719,
certStartOff: 56546,
certLength: 1390,
},
{
cn: "CN=Hongkong Post Root CA 3,O=Hongkong Post,L=Hong Kong,ST=Hong Kong,C=HK",
sha256Hash: "5a2fc03f0c83b090bbfa40604b0988446c7636183df9846e17101a447fb8efd6",
certStartOff: 59109,
certStartOff: 57936,
certLength: 1491,
},
{
cn: "CN=ISRG Root X1,O=Internet Security Research Group,C=US",
sha256Hash: "96bcec06264976f37460779acf28c5a7cfe8a3c0aae11a8ffcee05c0bddf08c6",
certStartOff: 60600,
certStartOff: 59427,
certLength: 1391,
},
{
cn: "CN=ISRG Root X2,O=Internet Security Research Group,C=US",
sha256Hash: "69729b8e15a86efc177a57afb7171dfc64add28c2fca8cf1507e34453ccb1470",
certStartOff: 61991,
certStartOff: 60818,
certLength: 543,
},
{
cn: "CN=IdenTrust Commercial Root CA 1,O=IdenTrust,C=US",
sha256Hash: "5d56499be4d2e08bcfcad08a3e38723d50503bde706948e42f55603019e528ae",
certStartOff: 62534,
certStartOff: 61361,
certLength: 1380,
},
{
cn: "CN=IdenTrust Public Sector Root CA 1,O=IdenTrust,C=US",
sha256Hash: "30d0895a9a448a262091635522d1f52010b5867acae12c78ef958fd4f4389f2f",
certStartOff: 63914,
certStartOff: 62741,
certLength: 1386,
},
{
cn: "CN=Izenpe.com,O=IZENPE S.A.,C=ES",
sha256Hash: "2530cc8e98321502bad96f9b1fba1b099e2d299e0f4548bb914f363bc0d4531f",
certStartOff: 65300,
certStartOff: 64127,
certLength: 1525,
},
{
cn: "CN=Microsec e-Szigno Root CA 2009,O=Microsec Ltd.,L=Budapest,C=HU,1.2.840.113549.1.9.1=#0c10696e666f40652d737a69676e6f2e6875",
sha256Hash: "3c5f81fea5fab82c64bfa2eaecafcde8e077fc8620a7cae537163df36edbf378",
certStartOff: 66825,
certStartOff: 65652,
certLength: 1038,
},
{
cn: "CN=Microsoft ECC Root Certificate Authority 2017,O=Microsoft Corporation,C=US",
sha256Hash: "358df39d764af9e1b766e9c972df352ee15cfac227af6ad1d70e8e4a6edcba02",
certStartOff: 67863,
certStartOff: 66690,
certLength: 605,
},
{
cn: "CN=Microsoft RSA Root Certificate Authority 2017,O=Microsoft Corporation,C=US",
sha256Hash: "c741f70f4b2a8d88bf2e71c14122ef53ef10eba0cfa5e64cfa20f418853073e0",
certStartOff: 68468,
certStartOff: 67295,
certLength: 1452,
},
{
cn: "CN=NAVER Global Root Certification Authority,O=NAVER BUSINESS PLATFORM Corp.,C=KR",
sha256Hash: "88f438dcf8ffd1fa8f429115ffe5f82ae1e06e0c70c375faad717b34a49e7265",
certStartOff: 69920,
certStartOff: 68747,
certLength: 1446,
},
{
cn: "CN=NetLock Arany (Class Gold) Főtanúsítvány,OU=Tanúsítványkiadók (Certification Services),O=NetLock Kft.,L=Budapest,C=HU",
sha256Hash: "6c61dac3a2def031506be036d2a6fe401994fbd13df9c8d466599274c446ec98",
certStartOff: 71366,
certStartOff: 70193,
certLength: 1049,
},
{
cn: "CN=OISTE Server Root ECC G1,O=OISTE Foundation,C=CH",
sha256Hash: "eec997c0c30f216f7e3b8b307d2bae42412d753fc8219dafd1520b2572850f49",
certStartOff: 72415,
certStartOff: 71242,
certLength: 569,
},
{
cn: "CN=OISTE Server Root RSA G1,O=OISTE Foundation,C=CH",
sha256Hash: "9ae36232a5189ffddb353dfd26520c015395d22777dac59db57b98c089a651e6",
certStartOff: 72984,
certStartOff: 71811,
certLength: 1415,
},
{
cn: "CN=OISTE WISeKey Global Root GB CA,OU=OISTE Foundation Endorsed,O=WISeKey,C=CH",
sha256Hash: "6b9c08e86eb0f767cfad65cd98b62149e5494a67f5845e7bd1ed019f27b86bd6",
certStartOff: 74399,
certStartOff: 73226,
certLength: 953,
},
{
cn: "CN=OISTE WISeKey Global Root GC CA,OU=OISTE Foundation Endorsed,O=WISeKey,C=CH",
sha256Hash: "8560f91c3624daba9570b5fea0dbe36ff11a8323be9486854fb3f34a5571198d",
certStartOff: 75352,
certStartOff: 74179,
certLength: 621,
},
{
cn: "CN=QuoVadis Root CA 1 G3,O=QuoVadis Limited,C=BM",
sha256Hash: "8a866fd1b276b57e578e921c65828a2bed58e9f2f288054134b7f1f4bfc9cc74",
certStartOff: 75973,
certStartOff: 74800,
certLength: 1380,
},
{
cn: "CN=QuoVadis Root CA 2 G3,O=QuoVadis Limited,C=BM",
sha256Hash: "8fe4fb0af93a4d0d67db0bebb23e37c71bf325dcbcdd240ea04daf58b47e1840",
certStartOff: 77353,
certStartOff: 76180,
certLength: 1380,
},
{
cn: "CN=QuoVadis Root CA 3 G3,O=QuoVadis Limited,C=BM",
sha256Hash: "88ef81de202eb018452e43f864725cea5fbd1fc2d9d205730709c5d8b8690f46",
certStartOff: 78733,
certStartOff: 77560,
certLength: 1380,
},
{
cn: "CN=SSL.com EV Root Certification Authority ECC,O=SSL Corporation,L=Houston,ST=Texas,C=US",
sha256Hash: "22a2c1f7bded704cc1e701b5f408c310880fe956b5de2a4a44f99c873a25a7c8",
certStartOff: 80113,
certStartOff: 78940,
certLength: 664,
},
{
cn: "CN=SSL.com EV Root Certification Authority RSA R2,O=SSL Corporation,L=Houston,ST=Texas,C=US",
sha256Hash: "2e7bf16cc22485a7bbe2aa8696750761b0ae39be3b2fe9d0cc6d4ef73491425c",
certStartOff: 80777,
certStartOff: 79604,
certLength: 1519,
},
{
cn: "CN=SSL.com Root Certification Authority ECC,O=SSL Corporation,L=Houston,ST=Texas,C=US",
sha256Hash: "3417bb06cc6007da1b961c920b8ab4ce3fad820e4aa30b9acbc4a74ebdcebc65",
certStartOff: 82296,
certStartOff: 81123,
certLength: 657,
},
{
cn: "CN=SSL.com Root Certification Authority RSA,O=SSL Corporation,L=Houston,ST=Texas,C=US",
sha256Hash: "85666a562ee0be5ce925c1d8890a6f76a87ec16d4d7d5f29ea7419cf20123b69",
certStartOff: 82953,
certStartOff: 81780,
certLength: 1505,
},
{
cn: "CN=SSL.com TLS ECC Root CA 2022,O=SSL Corporation,C=US",
sha256Hash: "c32ffd9f46f936d16c3673990959434b9ad60aafbb9e7cf33654f144cc1ba143",
certStartOff: 84458,
certStartOff: 83285,
certLength: 574,
},
{
cn: "CN=SSL.com TLS RSA Root CA 2022,O=SSL Corporation,C=US",
sha256Hash: "8faf7d2e2cb4709bb8e0b33666bf75a5dd45b5de480f8ea8d4bfe6bebc17f2ed",
certStartOff: 85032,
certStartOff: 83859,
certLength: 1421,
},
{
cn: "CN=SZAFIR ROOT CA2,O=Krajowa Izba Rozliczeniowa S.A.,C=PL",
sha256Hash: "a1339d33281a0b56e557d3d32b1ce7f9367eb094bd5fa72a7e5004c8ded7cafe",
certStartOff: 86453,
certStartOff: 85280,
certLength: 886,
},
{
cn: "CN=Sectigo Public Server Authentication Root E46,O=Sectigo Limited,C=GB",
sha256Hash: "c90f26f0fb1b4018b22227519b5ca2b53e2ca5b3be5cf18efe1bef47380c5383",
certStartOff: 87339,
certStartOff: 86166,
certLength: 574,
},
{
cn: "CN=Sectigo Public Server Authentication Root R46,O=Sectigo Limited,C=GB",
sha256Hash: "7bb647a62aeeac88bf257aa522d01ffea395e0ab45c73f93f65654ec38f25a06",
certStartOff: 87913,
certStartOff: 86740,
certLength: 1422,
},
{
cn: "CN=SecureSign Root CA12,O=Cybertrust Japan Co.\\, Ltd.,C=JP",
sha256Hash: "3f034bb5704d44b2d08545a02057de93ebf3905fce721acbc730c06ddaee904e",
certStartOff: 89335,
certLength: 886,
},
{
cn: "CN=SecureSign Root CA14,O=Cybertrust Japan Co.\\, Ltd.,C=JP",
sha256Hash: "4b009c1034494f9ab56bba3ba1d62731fc4d20d8955adcec10a925607261e338",
certStartOff: 90221,
certStartOff: 88162,
certLength: 1398,
},
{
cn: "CN=SecureSign Root CA15,O=Cybertrust Japan Co.\\, Ltd.,C=JP",
sha256Hash: "e778f0f095fe843729cd1a0082179e5314a9c291442805e1fb1d8fb6b8886c3a",
certStartOff: 91619,
certStartOff: 89560,
certLength: 551,
},
{
cn: "CN=Security Communication ECC RootCA1,O=SECOM Trust Systems CO.\\,LTD.,C=JP",
sha256Hash: "e74fbda55bd564c473a36b441aa799c8a68e077440e8288b9fa1e50e4bbaca11",
certStartOff: 92170,
certStartOff: 90111,
certLength: 572,
},
{
cn: "CN=Starfield Root Certificate Authority - G2,O=Starfield Technologies\\, Inc.,L=Scottsdale,ST=Arizona,C=US",
sha256Hash: "2ce1cb0bf9d2f9e102993fbe215152c3b2dd0cabde1c68e5319b839154dbb7f5",
certStartOff: 92742,
certStartOff: 90683,
certLength: 993,
},
{
cn: "CN=Starfield Services Root Certificate Authority - G2,O=Starfield Technologies\\, Inc.,L=Scottsdale,ST=Arizona,C=US",
sha256Hash: "568d6905a2c88708a4b3025190edcfedb1974a606a13c6e5290fcb2ae63edab5",
certStartOff: 93735,
certStartOff: 91676,
certLength: 1011,
},
{
cn: "CN=SwissSign RSA TLS Root CA 2022 - 1,O=SwissSign AG,C=CH",
sha256Hash: "193144f431e0fddb740717d4de926a571133884b4360d30e272913cbe660ce41",
certStartOff: 94746,
certStartOff: 92687,
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: 96177,
certStartOff: 94118,
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: 97144,
certStartOff: 95085,
certLength: 967,
},
{
cn: "CN=TWCA CYBER Root CA,OU=Root CA,O=TAIWAN-CA,C=TW",
sha256Hash: "3f63bb2814be174ec8b6439cf08d6d56f0b7c405883a5648a334424d6b3ec558",
certStartOff: 98111,
certStartOff: 96052,
certLength: 1425,
},
{
cn: "CN=TWCA Global Root CA,OU=Root CA,O=TAIWAN-CA,C=TW",
sha256Hash: "59769007f7685d0fcd50872f9f95d5755a5b2b457d81f3692b610a98672f0e1b",
certStartOff: 99536,
certStartOff: 97477,
certLength: 1349,
},
{
cn: "CN=TWCA Root Certification Authority,OU=Root CA,O=TAIWAN-CA,C=TW",
sha256Hash: "bfd88fe1101c41ae3e801bf8be56350ee9bad1a6b9bd515edc5c6d5b8711ac44",
certStartOff: 100885,
certStartOff: 98826,
certLength: 895,
},
{
cn: "CN=Telekom Security TLS ECC Root 2020,O=Deutsche Telekom Security GmbH,C=DE",
sha256Hash: "578af4ded0853f4e5998db4aeaf9cbea8d945f60b620a38d1a3c13b2bc7ba8e1",
certStartOff: 101780,
certStartOff: 99721,
certLength: 582,
},
{
cn: "CN=Telekom Security TLS RSA Root 2023,O=Deutsche Telekom Security GmbH,C=DE",
sha256Hash: "efc65cadbb59adb6efe84da22311b35624b71b3b1ea0da8b6655174ec8978646",
certStartOff: 102362,
certStartOff: 100303,
certLength: 1463,
},
{
cn: "CN=Telia Root CA v2,O=Telia Finland Oyj,C=FI",
sha256Hash: "242b69742fcb1e5b2abf98898b94572187544e5b4d9911786573621f6a74b82c",
certStartOff: 103825,
certStartOff: 101766,
certLength: 1400,
},
{
cn: "CN=TrustAsia Global Root CA G3,O=TrustAsia Technologies\\, Inc.,C=CN",
sha256Hash: "e0d3226aeb1163c2e48ff9be3b50b4c6431be7bb1eacc5c36b5d5ec509039a08",
certStartOff: 105225,
certStartOff: 103166,
certLength: 1449,
},
{
cn: "CN=TrustAsia Global Root CA G4,O=TrustAsia Technologies\\, Inc.,C=CN",
sha256Hash: "be4b56cb5056c0136a526df444508daa36a0b54f42e4ac38f72af470e479654c",
certStartOff: 106674,
certStartOff: 104615,
certLength: 601,
},
{
cn: "CN=TrustAsia TLS ECC Root CA,O=TrustAsia Technologies\\, Inc.,C=CN",
sha256Hash: "c0076b9ef0531fb1a656d67c4ebe97cd5dbaa41ef44598acc2489878c92d8711",
certStartOff: 107275,
certStartOff: 105216,
certLength: 565,
},
{
cn: "CN=TrustAsia TLS RSA Root CA,O=TrustAsia Technologies\\, Inc.,C=CN",
sha256Hash: "06c08d7dafd876971eb1124fe67f847ec0c7a158d3ea53cbe940e2ea9791f4c3",
certStartOff: 107840,
certStartOff: 105781,
certLength: 1412,
},
{
cn: "CN=TunTrust Root CA,O=Agence Nationale de Certification Electronique,C=TN",
sha256Hash: "2e44102ab58cb85419451c8e19d9acf3662cafbc614b6a53960a30f7d0e2eb41",
certStartOff: 109252,
certStartOff: 107193,
certLength: 1463,
},
{
cn: "CN=UCA Extended Validation Root,O=UniTrust,C=CN",
sha256Hash: "d43af9b35473755c9684fc06d7d8cb70ee5c28e773fb294eb41ee71722924d24",
certStartOff: 110715,
certStartOff: 108656,
certLength: 1374,
},
{
cn: "CN=UCA Global G2 Root,O=UniTrust,C=CN",
sha256Hash: "9bea11c976fe014764c1be56a6f914b5a560317abd9988393382e5161aa0493c",
certStartOff: 112089,
certStartOff: 110030,
certLength: 1354,
},
{
cn: "CN=USERTrust ECC Certification Authority,O=The USERTRUST Network,L=Jersey City,ST=New Jersey,C=US",
sha256Hash: "4ff460d54b9c86dabfbcfc5712e0400d2bed3fbc4d4fbdaa86e06adcd2a9ad7a",
certStartOff: 113443,
certStartOff: 111384,
certLength: 659,
},
{
cn: "CN=USERTrust RSA Certification Authority,O=The USERTRUST Network,L=Jersey City,ST=New Jersey,C=US",
sha256Hash: "e793c9b02fd8aa13e21c31228accb08119643b749c898964b1746d46c3d4cbd2",
certStartOff: 114102,
certStartOff: 112043,
certLength: 1506,
},
{
cn: "CN=e-Szigno Root CA 2017,O=Microsec Ltd.,L=Budapest,C=HU,2.5.4.97=#130e56415448552d3233353834343937",
sha256Hash: "beb00b30839b9bc32c32e4447905950641f26421b15ed089198b518ae2ea1b99",
certStartOff: 115608,
certStartOff: 113549,
certLength: 580,
},
{
cn: "CN=e-Szigno TLS Root CA 2023,O=Microsec Ltd.,L=Budapest,C=HU,2.5.4.97=#130e56415448552d3233353834343937",
sha256Hash: "b49141502d00663d740f2e7ec340c52800962666121a36d09cf7dd2b90384fb4",
certStartOff: 116188,
certStartOff: 114129,
certLength: 723,
},
{
cn: "CN=emSign ECC Root CA - C3,OU=emSign PKI,O=eMudhra Inc,C=US",
sha256Hash: "bc4d809b15189d78db3e1d8cf4f9726a795da1643ca5f1358e1ddb0edc0d7eb3",
certStartOff: 116911,
certStartOff: 114852,
certLength: 559,
},
{
cn: "CN=emSign ECC Root CA - G3,OU=emSign PKI,O=eMudhra Technologies Limited,C=IN",
sha256Hash: "86a1ecba089c4a8d3bbe2734c612ba341d813e043cf9e8a862cd5c57a36bbe6b",
certStartOff: 117470,
certStartOff: 115411,
certLength: 594,
},
{
cn: "CN=emSign Root CA - C1,OU=emSign PKI,O=eMudhra Inc,C=US",
sha256Hash: "125609aa301da0a249b97a8239cb6a34216f44dcac9f3954b14292f2e8c8608f",
certStartOff: 118064,
certStartOff: 116005,
certLength: 887,
},
{
cn: "CN=emSign Root CA - G1,OU=emSign PKI,O=eMudhra Technologies Limited,C=IN",
sha256Hash: "40f6af0346a99aa1cd1d555a4e9cce62c7f9634603ee406615833dc8c8d00367",
certStartOff: 118951,
certStartOff: 116892,
certLength: 920,
},
{
cn: "CN=vTrus ECC Root CA,O=iTrusChina Co.\\,Ltd.,C=CN",
sha256Hash: "30fbba2c32238e2a98547af97931e550428b9b3f1c8eeb6633dcfa86c5b27dd3",
certStartOff: 119871,
certStartOff: 117812,
certLength: 531,
},
{
cn: "CN=vTrus Root CA,O=iTrusChina Co.\\,Ltd.,C=CN",
sha256Hash: "8a71de6559336f426c26e53880d00d88a18da4c6a91f0dcb6194e206c5c96387",
certStartOff: 120402,
certStartOff: 118343,
certLength: 1370,
},
{
cn: "OU=AC RAIZ FNMT-RCM,O=FNMT-RCM,C=ES",
sha256Hash: "ebc5570c29018c4d67b1aa127baf12f703b4611ebc17b7dab5573894179b93fa",
certStartOff: 121772,
certStartOff: 119713,
certLength: 1415,
},
{
cn: "OU=Security Communication RootCA2,O=SECOM Trust Systems CO.\\,LTD.,C=JP",
sha256Hash: "513b2cecb810d4cde5dd85391adfc6c2dd60d87bb736d2b521484aa47a0ebef6",
certStartOff: 123187,
certStartOff: 121128,
certLength: 891,
},
{
cn: "OU=certSIGN ROOT CA G2,O=CERTSIGN SA,C=RO",
sha256Hash: "657cfe2fa73faa38462571f332a2363a46fce7020951710702cdfbb6eeda3305",
certStartOff: 124078,
certStartOff: 122019,
certLength: 1355,
},
{
cn: "OU=ePKI Root Certification Authority,O=Chunghwa Telecom Co.\\, Ltd.,C=TW",
sha256Hash: "c0a6f4dc63a24bfdcf54ef2a6a082a0a72de35803e2ff5ff527ae5d87206dfd5",
certStartOff: 125433,
certStartOff: 123374,
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: 126893,
certStartOff: 124834,
certLength: 1523,
},
}