package replica import ( "crypto/rand" "crypto/sha512" "encoding/base64" "errors" "io" "strings" "golang.org/x/crypto/ssh" ) const ( Namespace = "git" sigMagic = "SSHSIG" sigVersion = 1 sigHashAlg = "sha512" armorBegin = "-----BEGIN SSH SIGNATURE-----" armorEnd = "-----END SSH SIGNATURE-----" armorColumn = 70 ) var ErrUnsupportedKey = errors.New("replica: key is not ed25519") type Signer struct { key ssh.Signer } type signedData struct { Namespace string Reserved string HashAlg string Hash []byte } type sigBlob struct { Version uint32 PublicKey []byte Namespace string Reserved string HashAlg string Signature []byte } func NewSigner(key ssh.Signer) (*Signer, error) { if key == nil || key.PublicKey().Type() != ssh.KeyAlgoED25519 { return nil, ErrUnsupportedKey } return &Signer{key: key}, nil } func ParseSigner(openssh []byte) (*Signer, error) { key, err := ssh.ParsePrivateKey(openssh) if err != nil { return nil, ErrKeyFile } return NewSigner(key) } func (s *Signer) PublicKey() ssh.PublicKey { return s.key.PublicKey() } func (s *Signer) Sign(message io.Reader) ([]byte, error) { return s.sign(Namespace, message) } func (s *Signer) sign(namespace string, message io.Reader) ([]byte, error) { h := sha512.New() if _, err := io.Copy(h, message); err != nil { return nil, err } toSign := append([]byte(sigMagic), ssh.Marshal(signedData{ Namespace: namespace, HashAlg: sigHashAlg, Hash: h.Sum(nil), })...) sig, err := s.key.Sign(rand.Reader, toSign) if err != nil { return nil, err } blob := append([]byte(sigMagic), ssh.Marshal(sigBlob{ Version: sigVersion, PublicKey: s.key.PublicKey().Marshal(), Namespace: namespace, HashAlg: sigHashAlg, Signature: ssh.Marshal(sig), })...) return armor(blob), nil } func armor(blob []byte) []byte { enc := base64.StdEncoding.EncodeToString(blob) var b strings.Builder b.WriteString(armorBegin + "\n") for len(enc) > armorColumn { b.WriteString(enc[:armorColumn] + "\n") enc = enc[armorColumn:] } b.WriteString(enc + "\n") b.WriteString(armorEnd + "\n") return []byte(b.String()) } var ErrKeyFile = errors.New("replica: key is not an unencrypted OpenSSH key file") var ErrKnownHostsUnreadable = errors.New("replica: the known_hosts file cannot be read")