omarchy-send/internal/security/cert.go
28allday 2dd81700c0 Initial commit: Omarchy-Send v0.1.0
LocalSend-compatible file-transfer TUI for headless Arch/Omarchy servers.

- Pure-stdlib implementation of the LocalSend v2 protocol (discovery,
  HTTPS with matching cert fingerprint, send/receive, PIN).
- Bubble Tea TUI: Devices, Transfers, Manage (received-file housekeeping)
  and Settings, theme-aware on Omarchy.
- Dual-mode install.sh: curl-pipe download or build-from-clone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:26:08 +01:00

68 lines
2.2 KiB
Go

// Package security generates and handles the self-signed TLS identity used for
// LocalSend's encrypted (HTTPS) mode.
//
// The LocalSend fingerprint is the SHA-256 of the certificate's DER bytes,
// encoded as uppercase hex (verified against the official client's stored
// certificateHash). Peers do not validate the certificate chain; they pin this
// fingerprint, which is advertised in the discovery announce.
package security
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"math/big"
"strings"
"time"
)
// Identity is a self-signed certificate, its key, and its LocalSend fingerprint.
type Identity struct {
CertPEM string
KeyPEM string
Fingerprint string
}
// Fingerprint returns the LocalSend fingerprint of a DER-encoded certificate:
// uppercase hex of its SHA-256.
func Fingerprint(der []byte) string {
sum := sha256.Sum256(der)
return strings.ToUpper(hex.EncodeToString(sum[:]))
}
// Generate creates a fresh RSA-2048 self-signed certificate matching the shape
// the official LocalSend client uses (CN "LocalSend User", ~10-year validity).
func Generate() (Identity, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return Identity{}, err
}
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return Identity{}, err
}
tmpl := x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{CommonName: "LocalSend User"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().AddDate(10, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &key.PublicKey, key)
if err != nil {
return Identity{}, err
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
return Identity{
CertPEM: string(certPEM),
KeyPEM: string(keyPEM),
Fingerprint: Fingerprint(der),
}, nil
}