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}