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
|
// Package sshutil implements utilities to build SSH certificates based on JSON
// templates.
package sshutil
import (
"crypto/rand"
"encoding/binary"
"encoding/json"
"time"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh"
"go.step.sm/crypto/internal/utils"
"go.step.sm/crypto/randutil"
)
// Certificate is the json representation of ssh.Certificate.
type Certificate struct {
Nonce []byte `json:"nonce"`
Key ssh.PublicKey `json:"-"`
Serial uint64 `json:"serial"`
Type CertType `json:"type"`
KeyID string `json:"keyId"`
Principals []string `json:"principals"`
ValidAfter time.Time `json:"validAfter"`
ValidBefore time.Time `json:"validBefore"`
CriticalOptions map[string]string `json:"criticalOptions"`
Extensions map[string]string `json:"extensions"`
Reserved []byte `json:"reserved"`
SignatureKey ssh.PublicKey `json:"-"`
Signature *ssh.Signature `json:"-"`
}
// NewCertificate creates a new certificate with the given key after parsing a
// template given in the options.
func NewCertificate(cr CertificateRequest, opts ...Option) (*Certificate, error) {
o, err := new(Options).apply(cr, opts)
if err != nil {
return nil, err
}
if o.CertBuffer == nil {
return nil, errors.New("certificate template cannot be empty")
}
// With templates
var cert Certificate
if err := json.NewDecoder(o.CertBuffer).Decode(&cert); err != nil {
return nil, errors.Wrap(err, "error unmarshaling certificate")
}
// Complete with public key
cert.Key = cr.Key
return &cert, nil
}
// GetCertificate return the ssh.Certificate representation of the certificate.
func (c *Certificate) GetCertificate() *ssh.Certificate {
return &ssh.Certificate{
Nonce: c.Nonce,
Key: c.Key,
Serial: c.Serial,
CertType: uint32(c.Type),
KeyId: c.KeyID,
ValidPrincipals: c.Principals,
ValidAfter: toValidity(c.ValidAfter),
ValidBefore: toValidity(c.ValidBefore),
Permissions: ssh.Permissions{
CriticalOptions: c.CriticalOptions,
Extensions: c.Extensions,
},
Reserved: c.Reserved,
}
}
// CreateCertificate signs the given certificate with the given signer. If the
// certificate does not have a nonce or a serial, it will create random ones.
//
// If the signer is an RSA key, it will use rsa-sha2-256 instead of the default
// ssh-rsa (SHA-1), this method is currently deprecated and rsa-sha2-256/512 are
// supported since OpenSSH 7.2 (2016).
func CreateCertificate(cert *ssh.Certificate, signer ssh.Signer) (*ssh.Certificate, error) {
if len(cert.Nonce) == 0 {
nonce, err := randutil.ASCII(32)
if err != nil {
return nil, err
}
cert.Nonce = []byte(nonce)
}
if cert.Serial == 0 {
if err := binary.Read(rand.Reader, binary.BigEndian, &cert.Serial); err != nil {
return nil, errors.Wrap(err, "error reading random number")
}
}
// Set signer public key.
cert.SignatureKey = signer.PublicKey()
// Get bytes for signing trailing the signature length.
data := cert.Marshal()
data = data[:len(data)-4]
// Sign certificate.
//
// crypto/ssh signer defaults to SHA-1 with RSA signers, we will default to
// SHA256.
if cert.SignatureKey.Type() == "ssh-rsa" {
if algSigner, ok := signer.(ssh.AlgorithmSigner); ok {
sig, err := algSigner.SignWithAlgorithm(rand.Reader, data, ssh.KeyAlgoRSASHA256)
if err != nil {
return nil, errors.Wrap(err, "error signing certificate")
}
cert.Signature = sig
return cert, nil
}
}
// Rest of the keys
sig, err := signer.Sign(rand.Reader, data)
if err != nil {
return nil, errors.Wrap(err, "error signing certificate")
}
cert.Signature = sig
return cert, nil
}
func toValidity(t time.Time) uint64 {
if t.IsZero() {
return 0
}
return utils.MustUint64(t.Unix())
}
|