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}