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>
602 lines
14 KiB
Go
602 lines
14 KiB
Go
// Copyright 2012 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 (
|
|
"crypto/dsa"
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/elliptic"
|
|
"crypto/rsa"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math/big"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
// server wraps an Agent and uses it to implement the agent side of
|
|
// the SSH-agent, wire protocol.
|
|
type server struct {
|
|
agent Agent
|
|
}
|
|
|
|
func (s *server) processRequestBytes(reqData []byte) []byte {
|
|
rep, err := s.processRequest(reqData)
|
|
if err != nil {
|
|
if err != errLocked {
|
|
// TODO(hanwen): provide better logging interface?
|
|
log.Printf("agent %d: %v", reqData[0], err)
|
|
}
|
|
return []byte{agentFailure}
|
|
}
|
|
|
|
if rep == nil {
|
|
return []byte{agentSuccess}
|
|
}
|
|
|
|
return ssh.Marshal(rep)
|
|
}
|
|
|
|
func marshalKey(k *Key) []byte {
|
|
var record struct {
|
|
Blob []byte
|
|
Comment string
|
|
}
|
|
record.Blob = k.Marshal()
|
|
record.Comment = k.Comment
|
|
|
|
return ssh.Marshal(&record)
|
|
}
|
|
|
|
// See [PROTOCOL.agent], section 2.5.1.
|
|
const agentV1IdentitiesAnswer = 2
|
|
|
|
type agentV1IdentityMsg struct {
|
|
Numkeys uint32 `sshtype:"2"`
|
|
}
|
|
|
|
type agentRemoveIdentityMsg struct {
|
|
KeyBlob []byte `sshtype:"18"`
|
|
}
|
|
|
|
type agentLockMsg struct {
|
|
Passphrase []byte `sshtype:"22"`
|
|
}
|
|
|
|
type agentUnlockMsg struct {
|
|
Passphrase []byte `sshtype:"23"`
|
|
}
|
|
|
|
func (s *server) processRequest(data []byte) (interface{}, error) {
|
|
switch data[0] {
|
|
case agentRequestV1Identities:
|
|
return &agentV1IdentityMsg{0}, nil
|
|
|
|
case agentRemoveAllV1Identities:
|
|
return nil, nil
|
|
|
|
case agentRemoveIdentity:
|
|
var req agentRemoveIdentityMsg
|
|
if err := ssh.Unmarshal(data, &req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var wk wireKey
|
|
if err := ssh.Unmarshal(req.KeyBlob, &wk); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nil, s.agent.Remove(&Key{Format: wk.Format, Blob: req.KeyBlob})
|
|
|
|
case agentRemoveAllIdentities:
|
|
return nil, s.agent.RemoveAll()
|
|
|
|
case agentLock:
|
|
var req agentLockMsg
|
|
if err := ssh.Unmarshal(data, &req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nil, s.agent.Lock(req.Passphrase)
|
|
|
|
case agentUnlock:
|
|
var req agentUnlockMsg
|
|
if err := ssh.Unmarshal(data, &req); err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, s.agent.Unlock(req.Passphrase)
|
|
|
|
case agentSignRequest:
|
|
var req signRequestAgentMsg
|
|
if err := ssh.Unmarshal(data, &req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var wk wireKey
|
|
if err := ssh.Unmarshal(req.KeyBlob, &wk); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
k := &Key{
|
|
Format: wk.Format,
|
|
Blob: req.KeyBlob,
|
|
}
|
|
|
|
var sig *ssh.Signature
|
|
var err error
|
|
if extendedAgent, ok := s.agent.(ExtendedAgent); ok {
|
|
sig, err = extendedAgent.SignWithFlags(k, req.Data, SignatureFlags(req.Flags))
|
|
} else {
|
|
sig, err = s.agent.Sign(k, req.Data)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &signResponseAgentMsg{SigBlob: ssh.Marshal(sig)}, nil
|
|
|
|
case agentRequestIdentities:
|
|
keys, err := s.agent.List()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rep := identitiesAnswerAgentMsg{
|
|
NumKeys: uint32(len(keys)),
|
|
}
|
|
for _, k := range keys {
|
|
rep.Keys = append(rep.Keys, marshalKey(k)...)
|
|
}
|
|
return rep, nil
|
|
|
|
case agentAddIDConstrained, agentAddIdentity:
|
|
return nil, s.insertIdentity(data)
|
|
|
|
case agentExtension:
|
|
// Return a stub object where the whole contents of the response gets marshaled.
|
|
var responseStub struct {
|
|
Rest []byte `ssh:"rest"`
|
|
}
|
|
|
|
if extendedAgent, ok := s.agent.(ExtendedAgent); !ok {
|
|
// If this agent doesn't implement extensions, [PROTOCOL.agent] section 4.7
|
|
// requires that we return a standard SSH_AGENT_FAILURE message.
|
|
responseStub.Rest = []byte{agentFailure}
|
|
} else {
|
|
var req extensionAgentMsg
|
|
if err := ssh.Unmarshal(data, &req); err != nil {
|
|
return nil, err
|
|
}
|
|
res, err := extendedAgent.Extension(req.ExtensionType, req.Contents)
|
|
if err != nil {
|
|
// If agent extensions are unsupported, return a standard SSH_AGENT_FAILURE
|
|
// message as required by [PROTOCOL.agent] section 4.7.
|
|
if err == ErrExtensionUnsupported {
|
|
responseStub.Rest = []byte{agentFailure}
|
|
} else {
|
|
// As the result of any other error processing an extension request,
|
|
// [PROTOCOL.agent] section 4.7 requires that we return a
|
|
// SSH_AGENT_EXTENSION_FAILURE code.
|
|
responseStub.Rest = []byte{agentExtensionFailure}
|
|
}
|
|
} else {
|
|
if len(res) == 0 {
|
|
return nil, nil
|
|
}
|
|
responseStub.Rest = res
|
|
}
|
|
}
|
|
|
|
return responseStub, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("unknown opcode %d", data[0])
|
|
}
|
|
|
|
func parseConstraints(constraints []byte) (lifetimeSecs uint32, confirmBeforeUse bool, extensions []ConstraintExtension, err error) {
|
|
for len(constraints) != 0 {
|
|
switch constraints[0] {
|
|
case agentConstrainLifetime:
|
|
if len(constraints) < 5 {
|
|
return 0, false, nil, io.ErrUnexpectedEOF
|
|
}
|
|
lifetimeSecs = binary.BigEndian.Uint32(constraints[1:5])
|
|
constraints = constraints[5:]
|
|
case agentConstrainConfirm:
|
|
confirmBeforeUse = true
|
|
constraints = constraints[1:]
|
|
case agentConstrainExtension, agentConstrainExtensionV00:
|
|
var msg constrainExtensionAgentMsg
|
|
if err = ssh.Unmarshal(constraints, &msg); err != nil {
|
|
return 0, false, nil, err
|
|
}
|
|
extensions = append(extensions, ConstraintExtension{
|
|
ExtensionName: msg.ExtensionName,
|
|
ExtensionDetails: msg.ExtensionDetails,
|
|
})
|
|
constraints = msg.Rest
|
|
default:
|
|
return 0, false, nil, fmt.Errorf("unknown constraint type: %d", constraints[0])
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func setConstraints(key *AddedKey, constraintBytes []byte) error {
|
|
lifetimeSecs, confirmBeforeUse, constraintExtensions, err := parseConstraints(constraintBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
key.LifetimeSecs = lifetimeSecs
|
|
key.ConfirmBeforeUse = confirmBeforeUse
|
|
key.ConstraintExtensions = constraintExtensions
|
|
return nil
|
|
}
|
|
|
|
// checkRSAKeyParams enforces the same bounds as parseRSA in the ssh
|
|
// package, and additionally caps the prime factors. Without this,
|
|
// the rsa.PrivateKey built from an Add request would call Precompute()
|
|
// on arbitrary inputs; the CRT coefficient recomputation is cubic in
|
|
// |p| and can consume excessive CPU on oversized keys.
|
|
func checkRSAKeyParams(N, E, P, Q *big.Int) error {
|
|
if N.BitLen() > 8192 {
|
|
return errors.New("agent: RSA modulus too large")
|
|
}
|
|
if P.BitLen() > 4096 || Q.BitLen() > 4096 {
|
|
return errors.New("agent: RSA prime too large")
|
|
}
|
|
if E.BitLen() > 24 {
|
|
return errors.New("agent: RSA public exponent too large")
|
|
}
|
|
e := E.Int64()
|
|
if e < 3 || e&1 == 0 {
|
|
return errors.New("agent: incorrect RSA public exponent")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseRSAKey(req []byte) (*AddedKey, error) {
|
|
var k rsaKeyMsg
|
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := checkRSAKeyParams(k.N, k.E, k.P, k.Q); err != nil {
|
|
return nil, err
|
|
}
|
|
priv := &rsa.PrivateKey{
|
|
PublicKey: rsa.PublicKey{
|
|
E: int(k.E.Int64()),
|
|
N: k.N,
|
|
},
|
|
D: k.D,
|
|
Primes: []*big.Int{k.P, k.Q},
|
|
}
|
|
priv.Precompute()
|
|
|
|
addedKey := &AddedKey{PrivateKey: priv, Comment: k.Comments}
|
|
if err := setConstraints(addedKey, k.Constraints); err != nil {
|
|
return nil, err
|
|
}
|
|
return addedKey, nil
|
|
}
|
|
|
|
func parseEd25519Key(req []byte) (*AddedKey, error) {
|
|
var k ed25519KeyMsg
|
|
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}
|
|
if err := setConstraints(addedKey, k.Constraints); err != nil {
|
|
return nil, err
|
|
}
|
|
return addedKey, nil
|
|
}
|
|
|
|
func parseDSAKey(req []byte) (*AddedKey, error) {
|
|
var k dsaKeyMsg
|
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
|
return nil, err
|
|
}
|
|
priv := &dsa.PrivateKey{
|
|
PublicKey: dsa.PublicKey{
|
|
Parameters: dsa.Parameters{
|
|
P: k.P,
|
|
Q: k.Q,
|
|
G: k.G,
|
|
},
|
|
Y: k.Y,
|
|
},
|
|
X: k.X,
|
|
}
|
|
|
|
addedKey := &AddedKey{PrivateKey: priv, Comment: k.Comments}
|
|
if err := setConstraints(addedKey, k.Constraints); err != nil {
|
|
return nil, err
|
|
}
|
|
return addedKey, nil
|
|
}
|
|
|
|
func unmarshalECDSA(curveName string, keyBytes []byte, privScalar *big.Int) (priv *ecdsa.PrivateKey, err error) {
|
|
priv = &ecdsa.PrivateKey{
|
|
D: privScalar,
|
|
}
|
|
|
|
switch curveName {
|
|
case "nistp256":
|
|
priv.Curve = elliptic.P256()
|
|
case "nistp384":
|
|
priv.Curve = elliptic.P384()
|
|
case "nistp521":
|
|
priv.Curve = elliptic.P521()
|
|
default:
|
|
return nil, fmt.Errorf("agent: unknown curve %q", curveName)
|
|
}
|
|
|
|
priv.X, priv.Y = elliptic.Unmarshal(priv.Curve, keyBytes)
|
|
if priv.X == nil || priv.Y == nil {
|
|
return nil, errors.New("agent: point not on curve")
|
|
}
|
|
|
|
return priv, nil
|
|
}
|
|
|
|
func parseEd25519Cert(req []byte) (*AddedKey, error) {
|
|
var k ed25519CertMsg
|
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
|
return nil, err
|
|
}
|
|
pubKey, err := ssh.ParsePublicKey(k.CertBytes)
|
|
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 {
|
|
return nil, errors.New("agent: bad ED25519 certificate")
|
|
}
|
|
|
|
addedKey := &AddedKey{PrivateKey: &priv, Certificate: cert, Comment: k.Comments}
|
|
if err := setConstraints(addedKey, k.Constraints); err != nil {
|
|
return nil, err
|
|
}
|
|
return addedKey, nil
|
|
}
|
|
|
|
func parseECDSAKey(req []byte) (*AddedKey, error) {
|
|
var k ecdsaKeyMsg
|
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
priv, err := unmarshalECDSA(k.Curve, k.KeyBytes, k.D)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addedKey := &AddedKey{PrivateKey: priv, Comment: k.Comments}
|
|
if err := setConstraints(addedKey, k.Constraints); err != nil {
|
|
return nil, err
|
|
}
|
|
return addedKey, nil
|
|
}
|
|
|
|
func parseRSACert(req []byte) (*AddedKey, error) {
|
|
var k rsaCertMsg
|
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pubKey, err := ssh.ParsePublicKey(k.CertBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cert, ok := pubKey.(*ssh.Certificate)
|
|
if !ok {
|
|
return nil, errors.New("agent: bad RSA certificate")
|
|
}
|
|
|
|
// An RSA publickey as marshaled by rsaPublicKey.Marshal() in keys.go
|
|
var rsaPub struct {
|
|
Name string
|
|
E *big.Int
|
|
N *big.Int
|
|
}
|
|
if err := ssh.Unmarshal(cert.Key.Marshal(), &rsaPub); err != nil {
|
|
return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err)
|
|
}
|
|
|
|
if err := checkRSAKeyParams(rsaPub.N, rsaPub.E, k.P, k.Q); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
priv := rsa.PrivateKey{
|
|
PublicKey: rsa.PublicKey{
|
|
E: int(rsaPub.E.Int64()),
|
|
N: rsaPub.N,
|
|
},
|
|
D: k.D,
|
|
Primes: []*big.Int{k.Q, k.P},
|
|
}
|
|
priv.Precompute()
|
|
|
|
addedKey := &AddedKey{PrivateKey: &priv, Certificate: cert, Comment: k.Comments}
|
|
if err := setConstraints(addedKey, k.Constraints); err != nil {
|
|
return nil, err
|
|
}
|
|
return addedKey, nil
|
|
}
|
|
|
|
func parseDSACert(req []byte) (*AddedKey, error) {
|
|
var k dsaCertMsg
|
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
|
return nil, err
|
|
}
|
|
pubKey, err := ssh.ParsePublicKey(k.CertBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cert, ok := pubKey.(*ssh.Certificate)
|
|
if !ok {
|
|
return nil, errors.New("agent: bad DSA certificate")
|
|
}
|
|
|
|
// A DSA publickey as marshaled by dsaPublicKey.Marshal() in keys.go
|
|
var w struct {
|
|
Name string
|
|
P, Q, G, Y *big.Int
|
|
}
|
|
if err := ssh.Unmarshal(cert.Key.Marshal(), &w); err != nil {
|
|
return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err)
|
|
}
|
|
|
|
priv := &dsa.PrivateKey{
|
|
PublicKey: dsa.PublicKey{
|
|
Parameters: dsa.Parameters{
|
|
P: w.P,
|
|
Q: w.Q,
|
|
G: w.G,
|
|
},
|
|
Y: w.Y,
|
|
},
|
|
X: k.X,
|
|
}
|
|
|
|
addedKey := &AddedKey{PrivateKey: priv, Certificate: cert, Comment: k.Comments}
|
|
if err := setConstraints(addedKey, k.Constraints); err != nil {
|
|
return nil, err
|
|
}
|
|
return addedKey, nil
|
|
}
|
|
|
|
func parseECDSACert(req []byte) (*AddedKey, error) {
|
|
var k ecdsaCertMsg
|
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pubKey, err := ssh.ParsePublicKey(k.CertBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cert, ok := pubKey.(*ssh.Certificate)
|
|
if !ok {
|
|
return nil, errors.New("agent: bad ECDSA certificate")
|
|
}
|
|
|
|
// An ECDSA publickey as marshaled by ecdsaPublicKey.Marshal() in keys.go
|
|
var ecdsaPub struct {
|
|
Name string
|
|
ID string
|
|
Key []byte
|
|
}
|
|
if err := ssh.Unmarshal(cert.Key.Marshal(), &ecdsaPub); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
priv, err := unmarshalECDSA(ecdsaPub.ID, ecdsaPub.Key, k.D)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addedKey := &AddedKey{PrivateKey: priv, Certificate: cert, Comment: k.Comments}
|
|
if err := setConstraints(addedKey, k.Constraints); err != nil {
|
|
return nil, err
|
|
}
|
|
return addedKey, nil
|
|
}
|
|
|
|
func (s *server) insertIdentity(req []byte) error {
|
|
var record struct {
|
|
Type string `sshtype:"17|25"`
|
|
Rest []byte `ssh:"rest"`
|
|
}
|
|
|
|
if err := ssh.Unmarshal(req, &record); err != nil {
|
|
return err
|
|
}
|
|
|
|
var addedKey *AddedKey
|
|
var err error
|
|
|
|
switch record.Type {
|
|
case ssh.KeyAlgoRSA:
|
|
addedKey, err = parseRSAKey(req)
|
|
case ssh.InsecureKeyAlgoDSA:
|
|
addedKey, err = parseDSAKey(req)
|
|
case ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521:
|
|
addedKey, err = parseECDSAKey(req)
|
|
case ssh.KeyAlgoED25519:
|
|
addedKey, err = parseEd25519Key(req)
|
|
case ssh.CertAlgoRSAv01:
|
|
addedKey, err = parseRSACert(req)
|
|
case ssh.InsecureCertAlgoDSAv01:
|
|
addedKey, err = parseDSACert(req)
|
|
case ssh.CertAlgoECDSA256v01, ssh.CertAlgoECDSA384v01, ssh.CertAlgoECDSA521v01:
|
|
addedKey, err = parseECDSACert(req)
|
|
case ssh.CertAlgoED25519v01:
|
|
addedKey, err = parseEd25519Cert(req)
|
|
default:
|
|
return fmt.Errorf("agent: not implemented: %q", record.Type)
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.agent.Add(*addedKey)
|
|
}
|
|
|
|
// ServeAgent serves the agent protocol on the given connection. It
|
|
// returns when an I/O error occurs.
|
|
func ServeAgent(agent Agent, c io.ReadWriter) error {
|
|
s := &server{agent}
|
|
|
|
var length [4]byte
|
|
for {
|
|
if _, err := io.ReadFull(c, length[:]); err != nil {
|
|
return err
|
|
}
|
|
l := binary.BigEndian.Uint32(length[:])
|
|
if l == 0 {
|
|
return fmt.Errorf("agent: request size is 0")
|
|
}
|
|
if l > maxAgentResponseBytes {
|
|
// We also cap requests.
|
|
return fmt.Errorf("agent: request too large: %d", l)
|
|
}
|
|
|
|
req := make([]byte, l)
|
|
if _, err := io.ReadFull(c, req); err != nil {
|
|
return err
|
|
}
|
|
|
|
repData := s.processRequestBytes(req)
|
|
if len(repData) > maxAgentResponseBytes {
|
|
return fmt.Errorf("agent: reply too large: %d bytes", len(repData))
|
|
}
|
|
|
|
binary.BigEndian.PutUint32(length[:], uint32(len(repData)))
|
|
if _, err := c.Write(length[:]); err != nil {
|
|
return err
|
|
}
|
|
if _, err := c.Write(repData); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|