Raw
  1package replica
  2
  3import (
  4	"crypto/rand"
  5	"crypto/sha512"
  6	"encoding/base64"
  7	"errors"
  8	"io"
  9	"strings"
 10
 11	"golang.org/x/crypto/ssh"
 12)
 13
 14const (
 15	Namespace   = "git"
 16	sigMagic    = "SSHSIG"
 17	sigVersion  = 1
 18	sigHashAlg  = "sha512"
 19	armorBegin  = "-----BEGIN SSH SIGNATURE-----"
 20	armorEnd    = "-----END SSH SIGNATURE-----"
 21	armorColumn = 70
 22)
 23
 24var ErrUnsupportedKey = errors.New("replica: key is not ed25519")
 25
 26type Signer struct {
 27	key ssh.Signer
 28}
 29
 30type signedData struct {
 31	Namespace string
 32	Reserved  string
 33	HashAlg   string
 34	Hash      []byte
 35}
 36
 37type sigBlob struct {
 38	Version   uint32
 39	PublicKey []byte
 40	Namespace string
 41	Reserved  string
 42	HashAlg   string
 43	Signature []byte
 44}
 45
 46func NewSigner(key ssh.Signer) (*Signer, error) {
 47	if key == nil || key.PublicKey().Type() != ssh.KeyAlgoED25519 {
 48		return nil, ErrUnsupportedKey
 49	}
 50	return &Signer{key: key}, nil
 51}
 52
 53func ParseSigner(openssh []byte) (*Signer, error) {
 54	key, err := ssh.ParsePrivateKey(openssh)
 55	if err != nil {
 56		return nil, ErrKeyFile
 57	}
 58	return NewSigner(key)
 59}
 60
 61func (s *Signer) PublicKey() ssh.PublicKey {
 62	return s.key.PublicKey()
 63}
 64
 65func (s *Signer) Sign(message io.Reader) ([]byte, error) {
 66	return s.sign(Namespace, message)
 67}
 68
 69func (s *Signer) sign(namespace string, message io.Reader) ([]byte, error) {
 70	h := sha512.New()
 71	if _, err := io.Copy(h, message); err != nil {
 72		return nil, err
 73	}
 74	toSign := append([]byte(sigMagic), ssh.Marshal(signedData{
 75		Namespace: namespace,
 76		HashAlg:   sigHashAlg,
 77		Hash:      h.Sum(nil),
 78	})...)
 79	sig, err := s.key.Sign(rand.Reader, toSign)
 80	if err != nil {
 81		return nil, err
 82	}
 83	blob := append([]byte(sigMagic), ssh.Marshal(sigBlob{
 84		Version:   sigVersion,
 85		PublicKey: s.key.PublicKey().Marshal(),
 86		Namespace: namespace,
 87		HashAlg:   sigHashAlg,
 88		Signature: ssh.Marshal(sig),
 89	})...)
 90	return armor(blob), nil
 91}
 92
 93func armor(blob []byte) []byte {
 94	enc := base64.StdEncoding.EncodeToString(blob)
 95	var b strings.Builder
 96	b.WriteString(armorBegin + "\n")
 97	for len(enc) > armorColumn {
 98		b.WriteString(enc[:armorColumn] + "\n")
 99		enc = enc[armorColumn:]
100	}
101	b.WriteString(enc + "\n")
102	b.WriteString(armorEnd + "\n")
103	return []byte(b.String())
104}
105
106var ErrKeyFile = errors.New("replica: key is not an unencrypted OpenSSH key file")
107
108var ErrKnownHostsUnreadable = errors.New("replica: the known_hosts file cannot be read")