mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
The setup API accepts file paths for the LND certificate and macaroon and for the CLN lightning directory. Previously the raw file contents were read and stored without any validation. Validate these inputs before persisting them: - LND cert: parse the PEM and store only the re-encoded certificate(s), discarding any other PEM blocks (e.g. a bundled private key). - LND macaroon: unmarshal and store the re-marshalled macaroon. - CLN lightning directory: verify it contains the TLS credentials (ca.pem, client.pem, client-key.pem) that CLN loads at connect time, including the hold subdirectory when configured. On failure, return a generic error to the client and log the detail server-side. File paths remain supported for Umbrel-style installs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
161 lines
4.7 KiB
Go
161 lines
4.7 KiB
Go
package api
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
crand "crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"math/big"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"gopkg.in/macaroon.v2"
|
|
)
|
|
|
|
// generateTestCert returns a self-signed certificate PEM block and its
|
|
// matching EC private key PEM block.
|
|
func generateTestCert(t *testing.T) (certPEM []byte, keyPEM []byte) {
|
|
t.Helper()
|
|
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), crand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
template := x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{CommonName: "test"},
|
|
NotBefore: time.Unix(0, 0),
|
|
NotAfter: time.Unix(1<<31, 0),
|
|
}
|
|
der, err := x509.CreateCertificate(crand.Reader, &template, &template, &key.PublicKey, key)
|
|
require.NoError(t, err)
|
|
|
|
certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
|
|
|
keyDER, err := x509.MarshalPKCS8PrivateKey(key)
|
|
require.NoError(t, err)
|
|
keyPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
|
|
|
|
return certPEM, keyPEM
|
|
}
|
|
|
|
func TestReadAndCanonicalizeLNDCert(t *testing.T) {
|
|
certPEM, keyPEM := generateTestCert(t)
|
|
dir := t.TempDir()
|
|
|
|
t.Run("valid certificate", func(t *testing.T) {
|
|
path := filepath.Join(dir, "tls.cert")
|
|
require.NoError(t, os.WriteFile(path, certPEM, 0600))
|
|
|
|
got, err := readAndCanonicalizeLNDCert(path)
|
|
require.NoError(t, err)
|
|
|
|
raw, err := hex.DecodeString(got)
|
|
require.NoError(t, err)
|
|
require.True(t, x509.NewCertPool().AppendCertsFromPEM(raw))
|
|
})
|
|
|
|
t.Run("bundled private key is stripped", func(t *testing.T) {
|
|
path := filepath.Join(dir, "bundle.pem")
|
|
require.NoError(t, os.WriteFile(path, append(append([]byte{}, certPEM...), keyPEM...), 0600))
|
|
|
|
got, err := readAndCanonicalizeLNDCert(path)
|
|
require.NoError(t, err)
|
|
|
|
raw, err := hex.DecodeString(got)
|
|
require.NoError(t, err)
|
|
// Only the CERTIFICATE block must survive - the private key must not
|
|
// be persisted.
|
|
require.NotContains(t, string(raw), "PRIVATE KEY")
|
|
require.Contains(t, string(raw), "CERTIFICATE")
|
|
})
|
|
|
|
t.Run("arbitrary non-cert file is rejected", func(t *testing.T) {
|
|
path := filepath.Join(dir, "secret.txt")
|
|
require.NoError(t, os.WriteFile(path, []byte("root:x:0:0:root:/root:/bin/bash\n"), 0600))
|
|
|
|
_, err := readAndCanonicalizeLNDCert(path)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("missing file is rejected", func(t *testing.T) {
|
|
_, err := readAndCanonicalizeLNDCert(filepath.Join(dir, "does-not-exist"))
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestReadAndCanonicalizeLNDMacaroon(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
t.Run("valid macaroon", func(t *testing.T) {
|
|
mac, err := macaroon.New([]byte("root-key"), []byte("id"), "location", macaroon.LatestVersion)
|
|
require.NoError(t, err)
|
|
raw, err := mac.MarshalBinary()
|
|
require.NoError(t, err)
|
|
|
|
path := filepath.Join(dir, "admin.macaroon")
|
|
require.NoError(t, os.WriteFile(path, raw, 0600))
|
|
|
|
got, err := readAndCanonicalizeLNDMacaroon(path)
|
|
require.NoError(t, err)
|
|
|
|
gotRaw, err := hex.DecodeString(got)
|
|
require.NoError(t, err)
|
|
roundTrip := &macaroon.Macaroon{}
|
|
require.NoError(t, roundTrip.UnmarshalBinary(gotRaw))
|
|
})
|
|
|
|
t.Run("arbitrary non-macaroon file is rejected", func(t *testing.T) {
|
|
path := filepath.Join(dir, "id_rsa")
|
|
require.NoError(t, os.WriteFile(path, []byte("-----BEGIN OPENSSH PRIVATE KEY-----\nsecret\n"), 0600))
|
|
|
|
_, err := readAndCanonicalizeLNDMacaroon(path)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("missing file is rejected", func(t *testing.T) {
|
|
_, err := readAndCanonicalizeLNDMacaroon(filepath.Join(dir, "does-not-exist"))
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestValidateCLNLightningDir(t *testing.T) {
|
|
certPEM, keyPEM := generateTestCert(t)
|
|
|
|
writeCLNDir := func(t *testing.T, dir string) {
|
|
t.Helper()
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "ca.pem"), certPEM, 0600))
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "client.pem"), certPEM, 0600))
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "client-key.pem"), keyPEM, 0600))
|
|
}
|
|
|
|
t.Run("valid directory", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeCLNDir(t, dir)
|
|
require.NoError(t, validateCLNLightningDir(dir, false))
|
|
})
|
|
|
|
t.Run("valid directory with hold subdirectory", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeCLNDir(t, dir)
|
|
holdDir := filepath.Join(dir, "hold")
|
|
require.NoError(t, os.Mkdir(holdDir, 0700))
|
|
writeCLNDir(t, holdDir)
|
|
require.NoError(t, validateCLNLightningDir(dir, true))
|
|
})
|
|
|
|
t.Run("hold requested but subdirectory missing", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeCLNDir(t, dir)
|
|
require.Error(t, validateCLNLightningDir(dir, true))
|
|
})
|
|
|
|
t.Run("arbitrary directory is rejected", func(t *testing.T) {
|
|
require.Error(t, validateCLNLightningDir(t.TempDir(), false))
|
|
})
|
|
}
|