setup_test.go raw

   1  package api
   2  
   3  import (
   4  	"crypto/ecdsa"
   5  	"crypto/elliptic"
   6  	crand "crypto/rand"
   7  	"crypto/x509"
   8  	"crypto/x509/pkix"
   9  	"encoding/hex"
  10  	"encoding/pem"
  11  	"math/big"
  12  	"os"
  13  	"path/filepath"
  14  	"testing"
  15  	"time"
  16  
  17  	"github.com/stretchr/testify/require"
  18  	"gopkg.in/macaroon.v2"
  19  )
  20  
  21  // generateTestCert returns a self-signed certificate PEM block and its
  22  // matching EC private key PEM block.
  23  func generateTestCert(t *testing.T) (certPEM []byte, keyPEM []byte) {
  24  	t.Helper()
  25  
  26  	key, err := ecdsa.GenerateKey(elliptic.P256(), crand.Reader)
  27  	require.NoError(t, err)
  28  
  29  	template := x509.Certificate{
  30  		SerialNumber: big.NewInt(1),
  31  		Subject:      pkix.Name{CommonName: "test"},
  32  		NotBefore:    time.Unix(0, 0),
  33  		NotAfter:     time.Unix(1<<31, 0),
  34  	}
  35  	der, err := x509.CreateCertificate(crand.Reader, &template, &template, &key.PublicKey, key)
  36  	require.NoError(t, err)
  37  
  38  	certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
  39  
  40  	keyDER, err := x509.MarshalPKCS8PrivateKey(key)
  41  	require.NoError(t, err)
  42  	keyPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
  43  
  44  	return certPEM, keyPEM
  45  }
  46  
  47  func TestReadAndCanonicalizeLNDCert(t *testing.T) {
  48  	certPEM, keyPEM := generateTestCert(t)
  49  	dir := t.TempDir()
  50  
  51  	t.Run("valid certificate", func(t *testing.T) {
  52  		path := filepath.Join(dir, "tls.cert")
  53  		require.NoError(t, os.WriteFile(path, certPEM, 0600))
  54  
  55  		got, err := readAndCanonicalizeLNDCert(path)
  56  		require.NoError(t, err)
  57  
  58  		raw, err := hex.DecodeString(got)
  59  		require.NoError(t, err)
  60  		require.True(t, x509.NewCertPool().AppendCertsFromPEM(raw))
  61  	})
  62  
  63  	t.Run("bundled private key is stripped", func(t *testing.T) {
  64  		path := filepath.Join(dir, "bundle.pem")
  65  		require.NoError(t, os.WriteFile(path, append(append([]byte{}, certPEM...), keyPEM...), 0600))
  66  
  67  		got, err := readAndCanonicalizeLNDCert(path)
  68  		require.NoError(t, err)
  69  
  70  		raw, err := hex.DecodeString(got)
  71  		require.NoError(t, err)
  72  		// Only the CERTIFICATE block must survive - the private key must not
  73  		// be persisted.
  74  		require.NotContains(t, string(raw), "PRIVATE KEY")
  75  		require.Contains(t, string(raw), "CERTIFICATE")
  76  	})
  77  
  78  	t.Run("arbitrary non-cert file is rejected", func(t *testing.T) {
  79  		path := filepath.Join(dir, "secret.txt")
  80  		require.NoError(t, os.WriteFile(path, []byte("root:x:0:0:root:/root:/bin/bash\n"), 0600))
  81  
  82  		_, err := readAndCanonicalizeLNDCert(path)
  83  		require.Error(t, err)
  84  	})
  85  
  86  	t.Run("missing file is rejected", func(t *testing.T) {
  87  		_, err := readAndCanonicalizeLNDCert(filepath.Join(dir, "does-not-exist"))
  88  		require.Error(t, err)
  89  	})
  90  }
  91  
  92  func TestReadAndCanonicalizeLNDMacaroon(t *testing.T) {
  93  	dir := t.TempDir()
  94  
  95  	t.Run("valid macaroon", func(t *testing.T) {
  96  		mac, err := macaroon.New([]byte("root-key"), []byte("id"), "location", macaroon.LatestVersion)
  97  		require.NoError(t, err)
  98  		raw, err := mac.MarshalBinary()
  99  		require.NoError(t, err)
 100  
 101  		path := filepath.Join(dir, "admin.macaroon")
 102  		require.NoError(t, os.WriteFile(path, raw, 0600))
 103  
 104  		got, err := readAndCanonicalizeLNDMacaroon(path)
 105  		require.NoError(t, err)
 106  
 107  		gotRaw, err := hex.DecodeString(got)
 108  		require.NoError(t, err)
 109  		roundTrip := &macaroon.Macaroon{}
 110  		require.NoError(t, roundTrip.UnmarshalBinary(gotRaw))
 111  	})
 112  
 113  	t.Run("arbitrary non-macaroon file is rejected", func(t *testing.T) {
 114  		path := filepath.Join(dir, "id_rsa")
 115  		require.NoError(t, os.WriteFile(path, []byte("-----BEGIN OPENSSH PRIVATE KEY-----\nsecret\n"), 0600))
 116  
 117  		_, err := readAndCanonicalizeLNDMacaroon(path)
 118  		require.Error(t, err)
 119  	})
 120  
 121  	t.Run("missing file is rejected", func(t *testing.T) {
 122  		_, err := readAndCanonicalizeLNDMacaroon(filepath.Join(dir, "does-not-exist"))
 123  		require.Error(t, err)
 124  	})
 125  }
 126  
 127  func TestValidateCLNLightningDir(t *testing.T) {
 128  	certPEM, keyPEM := generateTestCert(t)
 129  
 130  	writeCLNDir := func(t *testing.T, dir string) {
 131  		t.Helper()
 132  		require.NoError(t, os.WriteFile(filepath.Join(dir, "ca.pem"), certPEM, 0600))
 133  		require.NoError(t, os.WriteFile(filepath.Join(dir, "client.pem"), certPEM, 0600))
 134  		require.NoError(t, os.WriteFile(filepath.Join(dir, "client-key.pem"), keyPEM, 0600))
 135  	}
 136  
 137  	t.Run("valid directory", func(t *testing.T) {
 138  		dir := t.TempDir()
 139  		writeCLNDir(t, dir)
 140  		require.NoError(t, validateCLNLightningDir(dir, false))
 141  	})
 142  
 143  	t.Run("valid directory with hold subdirectory", func(t *testing.T) {
 144  		dir := t.TempDir()
 145  		writeCLNDir(t, dir)
 146  		holdDir := filepath.Join(dir, "hold")
 147  		require.NoError(t, os.Mkdir(holdDir, 0700))
 148  		writeCLNDir(t, holdDir)
 149  		require.NoError(t, validateCLNLightningDir(dir, true))
 150  	})
 151  
 152  	t.Run("hold requested but subdirectory missing", func(t *testing.T) {
 153  		dir := t.TempDir()
 154  		writeCLNDir(t, dir)
 155  		require.Error(t, validateCLNLightningDir(dir, true))
 156  	})
 157  
 158  	t.Run("arbitrary directory is rejected", func(t *testing.T) {
 159  		require.Error(t, validateCLNLightningDir(t.TempDir(), false))
 160  	})
 161  }
 162