From 0fb843a472225645e917c84f1f9744757f0bab14 Mon Sep 17 00:00:00 2001 From: Nicola Date: Sun, 8 Feb 2026 15:28:56 +0100 Subject: [PATCH] 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 Reviewed-by: Neal Patel Reviewed-by: Neal Patel Auto-Submit: Neal Patel LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com --- ssh/agent/keyring.go | 13 ++++++++++--- ssh/agent/keyring_test.go | 15 +++++++++++++++ ssh/test/agent_unix_test.go | 5 ++--- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/ssh/agent/keyring.go b/ssh/agent/keyring.go index 75328bb..a9e29b6 100644 --- a/ssh/agent/keyring.go +++ b/ssh/agent/keyring.go @@ -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{} } @@ -146,7 +148,8 @@ func (r *keyring) List() ([]*Key, error) { // 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. +// Add returns an error if key contains ConstraintExtensions or +// ConfirmBeforeUse. func (r *keyring) Add(key AddedKey) error { r.mu.Lock() defer r.mu.Unlock() @@ -154,6 +157,10 @@ func (r *keyring) Add(key AddedKey) error { 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") } diff --git a/ssh/agent/keyring_test.go b/ssh/agent/keyring_test.go index 6c08252..f120392 100644 --- a/ssh/agent/keyring_test.go +++ b/ssh/agent/keyring_test.go @@ -144,3 +144,18 @@ func TestAddKeyWithConstraints(t *testing.T) { 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") + } +} diff --git a/ssh/test/agent_unix_test.go b/ssh/test/agent_unix_test.go index 9257bfe..fb99c90 100644 --- a/ssh/test/agent_unix_test.go +++ b/ssh/test/agent_unix_test.go @@ -24,9 +24,8 @@ func TestAgentForward(t *testing.T) { t.Fatalf("Error adding key: %s", err) } if err := keyring.Add(agent.AddedKey{ - PrivateKey: testPrivateKeys["ecdsa"], - ConfirmBeforeUse: true, - LifetimeSecs: 3600, + PrivateKey: testPrivateKeys["ecdsa"], + LifetimeSecs: 3600, }); err != nil { t.Fatalf("Error adding key with constraints: %s", err) }