// Package ca generates the instance's local certificate authority keypair
// and issues short-TTL OpenSSH user certificates, per DESIGN.md §5.1.
package ca
import (
"crypto/ed25519"
"crypto/rand"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"time"
"golang.org/x/crypto/ssh"
)
const (
privateKeyFile = "ca_key"
publicKeyFile = "ca_key.pub"
)
// CA holds the instance's signing keypair.
type CA struct {
signer ssh.Signer
pub ssh.PublicKey
}
// LoadOrCreate loads the CA keypair from dir, generating a new ed25519
// keypair on first run.
func LoadOrCreate(dir string) (*CA, error) {
privPath := filepath.Join(dir, privateKeyFile)
pubPath := filepath.Join(dir, publicKeyFile)
if _, err := os.Stat(privPath); err == nil {
return load(privPath)
} else if !os.IsNotExist(err) {
return nil, fmt.Errorf("ca: stat %s: %w", privPath, err)
}
if err := os.MkdirAll(dir, 0700); err != nil {
return nil, fmt.Errorf("ca: mkdir %s: %w", dir, err)
}
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("ca: generate key: %w", err)
}
block, err := ssh.MarshalPrivateKey(privKey, "gitfed CA")
if err != nil {
return nil, fmt.Errorf("ca: marshal private key: %w", err)
}
if err := os.WriteFile(privPath, pem.EncodeToMemory(block), 0600); err != nil {
return nil, fmt.Errorf("ca: write %s: %w", privPath, err)
}
sshPub, err := ssh.NewPublicKey(pubKey)
if err != nil {
return nil, fmt.Errorf("ca: derive public key: %w", err)
}
if err := os.WriteFile(pubPath, ssh.MarshalAuthorizedKey(sshPub), 0644); err != nil {
return nil, fmt.Errorf("ca: write %s: %w", pubPath, err)
}
return load(privPath)
}
func load(privPath string) (*CA, error) {
data, err := os.ReadFile(privPath)
if err != nil {
return nil, fmt.Errorf("ca: read %s: %w", privPath, err)
}
signer, err := ssh.ParsePrivateKey(data)
if err != nil {
return nil, fmt.Errorf("ca: parse private key: %w", err)
}
return &CA{signer: signer, pub: signer.PublicKey()}, nil
}
// PublicKeyAuthorized returns the CA's public key in authorized_keys format,
// e.g. for publishing in /.well-known/gitfed.json.
func (c *CA) PublicKeyAuthorized() string {
return string(ssh.MarshalAuthorizedKey(c.pub))
}
// PublicKey returns the CA's ssh.PublicKey.
func (c *CA) PublicKey() ssh.PublicKey {
return c.pub
}
// SignBytes signs arbitrary data with the CA's private key. Used outside
// certificate issuance for exactly one thing: proving authorship of
// outbound federated notifications (see internal/federation) — a receiving
// instance verifies it against the sender's CA public key, the same one
// already published for certificate trust.
func (c *CA) SignBytes(data []byte) (*ssh.Signature, error) {
return c.signer.Sign(rand.Reader, data)
}
// IssueParams describes a certificate to be issued for a local user.
type IssueParams struct {
Username string
Domain string
UserKey ssh.PublicKey // the user's own SSH public key being certified
TTL time.Duration
}
// IssueUserCert signs a short-lived OpenSSH user certificate for the given
// user key, with principal "<username>@<domain>" per DESIGN.md §5.1.
func (c *CA) IssueUserCert(p IssueParams) (*ssh.Certificate, error) {
if p.TTL <= 0 {
p.TTL = 48 * time.Hour
}
now := time.Now()
principal := fmt.Sprintf("%s@%s", p.Username, p.Domain)
keyID, err := randomKeyID()
if err != nil {
return nil, err
}
cert := &ssh.Certificate{
Key: p.UserKey,
Serial: 0,
CertType: ssh.UserCert,
KeyId: keyID,
ValidPrincipals: []string{principal},
ValidAfter: uint64(now.Add(-1 * time.Minute).Unix()),
ValidBefore: uint64(now.Add(p.TTL).Unix()),
Permissions: ssh.Permissions{
Extensions: map[string]string{
"permit-pty": "",
},
},
}
if err := cert.SignCert(rand.Reader, c.signer); err != nil {
return nil, fmt.Errorf("ca: sign certificate: %w", err)
}
return cert, nil
}
func randomKeyID() (string, error) {
buf := make([]byte, 16)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return fmt.Sprintf("gitfed-%x", buf), nil
}