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