1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
|
package sshsig
import (
crand "crypto/rand"
"errors"
"fmt"
"io"
"golang.org/x/crypto/ssh"
)
// ErrMissingNamespace is returned by Sign if the namespace value is missing.
var ErrMissingNamespace = errors.New("missing namespace")
// Signature represents the SSH signature of a message. It can be marshaled
// into an SSH wire format using Marshal, or into an armored (PEM) format using
// Armor.
//
// Manually construction of this type is not recommended. Use ParseSignature or
// Unarmor instead to retrieve a Signature from a wire or armored (PEM) format.
type Signature struct {
// Version is the version of the signature format.
// It currently supports version 1, any other value will be rejected with
// ErrUnsupportedSignatureVersion.
Version uint32
// PublicKey is the public key used to create the Signature.
PublicKey ssh.PublicKey
// Namespace is the domain of the signature, and is used to prevent signature
// reuse across different applications.
Namespace string
// HashAlgorithm is the hash algorithm used to hash the Signature message.
HashAlgorithm HashAlgorithm
// Signature is the SSH signature of the hash of the message.
Signature *ssh.Signature
}
// Marshal returns the Signature in SSH wire format.
func (s Signature) Marshal() []byte {
return blob{
Version: s.Version,
PublicKey: string(s.PublicKey.Marshal()),
Namespace: s.Namespace,
HashAlgorithm: s.HashAlgorithm.String(),
Signature: string(ssh.Marshal(s.Signature)),
}.Marshal()
}
// ParseSignature parses a signature in SSH wire format into a Signature.
// It returns an error if the signature is invalid.
func ParseSignature(b []byte) (*Signature, error) {
var sig blob
if err := ssh.Unmarshal(b, &sig); err != nil {
return nil, err
}
if err := sig.Validate(); err != nil {
return nil, err
}
sshSig := ssh.Signature{}
if err := ssh.Unmarshal([]byte(sig.Signature), &sshSig); err != nil {
return nil, err
}
pub, err := ssh.ParsePublicKey([]byte(sig.PublicKey))
if err != nil {
return nil, err
}
// For RSA signatures, the signature algorithm must be "rsa-sha2-512" or
// "rsa-sha2-256".
// xref: https://github.com/openssh/openssh-portable/blob/V_9_2_P1/PROTOCOL.sshsig#L69-L72
if pub.Type() == ssh.KeyAlgoRSA && sshSig.Format != ssh.KeyAlgoRSASHA256 && sshSig.Format != ssh.KeyAlgoRSASHA512 {
return nil, fmt.Errorf("invalid signature format %q: expected %q or %q", sshSig.Format, ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512)
}
return &Signature{
Version: sig.Version,
PublicKey: pub,
Namespace: sig.Namespace,
HashAlgorithm: HashAlgorithm(sig.HashAlgorithm),
Signature: &sshSig,
}, nil
}
// Sign generates a signature of the message from the io.Reader using the
// given ssh.Signer private key. The signature hash is computed using the provided
// HashAlgorithm.
//
// The purpose of the namespace value is to specify an unambiguous interpretation
// domain for the signature, e.g. file signing. This prevents cross-protocol
// attacks caused by signatures intended for one intended domain being accepted
// in another. The namespace must not be empty, or ErrMissingNamespace will be
// returned.
//
// When the signer is an RSA key, the signature algorithm will always be
// "rsa-sha2-512". This is the same default used by OpenSSH, and is required by
// the SSH signature wire protocol.
//
// Sign returns a Signature containing the signed message and metadata, or an
// error if the signing process fails.
func Sign(m io.Reader, signer ssh.Signer, h HashAlgorithm, namespace string) (*Signature, error) {
return SignWithRand(m, crand.Reader, signer, h, namespace)
}
// SignWithRand is like Sign, but uses the provided rand io.Reader to create any
// necessary random values. Most callers likely want to use Sign instead.
func SignWithRand(m, rand io.Reader, signer ssh.Signer, h HashAlgorithm, namespace string) (*Signature, error) {
if namespace == "" {
// xref: https://github.com/openssh/openssh-portable/blob/V_9_2_P1/PROTOCOL.sshsig#LL57C13-L57C13
return nil, ErrMissingNamespace
}
if err := h.Available(); err != nil {
return nil, err
}
hf := h.Hash()
if _, err := io.Copy(hf, m); err != nil {
return nil, err
}
mh := hf.Sum(nil)
var (
sd = signedData{
Namespace: namespace,
HashAlgorithm: h.String(),
Hash: string(mh),
}
sig *ssh.Signature
err error
)
switch signer.PublicKey().Type() {
case ssh.KeyAlgoRSA:
// For RSA signatures, the signature algorithm must be "rsa-sha2-512" or
// "rsa-sha2-256". We use the same "rsa-sha2-512" default as OpenSSH.
// xref: https://github.com/openssh/openssh-portable/blob/V_9_2_P1/PROTOCOL.sshsig#L69-L72
// xref: https://github.com/openssh/openssh-portable/blob/V_9_2_P1/ssh-keygen.c#L1804-L1805
algo := ssh.KeyAlgoRSASHA512
// This should always succeed as an SSH signer must implement the
// AlgorithmSigner, but we check anyway.
as, ok := signer.(ssh.AlgorithmSigner)
if !ok {
return nil, fmt.Errorf("signer does not support non-default signature algorithm %q", algo)
}
if sig, err = as.SignWithAlgorithm(rand, sd.Marshal(), algo); err != nil {
return nil, err
}
default:
if sig, err = signer.Sign(rand, sd.Marshal()); err != nil {
return nil, err
}
}
return &Signature{
Version: sigVersion,
PublicKey: signer.PublicKey(),
Namespace: namespace,
HashAlgorithm: h,
Signature: sig,
}, nil
}
|