GitRoot
craft your forge, build your project, grow your community freely
1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5package user
6
7import (
8 "bytes"
9 "crypto/ed25519"
10 "crypto/rand"
11 "encoding/pem"
12 "errors"
13 "fmt"
14 "io"
15 "os"
16 "path/filepath"
17
18 "github.com/42wim/sshsig"
19 "github.com/samber/oops"
20 "golang.org/x/crypto/ssh"
21)
22
23type Signer struct {
24 private ed25519.PrivateKey
25 signer ssh.Signer
26 public string
27}
28
29func generateIfNotExist(path, name string) (*Signer, error) {
30 privateSshKeyPath := filepath.Join(path, fmt.Sprintf("%s.priv", name))
31 publicSshKeyPath := filepath.Join(path, fmt.Sprintf("%s.pub", name))
32 errorHandler := oops.With("privateSshKeyPath", privateSshKeyPath)
33
34 err := os.MkdirAll(path, os.ModePerm)
35 if err != nil {
36 return nil, errorHandler.Wrapf(err, "can't MkdirAll")
37 }
38
39 var privateKey ed25519.PrivateKey
40 if _, err := os.Stat(privateSshKeyPath); err == nil {
41 content, err := os.ReadFile(privateSshKeyPath)
42 if err != nil {
43 return nil, errorHandler.Wrapf(err, "can't read private key")
44 }
45 privateKeyRaw, err := ssh.ParseRawPrivateKey(content)
46 if err != nil {
47 return nil, errorHandler.Wrapf(err, "can't parse private key")
48 }
49 pk, ok := privateKeyRaw.(*ed25519.PrivateKey)
50 if !ok {
51 return nil, errorHandler.Wrapf(err, "private key is not ed25519")
52 }
53 privateKey = *pk
54 } else if errors.Is(err, os.ErrNotExist) {
55 _, privateKey, err = ed25519.GenerateKey(rand.Reader)
56 if err != nil {
57 return nil, errorHandler.Wrapf(err, "can't generate private key")
58 }
59
60 p, err := ssh.MarshalPrivateKey(privateKey, "GitRoot master ssh key")
61 if err != nil {
62 return nil, errorHandler.Wrapf(err, "can't marshal private key")
63 }
64 fpriv, err := os.Create(privateSshKeyPath)
65 if err != nil {
66 return nil, errorHandler.Wrapf(err, "can't create file private key")
67 }
68 err = pem.Encode(fpriv, p)
69 if err != nil {
70 return nil, errorHandler.Wrapf(err, "can't encode pem private key")
71 }
72
73 sshSigner, err := ssh.NewSignerFromSigner(privateKey)
74 if err != nil {
75 return nil, errorHandler.Wrapf(err, "can't create signer from private key")
76 }
77
78 sshString := bytes.TrimSuffix(ssh.MarshalAuthorizedKey(sshSigner.PublicKey()), []byte("\n"))
79 fpub, err := os.Create(publicSshKeyPath)
80 if err != nil {
81 return nil, errorHandler.Wrapf(err, "can't create file public key")
82 }
83 _, err = fpub.Write(sshString)
84 if err != nil {
85 return nil, errorHandler.Wrapf(err, "can't write file public key")
86 }
87 err = fpub.Close()
88 if err != nil {
89 return nil, errorHandler.Wrapf(err, "can't close file public key")
90 }
91 } else {
92 return nil, errorHandler.Wrapf(err, "can't stat file")
93 }
94 sshSigner, _ := ssh.NewSignerFromSigner(privateKey)
95 return &Signer{signer: sshSigner, private: privateKey, public: string(bytes.TrimSuffix(ssh.MarshalAuthorizedKey(sshSigner.PublicKey()), []byte("\n")))}, nil
96}
97
98func (s *Signer) PublicKey() string {
99 return s.public
100}
101
102func (s *Signer) Signer() ssh.Signer {
103 return s.signer
104}
105
106func (s *Signer) Sign(message io.Reader) ([]byte, error) {
107 p, err := ssh.MarshalPrivateKey(s.private, "GitRoot master ssh key")
108 if err != nil {
109 return nil, oops.Wrapf(err, "can't marshal private key")
110 }
111 return sshsig.Sign(pem.EncodeToMemory(p), message, "git")
112}