From a651e408ed87fb9311c6f7bbb72cdc8d5441206a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Emmanuel=20Beno=C3=AEt?= <tseeker@nocternity.net>
Date: Fri, 5 Nov 2021 14:24:04 +0100
Subject: [PATCH] File builder

  * Code that will build a certificate file from its configuration
  * Run it on all configured files from main
---
 buildcert.go | 177 +++++++++++++++++++++++++++++++++++++++++++++++++++
 main.go      |  15 +++++
 2 files changed, 192 insertions(+)
 create mode 100644 buildcert.go

diff --git a/buildcert.go b/buildcert.go
new file mode 100644
index 0000000..07996bf
--- /dev/null
+++ b/buildcert.go
@@ -0,0 +1,177 @@
+package main
+
+import (
+	"encoding/pem"
+	"fmt"
+	"io/ioutil"
+)
+
+// Max supported CA chain length
+const MAX_CA_CHAIN_LENGTH = 8
+
+type (
+	// Certificate building, including the configuration, LDAP connection,
+	// and the array of chunks that's being built.
+	tCertificateBuilder struct {
+		config *tCertificateFileConfig
+		conn   *tLdapConn
+		data   [][]byte
+	}
+)
+
+// Initialize a certificate file building using a LDAP connection and
+// certificate file configuration.
+func NewCertificateBuilder(conn *tLdapConn, config *tCertificateFileConfig) tCertificateBuilder {
+	return tCertificateBuilder{
+		config: config,
+		conn:   conn,
+		data:   make([][]byte, 0),
+	}
+}
+
+// Build the certificate file's data, returning any error that occurs while
+// reading the source data.
+func (b *tCertificateBuilder) Build() error {
+	err := b.appendPemFiles(b.config.PrependFiles)
+	if err != nil {
+		return err
+	}
+	err = b.appendCertificate()
+	if err != nil {
+		return err
+	}
+	err = b.appendCaCertificates()
+	if err != nil {
+		return err
+	}
+	err = b.appendPemFiles(b.config.AppendFiles)
+	if err != nil {
+		return err
+	}
+	if b.config.Reverse {
+		b.reverseChunks()
+	}
+	return nil
+}
+
+// Append PEM files from a list.
+func (b *tCertificateBuilder) appendPemFiles(files []string) error {
+	for _, path := range files {
+		var err error
+		err = b.appendPem(path)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// Append a PEM file to the current list of data chunks
+func (b *tCertificateBuilder) appendPem(input string) error {
+	data, err := ioutil.ReadFile(input)
+	if err != nil {
+		return fmt.Errorf("Could not load '%s': %w", input, err)
+	}
+	rest := data
+	hadBlock := false
+	for {
+		var block *pem.Block
+		block, rest = pem.Decode(rest)
+		if block == nil {
+			break
+		}
+		hadBlock = true
+		b.data = append(b.data, pem.EncodeToMemory(block))
+	}
+	if hadBlock {
+		return nil
+	} else {
+		return fmt.Errorf("No PEM blocks found in '%s'", input)
+	}
+}
+
+// Append the main, end-entity certificate from the LDAP
+func (b *tCertificateBuilder) appendCertificate() error {
+	if b.config.Certificate != "" {
+		dn := b.conn.Config.Structure.BaseDN
+		if dn != "" {
+			dn = "," + dn
+		}
+		dn = b.config.Certificate + dn
+		data, err := b.conn.getEndEntityCertificate(dn)
+		if err != nil {
+			return err
+		}
+		b.data = append(b.data, data)
+	}
+	return nil
+}
+
+// Append all CA certificates, reading the list from the LDAP or from the
+// configuration.
+func (b *tCertificateBuilder) appendCaCertificates() error {
+	if len(b.config.CACertificates) != 0 {
+		return b.appendListedCaCerts()
+	} else if b.config.CAChainOf != "" {
+		return b.appendChainedCaCerts()
+	} else {
+		return nil
+	}
+}
+
+// Append CA certificates based on a list of DNs
+func (b *tCertificateBuilder) appendListedCaCerts() error {
+	bdn := b.conn.Config.Structure.BaseDN
+	if bdn != "" {
+		bdn = "," + bdn
+	}
+	for _, dn := range b.config.CACertificates {
+		data, _, err := b.conn.getCaCertificate(dn + bdn)
+		if err != nil {
+			return err
+		}
+		if data == nil {
+			return fmt.Errorf("No CA certificate at DN '%s'", dn)
+		}
+		b.data = append(b.data, data)
+	}
+	return nil
+}
+
+// Append CA certificates by following a chain starting at some DN
+func (b *tCertificateBuilder) appendChainedCaCerts() error {
+	nFound := 0
+	dn := b.config.CAChainOf
+	if b.conn.Config.Structure.BaseDN != "" {
+		dn = dn + "," + b.conn.Config.Structure.BaseDN
+	}
+	for {
+		data, nextDn, err := b.conn.getCaCertificate(dn)
+		if err != nil {
+			return err
+		}
+		if nFound != 0 {
+			if data == nil {
+				return fmt.Errorf("No CA certificate at DN '%s'", dn)
+			}
+			b.data = append(b.data, data)
+		}
+		if nextDn == "" {
+			return nil
+		}
+		dn = nextDn
+		nFound += 1
+		if nFound == MAX_CA_CHAIN_LENGTH {
+			return fmt.Errorf("DN '%s': CA chain length exceeded", dn)
+		}
+	}
+}
+
+// Reverse the chunks in the list
+func (b *tCertificateBuilder) reverseChunks() {
+	l := len(b.data) / 2
+	for i := 0; i < l/2; i++ {
+		j := l - i - 1
+		b.data[i], b.data[j] = b.data[j], b.data[i]
+	}
+}
diff --git a/main.go b/main.go
index fcc70b6..c8b6b25 100644
--- a/main.go
+++ b/main.go
@@ -70,4 +70,19 @@ func main() {
 		log.WithField("error", err).Fatal("Failed to initialize socket.")
 	}
 	listener.Close()
+
+	conn := getLdapConnection(cfg.LdapConfig)
+	if conn == nil {
+		return
+	}
+	defer conn.close()
+	for i := range cfg.Certificates {
+		builder := NewCertificateBuilder(conn, &cfg.Certificates[i])
+		err := builder.Build()
+		if err != nil {
+			log.WithField("error", err).Error("Failed to build data for certificate '", cfg.Certificates[i].Path)
+			continue
+		}
+		// FIXME: check existing file, try to write
+	}
 }