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")