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>
264 lines
5.8 KiB
Go
264 lines
5.8 KiB
Go
// Copyright 2014 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 (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
type privKey struct {
|
|
signer ssh.Signer
|
|
comment string
|
|
expire *time.Time
|
|
}
|
|
|
|
type keyring struct {
|
|
mu sync.Mutex
|
|
keys []privKey
|
|
|
|
locked bool
|
|
passphrase []byte
|
|
}
|
|
|
|
var errLocked = errors.New("agent: locked")
|
|
|
|
// 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{}
|
|
}
|
|
|
|
// RemoveAll removes all identities.
|
|
func (r *keyring) RemoveAll() error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.locked {
|
|
return errLocked
|
|
}
|
|
|
|
r.keys = nil
|
|
return nil
|
|
}
|
|
|
|
// removeLocked does the actual key removal. The caller must already be holding the
|
|
// keyring mutex.
|
|
func (r *keyring) removeLocked(want []byte) error {
|
|
found := false
|
|
for i := 0; i < len(r.keys); {
|
|
if bytes.Equal(r.keys[i].signer.PublicKey().Marshal(), want) {
|
|
found = true
|
|
r.keys[i] = r.keys[len(r.keys)-1]
|
|
r.keys = r.keys[:len(r.keys)-1]
|
|
continue
|
|
} else {
|
|
i++
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return errors.New("agent: key not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Remove removes all identities with the given public key.
|
|
func (r *keyring) Remove(key ssh.PublicKey) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.locked {
|
|
return errLocked
|
|
}
|
|
|
|
return r.removeLocked(key.Marshal())
|
|
}
|
|
|
|
// Lock locks the agent. Sign and Remove will fail, and List will return an empty list.
|
|
func (r *keyring) Lock(passphrase []byte) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.locked {
|
|
return errLocked
|
|
}
|
|
|
|
r.locked = true
|
|
r.passphrase = passphrase
|
|
return nil
|
|
}
|
|
|
|
// Unlock undoes the effect of Lock
|
|
func (r *keyring) Unlock(passphrase []byte) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if !r.locked {
|
|
return errors.New("agent: not locked")
|
|
}
|
|
if 1 != subtle.ConstantTimeCompare(passphrase, r.passphrase) {
|
|
return fmt.Errorf("agent: incorrect passphrase")
|
|
}
|
|
|
|
r.locked = false
|
|
r.passphrase = nil
|
|
return nil
|
|
}
|
|
|
|
// expireKeysLocked removes expired keys from the keyring. If a key was added
|
|
// with a lifetimesecs constraint and seconds >= lifetimesecs seconds have
|
|
// elapsed, it is removed. The caller *must* be holding the keyring mutex.
|
|
func (r *keyring) expireKeysLocked() {
|
|
for _, k := range r.keys {
|
|
if k.expire != nil && time.Now().After(*k.expire) {
|
|
r.removeLocked(k.signer.PublicKey().Marshal())
|
|
}
|
|
}
|
|
}
|
|
|
|
// List returns the identities known to the agent.
|
|
func (r *keyring) List() ([]*Key, error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.locked {
|
|
// section 2.7: locked agents return empty.
|
|
return nil, nil
|
|
}
|
|
|
|
r.expireKeysLocked()
|
|
var ids []*Key
|
|
for _, k := range r.keys {
|
|
pub := k.signer.PublicKey()
|
|
ids = append(ids, &Key{
|
|
Format: pub.Type(),
|
|
Blob: pub.Marshal(),
|
|
Comment: k.comment})
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
// 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 {
|
|
return err
|
|
}
|
|
|
|
if cert := key.Certificate; cert != nil {
|
|
signer, err = ssh.NewCertSigner(cert, signer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
p := privKey{
|
|
signer: signer,
|
|
comment: key.Comment,
|
|
}
|
|
|
|
if key.LifetimeSecs > 0 {
|
|
t := time.Now().Add(time.Duration(key.LifetimeSecs) * time.Second)
|
|
p.expire = &t
|
|
}
|
|
|
|
// If we already have a Signer with the same public key, replace it with the
|
|
// new one.
|
|
for idx, k := range r.keys {
|
|
if bytes.Equal(k.signer.PublicKey().Marshal(), p.signer.PublicKey().Marshal()) {
|
|
r.keys[idx] = p
|
|
return nil
|
|
}
|
|
}
|
|
|
|
r.keys = append(r.keys, p)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Sign returns a signature for the data.
|
|
func (r *keyring) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) {
|
|
return r.SignWithFlags(key, data, 0)
|
|
}
|
|
|
|
func (r *keyring) SignWithFlags(key ssh.PublicKey, data []byte, flags SignatureFlags) (*ssh.Signature, error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.locked {
|
|
return nil, errLocked
|
|
}
|
|
|
|
r.expireKeysLocked()
|
|
wanted := key.Marshal()
|
|
for _, k := range r.keys {
|
|
if bytes.Equal(k.signer.PublicKey().Marshal(), wanted) {
|
|
if flags == 0 {
|
|
return k.signer.Sign(rand.Reader, data)
|
|
} else {
|
|
if algorithmSigner, ok := k.signer.(ssh.AlgorithmSigner); !ok {
|
|
return nil, fmt.Errorf("agent: signature does not support non-default signature algorithm: %T", k.signer)
|
|
} else {
|
|
var algorithm string
|
|
switch flags {
|
|
case SignatureFlagRsaSha256:
|
|
algorithm = ssh.KeyAlgoRSASHA256
|
|
case SignatureFlagRsaSha512:
|
|
algorithm = ssh.KeyAlgoRSASHA512
|
|
default:
|
|
return nil, fmt.Errorf("agent: unsupported signature flags: %d", flags)
|
|
}
|
|
return algorithmSigner.SignWithAlgorithm(rand.Reader, data, algorithm)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil, errors.New("not found")
|
|
}
|
|
|
|
// Signers returns signers for all the known keys.
|
|
func (r *keyring) Signers() ([]ssh.Signer, error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.locked {
|
|
return nil, errLocked
|
|
}
|
|
|
|
r.expireKeysLocked()
|
|
s := make([]ssh.Signer, 0, len(r.keys))
|
|
for _, k := range r.keys {
|
|
s = append(s, k.signer)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// The keyring does not support any extensions
|
|
func (r *keyring) Extension(extensionType string, contents []byte) ([]byte, error) {
|
|
return nil, ErrExtensionUnsupported
|
|
}
|