Compare commits

...
99 Commits
Author SHA1 Message Date
Xiaolin Zhao 7a4a4d6bea salsa20: add loong64 SIMD implementation
Benchmark results on Loongson-3C6000 @ 2.20GHz:

goos: linux
goarch: loong64
pkg: golang.org/x/crypto/salsa20
cpu: Loongson-3C6000/S @ 2200.00MHz
      |  old.bench   |              new.bench              |
      |    sec/op    |   sec/op     vs base                |
XOR1K   3835.5n ± 0%   493.9n ± 0%  -87.12% (p=0.000 n=10)

      |  old.bench   |               new.bench                |
      |     B/s      |      B/s       vs base                 |
XOR1K   254.6Mi ± 0%   1977.4Mi ± 0%  +676.64% (p=0.000 n=10)

Change-Id: I9f795e7f4e2c467ba9a41c9e6f460612536021ba
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/683255
Reviewed-by: abner chenc <chenguoqi@loongson.cn>
Reviewed-by: Cherry Mui <cherryyz@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-09-21 00:02:45 -07:00
Xiaolin Zhao 1f7c531b64 blake2b: add loong64 SIMD implementation
Benchmark results on Loongson-3C6000 @ 2.20GHz:

goos: linux
goarch: loong64
pkg: golang.org/x/crypto/blake2b
cpu: Loongson-3C6000/S @ 2200.00MHz
         |  old.bench  |              new.bench              |
         |   sec/op    |   sec/op     vs base                |
Write128   350.0n ± 0%   178.3n ± 0%  -49.06% (p=0.000 n=10)
Write1K    2.687µ ± 0%   1.286µ ± 0%  -52.14% (p=0.000 n=10)
Sum128     362.6n ± 0%   183.7n ± 0%  -49.34% (p=0.000 n=10)
Sum1K      2.788µ ± 0%   1.295µ ± 0%  -53.54% (p=0.000 n=10)
geomean    987.4n        483.3n       -51.06%

         |  old.bench   |               new.bench               |
         |     B/s      |     B/s       vs base                 |
Write128   348.8Mi ± 0%   684.8Mi ± 0%   +96.31% (p=0.000 n=10)
Write1K    363.5Mi ± 0%   759.6Mi ± 0%  +108.99% (p=0.000 n=10)
Sum128     336.6Mi ± 0%   664.4Mi ± 0%   +97.37% (p=0.000 n=10)
Sum1K      350.3Mi ± 0%   754.0Mi ± 0%  +115.23% (p=0.000 n=10)
geomean    349.7Mi        714.5Mi       +104.32%

Change-Id: I181a0284c5913cca34f074466f7d83439554e450
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/659775
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: abner chenc <chenguoqi@loongson.cn>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
2026-09-19 18:40:00 -07:00
Xiaolin Zhao 234d0b5a5f chacha20: add loong64 SIMD implementation
Add an assembly implementation of ChaCha20 for Loong64 using LSX SIMD
instructions. This significantly improves performance across all message
sizes on LoongArch machines.

Benchmark results on Loongson-3C6000 @ 2.20GHz:

goos: linux
goarch: loong64
pkg: golang.org/x/crypto/chacha20
cpu: Loongson-3C6000/S @ 2200.00MHz
                 │  old.bench   │              new.bench              │
                 │    sec/op    │   sec/op     vs base                │
ChaCha20/64         188.4n ± 0%   156.9n ± 0%  -16.72% (p=0.000 n=10)
ChaCha20/256        666.4n ± 0%   151.5n ± 0%  -77.27% (p=0.000 n=10)
ChaCha20/10x25     1037.0n ± 0%   578.9n ± 0%  -44.18% (p=0.000 n=10)
ChaCha20/4096      10.207µ ± 0%   1.955µ ± 0%  -80.85% (p=0.000 n=10)
ChaCha20/100x40    11.856µ ± 0%   5.785µ ± 0%  -51.21% (p=0.000 n=10)
ChaCha20/65536     162.99µ ± 0%   31.60µ ± 0%  -80.61% (p=0.000 n=10)
ChaCha20/1000x65   165.57µ ± 0%   41.93µ ± 0%  -74.67% (p=0.000 n=10)
geomean             6.369µ        2.141µ       -66.38%

                 │  old.bench   │               new.bench                │
                 │     B/s      │      B/s       vs base                 │
ChaCha20/64        323.9Mi ± 0%    389.0Mi ± 0%   +20.10% (p=0.000 n=10)
ChaCha20/256       366.4Mi ± 0%   1611.2Mi ± 0%  +339.80% (p=0.000 n=10)
ChaCha20/10x25     229.8Mi ± 0%    411.8Mi ± 0%   +79.19% (p=0.000 n=10)
ChaCha20/4096      382.7Mi ± 0%   1998.1Mi ± 0%  +422.08% (p=0.000 n=10)
ChaCha20/100x40    321.8Mi ± 0%    659.4Mi ± 0%  +104.94% (p=0.000 n=10)
ChaCha20/65536     383.5Mi ± 0%   1977.8Mi ± 0%  +415.78% (p=0.000 n=10)
ChaCha20/1000x65   374.4Mi ± 0%   1478.4Mi ± 0%  +294.86% (p=0.000 n=10)
geomean            335.9Mi         999.2Mi       +197.50%

Change-Id: I6b8d243819d0ab1a1d6540648ed9bd00ecad5a2e
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/683196
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>
Reviewed-by: abner chenc <chenguoqi@loongson.cn>
Reviewed-by: Cherry Mui <cherryyz@google.com>
2026-09-19 18:39:20 -07:00
XiaolinZhao a083cb74b6 argon2: add loong64 LSX implementation of blamka
Benchmark results on Loongson-3C6000 @ 2.20GHz:

goos: linux
goarch: loong64
pkg: golang.org/x/crypto/argon2
cpu: Loongson-3C6000/S @ 2200.00MHz
                                             |  old.bench   |              new.bench              |
                                             |    sec/op    |   sec/op     vs base                |
Argon2i/_Time:_3_Memory:_32_MB,_Threads:_1     123.36m ± 0%   76.67m ± 0%  -37.85% (p=0.000 n=10)
Argon2i/_Time:_4_Memory:_32_MB,_Threads:_1      163.7m ± 0%   101.3m ± 0%  -38.09% (p=0.000 n=10)
Argon2i/_Time:_5_Memory:_32_MB,_Threads:_1      195.0m ± 0%   116.5m ± 8%  -40.24% (p=0.000 n=10)
Argon2i/_Time:_3_Memory:_64_MB,_Threads:_4      259.4m ± 0%   165.3m ± 0%  -36.28% (p=0.000 n=10)
Argon2i/_Time:_4_Memory:_64_MB,_Threads:_4      349.9m ± 0%   220.8m ± 0%  -36.92% (p=0.000 n=10)
Argon2i/_Time:_5_Memory:_64_MB,_Threads:_4      437.6m ± 0%   276.0m ± 0%  -36.92% (p=0.000 n=10)
Argon2d/_Time:_3,_Memory:_32_MB,_Threads:_1    116.34m ± 0%   72.07m ± 0%  -38.06% (p=0.000 n=10)
Argon2d/_Time:_4,_Memory:_32_MB,_Threads:_1    154.42m ± 0%   95.28m ± 0%  -38.30% (p=0.000 n=10)
Argon2d/_Time:_5,_Memory:_32_MB,_Threads:_1     192.4m ± 0%   115.9m ± 2%  -39.73% (p=0.000 n=10)
Argon2d/_Time:_3,_Memory:_64_MB,_Threads:_4     254.4m ± 0%   165.2m ± 0%  -35.06% (p=0.000 n=10)
Argon2d/_Time:_4,_Memory:_64_MB,_Threads:_4     345.5m ± 1%   221.0m ± 0%  -36.04% (p=0.000 n=10)
Argon2d/_Time:_5,_Memory:_64_MB,_Threads:_4     432.1m ± 0%   276.2m ± 0%  -36.09% (p=0.000 n=10)
Argon2id/_Time:_3,_Memory:_32_MB,_Threads:_1   116.49m ± 0%   72.38m ± 0%  -37.86% (p=0.000 n=10)
Argon2id/_Time:_4,_Memory:_32_MB,_Threads:_1   154.59m ± 0%   95.71m ± 3%  -38.09% (p=0.000 n=10)
Argon2id/_Time:_5,_Memory:_32_MB,_Threads:_1    192.5m ± 0%   115.0m ± 0%  -40.25% (p=0.000 n=10)
Argon2id/_Time:_3,_Memory:_64_MB,_Threads:_4    255.6m ± 0%   165.7m ± 0%  -35.18% (p=0.000 n=10)
Argon2id/_Time:_4,_Memory:_64_MB,_Threads:_4    345.4m ± 0%   221.5m ± 0%  -35.87% (p=0.000 n=10)
Argon2id/_Time:_5,_Memory:_64_MB,_Threads:_4    431.9m ± 0%   276.6m ± 0%  -35.96% (p=0.000 n=10)
geomean                                         227.7m        142.6m       -37.40%

                                             |  old.bench   |               new.bench               |
                                             |     B/op     |     B/op      vs base                 |
Argon2i/_Time:_3_Memory:_32_MB,_Threads:_1     32.00Mi ± 0%   32.00Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2i/_Time:_4_Memory:_32_MB,_Threads:_1     32.00Mi ± 0%   32.00Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2i/_Time:_5_Memory:_32_MB,_Threads:_1     32.00Mi ± 0%   32.00Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2i/_Time:_3_Memory:_64_MB,_Threads:_4     64.01Mi ± 0%   64.01Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2i/_Time:_4_Memory:_64_MB,_Threads:_4     64.01Mi ± 0%   64.01Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2i/_Time:_5_Memory:_64_MB,_Threads:_4     64.01Mi ± 0%   64.01Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2d/_Time:_3,_Memory:_32_MB,_Threads:_1    32.00Mi ± 0%   32.00Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2d/_Time:_4,_Memory:_32_MB,_Threads:_1    32.00Mi ± 0%   32.00Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2d/_Time:_5,_Memory:_32_MB,_Threads:_1    32.00Mi ± 0%   32.00Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2d/_Time:_3,_Memory:_64_MB,_Threads:_4    64.01Mi ± 0%   64.01Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2d/_Time:_4,_Memory:_64_MB,_Threads:_4    64.01Mi ± 0%   64.01Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2d/_Time:_5,_Memory:_64_MB,_Threads:_4    64.01Mi ± 0%   64.01Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2id/_Time:_3,_Memory:_32_MB,_Threads:_1   32.00Mi ± 0%   32.00Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2id/_Time:_4,_Memory:_32_MB,_Threads:_1   32.00Mi ± 0%   32.00Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2id/_Time:_5,_Memory:_32_MB,_Threads:_1   32.00Mi ± 0%   32.00Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2id/_Time:_3,_Memory:_64_MB,_Threads:_4   64.01Mi ± 0%   64.01Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2id/_Time:_4,_Memory:_64_MB,_Threads:_4   64.01Mi ± 0%   64.01Mi ± 0%       ~ (p=1.000 n=10) ¹
Argon2id/_Time:_5,_Memory:_64_MB,_Threads:_4   64.01Mi ± 0%   64.01Mi ± 0%       ~ (p=1.000 n=10) ¹
geomean                                        45.26Mi        45.26Mi       +0.00%
¹ all samples are equal

                                             | old.bench  |              new.bench              |
                                             | allocs/op  | allocs/op   vs base                 |
Argon2i/_Time:_3_Memory:_32_MB,_Threads:_1     31.00 ± 0%   31.00 ± 0%       ~ (p=1.000 n=10) ¹
Argon2i/_Time:_4_Memory:_32_MB,_Threads:_1     39.00 ± 0%   39.00 ± 0%       ~ (p=1.000 n=10) ¹
Argon2i/_Time:_5_Memory:_32_MB,_Threads:_1     47.00 ± 0%   47.00 ± 0%       ~ (p=1.000 n=10) ¹
Argon2i/_Time:_3_Memory:_64_MB,_Threads:_4     73.00 ± 0%   73.00 ± 0%       ~ (p=1.000 n=10) ¹
Argon2i/_Time:_4_Memory:_64_MB,_Threads:_4     93.00 ± 0%   93.00 ± 0%       ~ (p=1.000 n=10) ¹
Argon2i/_Time:_5_Memory:_64_MB,_Threads:_4     113.0 ± 0%   113.0 ± 0%       ~ (p=1.000 n=10) ¹
Argon2d/_Time:_3,_Memory:_32_MB,_Threads:_1    31.00 ± 0%   31.00 ± 0%       ~ (p=1.000 n=10) ¹
Argon2d/_Time:_4,_Memory:_32_MB,_Threads:_1    39.00 ± 0%   39.00 ± 0%       ~ (p=1.000 n=10) ¹
Argon2d/_Time:_5,_Memory:_32_MB,_Threads:_1    47.00 ± 0%   47.00 ± 0%       ~ (p=1.000 n=10) ¹
Argon2d/_Time:_3,_Memory:_64_MB,_Threads:_4    73.00 ± 0%   73.00 ± 0%       ~ (p=1.000 n=10) ¹
Argon2d/_Time:_4,_Memory:_64_MB,_Threads:_4    93.00 ± 0%   93.00 ± 0%       ~ (p=1.000 n=10) ¹
Argon2d/_Time:_5,_Memory:_64_MB,_Threads:_4    113.0 ± 0%   113.0 ± 0%       ~ (p=1.000 n=10) ¹
Argon2id/_Time:_3,_Memory:_32_MB,_Threads:_1   31.00 ± 0%   31.00 ± 0%       ~ (p=1.000 n=10) ¹
Argon2id/_Time:_4,_Memory:_32_MB,_Threads:_1   39.00 ± 0%   39.00 ± 0%       ~ (p=1.000 n=10) ¹
Argon2id/_Time:_5,_Memory:_32_MB,_Threads:_1   47.00 ± 0%   47.00 ± 0%       ~ (p=1.000 n=10) ¹
Argon2id/_Time:_3,_Memory:_64_MB,_Threads:_4   73.00 ± 0%   73.00 ± 0%       ~ (p=1.000 n=10) ¹
Argon2id/_Time:_4,_Memory:_64_MB,_Threads:_4   93.00 ± 0%   93.00 ± 0%       ~ (p=1.000 n=10) ¹
Argon2id/_Time:_5,_Memory:_64_MB,_Threads:_4   113.0 ± 0%   113.0 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                        59.32        59.32       +0.00%
¹ all samples are equal

Change-Id: Ia814057ddfdefbe1d23431fa366189ffe5852b96
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/826304
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>
Reviewed-by: abner chenc <chenguoqi@loongson.cn>
Reviewed-by: Cherry Mui <cherryyz@google.com>
2026-09-19 18:38:21 -07:00
Daniel McCarney b4dcfb54b8 acme: support standards-compliant order finalization
When an order finalization response is not immediately valid,
Client.CreateOrderCert depends on the ACME server returning a Location
header. The header value is used as the URL for polling the updated
order status. RFC 8555 does not require this header, so CreateOrderCert
does not interoperate with ACME servers that omit it (e.g. Pebble
v2.9.0+).

This commit adds Client.CreateCertFromOrder, which accepts an *Order and
uses its URI for polling instead of relying on the finalization
response's Location header. It also updates existing callers and
documentation to use the new method and updates the integration tests to
Pebble v2.10.1 (the latest available version), which omits the
finalization Location header.

Fixes golang/go#77704

Change-Id: I0852f19c5d18d6debc9c9686fa2c14a2e39663c2
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/817000
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: Roland Shoemaker <roland@golang.org>
2026-09-18 12:05:15 -07:00
Gopher Robot b853e4b620 x509roots/fallback: update bundle
This is an automated CL which updates the NSS root bundle.

[git-generate]
go generate ./x509roots

Change-Id: I975f934aabe6eaf6b9f256db2b6942c45c9bd7b4
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/833664
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
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>
2026-09-16 08:43:51 -07:00
Gopher Robot 3f62bf119e go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: I39f495ef42016b2ab0c91f54b792ac16b967006f
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/829324
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>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: Cherry Mui <cherryyz@google.com>
2026-09-08 11:05:01 -07:00
Nicola Murino 86efde54dc ssh: reject unexpected message types on established channels
ch.msg is only read while the channel open or a channel request with a
reply is pending, so anything the default arm of channel.handlePacket
delivered to it was never consumed. The blocking send there let a
misbehaving peer fill the buffer with well-formed but unexpected message
types carrying a valid channel id and stall the mux read loop,
deadlocking the whole connection.

No conforming peer sends such messages during the connection protocol.
Treat them as a protocol error and tear the connection down, as
handleUnknownChannelPacket already does for the same messages when the
channel id is not in use.

Fixes CVE-2026-56855
Fixes golang/go#81317

Change-Id: I87420dfe68fcb62a17df4b47dc5ffb6ccd72ba26
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/826524
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>
Auto-Submit: Neal Patel <nealpatel@google.com>
Reviewed-by: Nicholas Husin <husin@google.com>
2026-09-02 11:02:47 -07:00
Nicola Murino a6cdac6084 ssh: drop traffic on undecided channels
A channel in the mux's chanList is not usable until it is established:
an outbound channel has no confirmed remote id until the peer's open
confirmation, and an inbound channel is not serviced by the application
until it is accepted. handlePacket processed any channel message on it,
so a misbehaving peer could flood channel requests and block the mux
read loop on the send to incomingRequests, deadlocking the connection,
or close an outbound channel before confirming it, making the victim
tear down a half-initialized channel and emit a close for remote id 0,
an unrelated channel of the peer.

No such packet can be legitimate: the peer learns an inbound channel's
local id only from the confirmation we have not sent yet, and on an
outbound channel RFC 4254 lets it answer the open request only with a
confirmation or a failure.

Add an established flag, set when the channel becomes usable: for an
outbound channel when the open response is received, for an inbound
channel by Accept before the confirmation is sent. Until then
handlePacket drops every packet other than the open response. The flag
is separate from decided, which Reject also sets: a rejected channel is
decided but must never carry traffic.

Fixes CVE-2026-78662
Fixes golang/go#81316

Change-Id: Ib0983bb216a49808a2db1f4a4d92ee9fe38a3c51
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/826504
Auto-Submit: Gopher Robot <gobot@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Neal Patel <nealpatel@google.com>
Reviewed-by: Nicholas Husin <husin@google.com>
2026-09-02 10:48:31 -07:00
Nicola Murino 39dc44e69c ssh: don't skip the source-address critical option in CheckCert
CertChecker.CheckCert ignored source-address on the assumption that
serverAuthenticate would enforce it, but that only happens in the
server-side user authentication path. Nothing enforced it in the host key
path, so CheckHostKey accepted CA-signed host certificates carrying
source-address regardless of the server's address, and the same applied to
applications calling CheckCert directly.

Drop the special case: every critical option must be listed in
SupportedCriticalOptions, host and user certificates alike. Authenticate,
the only path where serverAuthenticate does enforce source-address, checks
against a copy of the CertChecker with that option appended.

Fixes golang/go#80872

Change-Id: I3c3554b71ea2a4ce4b17696a9596b973b7d74b00
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/816840
Reviewed-by: David Chase <drchase@google.com>
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>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Auto-Submit: Nicola Murino <nicola.murino@gmail.com>
2026-08-30 20:04:51 -07:00
Dmitri Shuralyov afebf4cb4e x509roots/fallback/bundle: make subjectsEqual stricter on Go 1.27+
This gradual step towards removing/inlining subjectsEqual can happen
after Go 1.27.0 is released and the bundle starts being generated
with it.

Change-Id: Ib315bae69c45922c3f0aac5d2c0155fb5d94229f
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/775782
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>
Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
2026-08-26 07:40:58 -07:00
Gopher Robot 89f4e9bb5b x509roots/fallback: update bundle
This is an automated CL which updates the NSS root bundle.

[git-generate]
go generate ./x509roots

Change-Id: I3be04cd4cb6e44856beaa074fdb62a58ff8ce35b
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/820300
Auto-Submit: Gopher Robot <gobot@golang.org>
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: Roland Shoemaker <roland@golang.org>
2026-08-24 13:54:08 -07:00
Nicola Murino 71488c48c2 ssh/knownhosts: compare only public key portions for revocation
Revocation matching compared full marshaled key blobs, so a @revoked
line containing a plain host key did not revoke a certificate
certifying that key: when the server presented such a certificate,
IsRevoked only matched the whole certificate blob or the CA key, and
verification succeeded even though the underlying host key was revoked.

OpenSSH's check_key_not_revoked compares keys with sshkey_equal_public,
which considers only the public portions and explicitly allows
comparisons between certificates and plain keys. Match that behavior by
normalizing both @revoked entries and lookups to the underlying public
key: for a certificate, the certified key rather than the certificate
blob. This also makes a @revoked line containing a certificate revoke
the plain key it certifies

Change-Id: I3029d59e68fb01f2340763e2eae25cac23c6193d
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/816841
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: David Chase <drchase@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
2026-08-23 06:53:19 -07:00
Daniel McCarney 82adefa711 ssh: synchronize unexpected response test
Wait for an ordered channel request after sending the unsolicited
response flood. This ensures that the client mux has discarded the
entire flood before SendRequest opens its response gate, avoiding
confusion between an in-flight unsolicited response and the expected
reply.

This fixes TestChannelUnexpectedResponsesDiscarded from flaking, where
otherwise running 1000000 iterations of the test with GOMAXPROCS=16
produced ~15 distinct failures of the form  "expected success reply".

Change-Id: Ic1125c1f44bd919a5cf32b889d331bbc94c06c25
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/817140
Reviewed-by: Neal Patel <neal@golang.org>
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>
Auto-Submit: Daniel McCarney <daniel@binaryparadox.net>
Reviewed-by: David Chase <drchase@google.com>
2026-08-19 13:42:46 -07:00
Gopher Robot c757c9851f all: upgrade go directive to at least 1.26.0 [generated]
By now Go 1.27.0 has been released, and Go 1.25 is no longer supported
per the Go Release Policy (see https://go.dev/doc/devel/release#policy).

See https://go.dev/doc/godebug#go-126 for GODEBUG setting changes
relevant to Go 1.26.

For golang/go#69095.

[git-generate]
(cd . && go get go@1.26.0 && go mod tidy)
(cd x509roots/fallback && go get go@1.26.0 && go mod tidy)

Change-Id: I54826e019edd4e12ad821915b1d102de719bfb31
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/817981
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>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Auto-Submit: Gopher Robot <gobot@golang.org>
2026-08-19 10:16:24 -07:00
Damien Miller 593c81af8a ssh: correctly ignore pre-banner lines
Reading the peer's benner in readVersion() was incorrectly counting
lines sent by the server before the actual `SSH-2.0` banner against
the line limit for the banner itself.

This is incorrect per RFC 4253 section 4.2, where the limit of 255
characters is written to apply only to the `SSH-` banner string
itself.

This instead limits pre-banner lines to 255 characters each but
instead allows a maximum of 1024 lines.

Change-Id: Ief014bb3413e26e8d2077d3eb204ceaea9dc9444
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/811320
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: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
2026-08-17 08:34:06 -07:00
Daniel McCarney 46efc8bc62 acme: add crypto.SignMessage test coverage
This commit updates the pre-existing TestJWSEncodeJSONCustom test to
also verify JWS signature production works with a crypto.MessageSigner.

Change-Id: I5156cb4da9a7feee98340ff4fb75848da5827055
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/815480
Auto-Submit: Daniel McCarney <daniel@binaryparadox.net>
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: Carlos Amedee <carlos@golang.org>
2026-08-17 08:22:44 -07:00
Daniel Tang e0a54fdc4b acme: use crypto.SignMessage instead of calling (crypto.Signer).Sign
crypto.SignMessage allows using crypto.Signers that only usefully
implement SignMessage to be used for the ACME account key.

This would be useful for opening up integrations with opinionated
remote signing APIs that don't support pre-hashing.

Fixes golang/go#80877

Change-Id: I86bb76cf5865d3d2b8606a6dbd813cbb26447c86
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/815340
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Sean Liao <sean@liao.dev>
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>
2026-08-17 08:21:42 -07:00
Gopher Robot f44d03d253 go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: I6e7a078f1a8ffe71bace5615ad4fc03a12466985
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/813280
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>
Reviewed-by: Mark Freeman <markfreeman@google.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
2026-08-11 10:56:31 -07:00
Julian Zhu 5ed494470b crypto/internal/poly1305: provide optimised assembly for riscv64
goos: linux
goarch: riscv64
pkg: golang.org/x/crypto/internal/poly1305
                   │ sum1305-old  │            sum1305-new              │
                   │    sec/op    │   sec/op     vs base                │
64-4                  195.9n ± 1%   144.8n ± 0%  -26.07% (p=0.000 n=10)
1K-4                  1.853µ ± 0%   1.012µ ± 0%  -45.39% (p=0.000 n=10)
2M-4                  3.627m ± 0%   1.880m ± 0%  -48.17% (p=0.000 n=10)
64Unaligned-4         196.4n ± 1%   174.7n ± 0%  -11.05% (p=0.000 n=10)
1KUnaligned-4         1.853µ ± 0%   1.491µ ± 0%  -19.54% (p=0.000 n=10)
2MUnaligned-4         3.643m ± 0%   2.850m ± 0%  -21.78% (p=0.000 n=10)
Write64-4            126.85n ± 0%   77.40n ± 0%  -38.98% (p=0.000 n=10)
Write1K-4            1784.5n ± 1%   937.2n ± 0%  -47.48% (p=0.000 n=10)
Write2M-4             3.654m ± 2%   1.880m ± 0%  -48.55% (p=0.000 n=10)
Write64Unaligned-4    125.7n ± 0%   107.5n ± 0%  -14.48% (p=0.000 n=10)
Write1KUnaligned-4    1.782µ ± 0%   1.424µ ± 0%  -20.09% (p=0.000 n=10)
Write2MUnaligned-4    3.637m ± 0%   2.850m ± 0%  -21.64% (p=0.000 n=10)
geomean               10.14µ        6.925µ       -31.68%

                   │ sum1305-old  │             sum1305-new-3             │
                   │     B/s      │      B/s       vs base                │
64-4                 311.7Mi ± 1%    421.5Mi ± 0%  +35.25% (p=0.000 n=10)
1K-4                 527.0Mi ± 0%    965.0Mi ± 0%  +83.10% (p=0.000 n=10)
2M-4                 551.4Mi ± 0%   1063.8Mi ± 0%  +92.94% (p=0.000 n=10)
64Unaligned-4        310.8Mi ± 1%    349.3Mi ± 0%  +12.38% (p=0.000 n=10)
1KUnaligned-4        526.9Mi ± 0%    654.9Mi ± 0%  +24.30% (p=0.000 n=10)
2MUnaligned-4        548.9Mi ± 0%    701.8Mi ± 0%  +27.85% (p=0.000 n=10)
Write64-4            481.2Mi ± 0%    788.6Mi ± 0%  +63.89% (p=0.000 n=10)
Write1K-4            547.2Mi ± 1%   1042.0Mi ± 0%  +90.42% (p=0.000 n=10)
Write2M-4            547.4Mi ± 2%   1064.0Mi ± 0%  +94.38% (p=0.000 n=10)
Write64Unaligned-4   485.5Mi ± 0%    567.7Mi ± 0%  +16.94% (p=0.000 n=10)
Write1KUnaligned-4   547.9Mi ± 0%    685.8Mi ± 0%  +25.17% (p=0.000 n=10)
Write2MUnaligned-4   550.0Mi ± 0%    701.9Mi ± 0%  +27.62% (p=0.000 n=10)
geomean              485.5Mi         710.7Mi       +46.37%

Change-Id: I33a3df03382eb42ef062b0366521f02559290482
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/738760
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>
Reviewed-by: Meng Zhuo <mengzhuo1203@gmail.com>
Reviewed-by: Mark Freeman <markfreeman@google.com>
2026-08-03 22:01:12 -07:00
Nicola Murino b07833c067 ssh: return window credit for discarded extended data
handleData subtracts the length of every data packet from the receive
window, but extended data with a type code other than
SSH2_EXTENDED_DATA_STDERR is then silently discarded and, since no API
exists to read it, its window credit was never returned: adjustWindow
only runs as a side effect of reading a stream. A peer sending
extended data with an unknown type code could therefore permanently
shrink the channel window, down to a complete stall of the channel.

Credit the discarded bytes back through adjustWindow. adjustWindow
returns io.EOF if the local side has already sent a channel close;
ignore it like ReadExtended does, since an error returned here would
terminate the mux read loop and tear down the whole connection. For
comparison, OpenSSH rejects extended data with unexpected type codes
outright and never buffers it, so the dropped bytes are simply never
credited back to the misbehaving peer.

Fixes golang/go#80333

Change-Id: Ia311ce3ad008e7c89ce9752f1937c91374a4e74f
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/802901
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: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Cherry Mui <cherryyz@google.com>
2026-08-03 14:00:09 -07:00
Brad Fitzpatrick d701c51f7e acme: fix nil pointer dereference in pebble test error reporting
The WaitOrder failure path used errors.Is with a nil *OrderError
target, which never matches and left orderErr nil at the point its
Problem field was dereferenced. Use errors.As to populate the target
instead.

Found by govet's nilness check.

Change-Id: Ieef849fcbf04c85ef32651059db2b5fe162af325
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/803181
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>
2026-07-23 08:25:44 -07:00
Brad Fitzpatrick 999d053994 ssh: fix parsing of GSSAPI payloads offering multiple mechanisms
parseGSSAPIPayload overwrote the remaining payload with the trailing
bytes returned by asn1.Unmarshal after decoding each OID, so any
request offering more than one mechanism failed to parse after the
first one. Track the remaining payload across iterations instead, and
be strict: reject payloads with trailing bytes after an OID or after
the mechanism list.

Add a test with a two-mechanism payload (Kerberos V5 followed by
SPNEGO) that failed before this change.

Change-Id: If0984efcf103da76323d5d03ad7443d8ec9d3aed
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/803180
Reviewed-by: Carlos Amedee <carlos@golang.org>
Reviewed-by: Nicola Murino <nicola.murino@gmail.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-07-23 08:19:57 -07:00
Nicola Murino 90f76b8ffe ssh: reject certificate signature keys before recursing
parseCert parsed a certificate's SignatureKey by calling ParsePublicKey
again, and only afterwards rejected a certificate used as a signing key.
For a chain of nested certificates this recursed once per level, so a key
with enough nesting exhausted the goroutine stack and crashed with a fatal
stack overflow that recover cannot catch.

Reject the signature key by its declared algorithm before parsing it, which
bounds parsing.

Fixes golang/go#80414

Change-Id: Id51401aae15bfbec006c03ab97635a3b68bd6058
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/802145
Auto-Submit: Nicola Murino <nicola.murino@gmail.com>
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: Cherry Mui <cherryyz@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
2026-07-22 10:37:06 -07:00
database64128 b53964a1ca ssh: permit empty but non-nil HostKeyAlgorithms, KeyExchanges, Ciphers, MACs
The documentation of ClientConfig.HostKeyAlgorithms says:

  If empty, a reasonable default is used.

However, if HostKeyAlgorithms is empty but non-nil, the reasonable
default won't be applied, and the SSH handshake will fail. Fix this by
changing the nil check to a length check.

While here, do the same for KeyExchanges, Ciphers, MACs in Config.

Change-Id: I1b3d2d2159f3c7d57a9a690ad04f1f6818b39a5c
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/800080
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: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Reviewed-by: Cherry Mui <cherryyz@google.com>
Auto-Submit: Nicola Murino <nicola.murino@gmail.com>
2026-07-22 10:37:01 -07:00
Nicola Murino 626e40fc98 ssh: drain stderr on forwarded TCP and Unix channels
tcpListener.Accept, unixListener.Accept, Client.dial and
Client.dialStreamLocal only read the main stream of the channels they
return. Data sent by the peer on the extended (stderr) stream
accumulates in the extPending buffer and its window credit is only
returned by ReadExtended, which is never called. Since the window is
shared between the two streams, a misbehaving peer can pin up to 2 MiB
per channel and eventually stall it; well-behaved peers never send
stderr on these channel types.

Drain the stderr stream into io.Discard, mirroring CL 783720 which
fixed the same issue in the agent forwarders.

The drain takes precedence over reading Stderr through a type assertion
on the returned net.Conn, which was never a supported use. For
comparison, OpenSSH refuses extended data on these channel types and
drops the payload without buffering it.

Updates golang/go#80333

Change-Id: I4f1445eee9ce1b56ca62cec342812d63cfdd3e13
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/802900
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Reviewed-by: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-07-22 10:34:36 -07:00
Gopher Robot 31914c699b x509roots/fallback: update bundle
This is an automated CL which updates the NSS root bundle.

[git-generate]
go generate ./x509roots

Change-Id: Ic89f2ab75500d9b03a76236f2228657508a4d16d
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/803060
Reviewed-by: Carlos Amedee <carlos@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Auto-Submit: Gopher Robot <gobot@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-07-21 14:38:23 -07:00
Brad Fitzpatrick f2135b814c all: clean up minor issues found by staticcheck
Use bytes.Equal instead of comparing bytes.Compare results to zero,
avoid Yoda conditions, drop a redundant zero slice capacity, use
strings.ReplaceAll, build acme error strings with a strings.Builder,
and lowercase an error string in ocsp. No behavior changes.

Change-Id: I5d2b3fa5c241adc75e61be0f2d2d7af1eb9e8d66
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/803200
Reviewed-by: Roland Shoemaker <roland@golang.org>
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: Carlos Amedee <carlos@golang.org>
2026-07-21 14:30:59 -07:00
Akira ff03dafdb0 ssh: report verified signature format to callback
VerifiedPublicKeyCallback currently receives the public-key algorithm
declared in the outer authentication request. For RSA authentication,
that value can differ from the format of the signature that was
successfully verified because compatible RSA algorithm combinations are
accepted.

This can cause post-verification policy or audit decisions to use the
request algorithm instead of the signature format actually verified.

Pass sig.Format to VerifiedPublicKeyCallback so signatureAlgorithm
identifies the successfully verified signature format. Clarify the
callback documentation and add a regression test covering compatible
but different RSA request algorithms and signature formats.

Fixes golang/go#80411

Change-Id: Ib405378d75367a90536e0814c9d26d4dec9aaa25
GitHub-Last-Rev: 1a4acd4e639361be1f2948a4aef1013436a75ecf
GitHub-Pull-Request: golang/crypto#364
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/800740
Reviewed-by: Michael Pratt <mpratt@google.com>
Reviewed-by: Nicola Murino <nicola.murino@gmail.com>
Reviewed-by: David Chase <drchase@google.com>
Auto-Submit: Michael Pratt <mpratt@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-07-17 15:41:46 -07:00
Nicola Murino 10b54ffa51 ssh: raise the RSA modulus limit to 16384 bits
the fix for CVE-2026-39829 capped the accepted RSA modulus at 8192 bits,
matching crypto/tls, to bound the CPU cost of verifying an
attacker-supplied key. That rejected keys OpenSSH generates: ssh-keygen
allows -b up to 16384. Raise the limit to 16384 so those keys interoperate,
both in the public key parser and the OpenSSH private key parser, and
correspondingly raise the prime-factor bound to 8192 bits (each prime is
about half the modulus). The agent's checkRSAKeyParams is updated to match.

Verifying a 16384-bit key costs roughly 3x an 8192-bit one.

Fixes golang/go#80075

Change-Id: Ib08ddd7b48552e692c48dbdf105bbc8579120bbc
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/795421
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: Filippo Valsorda <filippo@golang.org>
Reviewed-by: David Chase <drchase@google.com>
2026-07-13 20:33:21 -07:00
Nicholas S. Husin 0f039f3a4a ssh: sanitize various error messages
Similar to CL 780180, sanitize other error messages that are controlled
by the remote peer, to avoid potential injection of control characters.

Also remove sanitizeString usage as it is not strictly needed to escape
error messages safely, and for consistency with how we sanitize other
error messages throughout the codebase.

BannerDisplayStderr is left unsanitized for now, as it is one instance
where we might actually want control characters for valid reasons (see
golang/go#80302).

Updates golang/go#80302
Fixes golang/go#80301

Change-Id: Ie40f4ac2f76afed740dceb548a4b0fa96a6a6964
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/798420
Auto-Submit: Nicholas Husin <nsh@golang.org>
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>
Reviewed-by: Nicola Murino <nicola.murino@gmail.com>
Reviewed-by: Nicholas Husin <husin@google.com>
2026-07-13 12:23:17 -07:00
Nicola Murino b27e1504e3 ssh: don't close the connection on an unparseable public key
In the server's publickey authentication path, a public key blob that
ParsePublicKey could not decode caused serverAuthenticate to return an
error, tearing down the whole connection. A key we cannot parse is part
of an individual authentication attempt, not a transport-level framing
error, so it must fail only that attempt. This lets the client fall
back to other keys or authentication methods.

Updates golang/go#80075

Change-Id: I8d882d08bb50eb33e0054d9d9c2bd9e0b3039443
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/795420
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: Michael Pratt <mpratt@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Auto-Submit: Nicola Murino <nicola.murino@gmail.com>
2026-07-13 12:20:29 -07:00
Nicola Murino c1a3b97d70 ssh: document and test source-address behavior for non-IP transports
The source-address critical option is a comma-separated list of IP
addresses and CIDR blocks, so it can never match a connection whose
remote address is not IP-based, such as a Unix domain socket.

Change-Id: I45e09fe5c8c13922cb0735278acf33320857322c
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/797042
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>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
2026-07-12 08:19:47 -07:00
Nicola Murino b9229ecc70 ssh: reject a present but empty source-address critical option
checkSourceAddressCriticalOption treated a source-address critical
option with an empty value as if the option was absent, allowing
authentication from any address. A callback (or certificate authority)
building the value dynamically from an empty allowlist would intend to
deny all sources, not to lift the restriction. This also contradicts
the documented contract that the option is validated whenever it is
present, and differs from OpenSSH, where an empty address list matches
nothing.

Change-Id: I1c0de27cbcab8817a7ff446019c25cd19957a7d7
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/797041
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-07-12 08:19:43 -07:00
Nicola Murino e557b08ec2 ssh: enforce the source-address critical option for all auth callbacks
CVE-2026-46595 extended source-address validation, historically applied
only to the Permissions returned by PublicKeyCallback, to the ones
returned by VerifiedPublicKeyCallback. The documented contract of
Permissions.CriticalOptions does not restrict enforcement to a specific
authentication method, so move the check to a single point at the end of
each authentication attempt, where it covers the Permissions returned by
any callback (password, keyboard-interactive, none and GSSAPI included).

The check at public key cache insertion time is kept: it remains
authoritative for the Permissions returned by PublicKeyCallback, which
VerifiedPublicKeyCallback may replace before the check at the end of the
authentication attempt runs and which are not re-checked on partial
success. It also still makes public key queries fail before the client
produces a signature when PublicKeyCallback supplies the restriction.

Fixes CVE-2026-56854
Fixes golang/go#80213

Change-Id: Ibd612a8e4240bd710e33754f3ceb95bb29169d9a
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/797040
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>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: David Chase <drchase@google.com>
2026-07-12 08:19:37 -07:00
Nicola Murino 4044611da8 ssh: fix data race in the doClientServerAuth test helper
The helper returned the auth errors collected via AuthLogCallback
without waiting for the server goroutine that appends to them. Close
the client side of the connection on handshake failure, to unblock a
server still reading from it, and join the server goroutine before
returning.

Change-Id: Ibe8c3b236491769f3433f7457d4f2b9a07a65be0
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/797043
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>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: David Chase <drchase@google.com>
2026-07-12 08:19:29 -07:00
Nicola Murino e8ad5cb7aa ssh: initialize new channels fully before adding them to chanList
newChannel published the channel into the mux's chanList (via
chanList.add) before assigning its localId, and openChannel/Accept set
maxIncomingPayload only after newChannel returned. For an outbound
channel, whose newChannel runs on an application goroutine, the mux
loop could observe the channel through getChan before those fields
were initialized, reading a zero localId or maxIncomingPayload with no
happens-before relationship.

Assign localId inside add while the chanList lock is held, and set
maxIncomingPayload (always channelMaxPacket) in newChannel itself, so
a channel returned by getChan is always fully initialized.

Change-Id: Ib54a5d10dcb5c1ad8ed4dabe5d8d2790778be298
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/797061
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: David Chase <drchase@google.com>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
2026-07-12 08:19:12 -07:00
Nicola Murino fe1af3a418 ssh: make channel.close idempotent
channel.close closes the internal Go channels (msg, incomingRequests);
calling it twice panics with "close of closed channel". Both current
callers run on the mux loop and are serialized, so the panic is not
reachable today. Harden close by guarding it with sync.Once, so that
the connection does not become fragile to any future bug that reaches
close twice.

Change-Id: I5d8d948367538ea5561724033c4ee4daa668e221
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/797060
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>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
2026-07-12 08:19:06 -07:00
Daniel McCarney 243e02a382 ocsp: update ParseRequest docs & error conditions
ParseRequest was documented as erroring for inputs that contained
an optional request signature, and as only supporting requests for
a single certificate.

However, prior to this commit DER inputs that contained a signature
succeeded without verifying the signature, and inputs that contained
multiple certificate identifiers succeeded but only the first
certificate identifier was unmarshalled.

This commit updates the function behaviour to return an error for the
signature case as described in the docs, and to clarify the
documentation for inputs with multiple certificate identifiers.

Fixes golang/go#80300

Change-Id: I7446422d6c98bd406b8c43e55fea9a16ace9d4a6
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/798220
Reviewed-by: Roland Shoemaker <roland@golang.org>
Auto-Submit: Daniel McCarney <daniel@binaryparadox.net>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Nicholas Husin <nsh@golang.org>
Reviewed-by: Nicholas Husin <husin@google.com>
2026-07-09 11:40:58 -07:00
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
Gopher Robot a1c0d99298 go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: Ia739869d49c750c7fa578b9dbd7bb998d8c87087
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781683
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Nicholas Husin <nsh@golang.org>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: Nicholas Husin <husin@google.com>
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-05-21 17:26:06 -07:00
Nicola 3c7c86938f ssh: fix deadlock on unexpected channel responses
Previously, channel.handlePacket sent channelRequestSuccess and
channelRequestFailure messages to ch.msg unconditionally via the default
arm of its type switch. Because ch.msg is a bounded buffer (chanSize),
a peer that sends a burst of unsolicited channel request responses for
an open, idle channel fills the buffer and blocks the mux read loop on
the next send. That stalls all packet processing on the connection,
and because readLoop then backs up on t.incoming, closing the
underlying net.Conn does not unblock either goroutine: user code
observes Close() returning promptly while Wait() hangs and the mux,
readLoop, and kexLoop goroutines leak permanently.

This change mirrors the fix for the mux-level SendRequest path: a
sentRequestPending atomic gate is set while a SendRequest with
WantReply is in flight, handlePacket drops responses when the gate is
closed, and uses a non-blocking send otherwise. SendRequest drains
any spurious response that slipped through before discarding it, so
the caller always observes the reply to its own request.

This aligns with OpenSSH, which silently ignores channel confirm
messages that do not match a pending request.

Fixes golang/go#79564
Fixes CVE-2026-39830

Change-Id: I15e2add4bf7876bb0c6f921f8b57203d97e83f47
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781664
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Neal Patel <nealpatel@google.com>
Reviewed-by: Neal Patel <nealpatel@google.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
2026-05-21 17:09:12 -07:00
Neal Patel 533fb3f7e4 ssh: fix source-address critical option bypass
Previously, CVE-2024-45337 fixed an authorization bypass
for misused ssh server configurations; if any other type
of callback is passed other than public key, then the
source-address validation would be skipped.

Fixes CVE-2026-46595
Fixes golang/go#79570

Change-Id: I08d86a961048a232c8672f23000e693ed5a0e2fd
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781642
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: Roland Shoemaker <roland@golang.org>
2026-05-21 17:03:26 -07:00
Neal Patel abbc44d451 ssh: fix incorrect operator order
Arithmetic is incorrectly applied to 'byte'
instead of 'int' resulting in a possible
overflow that allows for a panic.

Fixes CVE-2026-46597
Fixes golang/go#79561

Change-Id: I83edabeeda676f0209d29d5e2554890bbd0eef8f
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781620
Reviewed-by: Roland Shoemaker <roland@golang.org>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: Nicholas Husin <nsh@golang.org>
Reviewed-by: Nicholas Husin <husin@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-05-21 17:03:25 -07:00
Nicola Murino e052873987 ssh: fix infinite loop on large channel writes due to integer overflow
The internal 'min' helper function in channel.go incorrectly cast the
input data length (int) to uint32 before comparing it with the
maximum packet size. On 64-bit systems, if the data length is a
multiple of 2^32 (approx. 4GB), this cast results in 0.

Consequently, the function returns 0, causing the WriteExtended loop
to spin indefinitely because it attempts to reserve 0 bytes while
the remaining data length is still positive.

This change renames the helper to 'minPayloadSize' to avoid confusion
with the Go 1.21 built-in 'min' and updates the logic to use int64
for comparisons, preventing truncation and the resulting infinite loop.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport.

Fixes golang/go#79567
Fixes CVE-2026-39834

Change-Id: Id5bf81d9f06c7042452acffe1c76580ff878665e
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781663
Reviewed-by: Neal Patel <nealpatel@google.com>
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>
2026-05-21 17:03:23 -07:00
Nicola Murino b61cf853a8 ssh: enforce user presence verification for security keys
Previously the library did not verify the "User Presence" (UP) bit in
signatures generated by FIDO/U2F security keys
(sk-ecdsa-sha2-nistp256@openssh.com and sk-ssh-ed25519@openssh.com).
This allowed signatures without physical interaction to be accepted
if the underlying hardware produced them, deviating from the default
secure behavior expected by the FIDO standards and OpenSSH.

skECDSAPublicKey.Verify and skEd25519PublicKey.Verify now enforce
the user-presence bit (0x01, constant flagUserPresence) by default.
Signatures whose flags byte has UP clear fail with the sentinel
errSKMissingUserPresence.

The server public-key authentication path honors the OpenSSH
"no-touch-required" extension as an opt-out. noTouchAllowed reports
true when the extension is present either in the Permissions
returned by PublicKeyCallback (authorized_keys-level opt-out) or in
the certificate's own Extensions (CA-level opt-out); in that case
skKeyWithoutUP is used to derive a clone of the SK public key (and,
for certificates, a clone of the wrapping Certificate whose inner
Key is the cloned SK key) whose Verify accepts UP-clear signatures.
The originals are never mutated, so a per-session opt-out cannot
leak across authentication attempts or connections. Matching
OpenSSH, the opt-out is read only from Extensions, never from
CriticalOptions.

skKeyWithoutUP is iterative and unwraps at most one level of
*Certificate: the SSH cert format forbids Certificate.Key from being
another Certificate (parseCert rejects it) but callers can still
construct such a value directly in Go, so a recursive descent would
be driven to unbounded depth by malformed or cyclic input. Any such
pathological *Certificate is returned unchanged.

CertChecker.CheckCert applies skKeyWithoutUP unconditionally to the
certificate's CA key before verifying the CA signature, matching
OpenSSH, which calls sshkey_verify with detailsp==NULL in
sshkey.c:cert_parse and never extracts or enforces UP/UV flags on
CA signatures. The UP bit on a CA signature reflects the CA
operator's presence at cert-issuance time, which has no bearing on
whether the user being authenticated is present now, so enforcing it
here would only break interop with certificates issued by
non-interactive SK CAs without a corresponding security benefit. The
skKeyWithoutUP call is a no-op for non-SK CA keys (the common case).

This change breaks backward compatibility for clients or keys that
generate user-authentication signatures without the User Presence
flag set. Previously those signatures were accepted by the server.
They will now be rejected with "ssh: signature missing required user
presence flag" unless the "no-touch-required" extension is
explicitly granted to the session by the server callbacks, or
carried by the user certificate.

This issue was found during a security audit by NCC Group
Cryptography Services, sponsored by Teleport.

Fixes golang/go#79566
Fixes CVE-2026-39831

Change-Id: I74b6de3bb6a2d7a0f34d7fa36bbbbf06f0b3fc6b
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781662
Reviewed-by: Neal Patel <nealpatel@google.com>
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>
2026-05-21 17:03:20 -07:00
Nicola Murino 9c2cd33e8d ssh: enforce strict limits on DSA key parameters
The parseDSA function previously accepted DSA keys with arbitrary values
for the sub-prime Q and did not validate that group elements G and Y
were within the modulus P.

Malicious actors could provide a key with a massively large Q (e.g.,
millions of bits), leading to excessive CPU consumption during signature
verification.

This change restricts the sub-prime Q to exactly 160 bits, as required
by FIPS 186-2, and ensures that G and Y are strictly less than P.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport.

Fixes golang/go#79565
Fixes CVE-2026-39829

Change-Id: I526118d94684076088d0625178844f64c1303ec8
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781661
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: Neal Patel <nealpatel@google.com>
2026-05-21 17:03:16 -07:00
Nicola Murino 890731877d ssh: reject RSA keys with excessively large moduli
Previously, the RSA key parser accepted keys with arbitrary modulus
sizes. Processing keys with extremely large moduli (e.g., > 8192 bits)
can consume excessive CPU resources during verification, potentially
leading to a Denial of Service (DoS).

This change introduces a limit of 8192 bits for the RSA modulus in
parseRSA, rejecting keys that exceed this size in line with the limit
enforced by crypto/tls.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport.

Fixes golang/go#79565
Fixes CVE-2026-39829

Change-Id: Ibdddad1859a4d9db5c9f052d06c82f29bfc2e5e5
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781641
Reviewed-by: Neal Patel <nealpatel@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
2026-05-21 17:03:13 -07:00
Nicola Murino ffd87b4878 ssh: fix panic when authority callbacks are nil
Previously, if CertChecker.IsHostAuthority or CertChecker.IsUserAuthority
were left unset, calling CheckHostKey or Authenticate would result in a
nil pointer dereference panic.

This change adds checks to ensure these callbacks are defined before
invocation, returning an error instead of panicking.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport.

Fixes golang/go#79563
Fixes CVE-2026-39835

Change-Id: I2bd9c8d76646232e49f6aedc7b5334f3825918be
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781660
Commit-Queue: Neal Patel <nealpatel@google.com>
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: Neal Patel <nealpatel@google.com>
2026-05-21 17:03:09 -07:00
Nicola Murino 4e7a7384ec ssh: fix deadlock on unexpected global responses
Previously, the mux implementation handled global request responses by
blocking until the response could be sent to the globalResponses channel.
Since this channel has a buffer size of 1, unsolicited responses from a
server (or responses arriving after a timeout) would fill the buffer.
Subsequent unsolicited responses would block handleGlobalPacket, stalling
the entire connection's read loop and causing a denial of service.

This change modifies handleGlobalPacket to use a non-blocking send. If
no goroutine is waiting for a response (or the buffer is full), the
message is dropped. This aligns with OpenSSH behavior, which ignores
unexpected global responses.

Additionally, SendRequest now drains the globalResponses channel after
acquiring the mutex but before sending the request. This ensures that
any stale responses or "spam" buffered just before the lock was acquired
are discarded, preventing race conditions where a legitimate request
might otherwise consume an unrelated response.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport.

Fixes golang/go#79564
Fixes CVE-2026-39830

Change-Id: Ia0c46355203d557eadcd432c10b87c8a044e1089
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781640
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Neal Patel <nealpatel@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-05-21 17:02:53 -07:00
Nicola Murino b25012b37b ssh: enforce nil Permissions when returning PartialSuccessError
In serverAuthenticate, the permissions variable is reset to nil at the
beginning of the authentication loop. If an authentication callback
returns a PartialSuccessError along with non-nil Permissions, those
permissions are currently silently discarded before the next
authentication step.

This change returns an error if a callback returns both a PartialSuccessError
and non-nil Permissions, preventing API misuse where the user might
erroneously expect those permissions to be preserved or merged into the
final session permissions.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport.

Fixes golang/go#79562
Fixes CVE-2026-39828

Change-Id: I632c9e46e2b5e8804ef88081063a3612a2462f9f
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781621
Reviewed-by: Neal Patel <nealpatel@google.com>
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: Roland Shoemaker <roland@golang.org>
2026-05-21 17:00:58 -07:00
Nicola Murino 6c195c8a97 ssh: prevent memory leak when rejecting channels
When a server rejects an incoming channel request via
NewChannel.Reject, the channel is left in the multiplexer's
channel list. Because the channel is never explicitly removed or
closed, its internal buffers and sync primitives remain allocated
for the lifetime of the SSH connection.

A malicious client could exploit this behavior by repeatedly
requesting to open channels that are destined to be rejected,
causing unbounded memory growth and potentially leading to a
Denial of Service (DoS) via resource exhaustion.

This change fixes the leak by calling ch.mux.chanList.remove
within the Reject method, removing the channel from the list and allowing the
garbage collector to reclaim the associated memory immediately.

Fixes golang/go#35127
Fixes CVE-2026-3982

Change-Id: Iaa177f5dfd151812dd404e528a4a1c77527a0e29
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781320
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Nicholas Husin <nsh@golang.org>
Reviewed-by: Nicholas Husin <husin@google.com>
2026-05-21 11:00:25 -07:00
Neal Patel f717e29698 ssh/knownhosts: respect @revoked CA keys
Fixes CVE-2026-42508
Fixes golang/go#79568

Change-Id: I20f33cba20756b048726ff3464b83871859d3b5c
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781220
Reviewed-by: Nicholas Husin <husin@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Nicholas Husin <nsh@golang.org>
Reviewed-by: Nicola Murino <nicola.murino@gmail.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
2026-05-21 10:58:49 -07:00
Neal Patel e7c36ccb47 ssh/agent: prevent panic on pathological ed25519 inputs
parseEd25519Key and parseEd25519Cert cast wire bytes
to ed25519.PrivateKey without checking length; a short
payload panics at priv[32:] in Public().

Fixes CVE-2026-46598
Fixes golang/go#46598

Change-Id: I127bc6a22adff1c4beb4d54533062bebc388de47
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781360
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Nicholas Husin <nsh@golang.org>
Reviewed-by: Nicholas Husin <husin@google.com>
2026-05-21 10:55:07 -07:00
Nicola 0fb843a472 ssh/agent: reject keys with unsupported confirm constraint
The in-memory keyring supports the "lifetime" constraint but does not
implement the "confirm" constraint. Previously, keyring.Add silently
ignored ConfirmBeforeUse: the key was stored, advertised through List,
and used for signing without any interactive confirmation, potentially
misleading callers into believing this security measure was enforced.

Return an error when ConfirmBeforeUse is set instead of silently
downgrading the caller's security expectations. Implementing real
confirm-before-use in an in-memory library keyring is infeasible (there
is no UI or confirmation callback), so failing closed is the correct
behavior; adding actual confirm support would require an API addition
and is out of scope.

This is a deliberate behavior change: keyring.Add previously accepted
and ignored ConfirmBeforeUse and now returns an error. This change also
updates the keyring doc comments to document the supported constraints.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport.

Fixes CVE-2026-39833
Updates golang/go#47533
Fixes golang/go#79436

Change-Id: I1b3a286f0c1e4a4e08ac37109f7e491692ca90ae
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/778642
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Neal Patel <nealpatel@google.com>
Reviewed-by: Neal Patel <neal@golang.org>
Auto-Submit: Neal Patel <nealpatel@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2026-05-21 10:54:57 -07:00
Nicola e3d1254f1e ssh/agent: don't accept keys with unsupported constraints
The in-memory keyring cannot enforce constraint extensions, so silently
accepting a key that carries them gave callers a false sense of
restriction. Refuse keys with constraint extensions instead: a key
whose constraints cannot be enforced must not be loaded. This behavior
is consistent with OpenSSH.

This is a deliberate behavior change: keyring.Add previously accepted
and ignored ConstraintExtensions and now returns an error.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport.

Fixes CVE-2026-39832
Fixes golang/go#79435

Change-Id: I6ca4f1c29f8edfabb287fe07299641f70896d5fe
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/778641
Auto-Submit: Neal Patel <nealpatel@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Neal Patel <neal@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Neal Patel <nealpatel@google.com>
2026-05-21 10:47:08 -07:00
Nicola a1ce0fee12 ssh/agent: preserve constraint extensions when adding keys
The client Add method only serialized the lifetime and confirm
constraints and silently dropped AddedKey.ConstraintExtensions before
sending the SSH_AGENTC_ADD_IDENTITY request. As a result the remote
agent always received the key with no extension constraints, regardless
of what the caller requested.

Applications that add a key believing custom constraint extensions
(such as restrict-destination-v00@openssh.com) would be enforced
instead loaded a completely unrestricted key into the agent. For
example, an administrator forwarding their agent into an untrusted jump
host and trying to limit the forwarded key with restrict-destination
never had that restriction reach the agent: any user or compromised
process on that host could make the agent sign arbitrary challenges.

Serialize each entry in key.ConstraintExtensions as an
agentConstrainExtension constraint so the constraints reach the agent,
and add a round-trip regression test that verifies the extensions
survive client serialization and server parsing.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport.

Updates CVE-2026-39832
Updates golang/go#79435

Change-Id: I14c5583b106cbf0d282d2ba01e000e0f586f08c7
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/778640
Reviewed-by: Neal Patel <neal@golang.org>
Reviewed-by: Neal Patel <nealpatel@google.com>
Reviewed-by: Keith Randall <khr@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-05-21 10:43:27 -07:00
Roland Shoemaker a749d17980 chacha20poly1305: remove usages of BYTE instr
We have had VBROADCASTI128 since at least Go 1.11, so no need to use
BYTE instructions.

Change-Id: I297bbc636320ae830f6e8f83eb174efe8251a9a3
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/672838
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
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: Cherry Mui <cherryyz@google.com>
2026-05-18 11:44:40 -07:00
Roland Shoemaker 7ee59707ff chacha20poly1305: drop pre-AVX assembly impl
Updates golang/go#69587

Change-Id: Ic158558f879b8b8ad23155bf887e083169096d19
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/672837
Auto-Submit: 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>
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
2026-05-18 11:44:36 -07:00
Dmitri Shuralyov 44decbfe70 blake2b: merge go125.go into blake2b_test.go
The go1.25 build constraint is guaranteed to always be satisfied because
the go directive is at 1.25.0, so the separated out go125.go file is not
needed. Move the assertion that the *xof type implements the hash.XOF
interface into a _test.go file to let it happen alongside other tests
in this package.

Change-Id: I65c886ede4d574a3168f28689f9529aa56586697
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/775781
Auto-Submit: Dmitri Shuralyov <dmitshur@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>
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
Auto-Submit: Dmitri Shuralyov <dmitshur@google.com>
2026-05-11 07:38:31 -07:00
Gopher Robot b8a14a8d65 go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: Ie3eba6549d69698b6dd089c7e49f7c30198dc2bb
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/775841
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>
Auto-Submit: Gopher Robot <gobot@golang.org>
2026-05-08 11:32:18 -07:00
Daniel McCarney 9d9d507896 x509roots/fallback/bundle: fix bundle test with Go 1.27+
In Go 1.27 we've updated crypto/x509/pkix to avoid hex-encoding
attribute values that are string-typed. However, in TestBundle() we
assert the parsed certificate subject CN matches expected and now the
parsed value differs based on Go version.

This commit introduces some small helpers that on Go 1.25/1.26 replicate
the Go 1.27 behavior, decoding hex-encoded attribute values before
making the comparison.

In this way the test continues to pass without losing any coverage, or
introducing duplicated per-version bundles. In the future when only Go
1.27+ are supported we can revert this extra machinery.

Change-Id: I66bf6439e421169c0f9c750f88116b73ec5188fe
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/775760
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>
Auto-Submit: Daniel McCarney <daniel@binaryparadox.net>
2026-05-08 08:10:15 -07:00
repon fd0b90d21f acme: include Problem in OrderError.Error
An ACME Order object can contain an error field when it is invalid.
Previously, OrderError.Error() ignored this field, making it difficult
to diagnose why an order failed. This change includes the problem
details if available.


Change-Id: I461fd02ef02251a2ba1e8cea39b5df8070938117
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/772880
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Sean Liao <sean@liao.dev>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
2026-05-01 10:44:32 -07:00
Filippo Valsorda b9e53593a6 pbkdf2: turn into a wrapper for crypto/pbkdf2
Change-Id: If95f1d771404fe88a8f9bc0a17b5a4d16a6a6964
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/769721
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
2026-04-23 08:20:11 -07:00
Filippo Valsorda cc0e4fc1d4 hkdf: forward Extract to the standard library
Change-Id: I82406eb153c53e7cbf167360e472724a6a6a6964
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/769720
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
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: Filippo Valsorda <filippo@golang.org>
2026-04-23 08:20:08 -07:00
Gopher Robot a8e9237a21 x509roots/fallback: update bundle
This is an automated CL which updates the NSS root bundle.

[git-generate]
go generate ./x509roots

Change-Id: I9bad7b49959a336a125bdc3aa340c94292b26899
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/766500
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>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
2026-04-13 10:03:23 -07:00
Gopher Robot 03ca0dcccb go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: Ia33bd1cd73db091960b285c234d2cf2622f30943
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/764501
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
2026-04-09 08:33:22 -07:00
Nicola Murino 8400f4a938 ssh: respect signer's algorithm preference in pickSignatureAlgorithm
Previously, pickSignatureAlgorithm constructed the list of candidate
algorithms by iterating over the static list returned by
algorithmsForKeyFormat. This caused the Signer's preference order
to be ignored in favor of the library's default internal order.

This change inverts the filtering logic to iterate over the signer's
supported algorithms first. This ensures that if a MultiAlgorithmSigner
explicitly prefers a specific algorithm (e.g., rsa-sha2-512 over
rsa-sha2-256), that preference is preserved and respected during the
handshake negotiation.

Fixes golang/go#78248

Change-Id: I48a0aac720be7f973963342b82047ce32fc96699
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/746020
Reviewed-by: Lonny Wong <lonnywang.cn@gmail.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Nicola Murino <nicola.murino@gmail.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
2026-03-23 08:34:51 -07:00
Rob Picard 81c6cb34a8 ssh: swap cbcMinPaddingSize to cbcMinPacketSize to get encLength
The existing code uses cbcMinPaddingSize incorrectly. That value is
also used in the first parameter of the max call, meaning it will
never be used.

Fixes golang/go#78062

Change-Id: I4243ab668168313919df33d78c6965e9eff0e934
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/754780
Reviewed-by: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Reviewed-by: Nicola Murino <nicola.murino@gmail.com>
2026-03-20 08:28:06 -07:00
122 changed files with 9442 additions and 9607 deletions
+1 -1
View File
@@ -221,7 +221,7 @@ func (c *Client) directoryURL() string {
// CreateCert was part of the old version of ACME. It is incompatible with RFC 8555.
//
// Deprecated: this was for the pre-RFC 8555 version of ACME. Callers should use CreateOrderCert.
// Deprecated: this was for the pre-RFC 8555 version of ACME. Callers should use CreateCertFromOrder.
func (c *Client) CreateCert(ctx context.Context, csr []byte, exp time.Duration, bundle bool) (der [][]byte, certURL string, err error) {
return nil, "", errPreRFC
}
+1 -1
View File
@@ -734,7 +734,7 @@ func TestTLSALPN01ChallengeCert(t *testing.T) {
if !acmeExts[0].Critical {
t.Errorf("acmeExt.Critical = %v; want true", acmeExts[0].Critical)
}
if bytes.Compare(acmeExts[0].Value, extValue) != 0 {
if !bytes.Equal(acmeExts[0].Value, extValue) {
t.Errorf("acmeExt.Value = %v; want %v", acmeExts[0].Value, extValue)
}
+22 -24
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.
@@ -672,7 +671,7 @@ func (m *Manager) authorizedCert(ctx context.Context, key crypto.Signer, ck cert
if err != nil {
return nil, nil, err
}
chain, _, err := client.CreateOrderCert(ctx, o.FinalizeURL, csr, true)
chain, _, err := client.CreateCertFromOrder(ctx, o, csr, true)
if err != nil {
return nil, nil, err
}
@@ -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
+4 -4
View File
@@ -182,9 +182,9 @@ func (p *prober) runOrder(ctx context.Context, identifiers []acme.AuthzID) {
log.Fatalf("WaitOrder(%q): %v", o.URI, err)
}
csr, certkey := newCSR(identifiers)
der, curl, err := p.client.CreateOrderCert(ctx, o.FinalizeURL, csr, true)
der, curl, err := p.client.CreateCertFromOrder(ctx, o, csr, true)
if err != nil {
log.Fatalf("CreateOrderCert: %v", err)
log.Fatalf("CreateCertFromOrder: %v", err)
}
log.Printf("cert URL: %s", curl)
if err := checkCert(der, identifiers); err != nil {
@@ -247,9 +247,9 @@ func (p *prober) runPreauthz(ctx context.Context, identifiers []acme.AuthzID) {
log.Fatalf("WaitOrder(%q): %v", o.URI, err)
}
csr, certkey := newCSR(identifiers)
der, curl, err := p.client.CreateOrderCert(ctx, o.FinalizeURL, csr, true)
der, curl, err := p.client.CreateCertFromOrder(ctx, o, csr, true)
if err != nil {
log.Fatalf("CreateOrderCert: %v", err)
log.Fatalf("CreateCertFromOrder: %v", err)
}
log.Printf("cert URL: %s", curl)
if err := checkCert(der, identifiers); err != nil {
+5 -8
View File
@@ -103,9 +103,7 @@ func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid KeyID, nonce, ur
}
payload = base64.RawURLEncoding.EncodeToString(cs)
}
hash := sha.New()
hash.Write([]byte(phead + "." + payload))
sig, err := jwsSign(key, sha, hash.Sum(nil))
sig, err := jwsSign(key, sha, []byte(phead+"."+payload))
if err != nil {
return nil, err
}
@@ -195,14 +193,13 @@ func jwkEncode(pub crypto.PublicKey) (string, error) {
return "", ErrUnsupportedKey
}
// jwsSign signs the digest using the given key.
// The hash is unused for ECDSA keys.
func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {
// jwsSign signs the payload using the given key.
func jwsSign(key crypto.Signer, hash crypto.Hash, payload []byte) ([]byte, error) {
switch pub := key.Public().(type) {
case *rsa.PublicKey:
return key.Sign(rand.Reader, digest, hash)
return crypto.SignMessage(key, rand.Reader, payload, hash)
case *ecdsa.PublicKey:
sigASN1, err := key.Sign(rand.Reader, digest, hash)
sigASN1, err := crypto.SignMessage(key, rand.Reader, payload, hash)
if err != nil {
return nil, err
}
+39 -5
View File
@@ -14,6 +14,7 @@ import (
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"math/big"
@@ -353,6 +354,22 @@ func (s *customTestSigner) Sign(io.Reader, []byte, crypto.SignerOpts) ([]byte, e
return s.sig, nil
}
type customTestMessageSigner struct {
customTestSigner
msg []byte
opts crypto.SignerOpts
}
func (s *customTestMessageSigner) Sign(io.Reader, []byte, crypto.SignerOpts) ([]byte, error) {
return nil, errors.New("customTestSigner: Sign() called instead of SignMessage()")
}
func (s *customTestMessageSigner) SignMessage(_ io.Reader, msg []byte, opts crypto.SignerOpts) ([]byte, error) {
s.msg = msg
s.opts = opts
return s.sig, nil
}
func TestJWSEncodeJSONCustom(t *testing.T) {
claims := struct{ Msg string }{"hello"}
const (
@@ -391,24 +408,33 @@ func TestJWSEncodeJSONCustom(t *testing.T) {
)
tt := []struct {
alg, phead string
name, phead string
pub crypto.PublicKey
stdsig, jwsig string
msgSigner bool
}{
{"ES256", es256phead, testKeyEC.Public(), es256stdsig, es256jwsig},
{"RS256", rs256phead, testKey.Public(), testsig, testsig},
{"ES256", es256phead, testKeyEC.Public(), es256stdsig, es256jwsig, false},
{"RS256", rs256phead, testKey.Public(), testsig, testsig, false},
{"ES256-MessageSigner", es256phead, testKeyEC.Public(), es256stdsig, es256jwsig, true},
{"RS256-MessageSigner", rs256phead, testKey.Public(), testsig, testsig, true},
}
for _, tc := range tt {
tc := tc
t.Run(tc.alg, func(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
stdsig, err := base64.RawStdEncoding.DecodeString(tc.stdsig)
if err != nil {
t.Errorf("couldn't decode test vector: %v", err)
}
signer := &customTestSigner{
cs := customTestSigner{
sig: stdsig,
pub: tc.pub,
}
var signer crypto.Signer = &cs
var ms *customTestMessageSigner
if tc.msgSigner {
ms = &customTestMessageSigner{customTestSigner: cs}
signer = ms
}
b, err := jwsEncodeJSON(claims, signer, noKeyID, "nonce", "url")
if err != nil {
@@ -427,6 +453,14 @@ func TestJWSEncodeJSONCustom(t *testing.T) {
if j.Sig != tc.jwsig {
t.Errorf("j.Sig = %q\nwant %q", j.Sig, tc.jwsig)
}
if ms != nil {
if want := tc.phead + "." + payload; string(ms.msg) != want {
t.Errorf("msg = %q; want %q", ms.msg, want)
}
if ms.opts != crypto.SHA256 {
t.Errorf("opts = %v; want %v", ms.opts, crypto.SHA256)
}
}
})
}
}
+4 -4
View File
@@ -37,7 +37,7 @@ import (
const (
// pebbleModVersion is the module version used for Pebble and Pebble's
// challenge test server. It is ignored if `-pebble-local-dir` is provided.
pebbleModVersion = "v2.7.0"
pebbleModVersion = "v2.10.1"
// startingPort is the first port number used for binding interface
// addresses. Each call to takeNextPort() will increment a port number
// starting at this value.
@@ -383,7 +383,7 @@ func testIssuance(t *testing.T, env *environment, challSrv challengeServer) {
order, err = client.WaitOrder(ctx, order.URI)
if err != nil {
var orderErr *acme.OrderError
if errors.Is(err, orderErr) {
if errors.As(err, &orderErr) {
t.Fatalf("failed to wait for order %s: %s: %s", orderURL, err, orderErr.Problem)
} else {
t.Fatalf("failed to wait for order %s: %s", orderURL, err)
@@ -420,7 +420,7 @@ func testIssuance(t *testing.T, env *environment, challSrv challengeServer) {
}
// Finalize the order by creating a certificate with our CSR.
chain, _, err := client.CreateOrderCert(ctx, order.FinalizeURL, csrDer, true)
chain, _, err := client.CreateCertFromOrder(ctx, order, csrDer, true)
if err != nil {
t.Fatalf("failed to finalize order %s with finalize URL %s: %v",
orderURL, order.FinalizeURL, err)
@@ -638,7 +638,7 @@ func startPebbleEnvironment(t *testing.T, config *environmentConfig) environment
// Note: we specify -defaultIPv6 "" so that no AAAA records are served.
// The LUCI CI runners have issues with IPv6 connectivity on localhost.
spawnServerProcess(t, binDir, "pebble-challtestsrv",
"-dns01", fmt.Sprintf(":%d", config.dnsPort),
"-dnsserver", fmt.Sprintf(":%d", config.dnsPort),
"-defaultIPv6", "",
"-management", fmt.Sprintf(":%d", takeNextPort()),
"-doh", "",
+47 -5
View File
@@ -197,7 +197,7 @@ func (c *Client) accountKeyRollover(ctx context.Context, newKey crypto.Signer) e
// identify those with StatusPending status and fulfill a challenge using Accept.
// Once all authorizations are satisfied, the caller will typically want to poll
// order status using WaitOrder until it's in StatusReady state.
// To finalize the order and obtain a certificate, the caller submits a CSR with CreateOrderCert.
// To finalize the order and obtain a certificate, the caller submits a CSR with CreateCertFromOrder.
func (c *Client) AuthorizeOrder(ctx context.Context, id []AuthzID, opt ...OrderOption) (*Order, error) {
dir, err := c.Discover(ctx)
if err != nil {
@@ -250,7 +250,11 @@ func (c *Client) GetOrder(ctx context.Context, url string) (*Order, error) {
return nil, err
}
defer res.Body.Close()
return responseOrder(res)
o, err := responseOrder(res)
if err == nil && o.URI == "" {
o.URI = url
}
return o, err
}
// WaitOrder polls an order from the given URL until it is in one of the final states,
@@ -271,6 +275,9 @@ func (c *Client) WaitOrder(ctx context.Context, url string) (*Order, error) {
}
o, err := responseOrder(res)
res.Body.Close()
if err == nil && o.URI == "" {
o.URI = url
}
switch {
case err != nil:
// Skip and retry.
@@ -338,11 +345,43 @@ func responseOrder(res *http.Response) (*Order, error) {
// certificate chain. Otherwise, only a leaf certificate is returned.
// The returned URL can be used to re-fetch the certificate using FetchCert.
//
// This method is only supported by CAs implementing RFC 8555. See CreateCert for pre-RFC CAs.
// This method is only supported by CAs implementing RFC 8555 that return a
// Location header for order finalization responses. Most users should prefer
// CreateCertFromOrder, an alternative that has better interoperation with RFC 8555
// compliant CAs. See CreateCert for pre-RFC CAs.
//
// CreateOrderCert returns an error if the CA's response is unreasonably large.
// Callers are encouraged to parse the returned value to ensure the certificate is valid and has the expected features.
func (c *Client) CreateOrderCert(ctx context.Context, url string, csr []byte, bundle bool) (der [][]byte, certURL string, err error) {
return c.createOrderCert(ctx, url, "", csr, bundle)
}
// CreateCertFromOrder submits the CSR (Certificate Signing Request) to a CA for
// the specified Order created with AuthorizeOrder.
//
// If the bundle argument is true, the returned value also contain the CA (issuer)
// certificate chain. Otherwise, only a leaf certificate is returned.
// The returned URL can be used to re-fetch the certificate using FetchCert.
//
// This method is only supported by CAs implementing RFC 8555.
// See CreateCert for pre-RFC CAs.
//
// CreateCertFromOrder returns an error if the CA's response is unreasonably large.
// Callers are encouraged to parse the returned value to ensure the certificate is valid and has the expected features.
func (c *Client) CreateCertFromOrder(ctx context.Context, order *Order, csr []byte, bundle bool) (der [][]byte, certURL string, err error) {
if order == nil {
return nil, "", errors.New("acme: nil order")
}
if order.URI == "" {
return nil, "", errors.New("acme: order URI is empty")
}
if order.FinalizeURL == "" {
return nil, "", errors.New("acme: order finalize URL is empty")
}
return c.createOrderCert(ctx, order.FinalizeURL, order.URI, csr, bundle)
}
func (c *Client) createOrderCert(ctx context.Context, finalizeURL, orderURL string, csr []byte, bundle bool) (der [][]byte, certURL string, err error) {
if _, err := c.Discover(ctx); err != nil { // required by c.accountKID
return nil, "", err
}
@@ -353,7 +392,7 @@ func (c *Client) CreateOrderCert(ctx context.Context, url string, csr []byte, bu
}{
CSR: base64.RawURLEncoding.EncodeToString(csr),
}
res, err := c.post(ctx, nil, url, req, wantStatus(http.StatusOK))
res, err := c.post(ctx, nil, finalizeURL, req, wantStatus(http.StatusOK))
if err != nil {
return nil, "", err
}
@@ -365,7 +404,10 @@ func (c *Client) CreateOrderCert(ctx context.Context, url string, csr []byte, bu
// Wait for CA to issue the cert if they haven't.
if o.Status != StatusValid {
o, err = c.WaitOrder(ctx, o.URI)
if orderURL == "" {
orderURL = o.URI
}
o, err = c.WaitOrder(ctx, orderURL)
}
if err != nil {
return nil, "", err
+74
View File
@@ -979,6 +979,80 @@ func TestRFC_CreateOrderCert(t *testing.T) {
}
}
// TestRFC_CreateCertFromOrder tests that finalization responses work even when
// they omit a Location header. Such a header isn't required by RFC 8555 in this
// context and we should be able to complete an order without needing it.
func TestRFC_CreateCertFromOrder(t *testing.T) {
q := &x509.CertificateRequest{
Subject: pkix.Name{CommonName: "example.org"},
}
csr, err := x509.CreateCertificateRequest(rand.Reader, q, testKeyEC)
if err != nil {
t.Fatal(err)
}
tmpl := &x509.Certificate{SerialNumber: big.NewInt(1)}
leaf, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &testKeyEC.PublicKey, testKeyEC)
if err != nil {
t.Fatal(err)
}
s := newACMEServer()
s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", s.url("/accounts/1"))
w.Write([]byte(`{"status": "valid"}`))
})
s.handle("/finalize", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"status":"processing"}`)
})
s.handle("/orders/1", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `{"status":"valid", "certificate":%q}`, s.url("/crt"))
})
s.handle("/crt", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/pem-certificate-chain")
pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: leaf})
})
s.start()
defer s.close()
cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
order := &Order{URI: s.url("/orders/1"), FinalizeURL: s.url("/finalize")}
cert, curl, err := cl.CreateCertFromOrder(context.Background(), order, csr, true)
if err != nil {
t.Fatalf("CreateCertFromOrder: %v", err)
}
if _, err := x509.ParseCertificate(cert[0]); err != nil {
t.Errorf("ParseCertificate: %v", err)
}
if !reflect.DeepEqual(cert[0], leaf) {
t.Errorf("cert and leaf bytes don't match")
}
if u := s.url("/crt"); curl != u {
t.Errorf("curl = %q; want %q", curl, u)
}
}
// TestRFC_CreateCertFromOrderInvalidOrder tests that invalid CreateCertFromOrder
// calls fail before making a request.
func TestRFC_CreateCertFromOrderInvalidOrder(t *testing.T) {
cl := new(Client)
tests := []struct {
name string
order *Order
}{
{"nil", nil},
{"empty URI", &Order{FinalizeURL: "https://example.com/finalize"}},
{"empty finalize URL", &Order{URI: "https://example.com/order/1"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if _, _, err := cl.CreateCertFromOrder(context.Background(), tt.order, nil, false); err == nil {
t.Fatal("CreateCertFromOrder returned nil error")
}
})
}
}
func TestRFC_AlreadyRevokedCert(t *testing.T) {
s := newACMEServer()
s.handle("/acme/revoke-cert", func(w http.ResponseWriter, r *http.Request) {
+11 -6
View File
@@ -114,14 +114,15 @@ type Error struct {
}
func (e *Error) Error() string {
str := fmt.Sprintf("%d %s: %s", e.StatusCode, e.ProblemType, e.Detail)
var sb strings.Builder
fmt.Fprintf(&sb, "%d %s: %s", e.StatusCode, e.ProblemType, e.Detail)
if len(e.Subproblems) > 0 {
str += fmt.Sprintf("; subproblems:")
sb.WriteString("; subproblems:")
for _, sp := range e.Subproblems {
str += fmt.Sprintf("\n\t%s", sp)
fmt.Fprintf(&sb, "\n\t%s", sp)
}
}
return str
return sb.String()
}
// AuthorizationError indicates that an authorization for an identifier
@@ -167,7 +168,11 @@ type OrderError struct {
}
func (oe *OrderError) Error() string {
return fmt.Sprintf("acme: order %s status: %s", oe.OrderURL, oe.Status)
str := fmt.Sprintf("acme: order %s status: %s", oe.OrderURL, oe.Status)
if oe.Problem != nil {
str += fmt.Sprintf("; problem: %s", oe.Problem)
}
return str
}
// RateLimit reports whether err represents a rate limit error and
@@ -325,7 +330,7 @@ type Order struct {
// Possible values are StatusPending, StatusReady, StatusProcessing, StatusValid and StatusInvalid.
// Pending means the CA does not believe that the client has fulfilled the requirements.
// Ready indicates that the client has fulfilled all the requirements and can submit a CSR
// to obtain a certificate. This is done with Client's CreateOrderCert.
// to obtain a certificate. This is done with Client's CreateCertFromOrder.
// Processing means the certificate is being issued.
// Valid indicates the CA has issued the certificate. It can be downloaded
// from the Order's CertURL. This is done with Client's FetchCert.
+16
View File
@@ -217,3 +217,19 @@ func TestErrorStringerWithSubproblems(t *testing.T) {
t.Errorf("Unexpected error string: wanted %q, got %q", expectedStr, err.Error())
}
}
func TestOrderErrorProblem(t *testing.T) {
oe := &OrderError{
OrderURL: "url",
Status: "invalid",
Problem: &Error{
StatusCode: 400,
ProblemType: "type",
Detail: "detail",
},
}
want := "acme: order url status: invalid; problem: 400 type: detail"
if got := oe.Error(); got != want {
t.Errorf("oe.Error() = %q, want %q", got, want)
}
}
+18 -17
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)
}
+9
View File
@@ -24,6 +24,7 @@ var (
func TestArgon2(t *testing.T) {
defer func(sse4 bool) { useSSE4 = sse4 }(useSSE4)
defer func(lsx bool) { useLSX = lsx }(useLSX)
if useSSE4 {
t.Log("SSE4.1 version")
@@ -32,6 +33,14 @@ func TestArgon2(t *testing.T) {
testArgon2id(t)
useSSE4 = false
}
if useLSX {
t.Log("LSX version")
testArgon2i(t)
testArgon2d(t)
testArgon2id(t)
useLSX = false
}
t.Log("generic version")
testArgon2i(t)
testArgon2d(t)
+4
View File
@@ -6,6 +6,10 @@ package argon2
var useSSE4 bool
// useLSX reports whether the loong64 LSX vector implementation should be
// used. It is set from cpu.Loong64.HasLSX in blamka_loong64.go's init().
var useLSX bool
func processBlockGeneric(out, in1, in2 *block, xor bool) {
var t block
for i := range t {
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build loong64 && gc && !purego
package argon2
import "golang.org/x/sys/cpu"
func init() {
// useLSX (declared in blamka_generic.go) gates the LSX vector path.
// If the running CPU lacks LSX support, processBlockVX falls back
// to processBlockGeneric.
useLSX = cpu.Loong64.HasLSX
}
//go:noescape
func blamkaLOONG64(b *block)
//go:noescape
func mixBlocksVX(out, a, b, c *block)
//go:noescape
func xorBlocksVX(out, a, b, c *block)
func processBlock(out, in1, in2 *block) {
processBlockVX(out, in1, in2, false)
}
func processBlockXOR(out, in1, in2 *block) {
processBlockVX(out, in1, in2, true)
}
func processBlockVX(out, in1, in2 *block, xor bool) {
if !useLSX {
processBlockGeneric(out, in1, in2, xor)
return
}
var t block
mixBlocksVX(&t, in1, in2, &t)
blamkaLOONG64(&t)
if xor {
xorBlocksVX(out, in1, in2, &t)
} else {
mixBlocksVX(out, in1, in2, &t)
}
}
+240
View File
@@ -0,0 +1,240 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build loong64 && gc && !purego
#include "textflag.h"
// HALF_ROUND applies one BlaMka half-round (mul-add-xor-rotate) to two
// groups of 4 columns, each packed 2-wide into V registers (v0/v2/v4/v6
// hold one pair of columns in lane0/lane1, v1/v3/v5/v7 hold the other).
// t0, t1 are scratch registers, one per independent column group.
#define HALF_ROUND(v0, v1, v2, v3, v4, v5, v6, v7, t0, t1) \
VMULWEVVWU v0, v2, t0; \
VADDV t0, t0, t0; \
VADDV v0, v2, v0; \
VADDV v0, t0, v0; \
VXORV v6, v0, v6; \
VROTRV $32, v6, v6; \
VMULWEVVWU v1, v3, t1; \
VADDV t1, t1, t1; \
VADDV v1, v3, v1; \
VADDV v1, t1, v1; \
VXORV v7, v1, v7; \
VROTRV $32, v7, v7; \
VMULWEVVWU v4, v6, t0; \
VADDV t0, t0, t0; \
VADDV v4, v6, v4; \
VADDV v4, t0, v4; \
VXORV v2, v4, v2; \
VROTRV $24, v2, v2; \
VMULWEVVWU v5, v7, t1; \
VADDV t1, t1, t1; \
VADDV v5, v7, v5; \
VADDV v5, t1, v5; \
VXORV v3, v5, v3; \
VROTRV $24, v3, v3; \
VMULWEVVWU v0, v2, t0; \
VADDV t0, t0, t0; \
VADDV v0, v2, v0; \
VADDV v0, t0, v0; \
VXORV v6, v0, v6; \
VROTRV $16, v6, v6; \
VMULWEVVWU v1, v3, t1; \
VADDV t1, t1, t1; \
VADDV v1, v3, v1; \
VADDV v1, t1, v1; \
VXORV v7, v1, v7; \
VROTRV $16, v7, v7; \
VMULWEVVWU v4, v6, t0; \
VADDV t0, t0, t0; \
VADDV v4, v6, v4; \
VADDV v4, t0, v4; \
VXORV v2, v4, v2; \
VROTRV $63, v2, v2; \
VMULWEVVWU v5, v7, t1; \
VADDV t1, t1, t1; \
VADDV v5, v7, v5; \
VADDV v5, t1, v5; \
VXORV v3, v5, v3; \
VROTRV $63, v3, v3
// SHUFFLE / SHUFFLE_INV: reused verbatim from blake2b_loong64.s -- amd64
// avo source confirms argon2's SHUFFLE/SHUFFLE_INV and blake2b's SHUFFLE
// are byte-for-byte identical macros (diagonalize / undiagonalize),
// operating only on V2..V7 (V0/V1 untouched), with V13 held at zero.
#define SHUFFLE \
VADDV V2, V13, V12; \
VADDV V6, V13, V16; \
VADDV V4, V13, V14; \
VADDV V5, V13, V4; \
VADDV V14, V13, V5; \
VSHUF4IV $9, V3, V2; \
VSHUF4IV $9, V12, V3; \
VSHUF4IV $3, V7, V6; \
VSHUF4IV $3, V16, V7
#define SHUFFLE_INV \
VADDV V2, V13, V12; \
VADDV V6, V13, V16; \
VADDV V4, V13, V14; \
VADDV V5, V13, V4; \
VADDV V14, V13, V5; \
VSHUF4IV $3, V3, V2; \
VSHUF4IV $3, V12, V3; \
VSHUF4IV $9, V7, V6; \
VSHUF4IV $9, V16, V7
// LOAD_MSG_0 / STORE_MSG_0: row pass. `off` is the byte offset of the
// 128-byte (16-uint64) row group; within the macro the 8 loads step by
// 16B (2 uint64 per V register). Called from blamkaLOONG64 with off
// stepping by 128B across the 8 row groups -- do not confuse the two
// strides.
#define LOAD_MSG_0(off) \
VMOVQ (off+0)(R4), V0; \
VMOVQ (off+16)(R4), V1; \
VMOVQ (off+32)(R4), V2; \
VMOVQ (off+48)(R4), V3; \
VMOVQ (off+64)(R4), V4; \
VMOVQ (off+80)(R4), V5; \
VMOVQ (off+96)(R4), V6; \
VMOVQ (off+112)(R4), V7
#define STORE_MSG_0(off) \
VMOVQ V0, (off+0)(R4); \
VMOVQ V1, (off+16)(R4); \
VMOVQ V2, (off+32)(R4); \
VMOVQ V3, (off+48)(R4); \
VMOVQ V4, (off+64)(R4); \
VMOVQ V5, (off+80)(R4); \
VMOVQ V6, (off+96)(R4); \
VMOVQ V7, (off+112)(R4)
// LOAD_MSG_1 / STORE_MSG_1: diagonal pass. `off` is the byte offset within
// a 128-byte row group; within the macro the 8 loads step by 128B (one
// element from each of the 8 row groups, mirroring blamka_generic.go's
// t[off], t[128+off], t[256+off], ...). Called from blamkaLOONG64 with
// off stepping by 16B (2 uint64) across the 8 diagonal groups -- the
// inner/outer strides are swapped relative to LOAD_MSG_0/STORE_MSG_0.
#define LOAD_MSG_1(off) \
VMOVQ (off+0)(R4), V0; \
VMOVQ (off+128)(R4), V1; \
VMOVQ (off+256)(R4), V2; \
VMOVQ (off+384)(R4), V3; \
VMOVQ (off+512)(R4), V4; \
VMOVQ (off+640)(R4), V5; \
VMOVQ (off+768)(R4), V6; \
VMOVQ (off+896)(R4), V7
#define STORE_MSG_1(off) \
VMOVQ V0, (off+0)(R4); \
VMOVQ V1, (off+128)(R4); \
VMOVQ V2, (off+256)(R4); \
VMOVQ V3, (off+384)(R4); \
VMOVQ V4, (off+512)(R4); \
VMOVQ V5, (off+640)(R4); \
VMOVQ V6, (off+768)(R4); \
VMOVQ V7, (off+896)(R4)
// BLAMKA_ROUND_0(off): one full row pass over the 16-uint64 group at
// byte offset off -- corresponds to a single call to blamkaGeneric
// with the "row" argument order (t[off]..t[off+15]).
#define BLAMKA_ROUND_0(off) \
LOAD_MSG_0(off); \
HALF_ROUND(V0, V1, V2, V3, V4, V5, V6, V7, V8, V9); \
SHUFFLE; \
HALF_ROUND(V0, V1, V2, V3, V4, V5, V6, V7, V8, V9); \
SHUFFLE_INV; \
STORE_MSG_0(off)
// BLAMKA_ROUND_1(off): one full diagonal pass over the 16 uint64s at
// diagonal offset off -- corresponds to a single call to blamkaGeneric
// with the "diagonal" argument order (t[off], t[off+1], t[16+off], ...).
#define BLAMKA_ROUND_1(off) \
LOAD_MSG_1(off); \
HALF_ROUND(V0, V1, V2, V3, V4, V5, V6, V7, V8, V9); \
SHUFFLE; \
HALF_ROUND(V0, V1, V2, V3, V4, V5, V6, V7, V8, V9); \
SHUFFLE_INV; \
STORE_MSG_1(off)
// func blamkaLOONG64(b *block)
TEXT ·blamkaLOONG64(SB), NOSPLIT, $0-8
MOVV b+0(FP), R4
VXORV V13, V13, V13 // V13 = 0, used by SHUFFLE/SHUFFLE_INV
// Round 0: 8 row passes, stride 128 bytes (16 uint64s).
BLAMKA_ROUND_0(0)
BLAMKA_ROUND_0(128)
BLAMKA_ROUND_0(256)
BLAMKA_ROUND_0(384)
BLAMKA_ROUND_0(512)
BLAMKA_ROUND_0(640)
BLAMKA_ROUND_0(768)
BLAMKA_ROUND_0(896)
// Round 1: 8 diagonal passes, stride 16 bytes (2 uint64s).
BLAMKA_ROUND_1(0)
BLAMKA_ROUND_1(16)
BLAMKA_ROUND_1(32)
BLAMKA_ROUND_1(48)
BLAMKA_ROUND_1(64)
BLAMKA_ROUND_1(80)
BLAMKA_ROUND_1(96)
BLAMKA_ROUND_1(112)
RET
// func mixBlocksVX(out, a, b, c *block)
TEXT ·mixBlocksVX(SB), NOSPLIT, $0-32
MOVV out+0(FP), R4
MOVV a+8(FP), R5
MOVV b+16(FP), R6
MOVV c+24(FP), R7
MOVV $1024, R8
PCALIGN $16
mixloop:
VMOVQ (R5), V0
VMOVQ (R6), V1
VMOVQ (R7), V2
VXORV V0, V1, V0
VXORV V0, V2, V0
VMOVQ V0, (R4)
ADDV $16, R4
ADDV $16, R5
ADDV $16, R6
ADDV $16, R7
SUBV $16, R8
BNE R8, R0, mixloop
RET
// func xorBlocksVX(out, a, b, c *block)
TEXT ·xorBlocksVX(SB), NOSPLIT, $0-32
MOVV out+0(FP), R4
MOVV a+8(FP), R5
MOVV b+16(FP), R6
MOVV c+24(FP), R7
MOVV $1024, R8
PCALIGN $16
xorloop:
VMOVQ (R5), V0
VMOVQ (R6), V1
VMOVQ (R7), V2
VMOVQ (R4), V3
VXORV V0, V1, V0
VXORV V0, V2, V0
VXORV V0, V3, V0
VMOVQ V0, (R4)
ADDV $16, R4
ADDV $16, R5
ADDV $16, R6
ADDV $16, R7
SUBV $16, R8
BNE R8, R0, xorloop
RET
+1 -1
View File
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !amd64 || purego || !gc
//go:build (!amd64 && !loong64) || purego || !gc
package argon2
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build loong64 && gc && !purego
package blake2b
import "golang.org/x/sys/cpu"
//go:noescape
func hashBlocksVX(h *[8]uint64, c *[2]uint64, flag uint64, blocks []byte)
func hashBlocks(h *[8]uint64, c *[2]uint64, flag uint64, blocks []byte) {
if cpu.Loong64.HasLSX {
hashBlocksVX(h, c, flag, blocks)
} else {
hashBlocksGeneric(h, c, flag, blocks)
}
}
+252
View File
@@ -0,0 +1,252 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build loong64 && gc && !purego
#include "textflag.h"
DATA ·iv0<>+0(SB)/8, $0x6a09e667f3bcc908
DATA ·iv0<>+8(SB)/8, $0xbb67ae8584caa73b
GLOBL ·iv0<>(SB), RODATA|NOPTR, $16
DATA ·iv1<>+0(SB)/8, $0x3c6ef372fe94f82b
DATA ·iv1<>+8(SB)/8, $0xa54ff53a5f1d36f1
GLOBL ·iv1<>(SB), RODATA|NOPTR, $16
DATA ·iv2<>+0(SB)/8, $0x510e527fade682d1
DATA ·iv2<>+8(SB)/8, $0x9b05688c2b3e6c1f
GLOBL ·iv2<>(SB), RODATA|NOPTR, $16
DATA ·iv3<>+0(SB)/8, $0x1f83d9abfb41bd6b
DATA ·iv3<>+8(SB)/8, $0x5be0cd19137e2179
GLOBL ·iv3<>(SB), RODATA|NOPTR, $16
#define SHUFFLE_1 \
VADDV V2, V13, V12; \ // V12 = V2
VADDV V6, V13, V16; \ // V16 = V6
VADDV V4, V13, V14; \ // V14 = V4
VADDV V5, V13, V4; \ // V4 = V5
VADDV V14, V13, V5; \ // V5 = V4
VSHUF4IV $9, V3, V2; \
VSHUF4IV $9, V12, V3; \
VSHUF4IV $3, V7, V6; \
VSHUF4IV $3, V16, V7; \
#define SHUFFLE_2 \
VADDV V2, V13, V12; \ // V12 = V2
VADDV V6, V13, V16; \ // V16 = V6
VADDV V4, V13, V14; \ // V14 = V4
VADDV V5, V13, V4; \ // V4 = V5
VADDV V14, V13, V5; \ // V5 = V4
VSHUF4IV $3, V3, V2; \
VSHUF4IV $3, V12, V3; \
VSHUF4IV $9, V7, V6; \
VSHUF4IV $9, V16, V7; \
#define LOAD_M(a, b, c, d, e, f, g, h) \
VMOVQ a, V8.V[0]; \
VMOVQ b, V8.V[1]; \
VMOVQ c, V9.V[0]; \
VMOVQ d, V9.V[1]; \
VMOVQ e, V10.V[0]; \
VMOVQ f, V10.V[1]; \
VMOVQ g, V11.V[0]; \
VMOVQ h, V11.V[1]; \
#define ROUND_0 \
VADDV V0, V8, V0; \
VADDV V1, V9, V1; \
VADDV V0, V2, V0; \
VADDV V1, V3, V1; \
VXORV V6, V0, V6; \
VXORV V7, V1, V7; \
VROTRV $32, V6, V6; \
VROTRV $32, V7, V7; \
VADDV V4, V6, V4; \
VADDV V5, V7, V5; \
VXORV V2, V4, V2; \
VXORV V3, V5, V3; \
VROTRV $24, V2, V2; \
VROTRV $24, V3, V3; \
VADDV V0, V10, V0; \
VADDV V1, V11, V1; \
VADDV V0, V2, V0; \
VADDV V1, V3, V1; \
VXORV V6, V0, V6; \
VXORV V7, V1, V7; \
VROTRV $16, V6, V6; \
VROTRV $16, V7, V7; \
VADDV V4, V6, V4; \
VADDV V5, V7, V5; \
VXORV V2, V4, V2; \
VXORV V3, V5, V3; \
VROTRV $63, V2, V2; \
VROTRV $63, V3, V3; \
#define ROUND_8 ROUND_0
// func hashBlocksVX(h *[8]uint64, c *[2]uint64, flag uint64, blocks []byte)
TEXT ·hashBlocksVX(SB), NOSPLIT, $0-48
MOVV h+0(FP), R4
MOVV c+8(FP), R5
MOVV flag+16(FP), R6
MOVV blocks_base+24(FP), R7
MOVV blocks_len+32(FP), R8
MOVV (R5), R9 // c0
MOVV 8(R5), R10 // c1
loop:
ADDV $0x80, R9
SGTU $0x80, R9, R11
ADDV R10, R11, R10
VMOVQ R9, V8.V[0]
VMOVQ R10, V8.V[1]
MOVV $·iv0<>(SB), R11
MOVV $·iv1<>(SB), R12
MOVV $·iv2<>(SB), R13
MOVV $·iv3<>(SB), R14
VMOVQ (R13), V6
VMOVQ (R11), V4
VMOVQ (R12), V5
MOVV (R14), R15 // v14
MOVV 8(R14), R16
VXORV V6, V8, V6
XOR R15, R6, R15
VMOVQ R16, V7.V[1]
VMOVQ R15, V7.V[0]
VMOVQ (R4), V0
VMOVQ 16(R4), V1
VMOVQ 32(R4), V2
VMOVQ 48(R4), V3
MOVV 0(R7), R11
MOVV 8(R7), R12
MOVV 16(R7), R13
MOVV 24(R7), R14
MOVV 32(R7), R15
MOVV 40(R7), R16
MOVV 48(R7), R17
MOVV 56(R7), R18
MOVV 64(R7), R19
MOVV 72(R7), R24
MOVV 80(R7), R25
MOVV 88(R7), R26
MOVV 96(R7), R27
MOVV 104(R7), R28
MOVV 112(R7), R29
MOVV 120(R7), R30
VXORV V13, V13, V13 // V13=0
LOAD_M(R11, R13, R15, R17, R12, R14, R16, R18)
ROUND_0
SHUFFLE_1
LOAD_M(R19, R25, R27, R29, R24, R26, R28, R30)
ROUND_8
SHUFFLE_2
LOAD_M(R29, R15, R24, R28, R25, R19, R30, R17)
ROUND_0
SHUFFLE_1
LOAD_M(R12, R11, R26, R16, R27, R13, R18, R14)
ROUND_8
SHUFFLE_2
LOAD_M(R26, R27, R16, R30, R19, R11, R13, R28)
ROUND_0
SHUFFLE_1
LOAD_M(R25, R14, R18, R24, R29, R17, R12, R15)
ROUND_8
SHUFFLE_2
LOAD_M(R18, R14, R28, R26, R24, R12, R27, R29)
ROUND_0
SHUFFLE_1
LOAD_M(R13, R16, R15, R30, R17, R25, R11, R19)
ROUND_8
SHUFFLE_2
LOAD_M(R24, R16, R13, R25, R11, R18, R15, R30)
ROUND_0
SHUFFLE_1
LOAD_M(R29, R26, R17, R14, R12, R27, R19, R28)
ROUND_8
SHUFFLE_2
LOAD_M(R13, R17, R11, R19, R27, R25, R26, R14)
ROUND_0
SHUFFLE_1
LOAD_M(R15, R18, R30, R12, R28, R16, R29, R24)
ROUND_8
SHUFFLE_2
LOAD_M(R27, R12, R29, R15, R16, R30, R28, R25)
ROUND_0
SHUFFLE_1
LOAD_M(R11, R17, R24, R19, R18, R14, R13, R26)
ROUND_8
SHUFFLE_2
LOAD_M(R28, R18, R27, R14, R26, R29, R12, R24)
ROUND_0
SHUFFLE_1
LOAD_M(R16, R30, R19, R13, R11, R15, R17, R25)
ROUND_8
SHUFFLE_2
LOAD_M(R17, R29, R26, R11, R30, R24, R14, R19)
ROUND_0
SHUFFLE_1
LOAD_M(R27, R28, R12, R25, R13, R18, R15, R16)
ROUND_8
SHUFFLE_2
LOAD_M(R25, R19, R18, R12, R13, R15, R17, R16)
ROUND_0
SHUFFLE_1
LOAD_M(R30, R24, R14, R28, R26, R29, R27, R11)
ROUND_8
SHUFFLE_2
LOAD_M(R11, R13, R15, R17, R12, R14, R16, R18)
ROUND_0
SHUFFLE_1
LOAD_M(R19, R25, R27, R29, R24, R26, R28, R30)
ROUND_8
SHUFFLE_2
LOAD_M(R29, R15, R24, R28, R25, R19, R30, R17)
ROUND_0
SHUFFLE_1
LOAD_M(R12, R11, R26, R16, R27, R13, R18, R14)
ROUND_8
SHUFFLE_2
VMOVQ (R4), V8
VMOVQ 16(R4), V9
VMOVQ 32(R4), V10
VMOVQ 48(R4), V11
VXORV V0, V4, V0
VXORV V1, V5, V1
VXORV V2, V6, V2
VXORV V3, V7, V3
VXORV V0, V8, V0
VXORV V1, V9, V1
VXORV V2, V10, V2
VXORV V3, V11, V3
VMOVQ V0, (R4)
VMOVQ V1, 16(R4)
VMOVQ V2, 32(R4)
VMOVQ V3, 48(R4)
SUBV $128, R8
ADDV $128, R7
BNE R8, R0, loop
MOVV R9, (R5)
MOVV R10, 8(R5)
RET
+1 -1
View File
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !amd64 || purego || !gc
//go:build (!amd64 && !loong64) || purego || !gc
package blake2b
+2
View File
@@ -14,6 +14,8 @@ import (
"testing"
)
var _ hash.XOF = (*xof)(nil)
func fromHex(s string) []byte {
b, err := hex.DecodeString(s)
if err != nil {
-11
View File
@@ -1,11 +0,0 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.25
package blake2b
import "hash"
var _ hash.XOF = (*xof)(nil)
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build gc && !purego
package chacha20
import "golang.org/x/sys/cpu"
const bufSize = 256
//go:noescape
func xorKeyStreamVX(dst, src []byte, key *[8]uint32, nonce *[3]uint32, counter *uint32)
func (c *Cipher) xorKeyStreamBlocks(dst, src []byte) {
if cpu.Loong64.HasLSX {
xorKeyStreamVX(dst, src, &c.key, &c.nonce, &c.counter)
} else {
c.xorKeyStreamBlocksGeneric(dst, src)
}
}
+342
View File
@@ -0,0 +1,342 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// derived from chacha_arm64.s
//go:build gc && !purego
#include "textflag.h"
DATA ·constants+0x00(SB)/4, $0x61707865
DATA ·constants+0x04(SB)/4, $0x3320646e
DATA ·constants+0x08(SB)/4, $0x79622d32
DATA ·constants+0x0c(SB)/4, $0x6b206574
GLOBL ·constants(SB), NOPTR|RODATA, $32
DATA ·incRotMatrix+0x00(SB)/4, $0x00000000
DATA ·incRotMatrix+0x04(SB)/4, $0x00000001
DATA ·incRotMatrix+0x08(SB)/4, $0x00000002
DATA ·incRotMatrix+0x0c(SB)/4, $0x00000003
GLOBL ·incRotMatrix(SB), NOPTR|RODATA, $32
#define NUM_ROUNDS 10
// func xorKeyStreamVX(dst, src []byte, key *[8]uint32, nonce *[3]uint32, counter *uint32)
TEXT ·xorKeyStreamVX(SB), NOSPLIT, $0
MOVV dst+0(FP), R4
MOVV src+24(FP), R5
MOVV src_len+32(FP), R6
MOVV key+48(FP), R7
MOVV nonce+56(FP), R8
MOVV counter+64(FP), R9
MOVV $·constants(SB), R10
MOVV $·incRotMatrix(SB), R11
MOVW (R9), R12
loop:
MOVV $NUM_ROUNDS, R15
// load 4-32bit data from incRotMatrix added to counter
VMOVQ (R11), V30
// load contants
VMOVQ (R10), V0.W4
VMOVQ 4(R10), V1.W4
VMOVQ 8(R10), V2.W4
VMOVQ 12(R10), V3.W4
// load keys
VMOVQ (R7), V4.W4
VMOVQ 4(R7), V5.W4
VMOVQ 8(R7), V6.W4
VMOVQ 12(R7), V7.W4
VMOVQ 16(R7), V8.W4
VMOVQ 20(R7), V9.W4
VMOVQ 24(R7), V10.W4
VMOVQ 28(R7), V11.W4
// load counter + nonce
VMOVQ (R9), V12.W4
VMOVQ (R8), V13.W4
VMOVQ 4(R8), V14.W4
VMOVQ 8(R8), V15.W4
// update counter
VADDW V30, V12, V12
chacha:
// V0..V3 += V4..V7
// V12..V15 <<<= ((V12..V15 XOR V0..V3), 16)
VADDW V0, V4, V0
VADDW V1, V5, V1
VADDW V2, V6, V2
VADDW V3, V7, V3
VXORV V12, V0, V12
VXORV V13, V1, V13
VXORV V14, V2, V14
VXORV V15, V3, V15
VROTRW $16, V12, V12
VROTRW $16, V13, V13
VROTRW $16, V14, V14
VROTRW $16, V15, V15
// V8..V11 += V12..V15
// V4..V7 <<<= ((V4..V7 XOR V8..V11), 12)
VADDW V8, V12, V8
VADDW V9, V13, V9
VADDW V10, V14, V10
VADDW V11, V15, V11
VXORV V4, V8, V4
VXORV V5, V9, V5
VXORV V6, V10, V6
VXORV V7, V11, V7
VROTRW $20, V4, V4
VROTRW $20, V5, V5
VROTRW $20, V6, V6
VROTRW $20, V7, V7
// V0..V3 += V4..V7
// V12..V15 <<<= ((V12..V15 XOR V0..V3), 8)
VADDW V0, V4, V0
VADDW V1, V5, V1
VADDW V2, V6, V2
VADDW V3, V7, V3
VXORV V12, V0, V12
VXORV V13, V1, V13
VXORV V14, V2, V14
VXORV V15, V3, V15
VROTRW $24, V12, V12
VROTRW $24, V13, V13
VROTRW $24, V14, V14
VROTRW $24, V15, V15
// V8..V11 += V12..V15
// V4..V7 <<<= ((V4..V7 XOR V8..V11), 7)
VADDW V12, V8, V8
VADDW V13, V9, V9
VADDW V14, V10, V10
VADDW V15, V11, V11
VXORV V4, V8, V4
VXORV V5, V9, V5
VXORV V6, V10, V6
VXORV V7, V11, V7
VROTRW $25, V4, V4
VROTRW $25, V5, V5
VROTRW $25, V6, V6
VROTRW $25, V7, V7
// V0..V3 += V5..V7, V4
// V15,V12-V14 <<<= ((V15,V12-V14 XOR V0..V3), 16)
VADDW V0, V5, V0
VADDW V1, V6, V1
VADDW V2, V7, V2
VADDW V3, V4, V3
VXORV V15, V0, V15
VXORV V12, V1, V12
VXORV V13, V2, V13
VXORV V14, V3, V14
VROTRW $16, V15, V15
VROTRW $16, V12, V12
VROTRW $16, V13, V13
VROTRW $16, V14, V14
// V10,V11,V8,V9 += V15,V12,V13,V14
// V5,V6,V7,V4 <<<= ((V5,V6,V7,V4 XOR V10,V11,V8,V9), 12)
VADDW V10, V15, V10
VADDW V11, V12, V11
VADDW V8, V13, V8
VADDW V9, V14, V9
VXORV V5, V10, V5
VXORV V6, V11, V6
VXORV V7, V8, V7
VXORV V4, V9, V4
VROTRW $20, V5, V5
VROTRW $20, V6, V6
VROTRW $20, V7, V7
VROTRW $20, V4, V4
// V0..V3 += V5..V7, V4
// V15,V12-V14 <<<= ((V15,V12-V14 XOR V0..V3), 8)
VADDW V5, V0, V0
VADDW V6, V1, V1
VADDW V7, V2, V2
VADDW V4, V3, V3
VXORV V15, V0, V15
VXORV V12, V1, V12
VXORV V13, V2, V13
VXORV V14, V3, V14
VROTRW $24, V15, V15
VROTRW $24, V12, V12
VROTRW $24, V13, V13
VROTRW $24, V14, V14
// V10,V11,V8,V9 += V15,V12,V13,V14
// V5,V6,V7,V4 <<<= ((V5,V6,V7,V4 XOR V10,V11,V8,V9), 7)
VADDW V15, V10, V10
VADDW V12, V11, V11
VADDW V13, V8, V8
VADDW V14, V9, V9
VXORV V5, V10, V5
VXORV V6, V11, V6
VXORV V7, V8, V7
VXORV V4, V9, V4
VROTRW $25, V5, V5
VROTRW $25, V6, V6
VROTRW $25, V7, V7
VROTRW $25, V4, V4
SUBV $1, R15
BNE R15, R0, chacha
// load origin contants
VMOVQ (R10), V16.W4
VMOVQ 4(R10), V17.W4
VMOVQ 8(R10), V18.W4
VMOVQ 12(R10), V19.W4
// load origin keys
VMOVQ (R7), V20.W4
VMOVQ 4(R7), V21.W4
VMOVQ 8(R7), V22.W4
VMOVQ 12(R7), V23.W4
VMOVQ 16(R7), V24.W4
VMOVQ 20(R7), V25.W4
VMOVQ 24(R7), V26.W4
VMOVQ 28(R7), V27.W4
// add back the initial state to generate the key stream
VADDW V30, V12, V12 // update counter in advance to prevent V30 from being overwritten
VADDW V16, V0, V0
VADDW V17, V1, V1
VADDW V18, V2, V2
VADDW V19, V3, V3
// load origin counter + nonce
VMOVQ (R9), V28.W4
VMOVQ (R8), V29.W4
VMOVQ 4(R8), V30.W4
VMOVQ 8(R8), V31.W4
VADDW V20, V4, V4
VADDW V21, V5, V5
VADDW V22, V6, V6
VADDW V23, V7, V7
VADDW V24, V8, V8
VADDW V25, V9, V9
VADDW V26, V10, V10
VADDW V27, V11, V11
VADDW V28, V12, V12
VADDW V29, V13, V13
VADDW V30, V14, V14
VADDW V31, V15, V15
// shuffle
VILVLW V0, V1, V16
VILVHW V0, V1, V17
VILVLW V2, V3, V18
VILVHW V2, V3, V19
VILVLW V4, V5 ,V20
VILVHW V4, V5, V21
VILVLW V6, V7, V22
VILVHW V6, V7, V23
VILVLW V8, V9, V24
VILVHW V8, V9, V25
VILVLW V10, V11, V26
VILVHW V10, V11, V27
VILVLW V12, V13, V28
VILVHW V12, V13, V29
VILVLW V14, V15, V30
VILVHW V14, V15, V31
VILVLV V16, V18, V0
VILVHV V16, V18, V4
VILVLV V17, V19, V8
VILVHV V17, V19, V12
// load src data from R5
VMOVQ 0(R5), V16
VMOVQ 16(R5), V17
VMOVQ 32(R5), V18
VMOVQ 48(R5), V19
VILVLV V20, V22, V1
VILVHV V20, V22, V5
VILVLV V21, V23, V9
VILVHV V21, V23, V13
VMOVQ 64(R5), V20
VMOVQ 80(R5), V21
VMOVQ 96(R5), V22
VMOVQ 112(R5), V23
VILVLV V24, V26, V2
VILVHV V24, V26, V6
VILVLV V25, V27, V10
VILVHV V25, V27, V14
VMOVQ 128(R5), V24
VMOVQ 144(R5), V25
VMOVQ 160(R5), V26
VMOVQ 176(R5), V27
VILVLV V28, V30, V3
VILVHV V28, V30, V7
VILVLV V29, V31, V11
VILVHV V29, V31, V15
VMOVQ 192(R5), V28
VMOVQ 208(R5), V29
VMOVQ 224(R5), V30
VMOVQ 240(R5), V31
VXORV V0, V16, V16
VXORV V1, V17, V17
VXORV V2, V18, V18
VXORV V3, V19, V19
VMOVQ V16, 0(R4)
VMOVQ V17, 16(R4)
VMOVQ V18, 32(R4)
VMOVQ V19, 48(R4)
VXORV V4, V20, V20
VXORV V5, V21, V21
VXORV V6, V22, V22
VXORV V7, V23, V23
VMOVQ V20, 64(R4)
VMOVQ V21, 80(R4)
VMOVQ V22, 96(R4)
VMOVQ V23, 112(R4)
VXORV V8, V24, V24
VXORV V9, V25, V25
VXORV V10, V26, V26
VXORV V11, V27, V27
VMOVQ V24, 128(R4)
VMOVQ V25, 144(R4)
VMOVQ V26, 160(R4)
VMOVQ V27, 176(R4)
VXORV V12, V28, V28
VXORV V13, V29, V29
VXORV V14, V30, V30
VXORV V15, V31, V31
VMOVQ V28, 192(R4)
VMOVQ V29, 208(R4)
VMOVQ V30, 224(R4)
VMOVQ V31, 240(R4)
ADD $4, R12, R12
MOVW R12, (R9) // update counter
ADDV $256, R4, R4
ADDV $256, R5, R5
SUBV $256, R6, R6
BNE R6, R0, loop
RET
+1 -1
View File
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build (!arm64 && !s390x && !ppc64 && !ppc64le) || !gc || purego
//go:build (!arm64 && !loong64 && !s390x && !ppc64 && !ppc64le) || !gc || purego
package chacha20
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -20,7 +20,7 @@ func chacha20Poly1305Open(dst []byte, key []uint32, src, ad []byte) bool
func chacha20Poly1305Seal(dst []byte, key []uint32, src, ad []byte)
var (
useAVX2 = cpu.X86.HasAVX2 && cpu.X86.HasBMI2
useAVX2 = cpu.X86.HasSSSE3 && cpu.X86.HasAVX2 && cpu.X86.HasBMI2
)
// setupState writes a ChaCha20 input matrix to state. See
@@ -47,7 +47,7 @@ func setupState(state *[16]uint32, key *[32]byte, nonce []byte) {
}
func (c *chacha20poly1305) seal(dst, nonce, plaintext, additionalData []byte) []byte {
if !cpu.X86.HasSSSE3 {
if !useAVX2 {
return c.sealGeneric(dst, nonce, plaintext, additionalData)
}
@@ -66,7 +66,7 @@ func (c *chacha20poly1305) seal(dst, nonce, plaintext, additionalData []byte) []
}
func (c *chacha20poly1305) open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
if !cpu.X86.HasSSSE3 {
if !useAVX2 {
return c.openGeneric(dst, nonce, ciphertext, additionalData)
}
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -1,11 +1,11 @@
module golang.org/x/crypto
go 1.25.0
go 1.26.0
require (
golang.org/x/net v0.51.0 // tagx:ignore
golang.org/x/sys v0.42.0
golang.org/x/term v0.41.0
golang.org/x/net v0.58.0 // tagx:ignore
golang.org/x/sys v0.48.0
golang.org/x/term v0.46.0
)
require golang.org/x/text v0.35.0 // indirect
require golang.org/x/text v0.42.0 // indirect
+8 -8
View File
@@ -1,8 +1,8 @@
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
+13 -8
View File
@@ -11,6 +11,7 @@
package hkdf
import (
"crypto/hkdf"
"crypto/hmac"
"errors"
"hash"
@@ -24,15 +25,19 @@ import (
// Expand invocations and different context values. Most common scenarios,
// including the generation of multiple keys, should use New instead.
func Extract(hash func() hash.Hash, secret, salt []byte) []byte {
if salt == nil {
salt = make([]byte, hash().Size())
// Use the stdlib Extract, which disables FIPS 140 enforcement of the HMAC
// key (which in HKDF is the salt). The only possible error is FIPS 140
// enforcement of the hash, which had to panic under this API anyway. We
// don't use the stdlib Expand, because it switched to returning a []byte
// instead of an io.Reader, and Expand uses the HMAC key as a key.
out, err := hkdf.Extract(hash, secret, salt)
if err != nil {
panic(err)
}
extractor := hmac.New(hash, salt)
extractor.Write(secret)
return extractor.Sum(nil)
return out
}
type hkdf struct {
type hkdfReader struct {
expander hash.Hash
size int
@@ -43,7 +48,7 @@ type hkdf struct {
buf []byte
}
func (f *hkdf) Read(p []byte) (int, error) {
func (f *hkdfReader) Read(p []byte) (int, error) {
// Check whether enough data can be generated
need := len(p)
remains := len(f.buf) + int(255-f.counter+1)*f.size
@@ -84,7 +89,7 @@ func (f *hkdf) Read(p []byte) (int, error) {
// 3.3. Most common scenarios will want to use New instead.
func Expand(hash func() hash.Hash, pseudorandomKey, info []byte) io.Reader {
expander := hmac.New(hash, pseudorandomKey)
return &hkdf{expander, expander.Size(), info, 1, nil, nil}
return &hkdfReader{expander, expander.Size(), info, 1, nil, nil}
}
// New returns a Reader, from which keys can be read, using the given hash,
+1 -1
View File
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build (!amd64 && !loong64 && !ppc64le && !ppc64 && !s390x) || !gc || purego
//go:build (!amd64 && !loong64 && !ppc64le && !ppc64 && !riscv64 && !s390x) || !gc || purego
package poly1305
+1 -1
View File
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build gc && !purego && (amd64 || loong64 || ppc64 || ppc64le)
//go:build gc && !purego && (amd64 || loong64 || ppc64 || ppc64le || riscv64)
package poly1305
+158
View File
@@ -0,0 +1,158 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build gc && !purego
#define LOAD64U(base, offset, t0, t1, t2, t3, dst) \
MOVBU (offset+0*1)(base), t0; \
MOVBU (offset+1*1)(base), t1; \
MOVBU (offset+2*1)(base), t2; \
MOVBU (offset+3*1)(base), t3; \
SLL $8, t1; \
SLL $16, t2; \
SLL $24, t3; \
OR t1, t0; \
OR t3, t2; \
OR t2, t0, dst; \
MOVBU (offset+4*1)(base), t0; \
MOVBU (offset+5*1)(base), t1; \
MOVBU (offset+6*1)(base), t2; \
MOVBU (offset+7*1)(base), t3; \
SLL $32, t0; \
SLL $40, t1; \
SLL $48, t2; \
SLL $56, t3; \
OR t1, t0; \
OR t3, t2; \
OR t2, t0; \
OR t0, dst
// func update(state *macState, msg []byte)
TEXT ·update(SB), $0-32
MOV state+0(FP), X5
MOV msg_base+8(FP), X6
MOV msg_len+16(FP), X7
MOV $16, X8
AND $7, X6, X28
MOV (0*8)(X5), X9 // h0
MOV (1*8)(X5), X10 // h1
MOV (2*8)(X5), X11 // h2
MOV (3*8)(X5), X12 // r0
MOV (4*8)(X5), X13 // r1
BLT X7, X8, tail
loop:
BEQZ X28, aligned_load
LOAD64U(X6, 0*8, X16, X18, X19, X20, X15) // msg[0:8]
LOAD64U(X6, 1*8, X16, X18, X19, X20, X17) // msg[8:16]
JMP block
aligned_load:
MOV (0*8)(X6), X15 // msg[0:8]
MOV (1*8)(X6), X17 // msg[8:16]
block:
ADD X15, X9 // h0 (x1 + y1 = z1', if z1' < x1 then z1' overflow)
SLTU X15, X9, X19 // h0.carry
ADD X17, X10, X22
SLTU X17, X22, X23
ADD X22, X19, X10 // h1
SLTU X22, X10, X19
OR X23, X19 // h1.carry
ADD $1, X19
ADD X19, X11 // h2
ADD $16, X6 // msg = msg[16:]
multiply:
MULHU X9, X12, X16 // h0r0.hi
MUL X9, X12, X15 // h0r0.lo
MULHU X10, X12, X17 // h1r0.hi
MUL X10, X12, X14 // h1r0.lo
ADD X14, X16
SLTU X14, X16, X19
ADD X19, X17
MUL X11, X12, X20
ADD X17, X20
MULHU X9, X13, X17 // h0r1.hi
MUL X9, X13, X14 // h0r1.lo
ADD X14, X16
SLTU X14, X16, X19
ADD X19, X17
MOV X17, X9
MUL X11, X13, X21 // h2r1
MULHU X10, X13, X17 // h1r1.hi
MUL X10, X13, X14 // h1r1.lo
ADD X14, X20
ADD X17, X21, X22
SLTU X14, X20, X19
ADD X22, X19, X21
ADD X9, X20
SLTU X9, X20, X19
ADD X19, X21
AND $3, X20, X11
AND $-4, X20, X18
ADD X18, X15, X9
ADD X21, X16, X22
SLTU X18, X9, X19
SLTU X21, X22, X23
ADD X22, X19, X10
SLTU X22, X10, X19
OR X19, X23, X19
ADD X19, X11
SLL $62, X21, X22
SRL $2, X20, X23
SRL $2, X21, X21
OR X22, X23, X20
ADD X20, X9, X9
ADD X21, X10, X22
SLTU X20, X9, X19
SLTU X21, X22, X23
ADD X22, X19, X10
SLTU X22, X10, X19
OR X19, X23, X19
ADD X19, X11, X11
SUB $16, X7, X7
BGE X7, X8, loop
tail:
BEQ X7, X0, done
MOV $1, X15
MOV $0, X16
ADD X7, X6, X6
flush_buffer:
MOVBU -1(X6), X20
SRL $56, X15, X19
SLL $8, X16, X23
SLL $8, X15, X15
OR X19, X23, X16
XOR X20, X15
SUB $1, X7, X7
SUB $1, X6, X6
BNE X7, X0, flush_buffer
ADD X15, X9
SLTU X15, X9, X19
ADD X16, X10, X22
SLTU X16, X22, X23
ADD X22, X19, X10
SLTU X22, X10, X19
OR X23, X19
ADD X19, X11
MOV $16, X7
JMP multiply
done:
MOV X9, (0*8)(X5) // h0
MOV X10, (1*8)(X5)
MOV X11, (2*8)(X5)
RET
-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))
}
}
+12 -5
View File
@@ -85,7 +85,8 @@ type certID struct {
// https://tools.ietf.org/html/rfc2560#section-4.1.1
type ocspRequest struct {
TBSRequest tbsRequest
TBSRequest tbsRequest
OptionalSignature asn1.RawValue `asn1:"explicit,tag:0,optional"`
}
type tbsRequest struct {
@@ -321,10 +322,10 @@ type Request struct {
func (req *Request) Marshal() ([]byte, error) {
hashAlg := getOIDFromHashAlgorithm(req.HashAlgorithm)
if hashAlg == nil {
return nil, errors.New("Unknown hash algorithm")
return nil, errors.New("unknown hash algorithm")
}
return asn1.Marshal(ocspRequest{
tbsRequest{
TBSRequest: tbsRequest{
Version: 0,
RequestList: []request{
{
@@ -418,8 +419,10 @@ func (p ParseError) Error() string {
}
// ParseRequest parses an OCSP request in DER form. It only supports
// requests for a single certificate. Signed requests are not supported.
// If a request includes a signature, it will result in a ParseError.
// requests for a single certificate identifier. If a request includes
// multiple certificate identifiers, only the first will be included in
// the parsed Request. Signed requests are not supported. If a request
// includes a signature, it will result in a ParseError.
func ParseRequest(bytes []byte) (*Request, error) {
var req ocspRequest
rest, err := asn1.Unmarshal(bytes, &req)
@@ -430,6 +433,10 @@ func ParseRequest(bytes []byte) (*Request, error) {
return nil, ParseError("trailing data in OCSP request")
}
if len(req.OptionalSignature.FullBytes) > 0 {
return nil, ParseError("signed OCSP requests are not supported")
}
if len(req.TBSRequest.RequestList) == 0 {
return nil, ParseError("OCSP request contains no request body")
}
+127 -1
View File
@@ -15,10 +15,15 @@ import (
"encoding/asn1"
"encoding/hex"
"encoding/pem"
"errors"
"math/big"
"reflect"
"strings"
"testing"
"time"
"golang.org/x/crypto/cryptobyte"
cbasn1 "golang.org/x/crypto/cryptobyte/asn1"
)
func TestOCSPDecode(t *testing.T) {
@@ -190,7 +195,7 @@ func TestOCSPRequest(t *testing.T) {
t.Fatal(err)
}
if bytes.Compare(expectedBytes, marshaledRequest) != 0 {
if !bytes.Equal(expectedBytes, marshaledRequest) {
t.Errorf(
"Marshaled request doesn't match expected: wanted %x, got %x",
expectedBytes,
@@ -199,6 +204,127 @@ func TestOCSPRequest(t *testing.T) {
}
}
func TestOCSPRequestErrors(t *testing.T) {
tests := []struct {
name string
input []byte
wantErr string
}{
{
name: "signed request",
input: testOCSPRequest(1, true),
wantErr: "signed",
},
{
name: "trailing data",
input: append(testOCSPRequest(1, false), 0x00),
wantErr: "trailing",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, err := ParseRequest(tc.input)
if err == nil {
t.Fatalf(
"ParseRequest succeeded, want ParseError containing %q",
tc.wantErr,
)
}
var parseError ParseError
if !errors.As(err, &parseError) {
t.Fatalf("ParseRequest returned %T, want ParseError", err)
}
if !strings.Contains(parseError.Error(), tc.wantErr) {
t.Fatalf(
"ParseRequest error = %q, want it to contain %q",
parseError.Error(), tc.wantErr,
)
}
})
}
}
func TestOCSPMultipleCertRequest(t *testing.T) {
req, err := ParseRequest(testOCSPRequest(2, false))
if err != nil {
t.Fatalf("ParseRequest errored for multiple request input: %q", err)
}
// testOCSPRequest uses the request cert counter as the issuerNameHash,
// issuerKeyHash and serial. We expect the first request cert to be used
// (counter=1), not the second (counter=2).
wantHash := []byte{0x01}
if actual := req.IssuerNameHash; !bytes.Equal(actual, wantHash) {
t.Errorf(
"ParseRequest.IssuerNameHash returned = %q, wanted %q",
actual, wantHash,
)
}
if actual := req.IssuerKeyHash; !bytes.Equal(actual, wantHash) {
t.Errorf("ParseRequest.IssuerKeyHash returned = %q, wanted %q",
actual, wantHash)
}
wantSerial := big.NewInt(0x01)
if actual := req.SerialNumber; actual.Cmp(wantSerial) != 0 {
t.Errorf("ParseRequest.SerialNumber returned = %q, wanted %q",
actual, wantHash)
}
}
// testOCSPRequest constructs an unsupported OCSP request in DER form for testing.
//
// We build these by hand using cryptobyte to specifically target shapes that are
// not supported by ParseRequest, and can't be created with CreateRequest.
//
// The resulting requestList will contain numCerts RequestList entries. If
// includeSignature is true, the request will carry a signature field (with an
// arbitrary signatureAlgorithm and signature).
//
// See RFC 6960, §4.1.1.
func testOCSPRequest(numCerts int, includeSignature bool) []byte {
sha1AlgID, err := asn1.Marshal(pkix.AlgorithmIdentifier{
Algorithm: asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26},
Parameters: asn1.RawValue{Tag: 5 /* ASN.1 NULL */},
})
if err != nil {
panic(err)
}
var b cryptobyte.Builder
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // OCSPRequest
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // tbsRequest
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // requestList
for i := 0; i < numCerts; i++ {
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // Request
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // CertID
b.AddBytes(sha1AlgID) // hashAlgorithm
b.AddASN1OctetString([]byte{byte(i + 1)}) // issuerNameHash
b.AddASN1OctetString([]byte{byte(i + 1)}) // issuerKeyHash
b.AddASN1Int64(int64(i + 1)) // serialNumber
})
})
}
})
})
if includeSignature {
// optionalSignature [0] EXPLICIT Signature
b.AddASN1(cbasn1.Tag(0).ContextSpecific().Constructed(), func(b *cryptobyte.Builder) {
b.AddASN1(cbasn1.SEQUENCE, func(b *cryptobyte.Builder) { // Signature
b.AddBytes(sha1AlgID) // signatureAlgorithm
b.AddASN1BitString([]byte{0x09, 0x0A, 0x0B, 0x0C}) // signature
})
})
}
})
der, err := b.Bytes()
if err != nil {
panic(err)
}
return der
}
func TestOCSPResponse(t *testing.T) {
leafCert, _ := hex.DecodeString(leafCertHex)
leaf, err := x509.ParseCertificate(leafCert)
+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 (
+13 -60
View File
@@ -2,24 +2,17 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package pbkdf2 implements the key derivation function PBKDF2 as defined in RFC
2898 / PKCS #5 v2.0.
A key derivation function is useful when encrypting data based on a password
or any other not-fully-random data. It uses a pseudorandom function to derive
a secure encryption key based on the password.
While v2.0 of the standard defines only one pseudorandom function to use,
HMAC-SHA1, the drafted v2.1 specification allows use of all five FIPS Approved
Hash Functions SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512 for HMAC. To
choose, you can pass the `New` functions from the different SHA packages to
pbkdf2.Key.
*/
// Package pbkdf2 implements the key derivation function PBKDF2 as defined in
// RFC 8018 (PKCS #5 v2.1).
//
// This package is a wrapper for the PBKDF2 implementation in the
// [crypto/pbkdf2] package. It is [frozen] and is not accepting new features.
//
// [frozen]: https://go.dev/wiki/Frozen
package pbkdf2
import (
"crypto/hmac"
"crypto/pbkdf2"
"hash"
)
@@ -27,51 +20,11 @@ import (
// []byte of length keylen that can be used as cryptographic key. The key is
// derived based on the method described as PBKDF2 with the HMAC variant using
// the supplied hash function.
//
// For example, to use a HMAC-SHA-1 based PBKDF2 key derivation function, you
// can get a derived key for e.g. AES-256 (which needs a 32-byte key) by
// doing:
//
// dk := pbkdf2.Key([]byte("some password"), salt, 4096, 32, sha1.New)
//
// Remember to get a good random salt. At least 8 bytes is recommended by the
// RFC.
//
// Using a higher iteration count will increase the cost of an exhaustive
// search but will also make derivation proportionally slower.
func Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte {
prf := hmac.New(h, password)
hashLen := prf.Size()
numBlocks := (keyLen + hashLen - 1) / hashLen
var buf [4]byte
dk := make([]byte, 0, numBlocks*hashLen)
U := make([]byte, hashLen)
for block := 1; block <= numBlocks; block++ {
// N.B.: || means concatenation, ^ means XOR
// for each block T_i = U_1 ^ U_2 ^ ... ^ U_iter
// U_1 = PRF(password, salt || uint(i))
prf.Reset()
prf.Write(salt)
buf[0] = byte(block >> 24)
buf[1] = byte(block >> 16)
buf[2] = byte(block >> 8)
buf[3] = byte(block)
prf.Write(buf[:4])
dk = prf.Sum(dk)
T := dk[len(dk)-hashLen:]
copy(U, T)
// U_n = PRF(password, U_(n-1))
for n := 2; n <= iter; n++ {
prf.Reset()
prf.Write(U)
U = U[:0]
U = prf.Sum(U)
for x := range U {
T[x] ^= U[x]
}
}
out, err := pbkdf2.Key(h, string(password), salt, iter, keyLen)
if err != nil {
// FIPS 140 enforcement, or an invalid key length.
panic(err)
}
return dk[:keyLen]
return out
}
+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{
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build loong64 && !purego && gc
package salsa
import "golang.org/x/sys/cpu"
// xorKeyStreamVX is implemented in salsa20_loong64.s.
//
//go:noescape
func xorKeyStreamVX(out, in *byte, n uint64, nonce, key *byte)
// XORKeyStream crypts bytes from in to out using the given key and counters.
// In and out must overlap entirely or not at all. Counter
// contains the raw salsa20 counter bytes (both nonce and block counter).
func XORKeyStream(out, in []byte, counter *[16]byte, key *[32]byte) {
if len(in) == 0 {
return
}
_ = out[len(in)-1]
if cpu.Loong64.HasLSX {
xorKeyStreamVX(&out[0], &in[0], uint64(len(in)), &counter[0], &key[0])
} else {
genericXORKeyStream(out, in, counter, key)
}
}
+454
View File
@@ -0,0 +1,454 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !purego && gc
#include "textflag.h"
DATA ·constants+0x00(SB)/4, $0x61707865
DATA ·constants+0x04(SB)/4, $0x3320646e
DATA ·constants+0x08(SB)/4, $0x79622d32
DATA ·constants+0x0c(SB)/4, $0x6b206574
GLOBL ·constants(SB), NOPTR|RODATA, $32
#define NUM_ROUNDS 10
// func xorKeyStreamVX(out, in *byte, n uint64, counter, key *byte)
TEXT ·xorKeyStreamVX(SB), $0-40
MOVV out+0(FP), R4
MOVV in+8(FP), R5
MOVV n+16(FP), R6
MOVV counter+24(FP), R7
MOVV key+32(FP), R8
MOVV $·constants(SB), R10
BGE R0, R6, ret
MOVV 8(R7), R9 // counter[8:16]
loop256:
MOVV $NUM_ROUNDS, R15
VXORV V30, V30, V30 // V30 = 0
// load contants
VMOVQ (R10), V0.W4
VMOVQ 4(R10), V5.W4
VMOVQ 8(R10), V10.W4
VMOVQ 12(R10), V15.W4
// load keys
VMOVQ (R8), V1.W4
VMOVQ 4(R8), V2.W4
VMOVQ 8(R8), V3.W4
VMOVQ 12(R8), V4.W4
VMOVQ 16(R8), V11.W4
VMOVQ 20(R8), V12.W4
VMOVQ 24(R8), V13.W4
VMOVQ 28(R8), V14.W4
// load and update counter
VMOVQ (R7), V6.W4
VMOVQ 4(R7), V7.W4
ADDV $1, R9, R11
ADDV $2, R9, R12
ADDV $3, R9, R13
VMOVQ R9, V8.W[0]
VMOVQ R11, V8.W[1]
VMOVQ R12, V8.W[2]
VMOVQ R13, V8.W[3]
SRLV $32, R9, R14
SRLV $32, R11, R11
SRLV $32, R12, R12
SRLV $32, R13, R13
VMOVQ R14, V9.W[0]
VMOVQ R11, V9.W[1]
VMOVQ R12, V9.W[2]
VMOVQ R13, V9.W[3]
// backup V8 and V9
VADDV V8, V30, V24 // V21 = V8
VADDV V9, V30, V25 // V22 = V9
salsa20_256:
VADDW V0, V12, V26
VADDW V5, V1, V27
VADDW V10, V6, V28
VADDW V15, V11, V29
VROTRW $25, V26, V26
VROTRW $25, V27, V27
VROTRW $25, V28, V28
VROTRW $25, V29, V29
VXORV V4, V26, V4
VXORV V9, V27, V9
VXORV V14, V28, V14
VXORV V3, V29, V3
VADDW V4, V0, V26
VADDW V9, V5, V27
VADDW V14, V10, V28
VADDW V3, V15, V29
VROTRW $23, V26, V26
VROTRW $23, V27, V27
VROTRW $23, V28, V28
VROTRW $23, V29, V29
VXORV V8, V26, V8
VXORV V13, V27, V13
VXORV V2, V28, V2
VXORV V7, V29, V7
VADDW V8, V4, V26
VADDW V13, V9, V27
VADDW V2, V14, V28
VADDW V7, V3, V29
VROTRW $19, V26, V26
VROTRW $19, V27, V27
VROTRW $19, V28, V28
VROTRW $19, V29, V29
VXORV V12, V26, V12
VXORV V1, V27, V1
VXORV V6, V28, V6
VXORV V11, V29, V11
VADDW V12, V8, V26
VADDW V1, V13, V27
VADDW V6, V2, V28
VADDW V11, V7, V29
VROTRW $14, V26, V26
VROTRW $14, V27, V27
VROTRW $14, V28, V28
VROTRW $14, V29, V29
VXORV V0, V26, V0
VXORV V5, V27, V5
VXORV V10, V28, V10
VXORV V15, V29, V15
VADDW V0, V3, V26
VADDW V5, V4, V27
VADDW V10, V9, V28
VADDW V15, V14, V29
VROTRW $25, V26, V26
VROTRW $25, V27, V27
VROTRW $25, V28, V28
VROTRW $25, V29, V29
VXORV V1, V26, V1
VXORV V6, V27, V6
VXORV V11, V28, V11
VXORV V12, V29, V12
VADDW V1, V0, V26
VADDW V6, V5, V27
VADDW V11, V10, V28
VADDW V12, V15, V29
VROTRW $23, V26, V26
VROTRW $23, V27, V27
VROTRW $23, V28, V28
VROTRW $23, V29, V29
VXORV V2, V26, V2
VXORV V7, V27, V7
VXORV V8, V28, V8
VXORV V13, V29, V13
VADDW V2, V1, V26
VADDW V7, V6, V27
VADDW V8, V11, V28
VADDW V13, V12, V29
VROTRW $19, V26, V26
VROTRW $19, V27, V27
VROTRW $19, V28, V28
VROTRW $19, V29, V29
VXORV V3, V26, V3
VXORV V4, V27, V4
VXORV V9, V28, V9
VXORV V14, V29, V14
VADDW V3, V2, V26
VADDW V4, V7, V27
VADDW V9, V8, V28
VADDW V14, V13, V29
VROTRW $14, V26, V26
VROTRW $14, V27, V27
VROTRW $14, V28, V28
VROTRW $14, V29, V29
VXORV V0, V26, V0
VXORV V5, V27, V5
VXORV V10, V28, V10
VXORV V15, V29, V15
SUBV $1, R15
BNE R15, R0, salsa20_256
// load origin contants
VMOVQ (R10), V16.W4
VMOVQ 4(R10), V21.W4
VMOVQ 8(R10), V26.W4
VMOVQ 12(R10), V31.W4
// load origin keys
VMOVQ (R8), V17.W4
VMOVQ 4(R8), V18.W4
VMOVQ 8(R8), V19.W4
VMOVQ 12(R8), V20.W4
VMOVQ 16(R8), V27.W4
VMOVQ 20(R8), V28.W4
VMOVQ 24(R8), V29.W4
VMOVQ 28(R8), V30.W4
// load origin counter
VMOVQ (R7), V22.W4
VMOVQ 4(R7), V23.W4
// add back the initial state to generate the key stream
VADDW V0, V16, V0
VADDW V1, V17, V1
VADDW V2, V18, V2
VADDW V3, V19, V3
VADDW V4, V20, V4
VADDW V5, V21, V5
VADDW V6, V22, V6
VADDW V7, V23, V7
VADDW V8, V24, V8
VADDW V9, V25, V9
VADDW V10, V26, V10
VADDW V11, V27, V11
VADDW V12, V28, V12
VADDW V13, V29, V13
VADDW V14, V30, V14
VADDW V15, V31, V15
// shuffle
VILVLW V0, V1, V16
VILVHW V0, V1, V17
VILVLW V2, V3, V18
VILVHW V2, V3, V19
VILVLW V4, V5 ,V20
VILVHW V4, V5, V21
VILVLW V6, V7, V22
VILVHW V6, V7, V23
VILVLW V8, V9, V24
VILVHW V8, V9, V25
VILVLW V10, V11, V26
VILVHW V10, V11, V27
VILVLW V12, V13, V28
VILVHW V12, V13, V29
VILVLW V14, V15, V30
VILVHW V14, V15, V31
VILVLV V16, V18, V0
VILVHV V16, V18, V4
VILVLV V17, V19, V8
VILVHV V17, V19, V12
VILVLV V20, V22, V1
VILVHV V20, V22, V5
VILVLV V21, V23, V9
VILVHV V21, V23, V13
VILVLV V24, V26, V2
VILVHV V24, V26, V6
VILVLV V25, V27, V10
VILVHV V25, V27, V14
VILVLV V28, V30, V3
VILVHV V28, V30, V7
VILVLV V29, V31, V11
VILVHV V29, V31, V15
SGTU $256, R6, R11
BNE R11, R0, less_than_256
// load src data from R5
VMOVQ 0(R5), V16
VMOVQ 16(R5), V17
VMOVQ 32(R5), V18
VMOVQ 48(R5), V19
VMOVQ 64(R5), V20
VMOVQ 80(R5), V21
VMOVQ 96(R5), V22
VMOVQ 112(R5), V23
VMOVQ 128(R5), V24
VMOVQ 144(R5), V25
VMOVQ 160(R5), V26
VMOVQ 176(R5), V27
VMOVQ 192(R5), V28
VMOVQ 208(R5), V29
VMOVQ 224(R5), V30
VMOVQ 240(R5), V31
VXORV V0, V16, V16
VXORV V1, V17, V17
VXORV V2, V18, V18
VXORV V3, V19, V19
VXORV V4, V20, V20
VXORV V5, V21, V21
VXORV V6, V22, V22
VXORV V7, V23, V23
VXORV V8, V24, V24
VXORV V9, V25, V25
VXORV V10, V26, V26
VXORV V11, V27, V27
VXORV V12, V28, V28
VXORV V13, V29, V29
VXORV V14, V30, V30
VXORV V15, V31, V31
VMOVQ V16, 0(R4)
VMOVQ V17, 16(R4)
VMOVQ V18, 32(R4)
VMOVQ V19, 48(R4)
VMOVQ V20, 64(R4)
VMOVQ V21, 80(R4)
VMOVQ V22, 96(R4)
VMOVQ V23, 112(R4)
VMOVQ V24, 128(R4)
VMOVQ V25, 144(R4)
VMOVQ V26, 160(R4)
VMOVQ V27, 176(R4)
VMOVQ V28, 192(R4)
VMOVQ V29, 208(R4)
VMOVQ V30, 224(R4)
VMOVQ V31, 240(R4)
ADDV $4, R9, R9 // update counter
SUBV $256, R6, R6
ADDV $256, R4, R4
ADDV $256, R5, R5
SGTU $256, R6, R11
BEQ R11, R0, loop256
BEQ R6, R0, ret
less_than_256:
VXORV V30, V30, V30 // V30=0
SGTU $128, R6, R11
BNE R11, R0, less_than_128
SUBV $128, R6
VMOVQ (R5), V16
VMOVQ 16(R5), V17
VMOVQ 32(R5), V18
VMOVQ 48(R5), V19
VMOVQ 64(R5), V20
VMOVQ 80(R5), V21
VMOVQ 96(R5), V22
VMOVQ 112(R5), V23
VXORV V0, V16, V16
VXORV V1, V17, V17
VXORV V2, V18, V18
VXORV V3, V19, V19
VXORV V4, V20, V20
VXORV V5, V21, V21
VXORV V6, V22, V22
VXORV V7, V23, V23
VMOVQ V16, (R4)
VMOVQ V17, 16(R4)
VMOVQ V18, 32(R4)
VMOVQ V19, 48(R4)
VMOVQ V20, 64(R4)
VMOVQ V21, 80(R4)
VMOVQ V22, 96(R4)
VMOVQ V23, 112(R4)
BEQ R6, R0, ret
ADDV $128, R5, R5
ADDV $128, R4, R4
VADDV V8, V30, V0
VADDV V9, V30, V1
VADDV V10, V30, V2
VADDV V11, V30, V3
VADDV V12, V30, V4
VADDV V13, V30, V5
VADDV V14, V30, V6
VADDV V15, V30, V7
less_than_128:
SGTU $64, R6, R11
BNE R11, R0, less_than_64
SUBV $64, R6
VMOVQ (R5), V16
VMOVQ 16(R5), V17
VMOVQ 32(R5), V18
VMOVQ 48(R5), V19
VXORV V0, V16, V16
VXORV V1, V17, V17
VXORV V2, V18, V18
VXORV V3, V19, V19
VMOVQ V16, (R4)
VMOVQ V17, 16(R4)
VMOVQ V18, 32(R4)
VMOVQ V19, 48(R4)
BEQ R6, R0, ret
ADDV $64, R5
ADDV $64, R4
VADDV V4, V30, V0
VADDV V5, V30, V1
VADDV V6, V30, V2
VADDV V7, V30, V3
less_than_64:
SGTU $32, R6, R11
BNE R11, R0, less_than_32
SUBV $32, R6
VMOVQ (R5), V16
VMOVQ 16(R5), V17
VXORV V0, V16, V16
VXORV V1, V17, V17
VMOVQ V16, (R4)
VMOVQ V17, 16(R4)
BEQ R6, R0, ret
ADDV $32, R5
ADDV $32, R4
VADDV V2, V30, V0
VADDV V3, V30, V1
less_than_32:
SGTU $16, R6, R11
BNE R11, R0, less_than_16
SUBV $16, R6
VMOVQ (R5), V16
VXORV V16, V0, V16
VMOVQ V16, (R4)
BEQ R6, R0, ret
ADDV $16, R5
ADDV $16, R4
VADDV V1, V30, V0
less_than_16:
SGTU $8, R6, R11
BNE R11, R0, less_than_8
SUBV $8, R6
VMOVQ V0.V[0], R11
VMOVQ V0.V[1], R13
MOVV (R5), R12
XOR R11, R12, R12
MOVV R12, (R4)
BEQ R6, R0, ret
ADDV $8, R5
ADDV $8, R4
VMOVQ R13, V0.V[0]
less_than_8:
SGTU $4, R6, R11
BNE R11, R0, less_than_4
SUBV $4, R6
VMOVQ V0.W[0], R11
VMOVQ V0.W[1], R13
MOVWU (R5), R12
XOR R11, R12, R12
MOVWU R12, (R4)
BEQ R6, R0, ret
ADDV $4, R5
ADDV $4, R4
VMOVQ R13, V0.W[0]
less_than_4:
SGTU $2, R6, R11
BNE R11, R0, less_than_2
SUBV $2, R6
VMOVQ V0.H[0], R11
VMOVQ V0.H[1], R13
MOVHU (R5), R12
XOR R11, R12, R12
MOVHU R12, (R4)
BEQ R6, R0, ret
ADDV $2, R5
ADDV $2, R4
VMOVQ R13, V0.H[0]
less_than_2:
VMOVQ V0.B[0], R11
MOVBU (R5), R12
XOR R11, R12, R12
MOVBU R12, (R4)
ret:
RET
+1 -1
View File
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !amd64 || purego || !gc
//go:build (!amd64 && !loong64) || purego || !gc
package salsa
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build amd64 && !purego && gc
//go:build (amd64 || loong64) && !purego && gc
package salsa
+233 -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:],
@@ -663,6 +713,13 @@ func (c *client) Add(key AddedKey) error {
constraints = append(constraints, agentConstrainConfirm)
}
for _, ext := range key.ConstraintExtensions {
constraints = append(constraints, ssh.Marshal(constrainExtensionAgentMsg{
ExtensionName: ext.ExtensionName,
ExtensionDetails: ext.ExtensionDetails,
})...)
}
cert := key.Certificate
if cert == nil {
return c.insertKey(key.PrivateKey, key.Comment, constraints)
@@ -705,6 +762,9 @@ func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string
Constraints: constraints,
})
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(),
@@ -717,6 +777,9 @@ func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string
// general idiom is to pass ed25519.PrivateKey by value, not by pointer.
// 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(),
@@ -854,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
})
}
}
+94
View File
@@ -6,6 +6,7 @@ package agent
import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"encoding/binary"
"errors"
@@ -594,3 +595,96 @@ func TestAgentExtensions(t *testing.T) {
t.Fatal("should have gotten agent extension failure")
}
}
// capturingAgent records the AddedKey observed on the server side of the
// agent protocol. It forwards to a keyring with constraint fields stripped,
// so the keyring's own rejection of unsupported constraints does not
// interfere with what this test is measuring: the client-side wire
// serialization of ConstraintExtensions.
type capturingAgent struct {
Agent
lastAdd AddedKey
}
func newCapturingAgent() *capturingAgent {
return &capturingAgent{Agent: NewKeyring()}
}
func (a *capturingAgent) Add(key AddedKey) error {
a.lastAdd = key
stripped := key
stripped.ConstraintExtensions = nil
stripped.ConfirmBeforeUse = false
return a.Agent.Add(stripped)
}
// TestAddConstraintExtensionsWireFormat verifies that client.Add serializes
// ConstraintExtensions into the SSH_AGENTC_ADD_IDENTITY payload and the
// server deserializes them back into the AddedKey delivered to the backend.
// Regressions in the client marshal loop (missing, swapped fields, wrong
// framing) would be invisible to a keyring-based rejection test, which
// signals only "extensions were present", not "the right ones arrived".
func TestAddConstraintExtensionsWireFormat(t *testing.T) {
capturing := newCapturingAgent()
client, cleanup := startAgent(t, capturing)
defer cleanup()
constraints := []ConstraintExtension{
{ExtensionName: "ext-one@example.com", ExtensionDetails: []byte("details-one")},
{ExtensionName: "ext-two@example.com", ExtensionDetails: []byte("\x00\x01\x02\xff")},
}
if err := client.Add(AddedKey{
PrivateKey: testPrivateKeys["rsa"],
Comment: "wire-format-test",
ConstraintExtensions: constraints,
}); err != nil {
t.Fatalf("client.Add: %v", err)
}
got := capturing.lastAdd.ConstraintExtensions
if len(got) != len(constraints) {
t.Fatalf("server received %d extensions, want %d", len(got), len(constraints))
}
for i, want := range constraints {
if got[i].ExtensionName != want.ExtensionName {
t.Errorf("extension[%d] name: got %q, want %q", i, got[i].ExtensionName, want.ExtensionName)
}
if !bytes.Equal(got[i].ExtensionDetails, want.ExtensionDetails) {
t.Errorf("extension[%d] details: got %x, want %x", i, got[i].ExtensionDetails, want.ExtensionDetails)
}
}
}
func TestAddRejectsShortEd25519Key(t *testing.T) {
short := ed25519.PrivateKey(make([]byte, 16))
// insertCert reaches its ed25519 branch only when AddedKey.Certificate
// is non-nil. The length check returns before cert.Type/Marshal run, so
// an empty certificate is sufficient to drive the path.
emptyCert := &ssh.Certificate{}
cases := []struct {
name string
add AddedKey
}{
{"insertKey value", AddedKey{PrivateKey: short}},
{"insertKey pointer", AddedKey{PrivateKey: &short}},
{"insertCert value", AddedKey{PrivateKey: short, Certificate: emptyCert}},
{"insertCert pointer", AddedKey{PrivateKey: &short, Certificate: emptyCert}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
client, cleanup := startKeyringAgent(t)
defer cleanup()
err := client.Add(tc.add)
if err == nil {
t.Fatal("Add accepted ed25519 key shorter than 64 bytes")
}
if !strings.Contains(err.Error(), "bad ED25519 key size") {
t.Errorf("got error %q, want substring %q", err.Error(), "bad ED25519 key size")
}
})
}
}
+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)
}
}()
+19 -6
View File
@@ -32,8 +32,10 @@ type keyring struct {
var errLocked = errors.New("agent: locked")
// NewKeyring returns an Agent that holds keys in memory. It is safe
// for concurrent use by multiple goroutines.
// NewKeyring returns an Agent that holds keys in memory. It is safe for
// concurrent use by multiple goroutines.
//
// The returned Agent only supports the "lifetime" constraint.
func NewKeyring() Agent {
return &keyring{}
}
@@ -102,7 +104,7 @@ func (r *keyring) Unlock(passphrase []byte) error {
if !r.locked {
return errors.New("agent: not locked")
}
if 1 != subtle.ConstantTimeCompare(passphrase, r.passphrase) {
if subtle.ConstantTimeCompare(passphrase, r.passphrase) != 1 {
return fmt.Errorf("agent: incorrect passphrase")
}
@@ -143,15 +145,26 @@ func (r *keyring) List() ([]*Key, error) {
return ids, nil
}
// Insert adds a private key to the keyring. If a certificate
// is given, that certificate is added as public key. Note that
// any constraints given are ignored.
// Add adds a private key to the keyring. If a certificate is given, that
// certificate is added as public key.
//
// Add returns an error if key contains ConstraintExtensions or
// ConfirmBeforeUse.
func (r *keyring) Add(key AddedKey) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return errLocked
}
if key.ConfirmBeforeUse {
return errors.New("agent: confirm before use constraint is not supported")
}
if len(key.ConstraintExtensions) > 0 {
return errors.New("agent: constraint extensions are present but not supported")
}
signer, err := ssh.NewSignerFromKey(key.PrivateKey)
if err != nil {
+39
View File
@@ -120,3 +120,42 @@ func TestAddDuplicateKey(t *testing.T) {
t.Fatal("key with the updated comment not found")
}
}
func TestAddKeyWithConstraints(t *testing.T) {
// Verifies the keyring refuses keys carrying constraint extensions it
// cannot enforce.
agent, cleanup := startKeyringAgent(t)
defer cleanup()
constraints := []ConstraintExtension{
{
ExtensionName: "extension1",
ExtensionDetails: []byte("details1"),
},
}
key := testPrivateKeys["rsa"]
err := agent.Add(AddedKey{
PrivateKey: key,
ConstraintExtensions: constraints,
})
if err == nil {
t.Fatal("adding a key with unsupported constraints succeeded")
}
}
func TestAddKeyWithConfirmBeforeUse(t *testing.T) {
agent, cleanup := startKeyringAgent(t)
defer cleanup()
key := testPrivateKeys["rsa"]
err := agent.Add(AddedKey{
PrivateKey: key,
ConfirmBeforeUse: true,
})
if err == nil {
t.Fatal("adding a key with confirm before use constraint succeeded")
}
}
+79 -10
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() > 16384 {
return errors.New("agent: RSA modulus too large")
}
if P.BitLen() > 8192 || Q.BitLen() > 8192 {
return errors.New("agent: RSA prime too large")
}
if E.BitLen() > 24 {
return errors.New("agent: RSA public exponent too large")
}
e := E.Int64()
if e < 3 || e&1 == 0 {
return errors.New("agent: incorrect RSA public exponent")
}
return nil
}
func parseRSAKey(req []byte) (*AddedKey, error) {
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{
@@ -270,6 +292,9 @@ func parseEd25519Key(req []byte) (*AddedKey, error) {
if err := ssh.Unmarshal(req, &k); err != nil {
return nil, err
}
if len(k.Priv) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("agent: bad ED25519 key size: %d", len(k.Priv))
}
priv := ed25519.PrivateKey(k.Priv)
addedKey := &AddedKey{PrivateKey: &priv, Comment: k.Comments}
@@ -279,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,
}
@@ -336,6 +402,9 @@ func parseEd25519Cert(req []byte) (*AddedKey, error) {
if err != nil {
return nil, err
}
if len(k.Priv) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("agent: bad ED25519 key size: %d", len(k.Priv))
}
priv := ed25519.PrivateKey(k.Priv)
cert, ok := pubKey.(*ssh.Certificate)
if !ok {
@@ -393,8 +462,8 @@ func parseRSACert(req []byte) (*AddedKey, error) {
return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err)
}
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{
+193
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 {
@@ -271,3 +432,35 @@ func TestParseConstraints(t *testing.T) {
t.Errorf("unexpected error: %v", err)
}
}
func TestParseEd25519KeyShortPanic(t *testing.T) {
msg := ssh.Marshal(ed25519KeyMsg{
Type: ssh.KeyAlgoED25519,
Pub: []byte{1, 2, 3},
Priv: []byte{1, 2, 3, 4, 5},
})
a, b, err := netPipe()
if err != nil {
t.Fatal(err)
}
defer a.Close()
defer b.Close()
done := make(chan error, 1)
go func() { done <- ServeAgent(NewKeyring(), a) }()
c := NewClient(b)
_, err = c.(*client).call(msg)
if err != nil {
t.Fatal(err)
}
keys, err := c.List()
if err != nil {
t.Fatalf("agent died: %v", err)
}
if len(keys) != 0 {
t.Error("short ed25519 key was accepted into keyring")
}
}
+1 -1
View File
@@ -38,7 +38,7 @@ func TestBufferReadwrite(t *testing.T) {
b.write(alphabet[5:15])
r, _ = b.Read(make([]byte, 10))
r2, _ := b.Read(make([]byte, 10))
if r != 10 || r2 != 5 || 15 != r+r2 {
if r != 10 || r2 != 5 || r+r2 != 15 {
t.Fatal("Expected written == read == 15")
}
}
+44 -27
View File
@@ -10,6 +10,7 @@ import (
"fmt"
"io"
"net"
"slices"
"sort"
"time"
)
@@ -229,15 +230,20 @@ func parseCert(in []byte, privAlgo string) (*Certificate, error) {
return nil, err
}
c.Reserved = g.Reserved
// Reject a certificate whose signature key is itself a certificate before
// parsing it. Certificates signed by certificates are not supported (see
// PROTOCOL.certkeys), and rejecting after ParsePublicKey returns would allow
// a chain of nested certificates to recurse once per level, exhausting the
// goroutine stack.
if sigAlgo, _, ok := parseString(g.SignatureKey); !ok {
return nil, errShortRead
} else if _, ok := certKeyAlgoNames[string(sigAlgo)]; ok {
return nil, fmt.Errorf("ssh: the signature key type %q is invalid for certificates", sigAlgo)
}
k, err := ParsePublicKey(g.SignatureKey)
if err != nil {
return nil, err
}
// The Type() function is intended to return only certificate key types, but
// we use certKeyAlgoNames anyway for safety, to match [Certificate.Type].
if _, ok := certKeyAlgoNames[k.Type()]; ok {
return nil, fmt.Errorf("ssh: the signature key type %q is invalid for certificates", k.Type())
}
c.SignatureKey = k
c.Signature, rest, ok = parseSignatureBody(g.Signature)
if !ok || len(rest) > 0 {
@@ -300,8 +306,11 @@ const sourceAddressCriticalOption = "source-address"
// minimally, the IsAuthority callback should be set.
type CertChecker struct {
// SupportedCriticalOptions lists the CriticalOptions that the
// server application layer understands. These are only used
// for user certificates.
// application layer understands. A certificate carrying a critical
// option that is not listed here is rejected.
// CertChecker.Authenticate additionally accepts the source-address
// option, which the server enforces on the Permissions that
// Authenticate returns.
SupportedCriticalOptions []string
// IsUserAuthority should return true if the key is recognized as an
@@ -348,6 +357,9 @@ func (c *CertChecker) CheckHostKey(addr string, remote net.Addr, key PublicKey)
if cert.CertType != HostCert {
return fmt.Errorf("ssh: certificate presented as a host key has type %d", cert.CertType)
}
if c.IsHostAuthority == nil {
return errors.New("ssh: cannot verify certificate, IsHostAuthority not set")
}
if !c.IsHostAuthority(cert.SignatureKey, addr) {
return fmt.Errorf("ssh: no authorities for hostname: %v", addr)
}
@@ -361,8 +373,9 @@ func (c *CertChecker) CheckHostKey(addr string, remote net.Addr, key PublicKey)
return c.CheckCert(hostname, cert)
}
// Authenticate checks a user certificate. Authenticate can be used as
// a value for ServerConfig.PublicKeyCallback.
// Authenticate checks a user certificate. Authenticate can be used as a value
// for ServerConfig.PublicKeyCallback. The source-address critical option is
// allowed, as it will be enforced by the server.
func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permissions, error) {
cert, ok := pubKey.(*Certificate)
if !ok {
@@ -375,11 +388,17 @@ func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permis
if cert.CertType != UserCert {
return nil, fmt.Errorf("ssh: cert has type %d", cert.CertType)
}
if c.IsUserAuthority == nil {
return nil, errors.New("ssh: cannot verify certificate, IsUserAuthority not set")
}
if !c.IsUserAuthority(cert.SignatureKey) {
return nil, fmt.Errorf("ssh: certificate signed by unrecognized authority")
}
if err := c.CheckCert(conn.User(), cert); err != nil {
// The source-address critical option is enforced by serverAuthenticate,
// so it is supported regardless of SupportedCriticalOptions
cc := *c
cc.SupportedCriticalOptions = append(slices.Clip(cc.SupportedCriticalOptions), sourceAddressCriticalOption)
if err := cc.CheckCert(conn.User(), cert); err != nil {
return nil, err
}
@@ -387,27 +406,15 @@ func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permis
}
// CheckCert checks CriticalOptions, ValidPrincipals, revocation, timestamp and
// the signature of the certificate.
// the signature of the certificate. Critical options that are not listed in
// SupportedCriticalOptions are rejected.
func (c *CertChecker) CheckCert(principal string, cert *Certificate) error {
if c.IsRevoked != nil && c.IsRevoked(cert) {
return fmt.Errorf("ssh: certificate serial %d revoked", cert.Serial)
}
for opt := range cert.CriticalOptions {
// sourceAddressCriticalOption will be enforced by
// serverAuthenticate
if opt == sourceAddressCriticalOption {
continue
}
found := false
for _, supp := range c.SupportedCriticalOptions {
if supp == opt {
found = true
break
}
}
if !found {
if !slices.Contains(c.SupportedCriticalOptions, opt) {
return fmt.Errorf("ssh: unsupported critical option %q in certificate", opt)
}
}
@@ -438,7 +445,17 @@ func (c *CertChecker) CheckCert(principal string, cert *Certificate) error {
if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(CertTimeInfinity) && (unixNow >= before || before < 0) {
return fmt.Errorf("ssh: cert has expired")
}
if err := cert.SignatureKey.Verify(cert.bytesForSigning(), cert.Signature); err != nil {
// Match OpenSSH: the SK user-presence flag is never enforced on a
// certificate's CA signature. OpenSSH calls sshkey_verify with
// detailsp==NULL in sshkey.c:cert_parse, so the UP/UV flags are
// not even extracted. The UP bit on a CA signature reflects the
// CA operator's presence at signing time, which has no bearing on
// whether the user being authenticated is present now; enforcing
// it here would only break interop with certificates issued by
// non-interactive SK CAs. skKeyWithoutUP is a no-op for non-SK
// keys (the common case).
caKey := skKeyWithoutUP(cert.SignatureKey)
if err := caKey.Verify(cert.bytesForSigning(), cert.Signature); err != nil {
return fmt.Errorf("ssh: certificate signature does not verify")
}
+204
View File
@@ -9,10 +9,13 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"fmt"
"io"
"math/big"
"net"
"reflect"
"strings"
"testing"
"time"
@@ -43,6 +46,34 @@ func TestParseCert(t *testing.T) {
}
}
func TestParseCertNestedSignatureKey(t *testing.T) {
signer, err := NewSignerFromKey(testPrivateKeys["ed25519"])
if err != nil {
t.Fatal(err)
}
cert := &Certificate{
Key: signer.PublicKey(),
CertType: UserCert,
ValidBefore: CertTimeInfinity,
}
if err := cert.SignCert(rand.Reader, signer); err != nil {
t.Fatal(err)
}
inner := *cert
cert.SignatureKey = &inner
blob := cert.Marshal()
_, err = ParsePublicKey(blob)
if err == nil {
t.Fatal("ParsePublicKey: expected error for certificate signed by a certificate, got nil")
}
if !strings.Contains(err.Error(), "invalid for certificates") {
t.Errorf("ParsePublicKey: got error %q, want it to mention the signature key is invalid for certificates", err)
}
}
// Cert generated by ssh-keygen OpenSSH_6.8p1 OS X 10.10.3
// % ssh-keygen -s ca -I testcert -O source-address=192.168.1.0/24 -O force-command=/bin/sleep user.pub
// user.pub key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDACh1rt2DXfV3hk6fszSQcQ/rueMId0kVD9U7nl8cfEnFxqOCrNT92g4laQIGl2mn8lsGZfTLg8ksHq3gkvgO3oo/0wHy4v32JeBOHTsN5AL4gfHNEhWeWb50ev47hnTsRIt9P4dxogeUo/hTu7j9+s9lLpEQXCvq6xocXQt0j8MV9qZBBXFLXVT3cWIkSqOdwt/5ZBg+1GSrc7WfCXVWgTk4a20uPMuJPxU4RQwZW6X3+O8Pqo8C3cW0OzZRFP6gUYUKUsTI5WntlS+LAxgw1mZNsozFGdbiOPRnEryE3SRldh9vjDR3tin1fGpA5P7+CEB/bqaXtG3V+F2OkqaMN
@@ -128,6 +159,103 @@ func TestValidateCert(t *testing.T) {
}
}
// signSKCert builds an SK-ECDSA signature over cert.bytesForSigning()
// using caKey as the simulated hardware token. The SK signing flags
// byte (UP bit and others) is caller-controlled so tests can exercise
// the user-presence enforcement paths. caApp is the SK application
// string (typically "ssh:").
func signSKCert(t *testing.T, cert *Certificate, caKey *ecdsa.PrivateKey, caApp string, flags byte) {
t.Helper()
cert.SignatureKey = &skECDSAPublicKey{
application: caApp,
PublicKey: caKey.PublicKey,
}
cert.Nonce = make([]byte, 32)
if _, err := rand.Read(cert.Nonce); err != nil {
t.Fatal(err)
}
h := sha256.New()
h.Write([]byte(caApp))
appDigest := h.Sum(nil)
h.Reset()
h.Write(cert.bytesForSigning())
dataDigest := h.Sum(nil)
var counter uint32 = 1
blob := struct {
ApplicationDigest []byte `ssh:"rest"`
Flags byte
Counter uint32
MessageDigest []byte `ssh:"rest"`
}{appDigest, flags, counter, dataDigest}
h.Reset()
h.Write(Marshal(blob))
digest := h.Sum(nil)
r, s, err := ecdsa.Sign(rand.Reader, caKey, digest)
if err != nil {
t.Fatal(err)
}
cert.Signature = &Signature{
Format: KeyAlgoSKECDSA256,
Blob: Marshal(struct{ R, S *big.Int }{r, s}),
Rest: Marshal(struct {
Flags byte
Counter uint32
}{flags, counter}),
}
}
// TestCheckCertSKAuthorityNoUPRequired pins the OpenSSH-parity
// behavior of CertChecker.CheckCert for SK CA signatures: a
// certificate signed by an SK CA that produced a UP=0 signature is
// accepted, because the UP bit on a CA signature has no bearing on
// the current user authentication (see sshkey.c:cert_parse, which
// passes detailsp==NULL to sshkey_verify). Without this, certs issued
// by non-interactive SK CAs would fail to verify.
func TestCheckCertSKAuthorityNoUPRequired(t *testing.T) {
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
userKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
userPub, err := NewPublicKey(&userKey.PublicKey)
if err != nil {
t.Fatal(err)
}
mkCert := func(flags byte) *Certificate {
c := &Certificate{
CertType: UserCert,
Key: userPub,
ValidBefore: CertTimeInfinity,
ValidPrincipals: []string{"user"},
}
signSKCert(t, c, caKey, "ssh:", flags)
return c
}
checker := CertChecker{
IsUserAuthority: func(k PublicKey) bool {
caSK := &skECDSAPublicKey{application: "ssh:", PublicKey: caKey.PublicKey}
return bytes.Equal(k.Marshal(), caSK.Marshal())
},
}
// Both UP=0 and UP=1 CA signatures verify: UP is not enforced
// here, matching OpenSSH.
if err := checker.CheckCert("user", mkCert(0)); err != nil {
t.Errorf("UP=0 CA signature must verify (OpenSSH parity): %v", err)
}
if err := checker.CheckCert("user", mkCert(flagUserPresence)); err != nil {
t.Errorf("UP=1 CA signature must verify: %v", err)
}
}
func TestValidateCertTime(t *testing.T) {
cert := Certificate{
ValidPrincipals: []string{"user"},
@@ -257,6 +385,82 @@ func TestHostKeyCert(t *testing.T) {
}
}
type testConnMeta struct {
ConnMetadata
user string
}
func (c testConnMeta) User() string { return c.user }
func TestCertCriticalOptions(t *testing.T) {
appended := make([]string, 2)
appended[0], appended[1] = "supported-option", "appended-option"
supported := appended[:1]
checker := &CertChecker{
SupportedCriticalOptions: supported,
IsHostAuthority: func(p PublicKey, addr string) bool {
return bytes.Equal(testPublicKeys["ecdsa"].Marshal(), p.Marshal())
},
IsUserAuthority: func(p PublicKey) bool {
return bytes.Equal(testPublicKeys["ecdsa"].Marshal(), p.Marshal())
},
}
for _, test := range []struct {
name string
opts map[string]string
// succeed is the expected outcome of CheckHostKey, for a host
// certificate, and of CheckCert, for a user certificate.
succeed bool
// authSucceed is the expected outcome of Authenticate.
authSucceed bool
}{
{name: "no critical options", opts: nil, succeed: true, authSucceed: true},
{name: "source-address", opts: map[string]string{sourceAddressCriticalOption: "192.168.1.0/24"}, authSucceed: true},
{name: "unknown option", opts: map[string]string{"unknown-option": ""}},
{name: "supported option", opts: map[string]string{"supported-option": ""}, succeed: true, authSucceed: true},
} {
hostCert := &Certificate{
ValidPrincipals: []string{"hostname"},
Key: testPublicKeys["rsa"],
ValidBefore: CertTimeInfinity,
CertType: HostCert,
Permissions: Permissions{CriticalOptions: test.opts},
}
if err := hostCert.SignCert(rand.Reader, testSigners["ecdsa"]); err != nil {
t.Fatalf("SignCert: %v", err)
}
if err := checker.CheckHostKey("hostname:22", nil, hostCert); (err == nil) != test.succeed {
t.Errorf("CheckHostKey(%s): got %v, want success=%v", test.name, err, test.succeed)
}
userCert := &Certificate{
ValidPrincipals: []string{"user"},
Key: testPublicKeys["rsa"],
ValidBefore: CertTimeInfinity,
CertType: UserCert,
Permissions: Permissions{CriticalOptions: test.opts},
}
if err := userCert.SignCert(rand.Reader, testSigners["ecdsa"]); err != nil {
t.Fatalf("SignCert: %v", err)
}
if err := checker.CheckCert("user", userCert); (err == nil) != test.succeed {
t.Errorf("CheckCert(%s): got %v, want success=%v", test.name, err, test.succeed)
}
if _, err := checker.Authenticate(testConnMeta{user: "user"}, userCert); (err == nil) != test.authSucceed {
t.Errorf("Authenticate(%s): got %v, want success=%v", test.name, err, test.authSucceed)
}
}
if appended[1] != "appended-option" {
t.Errorf("Authenticate overwrote the caller's slice: got %q, want %q", appended[1], "appended-option")
}
}
type legacyRSASigner struct {
Signer
}
+123 -32
View File
@@ -11,6 +11,7 @@ import (
"io"
"log"
"sync"
"sync/atomic"
)
const (
@@ -131,11 +132,17 @@ func (r RejectionReason) String() string {
return fmt.Sprintf("unknown reason %d", int(r))
}
func min(a uint32, b int) uint32 {
if a < uint32(b) {
return a
// minPayloadSize returns min(limit, length) clamped to a uint32. It is used
// to compute the size of the next channel data packet from the remaining
// payload. The comparison is done in int64 because length is an int — on
// 64-bit systems len(data) can exceed 2^32, and a direct uint32(length)
// cast would silently truncate to 0 at every multiple of 2^32, causing
// WriteExtended's loop to spin without making progress.
func minPayloadSize(limit uint32, length int) uint32 {
if int64(length) > int64(limit) {
return limit
}
return uint32(b)
return uint32(length)
}
type channelDirection uint8
@@ -166,6 +173,12 @@ type channel struct {
// (for outbound channels) or received (for inbound channels).
decided bool
// established is set to true once the channel is open and may carry normal
// channel traffic: for an outbound channel when the peer's open
// confirmation is received, for an inbound channel when the local side
// accepts it. It is set and read from different goroutines.
established atomic.Bool
// direction contains either channelOutbound, for channels created
// locally, or channelInbound, for channels created by the peer.
direction channelDirection
@@ -177,6 +190,12 @@ type channel struct {
// with WantReply=true outstanding. This lock is held by a
// goroutine that has such an outgoing request pending.
sentRequestMu sync.Mutex
// sentRequestPending is set to true while a SendRequest call with
// WantReply=true is in flight. handlePacket uses it as a gate: responses
// arriving while no request is pending are dropped to prevent a
// misbehaving peer from stalling the mux read loop by filling ch.msg
// with unsolicited channelRequestSuccess/Failure messages.
sentRequestPending atomic.Bool
incomingRequests chan *Request
@@ -203,6 +222,10 @@ type channel struct {
// packetPool has a buffer for each extended channel ID to
// save allocations during writes.
packetPool map[uint32][]byte
// closeOnce guards close so it is idempotent: closing the internal Go
// channels (msg, incomingRequests) more than once would panic.
closeOnce sync.Once
}
// writePacket sends a packet. If the packet is a channel close, it updates
@@ -251,7 +274,7 @@ func (ch *channel) WriteExtended(data []byte, extendedCode uint32) (n int, err e
ch.writeMu.Unlock()
for len(data) > 0 {
space := min(ch.maxRemotePayload, len(data))
space := minPayloadSize(ch.maxRemotePayload, len(data))
if space, err = ch.remoteWin.reserve(space); err != nil {
return n, err
}
@@ -327,7 +350,18 @@ func (ch *channel) handleData(packet []byte) error {
if extended == 1 {
ch.extPending.write(data)
} else if extended > 0 {
// discard other extended data.
// RFC 4254, Section 5.2 defines no extended data types other
// than stderr (type 1, handled above) and this package provides
// no API to read them, so the data is discarded. Credit its
// window back immediately: it can never be read, so the
// deduction above would otherwise shrink the window permanently.
// adjustWindow returns io.EOF if the local side has already
// sent a channel close; ignore it like ReadExtended does, since
// an error returned here would terminate the mux read loop and
// tear down the whole connection.
if err := ch.adjustWindow(length); err != nil && err != io.EOF {
return err
}
} else {
ch.pending.write(data)
}
@@ -380,17 +414,19 @@ func (c *channel) ReadExtended(data []byte, extended uint32) (n int, err error)
}
func (c *channel) close() {
c.pending.eof()
c.extPending.eof()
close(c.msg)
close(c.incomingRequests)
c.writeMu.Lock()
// This is not necessary for a normal channel teardown, but if
// there was another error, it is.
c.sentClose = true
c.writeMu.Unlock()
// Unblock writers.
c.remoteWin.close()
c.closeOnce.Do(func() {
c.pending.eof()
c.extPending.eof()
close(c.msg)
close(c.incomingRequests)
c.writeMu.Lock()
// This is not necessary for a normal channel teardown, but if
// there was another error, it is.
c.sentClose = true
c.writeMu.Unlock()
// Unblock writers.
c.remoteWin.close()
})
}
// responseMessageReceived is called when a success or failure message is
@@ -404,10 +440,20 @@ func (ch *channel) responseMessageReceived() error {
return errors.New("ssh: duplicate response received for channel")
}
ch.decided = true
ch.established.Store(true)
return nil
}
func (ch *channel) handlePacket(packet []byte) error {
// Only the open response is expected before the channel is established.
if !ch.established.Load() {
switch packet[0] {
case msgChannelOpenConfirm, msgChannelOpenFailure:
default:
return nil
}
}
switch packet[0] {
case msgChannelData, msgChannelExtendedData:
return ch.handleData(packet)
@@ -460,27 +506,41 @@ func (ch *channel) handlePacket(packet []byte) error {
}
ch.incomingRequests <- &req
case *channelRequestSuccessMsg, *channelRequestFailureMsg:
// Drop responses that arrive when no SendRequest is waiting, to
// prevent a malicious peer from filling ch.msg and stalling the
// mux read loop. The non-blocking send additionally protects the
// loop if a well-behaved caller is slow to read.
if !ch.sentRequestPending.Load() {
return nil
}
select {
case ch.msg <- msg:
default:
}
default:
ch.msg <- msg
// No other message type is expected on an established channel.
return fmt.Errorf("ssh: unexpected message type %d on channel %d", packet[0], ch.localId)
}
return nil
}
func (m *mux) newChannel(chanType string, direction channelDirection, extraData []byte) *channel {
ch := &channel{
remoteWin: window{Cond: newCond()},
myWindow: channelWindowSize,
pending: newBuffer(),
extPending: newBuffer(),
direction: direction,
incomingRequests: make(chan *Request, chanSize),
msg: make(chan interface{}, chanSize),
chanType: chanType,
extraData: extraData,
mux: m,
packetPool: make(map[uint32][]byte),
remoteWin: window{Cond: newCond()},
myWindow: channelWindowSize,
maxIncomingPayload: channelMaxPacket,
pending: newBuffer(),
extPending: newBuffer(),
direction: direction,
incomingRequests: make(chan *Request, chanSize),
msg: make(chan interface{}, chanSize),
chanType: chanType,
extraData: extraData,
mux: m,
packetPool: make(map[uint32][]byte),
}
ch.localId = m.chanList.add(ch)
m.chanList.add(ch)
return ch
}
@@ -504,7 +564,6 @@ func (ch *channel) Accept() (Channel, <-chan *Request, error) {
if ch.decided {
return nil, nil, errDecidedAlready
}
ch.maxIncomingPayload = channelMaxPacket
confirm := channelOpenConfirmMsg{
PeersID: ch.remoteId,
MyID: ch.localId,
@@ -512,6 +571,7 @@ func (ch *channel) Accept() (Channel, <-chan *Request, error) {
MaxPacketSize: ch.maxIncomingPayload,
}
ch.decided = true
ch.established.Store(true)
if err := ch.sendMessage(confirm); err != nil {
return nil, nil, err
}
@@ -530,7 +590,17 @@ func (ch *channel) Reject(reason RejectionReason, message string) error {
Language: "en",
}
ch.decided = true
return ch.sendMessage(reject)
err := ch.sendMessage(reject)
// Remove the channel from the mux to prevent memory leaks.
// Do not call ch.close() here: no goroutine holds a reference to a
// rejected channel's internal channels (msg, incomingRequests), so
// removing it from chanList is sufficient for GC. Calling close()
// would race with the mux loop goroutine (handlePacket or dropAll),
// causing a panic from closing an already-closed channel.
ch.mux.chanList.remove(ch.localId)
return err
}
func (ch *channel) Read(data []byte) (int, error) {
@@ -586,6 +656,27 @@ func (ch *channel) SendRequest(name string, wantReply bool, payload []byte) (boo
if wantReply {
ch.sentRequestMu.Lock()
defer ch.sentRequestMu.Unlock()
// Open the gate so that responses arriving while this request is in
// flight are allowed to reach ch.msg. Responses arriving while no
// request is pending are dropped by handlePacket.
ch.sentRequestPending.Store(true)
defer ch.sentRequestPending.Store(false)
// Drain any spurious responses that may have been buffered. This
// prevents a previously buffered unexpected response from being
// consumed instead of the actual response for this request.
drain:
for {
select {
case _, ok := <-ch.msg:
if !ok {
break drain
}
default:
break drain
}
}
}
msg := channelRequestMsg{
+250
View File
@@ -0,0 +1,250 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"encoding/binary"
"io"
"math/bits"
"testing"
"time"
"unsafe"
)
func TestMinPayloadSize(t *testing.T) {
// 4 GiB (2^32). Declared as a var (not a const) so that int(bigPayload)
// is a runtime conversion: a constant conversion would fail to compile
// on 32-bit platforms with "constant 4294967296 overflows int". On
// 32-bit the value truncates to 0 at runtime, but the is64Bit cases
// that reference it are skipped by the runtime check below.
var bigPayload int64 = 1 << 32
tests := []struct {
name string
maxPayload uint32
dataLen int
want uint32
is64Bit bool // Flag to run only on 64-bit architectures
}{
{
name: "Normal Case - Data fits in payload",
maxPayload: 32768,
dataLen: 1000,
want: 1000,
},
{
name: "Normal Case - Data larger than payload",
maxPayload: 32768,
dataLen: 50000,
want: 32768,
},
{
name: "Boundary Case - Data zero",
maxPayload: 32768,
dataLen: 0,
want: 0,
},
{
name: "Overflow Case - Data is exactly 4GB (1<<32)",
maxPayload: 32768,
dataLen: int(bigPayload),
want: 32768,
is64Bit: true,
},
{
name: "Overflow Case - Data is 4GB + small amount",
maxPayload: 32768,
dataLen: int(bigPayload + 100),
want: 32768,
is64Bit: true,
},
}
is64Bit := bits.UintSize == 64
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.is64Bit && !is64Bit {
t.Skip("Skipping test requiring 64-bit int")
}
got := minPayloadSize(tt.maxPayload, tt.dataLen)
if got != tt.want {
t.Errorf("minPayloadSize(%d, %d) = %d; want %d", tt.maxPayload, tt.dataLen, got, tt.want)
}
})
}
}
// TestWriteExtendedNoInfiniteLoopOnLargeWrite is an end-to-end regression
// test for the integer-overflow bug in WriteExtended. Before the fix, a
// write whose len(data) was a multiple of 2^32 caused minPayloadSize to
// return 0; WriteExtended then spun forever, reserving 0 bytes per
// iteration and never advancing the data slice.
//
// We exercise the real WriteExtended path with a slice whose declared
// length is exactly 2^32. Allocating 4 GiB is unnecessary: each iteration
// only reads up to maxRemotePayload bytes from the head of the slice, and
// the loop blocks in remoteWin.reserve() once the channel window is
// exhausted — before the slice base advances past the underlying buffer.
//
// With the fix, the loop blocks in reserve(); we detect that via
// waitWriterBlocked(), then close the window to let WriteExtended return.
// With the bug, the loop never blocks and the test times out.
//
//go:nocheckptr
func TestWriteExtendedNoInfiniteLoopOnLargeWrite(t *testing.T) {
if bits.UintSize < 64 {
t.Skip("test requires 64-bit int to construct a slice with len >= 2^32")
}
reader, writer, mux := channelPair(t)
defer reader.Close()
defer writer.Close()
defer mux.Close()
// Sized to hold the full pre-update remote window so that no iteration
// reads past the backing buffer before reserve() blocks.
backing := make([]byte, channelWindowSize)
var bigLen int64 = 1 << 32
bigSlice := unsafe.Slice(&backing[0], int(bigLen))
done := make(chan int, 1)
go func() {
n, _ := writer.Write(bigSlice)
done <- n
}()
blocked := make(chan struct{})
go func() {
writer.remoteWin.waitWriterBlocked()
close(blocked)
}()
select {
case <-blocked:
// Good — the loop made progress and is now blocked in reserve().
// Close the window to let WriteExtended return.
writer.remoteWin.close()
case <-time.After(2 * time.Second):
t.Fatal("WriteExtended did not block in reserve within 2s — minPayloadSize likely returned 0 (integer overflow regression)")
}
select {
case n := <-done:
if n == 0 {
t.Fatalf("WriteExtended returned n=0; expected progress")
}
case <-time.After(2 * time.Second):
t.Fatal("WriteExtended did not return after closing the window")
}
}
func TestDiscardedExtendedDataReturnsWindowCredit(t *testing.T) {
client, _, serverChans := forwardingPair(t)
clientCh, reqs, err := client.OpenChannel("test", nil)
if err != nil {
t.Fatalf("OpenChannel: %v", err)
}
go DiscardRequests(reqs)
defer clientCh.Close()
serverCh := (<-serverChans).(*channel)
defer serverCh.Close()
// Write more than the default channel receive window (2 MiB) of
// extended data with a type code other than stderr. The peer
// discards such data: if it did not return the window credit the
// write would block forever once the window is exhausted.
const payload = 4 * 1024 * 1024
done := make(chan error, 1)
go func() {
buf := make([]byte, 32*1024)
remaining := payload
for remaining > 0 {
n := min(len(buf), remaining)
nw, err := serverCh.WriteExtended(buf[:n], 2)
if err != nil {
done <- err
return
}
remaining -= nw
}
done <- nil
}()
select {
case err := <-done:
if err != nil {
t.Fatalf("write of discarded extended data: %v", err)
}
case <-time.After(10 * time.Second):
t.Fatal("write of discarded extended data blocked: window credit is not returned")
}
// The main stream must still be usable.
want := []byte("main stream after extended data")
if _, err := serverCh.Write(want); err != nil {
t.Fatalf("write to main stream: %v", err)
}
got := make([]byte, len(want))
if _, err := io.ReadFull(clientCh, got); err != nil {
t.Fatalf("read from main stream: %v", err)
}
if string(got) != string(want) {
t.Fatalf("read %q from main stream, want %q", got, want)
}
// Reading the main stream payload above orders the test after all
// preceding packets: everything received has now been either read or
// discarded, so the full window credit must have been accounted.
// Anything less indicates a partial-credit leak that the liveness
// check above would not detect.
cc := clientCh.(*channel)
cc.windowMu.Lock()
window := cc.myWindow + cc.myConsumed
cc.windowMu.Unlock()
if window != channelWindowSize {
t.Errorf("myWindow+myConsumed = %d, want %d", window, channelWindowSize)
}
}
func TestDiscardedExtendedDataAfterClose(t *testing.T) {
client, _, serverChans := forwardingPair(t)
clientCh, reqs, err := client.OpenChannel("test", nil)
if err != nil {
t.Fatalf("OpenChannel: %v", err)
}
go DiscardRequests(reqs)
serverCh := <-serverChans
defer serverCh.Close()
ch := clientCh.(*channel)
// Close the channel locally: sentClose is now set, but the channel
// stays in the mux chanList until the peer's close arrives, so
// in-flight data packets are still dispatched to handleData.
if err := ch.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
// Deliver extended data packets with an unknown type code, enough to
// cross the adjustWindow send threshold of 3*maxIncomingPayload.
// The window adjust message cannot be sent after the close and
// adjustWindow fails with io.EOF: handleData must swallow that
// error, since any error it returns terminates the mux read loop
// and tears down every channel on the connection.
const packetLen = 32 * 1024
packet := make([]byte, 13+packetLen)
packet[0] = msgChannelExtendedData
binary.BigEndian.PutUint32(packet[1:], ch.localId)
binary.BigEndian.PutUint32(packet[5:], 2)
binary.BigEndian.PutUint32(packet[9:], packetLen)
for range 5 {
if err := ch.handleData(packet); err != nil {
t.Fatalf("handleData after local close: %v", err)
}
}
}
+2 -2
View File
@@ -407,7 +407,7 @@ func (c *gcmCipher) readCipherPacket(seqNum uint32, r io.Reader) ([]byte, error)
return nil, fmt.Errorf("ssh: illegal padding %d", padding)
}
if int(padding+1) >= len(plain) {
if int(padding)+1 >= len(plain) {
return nil, fmt.Errorf("ssh: padding %d too large", padding)
}
plain = plain[1 : length-uint32(padding)]
@@ -586,7 +586,7 @@ func (c *cbcCipher) writeCipherPacket(seqNum uint32, w io.Writer, rand io.Reader
// Length of encrypted portion of the packet (header, payload, padding).
// Enforce minimum padding and packet size.
encLength := maxUInt32(prefixLen+len(packet)+cbcMinPaddingSize, cbcMinPaddingSize)
encLength := maxUInt32(prefixLen+len(packet)+cbcMinPaddingSize, cbcMinPacketSize)
// Enforce block size.
encLength = (encLength + effectiveBlockSize - 1) / effectiveBlockSize * effectiveBlockSize
+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
+58 -18
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:
@@ -274,10 +310,14 @@ func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (MultiA
}
// Filter algorithms based on those supported by MultiAlgorithmSigner.
// Iterate over the signer's algorithms first to preserve its preference order.
supportedKeyAlgos := algorithmsForKeyFormat(keyFormat)
var keyAlgos []string
for _, algo := range algorithmsForKeyFormat(keyFormat) {
if slices.Contains(as.Algorithms(), underlyingAlgo(algo)) {
keyAlgos = append(keyAlgos, algo)
for _, signerAlgo := range as.Algorithms() {
if idx := slices.IndexFunc(supportedKeyAlgos, func(algo string) bool {
return underlyingAlgo(algo) == signerAlgo
}); idx >= 0 {
keyAlgos = append(keyAlgos, supportedKeyAlgos[idx])
}
}
@@ -373,11 +413,11 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
return authFailure, nil, err
}
// 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
}
}
@@ -758,7 +798,7 @@ func (g *gssAPIWithMICCallback) auth(session []byte, user string, c packetConn,
return authFailure, nil, fmt.Errorf("GSS-API Error:\n"+
"Major Status: %d\n"+
"Minor Status: %d\n"+
"Error Message: %s\n", userAuthGSSAPIErrorResp.MajorStatus, userAuthGSSAPIErrorResp.MinorStatus,
"Error Message: %q\n", userAuthGSSAPIErrorResp.MajorStatus, userAuthGSSAPIErrorResp.MinorStatus,
userAuthGSSAPIErrorResp.Message)
case msgUserAuthGSSAPIToken:
userAuthGSSAPITokenReq := &userAuthGSSAPIToken{}
+833
View File
@@ -7,10 +7,12 @@ package ssh
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"errors"
"fmt"
"io"
"log"
"math/big"
"net"
"os"
"runtime"
@@ -169,6 +171,723 @@ func TestClientAuthThirdKey(t *testing.T) {
}
}
// invalidRSASigner offers an RSA public key the server cannot parse: its public
// exponent is even, which parseRSA rejects regardless of any key size limit.
// The key is intentionally invalid for a structural reason rather than for its
// size, so the test stays meaningful even if the accepted modulus size changes.
// Its Sign method is never reached, because the key is rejected during the
// initial public key query.
type invalidRSASigner struct{}
func (invalidRSASigner) PublicKey() PublicKey {
n := new(big.Int).Lsh(big.NewInt(1), 2048)
pub, err := NewPublicKey(&rsa.PublicKey{N: n, E: 2}) // incorrect exponent.
if err != nil {
panic(err)
}
return pub
}
func (invalidRSASigner) Sign(rand io.Reader, data []byte) (*Signature, error) {
return nil, errors.New("ssh: invalid test key must not be used to sign")
}
func TestClientAuthInvalidPublicKey(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(invalidRSASigner{}, testSigners["rsa"]),
},
HostKeyCallback: InsecureIgnoreHostKey(),
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("client auth failed but should have fallen back to a valid key: %s", err)
}
}
// partialSuccessPublicKeyAndKbdInteractiveServer returns a server config
// where the rsa key gets partial success and the server offers both publickey
// and keyboard-interactive as next methods. The VerifiedPublicKeyCallback
// tracks state so that the same key only triggers partial success once,
// avoiding an impossible auth loop.
func partialSuccessPublicKeyAndKbdInteractiveServer() *ServerConfig {
rsaDone := false
serverConf := &ServerConfig{
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
return nil, nil
}
return nil, errors.New("invalid credentials")
},
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
if !rsaDone && bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
rsaDone = true
return nil, &PartialSuccessError{
Next: ServerAuthCallbacks{
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
return nil, nil
}
return nil, errors.New("invalid credentials")
},
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
answers, err := client("", "", []string{"token: "}, []bool{true})
if err != nil {
return nil, err
}
if len(answers) == 1 && answers[0] == "correct-token" {
return nil, nil
}
return nil, errors.New("invalid token")
},
},
}
}
return nil, errors.New("invalid credentials")
},
}
serverConf.AddHostKey(testSigners["rsa"])
return serverConf
}
// partialSuccessMultiKeyServer returns a server config that requires two
// public keys in sequence: the first key (rsa) triggers partial success
// requiring a second key (ecdsa) to complete authentication.
func partialSuccessMultiKeyServer() *ServerConfig {
firstKeyDone := false
serverConf := &ServerConfig{
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
return nil, nil
}
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
return nil, nil
}
return nil, errors.New("invalid credentials")
},
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
if !firstKeyDone && bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
firstKeyDone = true
return nil, &PartialSuccessError{
Next: ServerAuthCallbacks{
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
return nil, nil
}
return nil, errors.New("invalid credentials")
},
},
}
}
if firstKeyDone && bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
return nil, nil
}
return nil, errors.New("invalid credentials")
},
}
serverConf.AddHostKey(testSigners["rsa"])
return serverConf
}
func TestStaticMultiKeyPartialSuccess(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
clientConf := &ClientConfig{
User: "user",
Auth: []AuthMethod{
PublicKeys(testSigners["rsa"], testSigners["ecdsa"]),
},
HostKeyCallback: InsecureIgnoreHostKey(),
}
go NewServerConn(c1, partialSuccessMultiKeyServer())
_, _, _, err = NewClientConn(c2, "", clientConf)
if err != nil {
t.Fatalf("client login error: %s", err)
}
}
func TestStaticPartialSuccess(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
clientConf := &ClientConfig{
User: "user",
Auth: []AuthMethod{
PublicKeys(testSigners["rsa"]),
KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
return []string{"correct-token"}, nil
}),
},
HostKeyCallback: InsecureIgnoreHostKey(),
}
go NewServerConn(c1, partialSuccessPublicKeyAndKbdInteractiveServer())
_, _, _, err = NewClientConn(c2, "", clientConf)
if err != nil {
t.Fatalf("client login error: %s", err)
}
}
// partialSuccessKeyAThenKeyBServer returns a server config where keyA (rsa)
// gets partial success and the server offers BOTH publickey and
// keyboard-interactive. keyB (ecdsa) then completes auth successfully.
func partialSuccessKeyAThenKeyBServer() *ServerConfig {
keyADone := false
serverConf := &ServerConfig{
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
return nil, nil
}
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
return nil, nil
}
return nil, errors.New("invalid credentials")
},
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
if !keyADone && bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
keyADone = true
return nil, &PartialSuccessError{
Next: ServerAuthCallbacks{
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
return nil, nil
}
return nil, errors.New("invalid credentials")
},
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
answers, err := client("", "", []string{"token: "}, []bool{true})
if err != nil {
return nil, err
}
if len(answers) == 1 && answers[0] == "correct-token" {
return nil, nil
}
return nil, errors.New("invalid token")
},
},
}
}
if keyADone && bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
return nil, nil
}
return nil, errors.New("invalid credentials")
},
}
serverConf.AddHostKey(testSigners["rsa"])
return serverConf
}
func TestStaticPartialSuccessKeyAThenKeyB(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
clientConf := &ClientConfig{
User: "user",
Auth: []AuthMethod{
PublicKeys(testSigners["rsa"], testSigners["ecdsa"]),
},
HostKeyCallback: InsecureIgnoreHostKey(),
}
go NewServerConn(c1, partialSuccessKeyAThenKeyBServer())
_, _, _, err = NewClientConn(c2, "", clientConf)
if err != nil {
t.Fatalf("client login error: %s", err)
}
}
// partialSuccessMultiKeyThenKbdInteractiveServer returns a server config
// that strictly requires keyA (rsa) AND keyB (ecdsa) as public keys before
// allowing keyboard-interactive. After keyA, ONLY publickey is offered
// (for keyB). After keyB, ONLY keyboard-interactive is offered.
func partialSuccessMultiKeyThenKbdInteractiveServer() *ServerConfig {
serverConf := &ServerConfig{
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
return nil, &PartialSuccessError{
Next: ServerAuthCallbacks{
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
if bytes.Equal(key.Marshal(), testPublicKeys["ecdsa"].Marshal()) {
return nil, &PartialSuccessError{
Next: ServerAuthCallbacks{
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
answers, err := client("", "", []string{"token: "}, []bool{true})
if err != nil {
return nil, err
}
if len(answers) == 1 && answers[0] == "correct-token" {
return nil, nil
}
return nil, errors.New("invalid token")
},
},
}
}
return nil, errors.New("invalid credentials")
},
},
}
}
return nil, errors.New("invalid credentials")
},
}
serverConf.AddHostKey(testSigners["rsa"])
return serverConf
}
func TestStaticMultiKeyPartialThenKbdInteractive(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
clientConf := &ClientConfig{
User: "user",
Auth: []AuthMethod{
PublicKeys(testSigners["rsa"], testSigners["ecdsa"]),
KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
return []string{"correct-token"}, nil
}),
},
HostKeyCallback: InsecureIgnoreHostKey(),
}
go NewServerConn(c1, partialSuccessMultiKeyThenKbdInteractiveServer())
_, _, _, err = NewClientConn(c2, "", clientConf)
if err != nil {
t.Fatalf("client login error: %s", err)
}
}
func TestAuthMethodDynamic(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: nil,
HostKeyCallback: InsecureIgnoreHostKey(),
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
if len(ctx.Metadata.SessionID()) == 0 {
t.Error("empty sessionID")
}
if ctx.Algorithms.HostKey == "" {
t.Error("empty Host Key")
}
if slices.Contains(ctx.AllowedMethods, "password") {
return Password(clientPassword), nil
}
return nil, errors.New("auth callback error")
},
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("unable to dial remote side: %s", err)
}
}
func TestAuthCallbackError(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
Password(clientPassword),
},
HostKeyCallback: InsecureIgnoreHostKey(),
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
return nil, errors.New("auth callback error")
},
}
if err := tryAuth(t, config); err == nil {
t.Fatal("authentication unexpectedly succeeded after AuthCallback returned an error")
}
}
func TestAuthCallbackPartialSuccess(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
clientConf := &ClientConfig{
User: "user",
HostKeyCallback: InsecureIgnoreHostKey(),
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
if slices.Contains(ctx.AllowedMethods, "publickey") && !slices.Contains(ctx.PartialSuccessMethods, "publickey") {
return PublicKeys(testSigners["rsa"]), nil
}
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
return []string{"correct-token"}, nil
}), nil
}
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v", ctx.AllowedMethods, ctx.PartialSuccessMethods)
},
}
go NewServerConn(c1, partialSuccessPublicKeyAndKbdInteractiveServer())
_, _, _, err = NewClientConn(c2, "", clientConf)
if err != nil {
t.Fatalf("client login error: %s", err)
}
}
func TestAuthCallbackMultiKeyPartialSuccess(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
clientConf := &ClientConfig{
User: "user",
HostKeyCallback: InsecureIgnoreHostKey(),
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
if slices.Contains(ctx.AllowedMethods, "publickey") && !slices.Contains(ctx.PartialSuccessMethods, "publickey") {
return PublicKeys(testSigners["rsa"]), nil
}
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "publickey") {
return PublicKeys(testSigners["ecdsa"]), nil
}
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v", ctx.AllowedMethods, ctx.PartialSuccessMethods)
},
}
go NewServerConn(c1, partialSuccessMultiKeyServer())
_, _, _, err = NewClientConn(c2, "", clientConf)
if err != nil {
t.Fatalf("client login error: %s", err)
}
}
func TestAuthCallbackMultiKeyPartialThenKbdInteractive(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
clientConf := &ClientConfig{
User: "user",
HostKeyCallback: InsecureIgnoreHostKey(),
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
partialCount := len(ctx.PartialSuccessMethods)
if partialCount == 0 && slices.Contains(ctx.AllowedMethods, "publickey") {
return PublicKeys(testSigners["rsa"]), nil
}
if partialCount == 1 && slices.Contains(ctx.AllowedMethods, "publickey") {
return PublicKeys(testSigners["ecdsa"]), nil
}
if partialCount >= 2 && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
return []string{"correct-token"}, nil
}), nil
}
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v tried=%v",
ctx.AllowedMethods, ctx.PartialSuccessMethods, ctx.TriedMethods)
},
}
go NewServerConn(c1, partialSuccessMultiKeyThenKbdInteractiveServer())
_, _, _, err = NewClientConn(c2, "", clientConf)
if err != nil {
t.Fatalf("client login error: %s", err)
}
}
func TestAuthCallbackPartialSuccessKeyAThenKeyB(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
clientConf := &ClientConfig{
User: "user",
HostKeyCallback: InsecureIgnoreHostKey(),
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
if len(ctx.PartialSuccessMethods) == 0 && slices.Contains(ctx.AllowedMethods, "publickey") {
return PublicKeys(testSigners["rsa"]), nil
}
if len(ctx.PartialSuccessMethods) == 1 && slices.Contains(ctx.AllowedMethods, "publickey") {
return PublicKeys(testSigners["ecdsa"]), nil
}
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v",
ctx.AllowedMethods, ctx.PartialSuccessMethods)
},
}
go NewServerConn(c1, partialSuccessKeyAThenKeyBServer())
_, _, _, err = NewClientConn(c2, "", clientConf)
if err != nil {
t.Fatalf("client login error: %s", err)
}
}
func TestAuthCallbackPartialSuccessWithStaticAuth(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
clientConf := &ClientConfig{
User: "user",
Auth: []AuthMethod{
PublicKeys(testSigners["rsa"]),
},
HostKeyCallback: InsecureIgnoreHostKey(),
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
return []string{"correct-token"}, nil
}), nil
}
return nil, nil
},
}
go NewServerConn(c1, partialSuccessPublicKeyAndKbdInteractiveServer())
_, _, _, err = NewClientConn(c2, "", clientConf)
if err != nil {
t.Fatalf("client login error: %s", err)
}
}
func partialSuccessPubKeyForbiddenAfterPartialServer(maxTries int) *ServerConfig {
serverConf := &ServerConfig{
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
return nil, nil
}
return nil, errors.New("invalid credentials")
},
VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) {
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
return nil, &PartialSuccessError{
Next: ServerAuthCallbacks{
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
return nil, errors.New("legacy public key authentication is forbidden")
},
KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) {
answers, err := client("", "", []string{"token: "}, []bool{true})
if err != nil {
return nil, err
}
if len(answers) == 1 && answers[0] == "correct-token" {
return nil, nil
}
return nil, errors.New("invalid token")
},
},
}
}
return nil, errors.New("invalid credentials")
},
MaxAuthTries: maxTries,
}
serverConf.AddHostKey(testSigners["rsa"])
return serverConf
}
func TestAuthCallbackPartialSuccessWithStaticMultiKey(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
clientConf := &ClientConfig{
User: "user",
Auth: []AuthMethod{
PublicKeys(
testSigners["rsa"],
testSigners["ecdsa"],
testSigners["ed25519"],
),
},
HostKeyCallback: InsecureIgnoreHostKey(),
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
return []string{"correct-token"}, nil
}), nil
}
return nil, nil
},
}
go NewServerConn(c1, partialSuccessPubKeyForbiddenAfterPartialServer(2))
_, _, _, err = NewClientConn(c2, "", clientConf)
if err != nil {
t.Fatalf("client login error: %s", err)
}
}
func TestAuthCallbackPartialSuccessShortCircuit(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
serverConf := partialSuccessPubKeyForbiddenAfterPartialServer(2)
pubkeyAttempts := 0
clientConf := &ClientConfig{
User: "user",
HostKeyCallback: InsecureIgnoreHostKey(),
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
if slices.Contains(ctx.AllowedMethods, "publickey") && !slices.Contains(ctx.PartialSuccessMethods, "publickey") {
pubkeyAttempts++
return PublicKeys(testSigners["rsa"], testSigners["ecdsa"], testSigners["ed25519"]), nil
}
if slices.Contains(ctx.PartialSuccessMethods, "publickey") && slices.Contains(ctx.AllowedMethods, "keyboard-interactive") {
return KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
return []string{"correct-token"}, nil
}), nil
}
return nil, fmt.Errorf("unexpected auth state: allowed=%v partial=%v tried=%v",
ctx.AllowedMethods, ctx.PartialSuccessMethods, ctx.TriedMethods)
},
}
go NewServerConn(c1, serverConf)
_, _, _, err = NewClientConn(c2, "", clientConf)
if err != nil {
t.Fatalf("client login error: %s", err)
}
if pubkeyAttempts != 1 {
t.Errorf("expected publickey to be attempted once before short-circuit, got %d", pubkeyAttempts)
}
}
func TestAuthCallbackTriedLoopBound(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
serverConf := &ServerConfig{
MaxAuthTries: -1,
PasswordCallback: func(conn ConnMetadata, pass []byte) (*Permissions, error) {
return nil, errors.New("password rejected")
},
}
serverConf.AddHostKey(testSigners["rsa"])
invocations := 0
clientConf := &ClientConfig{
User: "user",
HostKeyCallback: InsecureIgnoreHostKey(),
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
invocations++
return Password("wrong"), nil
},
}
go NewServerConn(c1, serverConf)
_, _, _, err = NewClientConn(c2, "", clientConf)
if err == nil {
t.Fatal("expected the client to abort after exceeding the attempts cap")
}
if !strings.Contains(err.Error(), "too many authentication attempts") {
t.Errorf("unexpected error: %v", err)
}
if invocations != maxAuthClientTried {
t.Errorf("AuthCallback invoked %d times; expected %d",
invocations, maxAuthClientTried)
}
}
func alwaysPartialPubKeyServer() *ServerConfig {
var alwaysPartial func(ConnMetadata, PublicKey) (*Permissions, error)
alwaysPartial = func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
if !bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
return nil, errors.New("invalid credentials")
}
return nil, &PartialSuccessError{
Next: ServerAuthCallbacks{
PublicKeyCallback: alwaysPartial,
},
}
}
serverConf := &ServerConfig{
MaxAuthTries: -1,
PublicKeyCallback: alwaysPartial,
}
serverConf.AddHostKey(testSigners["rsa"])
return serverConf
}
func TestAuthCallbackPartialSuccessLoopBound(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
invocations := 0
clientConf := &ClientConfig{
User: "user",
HostKeyCallback: InsecureIgnoreHostKey(),
AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) {
invocations++
return PublicKeys(testSigners["rsa"]), nil
},
}
go NewServerConn(c1, alwaysPartialPubKeyServer())
_, _, _, err = NewClientConn(c2, "", clientConf)
if err == nil {
t.Fatal("expected the client to abort after exceeding the attempts cap")
}
if !strings.Contains(err.Error(), "too many authentication attempts") {
t.Errorf("unexpected error: %v", err)
}
if invocations != maxAuthClientTried {
t.Errorf("AuthCallback invoked %d times; expected %d",
invocations, maxAuthClientTried)
}
}
func TestAuthMethodPassword(t *testing.T) {
config := &ClientConfig{
User: "testuser",
@@ -971,6 +1690,74 @@ func TestAuthMethodGSSAPIWithMIC(t *testing.T) {
}
}
type maliciousGSSAPIWithMICAuthMethod struct{}
func (maliciousGSSAPIWithMICAuthMethod) method() string {
// Reuse an advertised auth method name so the client attempts this
// AuthMethod even when gssapi-with-mic is not advertised.
return "password"
}
func (maliciousGSSAPIWithMICAuthMethod) auth(session []byte, user string, c packetConn, rand io.Reader, _ map[string][]byte) (authResult, []string, error) {
req := &userAuthRequestMsg{
User: user,
Service: serviceSSH,
Method: "gssapi-with-mic",
}
req.Payload = appendU32(req.Payload, 1)
req.Payload = appendString(req.Payload, string(krb5OID))
if err := c.writePacket(Marshal(req)); err != nil {
return authFailure, nil, err
}
packet, err := c.readPacket()
if err != nil {
return authFailure, nil, err
}
switch packet[0] {
case msgUserAuthFailure:
var msg userAuthFailureMsg
if err := Unmarshal(packet, &msg); err != nil {
return authFailure, nil, err
}
if msg.PartialSuccess {
return authPartialSuccess, msg.Methods, nil
}
return authFailure, msg.Methods, nil
default:
return authFailure, nil, unexpectedMessageError(msgUserAuthFailure, packet[0])
}
}
func TestGSSAPIWithMICPartialConfigRejectedAtRuntime(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
maliciousGSSAPIWithMICAuthMethod{},
},
HostKeyCallback: InsecureIgnoreHostKey(),
}
clientErr, serverErrs := tryAuthBothSides(t, config, &GSSAPIWithMICConfig{
Server: &FakeServer{},
})
if clientErr == nil || !strings.Contains(clientErr.Error(), "ssh: unable to authenticate") {
t.Fatalf("client got %v, want authentication failure", clientErr)
}
found := false
for _, err := range serverErrs {
if err != nil && strings.Contains(err.Error(), "ssh: gssapi-with-mic auth not configured") {
found = true
break
}
}
if !found {
t.Fatalf("server auth errors = %v, want gssapi-with-mic auth not configured", serverErrs)
}
}
func TestCompatibleAlgoAndSignatures(t *testing.T) {
type testcase struct {
algo string
@@ -1159,6 +1946,52 @@ func TestPickSignatureAlgorithm(t *testing.T) {
}
}
func TestPickSignatureAlgorithmRespectsSignerPreference(t *testing.T) {
algoSigner, ok := testSigners["rsa"].(AlgorithmSigner)
if !ok {
t.Fatalf("rsa test signer does not implement the AlgorithmSigner interface")
}
serverExtensions := map[string][]byte{
"server-sig-algs": []byte(KeyAlgoRSASHA256 + "," + KeyAlgoRSASHA512),
}
tests := []struct {
name string
signerPrefs []string
expectedAlgo string
}{
{
name: "Signer prefers SHA512 then SHA256",
signerPrefs: []string{KeyAlgoRSASHA512, KeyAlgoRSASHA256},
expectedAlgo: KeyAlgoRSASHA512,
},
{
name: "Signer prefers SHA256 then SHA512",
signerPrefs: []string{KeyAlgoRSASHA256, KeyAlgoRSASHA512},
expectedAlgo: KeyAlgoRSASHA256,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
orderedSigner, err := NewSignerWithAlgorithms(algoSigner, tc.signerPrefs)
if err != nil {
t.Fatalf("failed to create ordered signer: %v", err)
}
_, selectedAlgo, err := pickSignatureAlgorithm(orderedSigner, serverExtensions)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if selectedAlgo != tc.expectedAlgo {
t.Errorf("Algorithm mismatch; got %q want %q", selectedAlgo, tc.expectedAlgo)
}
})
}
}
// configurablePublicKeyCallback is a public key callback that allows to
// configure the signature algorithm and format. This way we can emulate the
// behavior of buggy clients.
+4 -4
View File
@@ -419,7 +419,7 @@ type AlgorithmNegotiationError struct {
}
func (a *AlgorithmNegotiationError) Error() string {
return fmt.Sprintf("ssh: no common algorithm for %s; we offered: %v, peer offered: %v",
return fmt.Sprintf("ssh: no common algorithm for %s; we offered: %q, peer offered: %q",
a.What, a.SupportedAlgorithms, a.RequestedAlgorithms)
}
@@ -544,7 +544,7 @@ func (c *Config) SetDefaults() {
if c.Rand == nil {
c.Rand = rand.Reader
}
if c.Ciphers == nil {
if len(c.Ciphers) == 0 {
c.Ciphers = defaultCiphers
}
var ciphers []string
@@ -556,7 +556,7 @@ func (c *Config) SetDefaults() {
}
c.Ciphers = ciphers
if c.KeyExchanges == nil {
if len(c.KeyExchanges) == 0 {
c.KeyExchanges = defaultKexAlgos
}
var kexs []string
@@ -571,7 +571,7 @@ func (c *Config) SetDefaults() {
}
c.KeyExchanges = kexs
if c.MACs == nil {
if len(c.MACs) == 0 {
c.MACs = defaultMACs
}
var macs []string
+10 -2
View File
@@ -17,7 +17,7 @@ type OpenChannelError struct {
}
func (e *OpenChannelError) Error() string {
return fmt.Sprintf("ssh: rejected: %s (%s)", e.Reason, e.Message)
return fmt.Sprintf("ssh: rejected: %s (%q)", e.Reason, e.Message)
}
// ConnMetadata holds metadata for the connection.
@@ -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)
}
})
}
}
+1 -1
View File
@@ -162,7 +162,7 @@ func newClientTransport(conn keyingTransport, clientVersion, serverVersion []byt
t.remoteAddr = addr
t.hostKeyCallback = config.HostKeyCallback
t.bannerCallback = config.BannerCallback
if config.HostKeyAlgorithms != nil {
if len(config.HostKeyAlgorithms) > 0 {
t.hostKeyAlgorithms = config.HostKeyAlgorithms
} else {
t.hostKeyAlgorithms = defaultHostKeyAlgos
+3 -3
View File
@@ -195,7 +195,7 @@ func TestHandshakeBasic(t *testing.T) {
}
want := []byte{msgRequestSuccess, byte(i)}
if bytes.Compare(p, want) != 0 {
if !bytes.Equal(p, want) {
t.Errorf("message %d: got %v, want %v", i, p, want)
}
}
@@ -1336,7 +1336,7 @@ func TestAlgorithmNegotiationError(t *testing.T) {
if !errors.As(err, &negotiationError) {
t.Fatalf("expected algorithm negotiation error, got %v", err)
}
expectedErrorString := fmt.Sprintf("ssh: handshake failed: ssh: no common algorithm for client to server cipher; we offered: %v, peer offered: %v",
expectedErrorString := fmt.Sprintf("ssh: handshake failed: ssh: no common algorithm for client to server cipher; we offered: %q, peer offered: %q",
clientConf.Ciphers, serverConf.Ciphers)
if err.Error() != expectedErrorString {
t.Fatalf("expected error string %q, got %q", expectedErrorString, err.Error())
@@ -1352,7 +1352,7 @@ func TestAlgorithmNegotiationError(t *testing.T) {
if !errors.As(err, &negotiationError) {
t.Fatalf("expected algorithm negotiation error, got %v", err)
}
expectedErrorString = fmt.Sprintf("ssh: no common algorithm for client to server cipher; we offered: %v, peer offered: %v",
expectedErrorString = fmt.Sprintf("ssh: no common algorithm for client to server cipher; we offered: %q, peer offered: %q",
serverConf.Ciphers, clientConf.Ciphers)
if err.Error() != expectedErrorString {
t.Fatalf("expected error string %q, got %q", expectedErrorString, err.Error())
+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
+123 -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
@@ -469,6 +485,13 @@ func parseRSA(in []byte) (out PublicKey, rest []byte, err error) {
return nil, nil, err
}
// 16384 bits is the largest RSA key OpenSSH will generate (ssh-keygen
// caps -b at 16384), so it is the practical upper bound for keys seen on
// the wire. Rejecting anything larger bounds the CPU spent verifying an
// attacker-supplied key and signature, mitigating a denial of service.
if w.N.BitLen() > 16384 {
return nil, nil, errors.New("ssh: rsa modulus too large")
}
if w.E.BitLen() > 24 {
return nil, nil, errors.New("ssh: exponent too large")
}
@@ -574,6 +597,24 @@ func checkDSAParams(param *dsa.Parameters) error {
return fmt.Errorf("ssh: unsupported DSA key size %d", l)
}
// 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
}
@@ -596,6 +637,14 @@ func parseDSA(in []byte) (out PublicKey, rest []byte, err error) {
return nil, nil, err
}
// The public value Y must be a non-zero element of the group, i.e.
// strictly between 0 and P. crypto/dsa.Verify does not range-check Y,
// so we reject out-of-range values here to prevent a maliciously
// oversized Y from slowing verification.
if w.Y.Sign() <= 0 || w.Y.Cmp(w.P) >= 0 {
return nil, nil, errors.New("ssh: DSA public value Y out of range")
}
key := &dsaPublicKey{
Parameters: param,
Y: w.Y,
@@ -774,7 +823,7 @@ func supportedEllipticCurve(curve elliptic.Curve) bool {
}
// parseECDSA parses an ECDSA key according to RFC 5656, section 3.1.
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
@@ -785,6 +834,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 {
@@ -869,11 +924,25 @@ type skFields struct {
Counter uint32
}
// flagUserPresence is the "user present" bit (UP) in the SK signature
// flags, matching the FIDO CTAP2 authenticatorData UP flag. See
// openssh/PROTOCOL.u2f.
const flagUserPresence = 0x01
// errSKMissingUserPresence is returned by SK key Verify methods when
// the signature does not assert user presence and the key was not
// marked as no-touch-required.
var errSKMissingUserPresence = errors.New("ssh: signature missing required user presence flag")
type skECDSAPublicKey struct {
// application is a URL-like string, typically "ssh:" for SSH.
// see openssh/PROTOCOL.u2f for details.
application string
ecdsa.PublicKey
// noTouchRequired, when true, disables the default user-presence
// check in Verify. It is set by skKeyWithoutUP on a clone of the
// key, never on an instance shared across authentication attempts.
noTouchRequired bool
}
func (k *skECDSAPublicKey) Type() string {
@@ -959,6 +1028,10 @@ func (k *skECDSAPublicKey) Verify(data []byte, sig *Signature) error {
return err
}
if skf.Flags&flagUserPresence == 0 && !k.noTouchRequired {
return errSKMissingUserPresence
}
blob := struct {
ApplicationDigest []byte `ssh:"rest"`
Flags byte
@@ -992,6 +1065,10 @@ type skEd25519PublicKey struct {
// see openssh/PROTOCOL.u2f for details.
application string
ed25519.PublicKey
// noTouchRequired, when true, disables the default user-presence
// check in Verify. It is set by skKeyWithoutUP on a clone of the
// key, never on an instance shared across authentication attempts.
noTouchRequired bool
}
func (k *skEd25519PublicKey) Type() string {
@@ -1066,6 +1143,10 @@ func (k *skEd25519PublicKey) Verify(data []byte, sig *Signature) error {
return err
}
if skf.Flags&flagUserPresence == 0 && !k.noTouchRequired {
return errSKMissingUserPresence
}
blob := struct {
ApplicationDigest []byte `ssh:"rest"`
Flags byte
@@ -1408,6 +1489,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
@@ -1577,10 +1669,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 OpenSSH-generated maximum, reject oversized or
// invalid exponents, and additionally bound the prime factors to
// avoid the expensive CRT coefficient recomputation in pk.Precompute.
if key.N.BitLen() > 16384 {
return nil, errors.New("ssh: rsa modulus too large")
}
if key.P.BitLen() > 8192 || key.Q.BitLen() > 8192 {
return nil, errors.New("ssh: rsa prime too large")
}
if key.E.BitLen() > 24 {
return nil, errors.New("ssh: exponent too large")
}
e := key.E.Int64()
if e < 3 || e&1 == 0 {
return nil, errors.New("ssh: incorrect exponent")
}
pk := &rsa.PrivateKey{
PublicKey: rsa.PublicKey{
N: key.N,
E: int(key.E.Int64()),
E: int(e),
},
D: key.D,
Primes: []*big.Int{key.P, key.Q},
+720 -13
View File
@@ -12,6 +12,7 @@ import (
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
@@ -19,6 +20,7 @@ import (
"errors"
"fmt"
"io"
"math/big"
"reflect"
"strings"
"testing"
@@ -216,6 +218,221 @@ func TestParseRSAPrivateKey(t *testing.T) {
}
}
func TestParseRSAModulusLimit(t *testing.T) {
// A 16384-bit modulus is the largest OpenSSH will generate and must be
// accepted.
rsa16384 := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAIAQC0vyqtnpzMZ8Th6gv1t3+7dDVd2X+7MwjqKe08/wrKZaIuCAHX7jglC8FdHfiOcPhLHAreSJLZGXiSzCTUExbp9Zdt7tHluKMQxZCnbk02V89ggc4KpptQqfcmjMNgrEPF4PGJBe3eSYu9m7A1ptm0buWxaKtA17O1c2q2CVNYmuajSUAfi8+AZcdyRAX8eqga3u77DEj4O+CCuNA908NYv9Pc7SbaCwCgX3FXh7MvlaYcruTk529psNk1SK6uwBNak3CLRrXUBo5yTO7cCO0gmlvd8SwtY5mPaDLLCJa+Ed2t9OCt8HCPrdkH3aiQBTP3k/Iofy3NRv+/1lenJP1qWB2rnyagoj+osrLc6m8PRd7GBje7KdO6nyY1D+q1MvX4zV1qNQfD+38N6PixVX1MGKw72sD+oLCDjHKS24BHfnfaE7f8n+Alp2mxnSz9tYqsN3JXrbbwblzH8YFOz9MMKkTY5lQNQFoKdTHoPVXBqcMlGQGgC/kkH9TH110MDE3L+Z0i9kG53QLcHNVC0cJzo6X77PQF1WoS33Ssf1s21kUB6fe/5G2L2Tuy635ingtnQMUazwJMli+y/3U7QqqcuJzITbLw/zozRzbqFRE4HHOyGkJSptraH4/3cjmYro8JL20A7EUDiXxmn+z+eoxaBWaEP2Rs5LH0ocrwYmBmSUIrPy4e5ESlRRVuv+prNUB1pYGnFZUI9HRRFxRFg+SxXZZp4mq8jzroRLZFAqKY1/eDgsCGoV1HG3QW1t0jGNkv8BL/Zpz7Hhmt0lRr7rVN+PJaTQx8NNUJZ/l7nMXp/+7aTbkyI8PiNyemko0+joDrkBWdcK0vi8Du6k3oQRBUsJ0aM0iPxMWQq/oYyHqpf6revycYTUdOGeckGWXuXLNRIZQ/PsKuw/teqDaBkkMKaQH2oXWMsacL6WDu5VYUQidOtaTgHX8AKVHF/vobTQfw7HgPKlyRN41dAX+YIjRABYoqU7M2MYPuoPYbqzTz7iLZyFP8FCJ3dRQBTWSeer/s+Ih6Ev1EsznVmAw9SKb5q8QCSYk5mMugUTDVETOQo10CGAF6094/PcJRXgag/uWs4k+13c+/HaeNzrHQyqzWnf9bFFHIvs7Wqov9k+nWv/TwSGplkyBk4bHfhEZNbl+b6T8sZXsv8mTT+INzKT4DCVZ5a/G+NQ6Xo26Aa6Se5DOdzdXOLZ7yIhOi+olip8AOXIIFLb7Vee1+q7Ri6ScUJEKxdEvec5EceXEtWs0U4AnuHgiH0ewJSX9yUp8T4ImCzu00JhTMOH2nXU3PJuGgZgrDifNWXGpFruIBjHWRKpESpy+9HPGoNoxSO/hzSfuM5LKvwX+I96cMK5OC+fKsBh59cMpU4YnqFNh59ZP/kaMDxAXgrVku5s9oB3Rgc7t1rfm6dqycBiwn4QnYJpk7M+gzAbrF/nty8y84ajNdhINXgMgvv3JLC5YTqznBPm3koHSOEF8f4xtCZtzjNN6jNjX8gEvxoRpC9Z+X15c8XBSFyDMBoi0FMDor619XJKgTxNiOYmeH/DLhKEb8MIaug8IFu34FKbG81IsWD+zwA1A5xqXEVlgsfEOcML+Oe2rulkpwQBWnsmY+f/Z80004Mytreo/ME2TDuPJPOi15D0j0CbwG7xzE8qrn0EomBE9mrwH3uuH7df3lzx1HiLAmh2s4MO5R64AoWeAPQW9lWZPzp+2dNgk+0qWpoEsMN+r91yWxUZHgeoC+GJKs13LItH32sGInyUrquMYQjEh44fAEFrMREcS4A7l+875GUmWC6i2MSLyvtAzuPS2IV0t9GU0ooH8cfG5JUeDnsJcVanJTR+XuXPetM+cDyGFTGri3lsCndE9maLgs9iDHtJxzKUBUUakaIPLsXtZD51cS4jhSbbHuzgC21BRtnBhCE2phVYbz4Tj0t90wBmemBaP2eVwv9p4s/JJAHEELvV7k+Gro4FozOoC2WBdqdTPDFB00la08O6ADBdjd2el2pRA7HCTG6v3qwA6RQJfm4UHOXaFYhhiNngxHB4VHZMvpd+YMEqNmtYOb4lzWwI13iHU0Rh0Sj4IaY9EwEy/KjR5dc2DjWGj11SFFCP1uG24fccF+LhtdMNDItI1mBxfaeRelYlsCZwtbbVLnuVQ5izuTzXa48x7CBaHnD3i09BZCuQITLX4d7KkD6CLWWTHrc5onLFJKxRF/p5AEFoqtN3vP9CsLXSYNKxFV5UbxAY5TnAoLFCCo0WbgvtAV77jWSbru9Oq/7ORN9smog4zNuYBZE1uXjxM2xszDduOym7+CxwpJMc7XPknt2XwiANaf2QANMMMH6lmnjTH1RVR7oH4+ts4xVcsdiO/QOlMp+Th9/KIMYyUevR18vHbs+88uxzIG28/58xLZTs6rZc71g9mGw9Q21ugL2sfTc7e0VDMnAmdg+92s2RXQMvkmx+oRc+IsFqgOZzjYcTmnJxZaXsoIsydDVcTalgmhK6/dD0grPLaGgaeXMEw2hN8p8seAHrGRW8+6WBD63NBYaAG0/zwuGOCHUo/BeG9bz39Hsz9Yvs5KvJiLhmM5K+8RlnxIY329REqdFZVyuXyy0NpDueFQelnd0j47Quc7GJGX3QycJiKpLolMtDnpnjgYOvdfycM+JEMZGwpLsBBE8R6vJ3RVczT6DdMtpVQ4l7kOzsPSYlp4qAv5fiqUboyv5eP7G7MOD/qSUwBnMS1p9Vm4Wr8B9w=="
if _, _, _, _, err := ParseAuthorizedKey([]byte(rsa16384)); err != nil {
t.Fatalf("ParseAuthorizedKey rejected a 16384-bit modulus: %v", err)
}
// A modulus larger than 16384 bits must be rejected to bound the cost of
// verifying an attacker-supplied key.
n := new(big.Int).Lsh(big.NewInt(1), 16384) // 16385 bits
pub, err := NewPublicKey(&rsa.PublicKey{N: n, E: 65537})
if err != nil {
t.Fatalf("NewPublicKey: %v", err)
}
_, err = ParsePublicKey(pub.Marshal())
if err == nil {
t.Fatal("ParsePublicKey accepted a modulus larger than 16384 bits; expected it to be rejected")
}
expectedError := "rsa modulus too large"
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("unexpected error message: got %q, want substring %q", err.Error(), expectedError)
}
}
func TestParsePrivateKeyRSAModulus16384(t *testing.T) {
rsa16384 := []byte(`-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAIFwAAAAdzc2gtcn
NhAAAAAwEAAQAACAEAy58lBbrFRB4r18MqVA625r4nzxjTkMlN5LxoG7ARC9L7px5lPs4o
GyOrs/03MRRbBL0GOVVSBqhel5kzJTRN1cwLEBYliiQBB9XVcbPavAbZnrxCVIvGBT63D1
nnMoBgI42Y49rTHePNLcVkNVvj+dHHwf1kiBijNYOrIvcuTqt6upAmSzPH7f3s4YTSMpUj
KJy2+8n0OS42Oz0jZFoBBBoOSaalYEKMmcfJE29hLhOiSYoDZqJJsCVSCOVC3RjYEc+Cj1
uJCNiCkI3pymbCMc7bGE6Oc4FHzdCs8ck16VFwM/VI50I2I9ADR5UXbjTEuGq+gprUO5Oc
7NbXGaqcgfOCywWjpRsGd3ORs5o30/1Jk1d5b8l1fuwpJuvIbqunvqM0AVe6STRTKjn0PE
cSJ9Z/RKIq06BxKN1wqx0WC611ikyLZ5aNLCjRF0ER3b7T2eFr9ib1d8ngSPpz8oFoQnL0
/+umKRD+iIqAwzvoljC2DQTxOP+b+2TxkI0lpdU/uzIQmkTRMHXMqKxHr5o6dRtggv8Gov
Ednqu7mjxX9t2I0rz6ZAnp4xv7fwe4OWBqP/or1QyW+RGk+x9HOacv1CfIFdndetHbfM6n
q7TWbyrdOl4zj6IA+CU5gJdISCpXoQkDNz1TbuoNlAzeg2IUyPchWVf2nm4v6a5fzggCBK
/nTtU6O2bvyccr1fpbcyVKKi4aG+hE9pY1Vc45c4ucxqYzBog5An/wpcvVnbm5KkmV+Ok9
K89qZdVheGqyzjSIw8ww7EiX0Ybp3eHWJAR6fR5jUk6uJZi2xPzHpB5MHTvilnBlSQ1Ew7
tjsd1z0NM5fb4PznwJxE3c4WB2nMKw2qcE552f5h6htNISmo+gjWmKSjXeCdSKdlFdA6rH
RtNQg3MnwIFIezfj4KnK78YcBOTHfDnPVChGgSxJ57K56OiVIPgxXjiowfvY91aJX+arC7
9lCPrvIZyq2PHIdbssEzLY5ECgS5RA3z8pvXbxOAJOt0kXnJi2BL6puPjd1+1FQgoXObak
Uk7BEVZG4s2mq5nCNN41o30DRanrGJO1jUrC5tB6tpzOLPlv9QgwUvnhzK/vZFiiYkttja
FkZyZyEPfmEDBpdFH1DgOVh77Eq/mKClH79aa6xUNTtqd3Qby5Oqiq3dJb7RkGEIn69iv+
XHuTYhNYPHTwPZVuN324MdMoqiWOPHWR9Vvlc167tKYelR/UGeBildWcP94cIofaulF8kt
XLG6UQAzV5aStC50dHLD0RzMvQXx/UXqd5leIO4o6TQ4tQWhYf13sfisfAZ2us3WQnErl9
+j/tIBoPvCQgbFid6iWyq4Ou3L60JIdV9dvRstNZ38PEfx0ScpZbcUO87dQA2bZ+ECGMGF
fEVJTnm/R4HYwWmajvVCCNqnSPCW0lCJdVn+AVZBtnqDaH7taZUeQeaEfuNqWMOcqUNaYL
HPC+Yuzz3gMjLWZwSY5n0xOVHAIAXH4P+y9aCyM6VwJ8/k6laH3w+V+UUtfVX/w6M9efzH
VJfQ0onk8LwA0OhZ2OXb92eCWC7us+ebw/AoXi0ugBwVxSllxuV2ZbiJINkN4Ryx059/RO
8rt9lrCRa/I++BX/7ZeMtGyc2sNo20tWH0t+3Q4+viJerUcVrTk9XN45YfRaQCUCios04r
EPLKx5Q67WInstEAyOUoa0BIHb/CI0JA6/atwA8eg4NvdUf1VWf+BQ3vOxqmw3eFou97lV
JmLRugqleuCYTL1vxb3hQrZlnHwAJLt9WvBd6tqHQpf/juYQEVKW11eQE2+6fDsQHtmZGa
yzwseoMV2UBJ5AuuP5X7fofEwdVgmYaqQ8amF26KaRGfMWiFb9hIR1+dhvxDW+oDzzTzoJ
YF7xl3F5eViRo6zYODVHhgaE843Pz8Q6RmQ4RIYEkaPID9VVtoYZmzRduYWQuIghiOzMhj
pnNwcJudLmNsSgpiNUfbvCPfgt06TVpcaK1sPaeTEwnQ8aOJoPKG7HM5yYwuQ3AWwjTmCO
OuEVS550ALAeXPD0e/aBnlDfZ+6yktKRhK7lq36/KeaiRP6MWr+il4bkbgLMOdO0HFAkTd
1ZBlW/vUnGwDl9mxfVxwPMlQxc4aL5ovXHSN+Vwu9oIk8fcS8Snp7/ftvsUOpCLafVnKdV
Pw30qRHLT8Xo+tPy57wuZMac3na/xe3HCtWulspDRWVVIbVKQADlgRThCT44hlr+80ePAD
r0x5iZPTp85KYGwcPGoUFzJ8vzrHAg7wF3yan837T2syz3/MX1bLyFmPUPdZ5BOPzw1Ctp
6TEdibD1/DbW2s7tY7FGtJh+luiFXLZZhYud2+3kdUUFukEHJII2IrYWLkpBHcwTj7A0UP
aCFS0ryXW3AiWMayNjw3qULbuOv/HB0Px7QNOTkos8TywMBqdcITz1o5uk7QVRcZ3V47CS
5t8aNLdjanQoDX+XXdo8SSGd7LqIWLRjNRQTKNv6D0+WIfbFnOMZYwORALX9bpsushaGJg
tYxInvSVdcYER34gee5kpcPAzWt8mdNMOZDatmi67B5y0oZh6tUZZpWc5ej+SVfxAJJfB7
ajiwWrtMKPCF1DupOJ99jo57UvcVyGmwVzuKbCthWCobwK5UtYhnIzQci/a5j1sZqUYt+n
hRW2LX5nG9XlL923aXhbcnUHQfdQikH/ON4KPZCanAkQGN3vb9TPrOtDDRWNgC5oZXOzZB
YqzrXChLsnFTi+TcIvkAABxA2VU8G9lVPBsAAAAHc3NoLXJzYQAACAEAy58lBbrFRB4r18
MqVA625r4nzxjTkMlN5LxoG7ARC9L7px5lPs4oGyOrs/03MRRbBL0GOVVSBqhel5kzJTRN
1cwLEBYliiQBB9XVcbPavAbZnrxCVIvGBT63D1nnMoBgI42Y49rTHePNLcVkNVvj+dHHwf
1kiBijNYOrIvcuTqt6upAmSzPH7f3s4YTSMpUjKJy2+8n0OS42Oz0jZFoBBBoOSaalYEKM
mcfJE29hLhOiSYoDZqJJsCVSCOVC3RjYEc+Cj1uJCNiCkI3pymbCMc7bGE6Oc4FHzdCs8c
k16VFwM/VI50I2I9ADR5UXbjTEuGq+gprUO5Oc7NbXGaqcgfOCywWjpRsGd3ORs5o30/1J
k1d5b8l1fuwpJuvIbqunvqM0AVe6STRTKjn0PEcSJ9Z/RKIq06BxKN1wqx0WC611ikyLZ5
aNLCjRF0ER3b7T2eFr9ib1d8ngSPpz8oFoQnL0/+umKRD+iIqAwzvoljC2DQTxOP+b+2Tx
kI0lpdU/uzIQmkTRMHXMqKxHr5o6dRtggv8GovEdnqu7mjxX9t2I0rz6ZAnp4xv7fwe4OW
BqP/or1QyW+RGk+x9HOacv1CfIFdndetHbfM6nq7TWbyrdOl4zj6IA+CU5gJdISCpXoQkD
Nz1TbuoNlAzeg2IUyPchWVf2nm4v6a5fzggCBK/nTtU6O2bvyccr1fpbcyVKKi4aG+hE9p
Y1Vc45c4ucxqYzBog5An/wpcvVnbm5KkmV+Ok9K89qZdVheGqyzjSIw8ww7EiX0Ybp3eHW
JAR6fR5jUk6uJZi2xPzHpB5MHTvilnBlSQ1Ew7tjsd1z0NM5fb4PznwJxE3c4WB2nMKw2q
cE552f5h6htNISmo+gjWmKSjXeCdSKdlFdA6rHRtNQg3MnwIFIezfj4KnK78YcBOTHfDnP
VChGgSxJ57K56OiVIPgxXjiowfvY91aJX+arC79lCPrvIZyq2PHIdbssEzLY5ECgS5RA3z
8pvXbxOAJOt0kXnJi2BL6puPjd1+1FQgoXObakUk7BEVZG4s2mq5nCNN41o30DRanrGJO1
jUrC5tB6tpzOLPlv9QgwUvnhzK/vZFiiYkttjaFkZyZyEPfmEDBpdFH1DgOVh77Eq/mKCl
H79aa6xUNTtqd3Qby5Oqiq3dJb7RkGEIn69iv+XHuTYhNYPHTwPZVuN324MdMoqiWOPHWR
9Vvlc167tKYelR/UGeBildWcP94cIofaulF8ktXLG6UQAzV5aStC50dHLD0RzMvQXx/UXq
d5leIO4o6TQ4tQWhYf13sfisfAZ2us3WQnErl9+j/tIBoPvCQgbFid6iWyq4Ou3L60JIdV
9dvRstNZ38PEfx0ScpZbcUO87dQA2bZ+ECGMGFfEVJTnm/R4HYwWmajvVCCNqnSPCW0lCJ
dVn+AVZBtnqDaH7taZUeQeaEfuNqWMOcqUNaYLHPC+Yuzz3gMjLWZwSY5n0xOVHAIAXH4P
+y9aCyM6VwJ8/k6laH3w+V+UUtfVX/w6M9efzHVJfQ0onk8LwA0OhZ2OXb92eCWC7us+eb
w/AoXi0ugBwVxSllxuV2ZbiJINkN4Ryx059/RO8rt9lrCRa/I++BX/7ZeMtGyc2sNo20tW
H0t+3Q4+viJerUcVrTk9XN45YfRaQCUCios04rEPLKx5Q67WInstEAyOUoa0BIHb/CI0JA
6/atwA8eg4NvdUf1VWf+BQ3vOxqmw3eFou97lVJmLRugqleuCYTL1vxb3hQrZlnHwAJLt9
WvBd6tqHQpf/juYQEVKW11eQE2+6fDsQHtmZGayzwseoMV2UBJ5AuuP5X7fofEwdVgmYaq
Q8amF26KaRGfMWiFb9hIR1+dhvxDW+oDzzTzoJYF7xl3F5eViRo6zYODVHhgaE843Pz8Q6
RmQ4RIYEkaPID9VVtoYZmzRduYWQuIghiOzMhjpnNwcJudLmNsSgpiNUfbvCPfgt06TVpc
aK1sPaeTEwnQ8aOJoPKG7HM5yYwuQ3AWwjTmCOOuEVS550ALAeXPD0e/aBnlDfZ+6yktKR
hK7lq36/KeaiRP6MWr+il4bkbgLMOdO0HFAkTd1ZBlW/vUnGwDl9mxfVxwPMlQxc4aL5ov
XHSN+Vwu9oIk8fcS8Snp7/ftvsUOpCLafVnKdVPw30qRHLT8Xo+tPy57wuZMac3na/xe3H
CtWulspDRWVVIbVKQADlgRThCT44hlr+80ePADr0x5iZPTp85KYGwcPGoUFzJ8vzrHAg7w
F3yan837T2syz3/MX1bLyFmPUPdZ5BOPzw1Ctp6TEdibD1/DbW2s7tY7FGtJh+luiFXLZZ
hYud2+3kdUUFukEHJII2IrYWLkpBHcwTj7A0UPaCFS0ryXW3AiWMayNjw3qULbuOv/HB0P
x7QNOTkos8TywMBqdcITz1o5uk7QVRcZ3V47CS5t8aNLdjanQoDX+XXdo8SSGd7LqIWLRj
NRQTKNv6D0+WIfbFnOMZYwORALX9bpsushaGJgtYxInvSVdcYER34gee5kpcPAzWt8mdNM
OZDatmi67B5y0oZh6tUZZpWc5ej+SVfxAJJfB7ajiwWrtMKPCF1DupOJ99jo57UvcVyGmw
VzuKbCthWCobwK5UtYhnIzQci/a5j1sZqUYt+nhRW2LX5nG9XlL923aXhbcnUHQfdQikH/
ON4KPZCanAkQGN3vb9TPrOtDDRWNgC5oZXOzZBYqzrXChLsnFTi+TcIvkAAAADAQABAAAI
AFiWz8OzY5nkSozf022ozTiMqMM4eOt4OZR3yA+rxW7Qhz5JQiFWDirolQ6E71tCEOt51d
hh34MYA7ePJqpcHDUVRgbkq8ZzLaOcC/YhGtxNWqbuHymreibUB079fVICelFdjJQto0ZQ
0vbD93ojlYceFvu2Y+O2XGOu+mkHA7Wkc4vxpUd4qtZHcKUZZV4udpJ3xEC9t6ydB2k0i0
5gviprr6WphC/iJEvPmRMElVI3ppa6HgqsNsUVJ6DJJhMNeQwerR3z5CXeFMgRhhLSLFEB
P19O5jkomPXZgTTcpsDw9pEUeXhr3SQtnw+otP30pVXa0zH9bLLS4SZFvmXjTZ5YNKJhvL
XbkS+tL0nlob5wZ29cUnApRR5IXwsY8CX+NsgBN2ISKfEpe7lWZ4VGIocEknBo4ZsbJcBy
v08jI3FHMWlPLiOOY7M/uuCUJdLE8GTN52u7vXY1dYgqtwFd/d9TJnalrrAVPbhoEedfDC
0z2jDF4rE6vEFexJ5wWl3Q7p5iBMkpgZ3E0prcAYBL6H0EwTOdAuUnZAyhiMhs0pSMA7g0
EfguO/zcMsossKD96pwVGrbheFm6rH25OQLDU1LJUAr5s5t47DZbrqVM0zKggomfbG1kPW
m4wFDLAN6s1V0xj52b583MtMWh57lflc1tf6vgUmLRa7UOcY4w+7fQVCF/MYugmFjAd7jr
6JerNJ4vWpqjrkVeSqwfQe2cY0QmEPMZlzwYL7niefjaUc4tH7ugtkV2Q0M01+hM+6gtQQ
d2sh5K4wp82Qj49XLMJAKFkw6/PpMK4xHHLJChwdnCMS3kjpx+0lkaESDUSUwkadHkO/pJ
CvLGMYA4uMIxDkLDAaZHdMboss/5ybBHJsH/lfP9hbHfL7KUaZRr27FbxJ68HiJp3QAG6/
TVyn4RQiV657OLb0GdGDeg7jKJFstXTXJ/qPLmUb279cR+eNwpfmdpCOskSZ/lCovCfcWC
/oUHzUdZOqRHRwYZ8+4DbqVAk8C4YzZ1VlOwJCVe2R3VlBlMTJiGdu18aBFD6SlJ8eacyd
q4hruvaxoG+ErEEOv9B9jmAU2r4SO+DlCcFHI1V2D3dn7A9T0wteQ1wHoERwJM6GJIvcim
06gaNRdPD0AHHGCrFKTmukGSrjw8le0az7fOpDBIEkiQZ4wQjAhIUa0o8pFr+yqqcc9JUR
+Baf77cKi+GCkDudH2/lf4clABKjMVGvF7J0krG4TJ/JGla6s+QqWbsHdBlDrhLAZulPgM
kdGUbEcesXSBY6Me2A42wOUexMFRPk9Dau0UG0AE8hfMx/O2XC2QWFflMiSSUiA304dv8p
xpdFecvho6OiPeYa7KyWw0+dgZ00a+fkUuV2+kIbbB3W6tu4R3sHshR1qJa49q5ZpOqQdq
yzvLSsWt3gDq2xwUYc5qVwhqMt0wEmax68qSZAyqmgwsOomfrpi4+20adOFImJ0q9jvHkk
Kq18x2iAu85HB3VYFU6dPe4WbzkYxk/ir72zFPNqlVDspVWGgCaLw8b/cPesPM03M67ebj
mlFL0bgD19qJyBnIzdeH4PUJafXw1a8/BepGOozAgXnzMgi0WmRIb+3Zf0YQ7Q5Wtu1DVX
rQ3/nz9L4DsdsVJzTwSKfS1xDnnWDuALa6ARAvRV2Y21CWLDMVCeFi/f+Y8EYljbhU63u7
KOKyG5jOr72qsjVwthfbYrCjZp0pRgksiDnzlZADXEWoe7Jbr8OR6rRZl8yFr0idknKQKU
kdAP34tWvcsi92kkwlF5l9kIdodzn3zRPIGoNllu7m/+hxqlYxOL5DWYEDOwDu3AvHnAgy
nVYVH1+/KV3jnqClH6xi5+sqLNoJMFjPF5bgT5ou/g6sSITVQXTEK+k3HWGJ699/ocmwW7
87om+9R+XINknHcfU6vCJmkNuVC/k7bPyBLzFrimLOob1GumYxlATh0/K9TV29DiqfagX+
+tPl5EwjTyrrOnT7PEbOF9EUaNEcODGeesFmOU6eVDfkGLOiffnD45PLzidpQ0dO/s69m/
RBzsUR+WsALG2aZciUweW5W7ER+d7H9HMWuEPueeQ52xn0SfQKo1yvrk/zWOnsG0CXH90W
uWm0qrzCOu2CqMaslGEGTV+IBIxQxbs42KKbhMnl6CsSqHJ0gJNe8akBojR6zaTQLQgqn0
ViQE/IopHxMucE7kI2tfZ/zujoB6aGNCkkrE6LCzi8f2iY3sw79dHO6ilSTbIZfJAszsfm
WLTRsi+y8/ZmUP+KbemU6XoIVfQ6XD6TATAlJX+Xqps/tDnK7RPBxhEvJaoGRRTHnPsxrH
KfmMgHoLmvLgvzwPXskwbij8IAuZTGilOjc9O38UF/byxtNlCiA6kswOUgOm0EOWFcZn1T
gl6pPSfHe8RnjBrdyAX2VOwYe71aESqJCVwcBxvLFj3YDbJIqHhvMQDdtoFaPOd7yPjkun
WeMlJO4MpoT5VCfVcIW3RP3hCwSpgonJ76Gjnd2yf87kpIYmWPOOVyS8CuOVxS24R4192q
hF9z+DWQb34dGEparBoZ2leK0Ex9ApMvg1naJINkSWPArFnikm5uA+bS6lQXEoQ4xJNHVs
lH8DG2D67UARTkErnZWiio3gIfdgqKkjdE7+ii918b12Qs9YrZi7i0pQtpfrtMbhBzD+uF
0HcEwi0O37c3DhVqNMAhu6Xm30PljXjjtZGedyO8V93C9VkA5E0D1VppVHAT+vM233IIX0
zlAAAEAG001autfUEdND18iVd4pEgjRMX2oOtUd/N/AQFFAGHu4U2bo29czCpKLX7J8m79
FcF0Q+jFLaK7VrVQmHa+3jKhYrpNjKj6+yx9XgRAQ5L/b+4hTSd8/J+vABvXhz00NgmYvk
WX0JLdi8FudfEIbTA9gOjgZNjmuxxT38ER8UYzFJts/4U9ZWbqn/7AZtBn95bKsvbVZkf9
CIyfJkOEuiUWj697d+hpHIspMSyBpLjjVsG+zRu9euhSHWbP3nHUERvuiOo5jyCwcFLKZn
ve+He4MX+np0PUm7Ug/Tsb8bouIysc+ncTBDuZXwRjf10Wlt1ZZI5UsmvImBLS7D18y4wd
zu1NzcHYRQ1BuiGSpvvy4xWo2szpmhYqu/8pBINgAVRyhHTJ5PwXKzULggC4L1gVAqLs6J
xUhlgsbm4CADpmVR6oy+8tzi8XHwu40nxTeWY5Cr75yio8+C/UwPiGKo9SSXaveXtBPLqB
RAMFupBQ3zIP9/BoNBjxZ8qINThI+qr7Q9jbKwLGVqAvGx5k4IEjQxTHdgvNQFJj47tKM+
6ClytN3CS+paNgT+UywJpLzyGppOZvuxDiq6WkF+DXd9KFefJG3XRHQEGcq0WKY4gkL8oO
CaVPbeciSOrxuudjun26oQurWyqstckbWDdtONwpLPECA1oRqfDzUicI6lTQIoWcPWrida
XAU85XWyxgkhVTYo3jPPoDurzwjeNE8CaPrD870FFBpyDkoSmh9S0qfuwxrZac1TKeqc2N
RtK0V1XYoHh4JDWZUiVy2jK/2CluVvLnDrf1V7Diz25B/HLFUiZ2uKGta+ccTZWMhDdnOH
d1+E7dlS623833w2P0c7BbEUbBWuUY3EOnUAz2mqdw12kaHLlSHUrHY1IBVQrmCn498Rr9
TbgT6sO5aTOROU8Y2iEaXXxzmCZYYKJA3Of3VXBXUb4CDx1aBdFm/PpvjI9S5qgEdgVwmK
9nT0Ag3mmnKpefdRirEbQUy9Cb6JaC+UXlQ8s0ToHs8Sm1LxbFtWnemJ0Sgcxjqv6BSDe/
GO+6bhXXwwNldR7NX8+3HS2cnEjEMjdOZChbwKCF+WCf7v0U6+fu9UvOibFGpDLAbeTKRv
dC1QZqGNvdadx0zB5mzlfZ6K+ANY9DPrKQjn3o2OrRYNDGM80yiIzyYMd4g+cSU81ge+s4
7UYBXRYVlB+yeFfmnyiVyf4SEaayr7TjUK60B+OwPPqTQAucEtc9TYKmQzVQzxmhTDW6k4
15DfIZfyMcPmWljoLq8dmLQl2I53jm0SQ4gQx7rYpOoJiVejkT2DCGHu2w47QWBYuEhkdv
rNniy5fZBw+9OBuIujsfEpxMGAQ58PmTH5+1KNqHL40AAAQBAOmmLGLXkJabeN8qcIOaTP
DY+c1++lOuMGpleUMH4fIP5EbLyYOldBFV5klrDKmCM1dc1/jUF/kppgMhOdr8XNhoBZNH
BrzibkbE41BI8XvqWNIT6UMrHxgnEsij/F9rVw75KMJu0oeZgmNaAg+hm8+nOwV7PL7/GG
IrbXPKpoTtKXob3w3ig1+AJBbz+tlyjL2sfjG8M+DUgaa8R4SCOk6msVWhUfGWHLXB/Cgl
xX5wqmSvmq8wxctvdSvJ/xL4tCV4SNvhbj4BKg+Tc9wpRR04r/8DFAb/e+bLao7v1sl0OV
65+qtsmng93d8jp0wSD9ZNHCplqdEixyEMMyrOOCFp3vPYunz3GMosDkciR8ClnxBc6kg4
bATgWUxV1lC+ycGttdTSRx583djPZ68a3IVIo01fmhIGioiYCpTyFHtlljOapO7Qe7wO7S
THnoT2bZDlSu2GIA9CXl8KHqOLGtF75tEfDW4yEg/LpyoMfNxjJqN0w6zMh6gI5N0F31BR
73BmjjJ1usQT8kJ3Kdaru+U9IxsQTyfk/cu1WiBrrB6FYQ7XMwKvXPyeijI9oHXDP2SZtV
ry7RwvMgEPLh5MjUX3y8nOWuqR0gkPVDtPiTQKogE07TXDyUVsIiNSlVEur5mpFWppC1nm
K1nPCC8Smr6d4pScg3UCLOR3ZJXyPtRSDn/n5q2eN2GymHcpvK6yx0RUVaXmw4iU9vNIOO
4s7fdEcpvDJog5TQHpWp3qP/S3Ug38xP56qN88+j01sa27PuNY7gF5UouewQjfvJ8xtCVF
vcVJ4XgxDAhdrptMX4MJPXpgS536f+8y03ahSJt+qzQacTfyzdJUDQ1jICD9UXrjSRw07F
/I4dvnm4rn9mJNGFUFDcnK4CD7/RwJRlAtJrUG7/eyxXLjrBcbP76CaERitVFgJuMwNSme
2QY2EXyUkQ+nwYJJt3Vjofo9UFWONWikQshnrKwebaCYwpK6U1A16cdPKFc8cUuUvMdEB3
Vb9Iq2LmxjbCYwGqTpUp2LE1VWIyuZgjI4lOTeegWlQ+7w3zEqxofO9fNy4KgrWUxBEh5V
z22mdB4/3So9Oq/zE1o4cpQwQFooa3OlC+A30JdRJZ9FAJDMQ49IOeki7xEKp5B+5omCcI
MlsmsEwj4/pSCjOBREos86rfNQMJ/N03kFl+dk0iE7NHxLo26nzAsCzV9kEK7R0SxtWZSC
HJbhMNoeNwQJPhEpKO5TKlnFDQ08VlSqIWTKc3yWqCZb68+c5AiMLfEqiI8pix/MKHkSKV
XRfgxwWVuli1UEhTRXm1ZSxVJaJLt0s6ePgEi7a8qRvep8uyh8/QgTUEW71CV9RpvklCm/
VSdgCrS/bwXo3YsAAAQBAN8ZoTRbrSIOXheb6KeZqNKJiiYjVYPHNoJYoVqtBNj6pPeNm9
iMeZhFs8TnHCYEcQ31+5LwCShFbKfaO+aBTHB57LpY+H8rG5OTdim+fhqH061pOTzDCZhQ
h7NUwIxK/hM0QBR/69cPOTFKSH0R997MAh/FougAgX5SwIp0rg6wFOG0sXla9oc3qZXuOE
7SGHIPe3ffQk70R8EdVTPuPATNe5a7qLUZEC6qf9nScpIyAEluCZF8VCf93oeqaTF3n/qG
gRnrj8S/zYdgFKONnZZL49NR4M1bQaOw0Ls2fR97rrWnqZFzevgZYSV7tgn+hf3JNo0nWm
LbubR8mLBUjx5u1nq1Od22TwUhS2eXXldPn0TLrofK1l0TzkwSsk4FypxNc/7uZVY0OJN+
FIQpeK4hVcbX/tIpC1gMXJVqybRRT+x7G2EC5CHLJFV+G0FwKawBqhnLD0wWPWY++fpQ5g
ZEWPu2cVZppC7MDSExum8+xQikprZdkOKBC5UGCqHfVXs60tcEHUB0VWWWVq0+WV8KH2XN
OONfl+dg5NdbgdLunAjD80oKh7rMRnJ686Sr3Jh5HSOnJ0QEZ+8G1Iz0vFWTHwAKtrzSBk
XsnnGbPZiFEybhNfae0VSgBgL6Ch/8yOsby3ZvGS1v1MY7XLM74o0IDHXPBzm4NoukM9dD
ELcLgYdDaX3RSq0U2FmyyU/lIaasOArSwf3bgAIFGQ8ux45LWW/vQ51aRCwiw6gwwCzHj5
bS4KfUTxX6E+tucHaq1S108E4UtkizFyyhNdeg49P1ljhGt0EfjIoHKJOevtH6sfvjt9U7
nx6vuAiyGOHb3Od2SzMYXM3evC102F2xGqsZpgAyQ/WVcj6kI4mCd0LoLsSqNUrcNaG93Z
ntt+lm3Wif11EZW37+Xe0TLg/Fi3VUaeju89grBiS4hvRBKz+yjiNwFJ8PvS47T91ORzTD
3SepgdMIi26umLMBqXfTKLnJvKmnYHdNfaaH5rW81EFh3xyPaoaatZ4vwJzj0IIQdeBIke
KiDfEjYAERQfXi/SG8cGONz/VWA97LroWuaNDa6YN1p6PzTUIEDTHdz5a1jnA7zApeHU7Q
EO0zINv0O0TXVDYPWjB6Wysnw7hIFg5HOAUz5DamkEWCifKQ0MH9dgKb9BiFPxwuvWrBIR
hqB5YPvUvyz8aYVm+RdbjMV0jCyUMxNe74/o02TrXbj5u50H2KfqD3qjFxu4BXzsw8sXBM
e0IdOmYjTAYEpHQRqNLhKdIcycTkXZ+K4SZwmK9VLI0ggn6NwSR8H1hZ7+GMnSTLtPm2u2
OthateBQGfnsGpTLxkpydh4jlYzTCp9bVRK8YXwsocglOcaRQH5ghfmgsAAAALbmljb2xh
QHAxNnM=
-----END OPENSSH PRIVATE KEY-----
`)
// A 16384-bit modulus is the largest OpenSSH will generate and must be
// accepted.
if _, err := ParseRawPrivateKey(rsa16384); err != nil {
t.Fatalf("ParseRawPrivateKey rejected a 16384-bit modulus: %v", err)
}
}
func TestParseECPrivateKey(t *testing.T) {
key := testPrivateKeys["ecdsa"]
@@ -272,18 +489,18 @@ func TestParseEncryptedPrivateKeysWithPassphrase(t *testing.T) {
}
func TestParseEncryptedPrivateKeysWithUnsupportedCiphers(t *testing.T) {
for _, tt := range testdata.UnsupportedCipherData {
t.Run(tt.Name, func(t *testing.T){
_, err := ParsePrivateKeyWithPassphrase(tt.PEMBytes, []byte(tt.EncryptionKey))
if err == nil {
t.Fatalf("expected 'unknown cipher' error for %q, got nil", tt.Name)
// If this cipher is now supported, remove it from testdata.UnsupportedCipherData
}
if !strings.Contains(err.Error(), "unknown cipher") {
t.Errorf("wanted 'unknown cipher' error, got %v", err.Error())
}
})
}
for _, tt := range testdata.UnsupportedCipherData {
t.Run(tt.Name, func(t *testing.T) {
_, err := ParsePrivateKeyWithPassphrase(tt.PEMBytes, []byte(tt.EncryptionKey))
if err == nil {
t.Fatalf("expected 'unknown cipher' error for %q, got nil", tt.Name)
// If this cipher is now supported, remove it from testdata.UnsupportedCipherData
}
if !strings.Contains(err.Error(), "unknown cipher") {
t.Errorf("wanted 'unknown cipher' error, got %v", err.Error())
}
})
}
}
func TestParseEncryptedPrivateKeysWithIncorrectPassphrase(t *testing.T) {
@@ -296,6 +513,38 @@ func TestParseEncryptedPrivateKeysWithIncorrectPassphrase(t *testing.T) {
}
}
func TestParseEncryptedPrivateKeyExcessiveBcryptRounds(t *testing.T) {
// Craft a minimal openssh-key-v1 blob whose KdfOpts declares a bcrypt
// round count above the accepted maximum. The check must reject the file
// before bcrypt_pbkdf is invoked, so the rest of the blob (public key,
// encrypted body) can be empty.
kdfOpts := Marshal(struct {
Salt []byte
Rounds uint32
}{
Salt: []byte("salt-not-used"),
Rounds: (1 << 11) + 1,
})
header := Marshal(openSSHEncryptedPrivateKey{
CipherName: "aes256-ctr",
KdfName: "bcrypt",
KdfOpts: string(kdfOpts),
NumKeys: 1,
})
pemBytes := pem.EncodeToMemory(&pem.Block{
Type: "OPENSSH PRIVATE KEY",
Bytes: append([]byte(privateKeyAuthMagic), header...),
})
_, err := ParseRawPrivateKeyWithPassphrase(pemBytes, []byte("password"))
if err == nil {
t.Fatal("expected error for excessive bcrypt rounds, got nil")
}
if !strings.Contains(err.Error(), "bcrypt KDF rounds") {
t.Errorf("got error %q, want substring %q", err.Error(), "bcrypt KDF rounds")
}
}
func TestParseDSA(t *testing.T) {
// We actually exercise the ParsePrivateKey codepath here, as opposed to
// using the ParseRawPrivateKey+NewSignerFromKey path that testdata_test.go
@@ -356,6 +605,72 @@ func TestMarshalParsePublicKey(t *testing.T) {
}
}
func TestParseDSAHugeQ(t *testing.T) {
P := new(big.Int).Lsh(big.NewInt(1), 1023)
Q := new(big.Int).Lsh(big.NewInt(1), 20000) // very large
// G and Y: Dummy values, just needs to be < P to pass that specific check
G := big.NewInt(2)
Y := big.NewInt(5)
rawKey := struct {
P, Q, G, Y *big.Int
}{
P: P,
Q: Q,
G: G,
Y: Y,
}
inputBytes := Marshal(&rawKey)
_, _, err := parseDSA(inputBytes)
if err == nil {
t.Fatal("parseDSA accepted a DSA key with large Q")
}
expectedError := "ssh: unsupported DSA sub-prime size"
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("unexpected error message: got %q, want substring %q", err.Error(), expectedError)
}
}
func TestParseDSAYOutOfRange(t *testing.T) {
// Valid 1024/160 parameters (values don't need to be a real DSA group,
// they only need to pass the checkDSAParams bit-length checks and the
// G < P / G > 0 checks).
P := new(big.Int).Lsh(big.NewInt(1), 1023)
P.SetBit(P, 0, 1) // make P odd so it can pass as a prime candidate shape
Q := new(big.Int).Lsh(big.NewInt(1), 159)
Q.SetBit(Q, 0, 1)
G := big.NewInt(2)
for _, tc := range []struct {
name string
Y *big.Int
}{
{"Y_zero", big.NewInt(0)},
{"Y_negative", big.NewInt(-1)},
{"Y_equals_P", new(big.Int).Set(P)},
{"Y_greater_than_P", new(big.Int).Add(P, big.NewInt(1))},
{"Y_much_greater_than_P", new(big.Int).Lsh(big.NewInt(1), 20000)},
} {
t.Run(tc.name, func(t *testing.T) {
rawKey := struct {
P, Q, G, Y *big.Int
}{P: P, Q: Q, G: G, Y: tc.Y}
_, _, err := parseDSA(Marshal(&rawKey))
if err == nil {
t.Fatalf("parseDSA accepted a DSA key with Y=%s (P=%s)", tc.Y, P)
}
expectedError := "DSA public value Y out of range"
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("unexpected error message: got %q, want substring %q", err.Error(), expectedError)
}
})
}
}
func TestMarshalPrivateKey(t *testing.T) {
tests := []struct {
name string
@@ -540,6 +855,65 @@ func TestInvalidEntry(t *testing.T) {
}
}
func TestAuthorizedKeyOptionWithoutKeyType(t *testing.T) {
_, pubSerialized := getTestKey()
for _, opt := range []string{"restrict", "no-pty", "no-port-forwarding", "no-agent-forwarding"} {
// The key type ("ssh-rsa") is intentionally omitted.
line := opt + " " + pubSerialized
testAuthorizedKeys(t, []byte(line), []testAuthResult{
{nil, nil, "", "", false},
})
}
}
func TestAuthorizedKeyTypeMismatch(t *testing.T) {
_, pubSerialized := getTestKey() // an ssh-rsa key
lines := []string{
"ssh-ed25519 " + pubSerialized + " user@host",
"restrict ssh-ed25519 " + pubSerialized + " user@host",
"rsa-sha2-512 " + pubSerialized + " user@host",
}
for _, line := range lines {
testAuthorizedKeys(t, []byte(line), []testAuthResult{
{nil, nil, "", "", false},
})
}
}
func TestAuthorizedKeyCertificate(t *testing.T) {
certLine := testdata.SSHCertificates["rsa-user-testcertificate"]
key, _, options, _, err := ParseAuthorizedKey(certLine)
if err != nil {
t.Fatalf("ParseAuthorizedKey on certificate: %v", err)
}
if _, ok := key.(*Certificate); !ok {
t.Fatalf("got %T, want *Certificate", key)
}
if len(options) != 0 {
t.Errorf("got options %v, want none", options)
}
key, _, options, _, err = ParseAuthorizedKey(append([]byte("cert-authority "), certLine...))
if err != nil {
t.Fatalf("ParseAuthorizedKey on cert-authority certificate: %v", err)
}
if _, ok := key.(*Certificate); !ok {
t.Fatalf("got %T, want *Certificate", key)
}
if !reflect.DeepEqual(options, []string{"cert-authority"}) {
t.Errorf("got options %v, want [cert-authority]", options)
}
}
func TestAuthorizedKeyOptionWithKeyType(t *testing.T) {
pub, pubSerialized := getTestKey()
line := "restrict ssh-rsa " + pubSerialized + " user@host"
testAuthorizedKeys(t, []byte(line), []testAuthResult{
{pub, []string{"restrict"}, "user@host", "", true},
})
}
var knownHostsParseTests = []struct {
input string
err string
@@ -619,6 +993,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, "",
},
}
@@ -633,7 +1014,7 @@ func TestKnownHostsParsing(t *testing.T) {
input := test.input
if strings.Contains(input, rsaKeyToken) {
expectedKey = rsaPub
input = strings.Replace(test.input, rsaKeyToken, rsaPubSerialized, -1)
input = strings.ReplaceAll(test.input, rsaKeyToken, rsaPubSerialized)
}
marker, hosts, pubKey, comment, rest, err := ParseKnownHosts([]byte(input))
@@ -760,6 +1141,296 @@ func TestSKKeys(t *testing.T) {
}
}
// skTestHarness builds SK-formatted signatures over a fixed payload
// using a caller-supplied signing function, letting tests vary the UP
// flag byte without duplicating the wire-format scaffolding.
type skTestHarness struct {
format string
application string
data []byte
// sign takes the SHA-256 digest of the marshalled SK blob and
// returns the value to embed in Signature.Blob. For ECDSA keys the
// helper feeds the digest to ecdsa.Sign; for ed25519 the helper
// feeds the raw marshalled blob to ed25519.Sign.
signDigest func(digest []byte) []byte
signBlob func(blob []byte) []byte
}
func (h skTestHarness) sign(t *testing.T, flags byte) *Signature {
t.Helper()
hsh := sha256.New()
hsh.Write([]byte(h.application))
appDigest := hsh.Sum(nil)
hsh.Reset()
hsh.Write(h.data)
dataDigest := hsh.Sum(nil)
var counter uint32 = 1
blob := struct {
ApplicationDigest []byte `ssh:"rest"`
Flags byte
Counter uint32
MessageDigest []byte `ssh:"rest"`
}{appDigest, flags, counter, dataDigest}
marshalled := Marshal(blob)
var sigBlob []byte
if h.signDigest != nil {
hsh.Reset()
hsh.Write(marshalled)
sigBlob = h.signDigest(hsh.Sum(nil))
} else {
sigBlob = h.signBlob(marshalled)
}
return &Signature{
Format: h.format,
Blob: sigBlob,
Rest: Marshal(struct {
Flags byte
Counter uint32
}{flags, counter}),
}
}
func TestSKUserPresence(t *testing.T) {
ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
h := skTestHarness{
format: "sk-ecdsa-sha2-nistp256@openssh.com",
application: "ssh:",
data: []byte("test data"),
signDigest: func(digest []byte) []byte {
r, s, err := ecdsa.Sign(rand.Reader, ecKey, digest)
if err != nil {
t.Fatal(err)
}
return Marshal(struct{ R, S *big.Int }{r, s})
},
}
pk := &skECDSAPublicKey{
application: h.application,
PublicKey: ecKey.PublicKey,
}
// Valid signature with UP=1 should pass.
if err := pk.Verify(h.data, h.sign(t, flagUserPresence)); err != nil {
t.Errorf("Verify failed with UP=1: %v", err)
}
// Valid signature with UP=0 should fail with the user-presence sentinel.
sigNoUP := h.sign(t, 0)
if err := pk.Verify(h.data, sigNoUP); !errors.Is(err, errSKMissingUserPresence) {
t.Errorf("expected errSKMissingUserPresence, got: %v", err)
}
// UV set but UP clear must still fail: we only waive UP, never UV-only.
if err := pk.Verify(h.data, h.sign(t, 0x04)); !errors.Is(err, errSKMissingUserPresence) {
t.Errorf("UV-only (flags=0x04): expected errSKMissingUserPresence, got: %v", err)
}
// With noTouchRequired, UP=0 passes; UP=1+UV=1 also passes.
pk.noTouchRequired = true
if err := pk.Verify(h.data, sigNoUP); err != nil {
t.Errorf("Verify with noTouchRequired failed: %v", err)
}
if err := pk.Verify(h.data, h.sign(t, flagUserPresence|0x04)); err != nil {
t.Errorf("Verify UP|UV with noTouchRequired failed: %v", err)
}
}
func TestSKKeyWithoutUP(t *testing.T) {
ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
edPub, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
skEC := &skECDSAPublicKey{PublicKey: ecKey.PublicKey}
skED := &skEd25519PublicKey{PublicKey: edPub}
certEC := &Certificate{Key: &skECDSAPublicKey{PublicKey: ecKey.PublicKey}}
certED := &Certificate{Key: &skEd25519PublicKey{PublicKey: edPub}}
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
rsaPub, err := NewPublicKey(&rsaKey.PublicKey)
if err != nil {
t.Fatal(err)
}
certRSA := &Certificate{Key: rsaPub}
// SK raw keys: return a clone with the flag set, original untouched.
gotEC := skKeyWithoutUP(skEC)
if gotEC == skEC {
t.Error("skECDSAPublicKey: expected a clone, got the original pointer")
}
if !gotEC.(*skECDSAPublicKey).noTouchRequired {
t.Error("skECDSAPublicKey clone: noTouchRequired not set")
}
if skEC.noTouchRequired {
t.Error("skECDSAPublicKey: original mutated")
}
gotED := skKeyWithoutUP(skED)
if gotED == skED {
t.Error("skEd25519PublicKey: expected a clone, got the original pointer")
}
if !gotED.(*skEd25519PublicKey).noTouchRequired {
t.Error("skEd25519PublicKey clone: noTouchRequired not set")
}
if skED.noTouchRequired {
t.Error("skEd25519PublicKey: original mutated")
}
// Certificate wrapping SK: return a clone of the cert with a cloned
// SK key inside. Neither the original cert nor the original inner
// key must be mutated.
originalInnerEC := certEC.Key
gotCertEC := skKeyWithoutUP(certEC)
if gotCertEC == certEC {
t.Error("*Certificate(SK ecdsa): expected a clone, got the original pointer")
}
if got := gotCertEC.(*Certificate).Key.(*skECDSAPublicKey); !got.noTouchRequired {
t.Error("*Certificate(SK ecdsa): inner clone missing noTouchRequired")
}
if certEC.Key != originalInnerEC {
t.Error("*Certificate(SK ecdsa): original cert's Key pointer mutated")
}
if originalInnerEC.(*skECDSAPublicKey).noTouchRequired {
t.Error("*Certificate(SK ecdsa): original inner key mutated")
}
gotCertED := skKeyWithoutUP(certED)
if gotCertED == certED {
t.Error("*Certificate(SK ed25519): expected a clone, got the original pointer")
}
if got := gotCertED.(*Certificate).Key.(*skEd25519PublicKey); !got.noTouchRequired {
t.Error("*Certificate(SK ed25519): inner clone missing noTouchRequired")
}
// Non-SK key inside a cert: return original unchanged (nothing to clone).
if got := skKeyWithoutUP(certRSA); got != certRSA {
t.Error("*Certificate(RSA): expected the original pointer back")
}
// Plain non-SK key: return original unchanged.
if got := skKeyWithoutUP(rsaPub); got != rsaPub {
t.Error("rsaPublicKey: expected the original pointer back")
}
// Pathological: *Certificate whose Key is itself a *Certificate.
// The SSH cert format forbids this and parseCert rejects it, but
// a Go caller can still construct such a value. skKeyWithoutUP
// must not recurse into it (or panic); it returns the input
// unchanged. This also defends against a hypothetical cycle built
// from hand-constructed Certificate pointers.
nestedCert := &Certificate{Key: &Certificate{Key: skEC}}
if got := skKeyWithoutUP(nestedCert); got != nestedCert {
t.Error("*Certificate wrapping *Certificate: expected the original pointer back")
}
selfCycle := &Certificate{}
selfCycle.Key = selfCycle
if got := skKeyWithoutUP(selfCycle); got != selfCycle {
t.Error("self-referential *Certificate: expected the original pointer back")
}
}
func TestNoTouchAllowed(t *testing.T) {
ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
sk := &skECDSAPublicKey{PublicKey: ecKey.PublicKey}
certNoExt := &Certificate{Key: sk}
certOptOut := &Certificate{
Key: sk,
Permissions: Permissions{Extensions: map[string]string{"no-touch-required": ""}},
}
// Non-empty value: OpenSSH writes "" but any value must be accepted
// because the check is presence-only.
certOptOutNonEmpty := &Certificate{
Key: sk,
Permissions: Permissions{Extensions: map[string]string{"no-touch-required": "yes"}},
}
// no-touch-required belongs in Extensions, never CriticalOptions.
// Putting it in CriticalOptions must NOT be treated as opt-out.
certCritOnly := &Certificate{
Key: sk,
Permissions: Permissions{CriticalOptions: map[string]string{"no-touch-required": ""}},
}
permsEmpty := &Permissions{}
permsOptOut := &Permissions{Extensions: map[string]string{"no-touch-required": ""}}
permsCritOnly := &Permissions{CriticalOptions: map[string]string{"no-touch-required": ""}}
cases := []struct {
name string
pub PublicKey
perms *Permissions
want bool
}{
{"nil perms, no cert", sk, nil, false},
{"empty perms, no cert", sk, permsEmpty, false},
{"perms opt-out, raw key", sk, permsOptOut, true},
{"nil perms, cert opt-out", certOptOut, nil, true},
{"nil perms, cert opt-out non-empty value", certOptOutNonEmpty, nil, true},
{"nil perms, cert no ext", certNoExt, nil, false},
{"perms opt-out, cert no ext", certNoExt, permsOptOut, true},
{"empty perms, cert opt-out", certOptOut, permsEmpty, true},
// Negative controls: CriticalOptions must not waive UP.
{"critical-options only, raw key", sk, permsCritOnly, false},
{"critical-options only, cert", certCritOnly, nil, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := noTouchAllowed(tc.pub, tc.perms); got != tc.want {
t.Errorf("noTouchAllowed = %v, want %v", got, tc.want)
}
})
}
}
func TestSKUserPresenceEd25519(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
h := skTestHarness{
format: "sk-ssh-ed25519@openssh.com",
application: "ssh:",
data: []byte("test data"),
signBlob: func(blob []byte) []byte {
return ed25519.Sign(priv, blob)
},
}
pk := &skEd25519PublicKey{
application: h.application,
PublicKey: pub,
}
if err := pk.Verify(h.data, h.sign(t, flagUserPresence)); err != nil {
t.Errorf("Verify failed with UP=1: %v", err)
}
sigNoUP := h.sign(t, 0)
if err := pk.Verify(h.data, sigNoUP); !errors.Is(err, errSKMissingUserPresence) {
t.Errorf("expected errSKMissingUserPresence, got: %v", err)
}
pk.noTouchRequired = true
if err := pk.Verify(h.data, sigNoUP); err != nil {
t.Errorf("Verify with noTouchRequired failed: %v", err)
}
}
func TestNewSignerWithAlgos(t *testing.T) {
algorithSigner, ok := testSigners["rsa"].(AlgorithmSigner)
if !ok {
@@ -863,3 +1534,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)
}
})
}
}
+38 -8
View File
@@ -142,6 +142,15 @@ func keyEq(a, b ssh.PublicKey) bool {
return bytes.Equal(a.Marshal(), b.Marshal())
}
// plainKeyBlob returns the serialized public portion of key: for a
// certificate this is the certified key, otherwise the key itself.
func plainKeyBlob(key ssh.PublicKey) string {
if cert, ok := key.(*ssh.Certificate); ok {
return string(cert.Key.Marshal())
}
return string(key.Marshal())
}
// IsHostAuthority can be used as a callback in ssh.CertChecker
func (db *hostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool {
h, p, err := net.SplitHostPort(address)
@@ -160,8 +169,13 @@ func (db *hostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool
// IsRevoked can be used as a callback in ssh.CertChecker
func (db *hostKeyDB) IsRevoked(key *ssh.Certificate) bool {
_, ok := db.revoked[string(key.Marshal())]
return ok
if _, ok := db.revoked[plainKeyBlob(key)]; ok {
return true
}
if _, ok := db.revoked[plainKeyBlob(key.SignatureKey)]; ok {
return true
}
return false
}
const markerCert = "@cert-authority"
@@ -173,7 +187,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) {
@@ -183,12 +197,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")
}
@@ -204,6 +223,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
}
@@ -214,7 +237,7 @@ func (db *hostKeyDB) parseLine(line []byte, filename string, linenum int) error
}
if marker == markerRevoked {
db.revoked[string(key.Marshal())] = &KnownKey{
db.revoked[plainKeyBlob(key)] = &KnownKey{
Key: key,
Filename: filename,
Line: linenum,
@@ -327,7 +350,7 @@ func (r *RevokedError) Error() string {
// check checks a key against the host database. This should not be
// used for verifying certificates.
func (db *hostKeyDB) check(address string, remote net.Addr, remoteKey ssh.PublicKey) error {
if revoked := db.revoked[string(remoteKey.Marshal())]; revoked != nil {
if revoked := db.revoked[plainKeyBlob(remoteKey)]; revoked != nil {
return &RevokedError{Revoked: *revoked}
}
@@ -382,7 +405,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
}
@@ -530,3 +553,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")
}
+196 -1
View File
@@ -6,9 +6,13 @@ package knownhosts
import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"fmt"
"net"
"reflect"
"strings"
"testing"
"golang.org/x/crypto/ssh"
@@ -272,7 +276,107 @@ func TestWildcardMatch(t *testing.T) {
}
}
// TODO(hanwen): test coverage for certificates.
func TestRevokedCA(t *testing.T) {
_, caPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
caSigner, err := ssh.NewSignerFromKey(caPriv)
if err != nil {
t.Fatal(err)
}
caKey := caSigner.PublicKey()
_, hostPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
hostKey, err := ssh.NewPublicKey(hostPriv.Public())
if err != nil {
t.Fatal(err)
}
cert := &ssh.Certificate{
CertType: ssh.HostCert,
Key: hostKey,
ValidBefore: ssh.CertTimeInfinity,
ValidPrincipals: []string{"server.org"},
}
if err := cert.SignCert(rand.Reader, caSigner); err != nil {
t.Fatal(err)
}
caLine := "ssh-ed25519 " + serialize(caKey)[len("ssh-ed25519 "):]
knownHostsData := "@revoked server.org " + caLine + "\n" +
"@cert-authority server.org " + caLine + "\n"
db := testDB(t, knownHostsData)
if !db.IsRevoked(cert) {
t.Error("IsRevoked returned false for certificate signed by revoked CA")
}
}
func TestRevokedCertifiedKey(t *testing.T) {
_, caPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
caSigner, err := ssh.NewSignerFromKey(caPriv)
if err != nil {
t.Fatal(err)
}
_, hostPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
hostKey, err := ssh.NewPublicKey(hostPriv.Public())
if err != nil {
t.Fatal(err)
}
signCert := func(serial uint64, principal string) *ssh.Certificate {
cert := &ssh.Certificate{
CertType: ssh.HostCert,
Key: hostKey,
Serial: serial,
ValidBefore: ssh.CertTimeInfinity,
ValidPrincipals: []string{principal},
}
if err := cert.SignCert(rand.Reader, caSigner); err != nil {
t.Fatal(err)
}
return cert
}
cert := signCert(1, "server.org")
// Revoking the host key revokes any certificate for it, even when
// the CA is trusted, matching OpenSSH's check_key_not_revoked.
db := testDB(t, "@revoked server.org "+serialize(hostKey)+"\n"+
"@cert-authority server.org "+serialize(caSigner.PublicKey())+"\n")
if !db.IsRevoked(cert) {
t.Error("IsRevoked returned false for certificate with revoked certified key")
}
// Conversely, a @revoked line containing a certificate revokes the
// plain key it certifies.
db = testDB(t, "@revoked server.org "+serialize(cert)+"\n")
if err := db.check("server.org:22", testAddr, hostKey); err == nil {
t.Error("no error for plain key revoked via certificate")
} else if _, ok := err.(*RevokedError); !ok {
t.Errorf("got %T (%v), want *RevokedError", err, err)
}
// Since only public portions are compared, revoking a certificate
// also revokes any sibling certificate certifying the same key,
// whatever its serial number and principals.
sibling := signCert(2, "other.org")
db = testDB(t, "@revoked * "+serialize(cert)+"\n"+
"@cert-authority * "+serialize(caSigner.PublicKey())+"\n")
if !db.IsRevoked(sibling) {
t.Error("IsRevoked returned false for sibling certificate of a revoked certificate")
}
}
const testHostname = "hostname"
@@ -367,3 +471,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)
}
}
+1 -1
View File
@@ -44,7 +44,7 @@ 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, d.Message)
}
// 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", `"line1\nline2\nline3"`},
{"has\x00null\x00bytes", `"has\x00null\x00bytes"`},
{"\x1b[31mred\x1b[0m", `"\x1b[31mred\x1b[0m"`},
{"newline\r\nCRLF", `"newline\r\nCRLF"`},
{"tab\there", `"tab\there"`},
} {
t.Run(tc.in, func(t *testing.T) {
d := &disconnectMsg{Reason: 11, Message: tc.in}
got := d.Error()
if !strings.Contains(got, tc.want) {
t.Errorf("got %q, want %q", got, tc.want)
}
if bad := "\n\r\x00\x1b"; strings.ContainsAny(got, bad) {
t.Errorf("got %q, want none of %q", got, bad)
}
})
}
}
func randomBytes(out []byte, rand *rand.Rand) {
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)
}
}

Some files were not shown because too many files have changed in this diff Show More