Skip to content

Commit

Permalink
Added support for multiple certs via 1 binding
Browse files Browse the repository at this point in the history
  • Loading branch information
pivotal-david-osullivan committed May 3, 2022
1 parent 14e0720 commit 1c84d6d
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 38 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ bin/
dependencies/
package/
scratch/
.DS_Store
11 changes: 10 additions & 1 deletion cacerts/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package cacerts
import (
"errors"
"fmt"
"io/ioutil"
"sort"
"strings"

Expand Down Expand Up @@ -47,6 +48,8 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {
return libcnb.BuildResult{}, fmt.Errorf("unable to create configuration resolver\n%w", err)
}

certDir, err := ioutil.TempDir("", "ca-certificates")

var certPaths []string
var contributedHelper bool
for _, e := range context.Plan.Entries {
Expand All @@ -56,7 +59,13 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {
if err != nil {
return libcnb.BuildResult{}, fmt.Errorf("failed to decode CA certificate paths from plan entry:\n%w", err)
}
certPaths = append(certPaths, paths...)
for _, p := range paths {
if extraPaths, err := SplitCerts(p, certDir); err != nil {
return libcnb.BuildResult{}, fmt.Errorf("failed to split certificates at path %s \n%w", p, err)
} else {
certPaths = append(certPaths, extraPaths...)
}
}
case PlanEntryCACertsHelper:
if contributedHelper {
continue
Expand Down
33 changes: 17 additions & 16 deletions cacerts/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package cacerts_test
import (
"io/ioutil"
"os"
"path/filepath"
"testing"

"github.com/buildpacks/libcnb"
Expand Down Expand Up @@ -58,9 +59,9 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
Name: cacerts.PlanEntryCACerts,
Metadata: map[string]interface{}{
"paths": []interface{}{
"some/path/cert1.pem",
"some/path/cert2.pem",
"some/path/cert3.pem",
filepath.Join("testdata", "SecureTrust_CA.pem"),
filepath.Join("testdata", "SecureTrust_CA_Duplicate.pem"),
filepath.Join("testdata", "Go_Daddy_Class_2_CA.pem"),
},
},
},
Expand All @@ -76,11 +77,11 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
contributor, ok := result.Layers[0].(*cacerts.TrustedCACerts)
Expect(ok).To(BeTrue())
Expect(len(contributor.CertPaths)).To(Equal(3))
Expect(contributor.CertPaths).To(ConsistOf([]string{
"some/path/cert1.pem",
"some/path/cert2.pem",
"some/path/cert3.pem",
}))
Expect(contributor.CertPaths).To(ConsistOf(
ContainSubstring(filepath.Join("testdata", "SecureTrust_CA.pem")),
ContainSubstring(filepath.Join("testdata", "SecureTrust_CA_Duplicate.pem")),
ContainSubstring(filepath.Join("testdata", "Go_Daddy_Class_2_CA.pem")),
))
})
})

Expand All @@ -93,16 +94,16 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
Name: cacerts.PlanEntryCACerts,
Metadata: map[string]interface{}{
"paths": []interface{}{
"some/path/cert1.pem",
"some/path/cert3.pem",
filepath.Join("testdata", "SecureTrust_CA.pem"),
filepath.Join("testdata", "Go_Daddy_Class_2_CA.pem"),
},
},
},
{
Name: cacerts.PlanEntryCACerts,
Metadata: map[string]interface{}{
"paths": []interface{}{
"some/path/cert2.pem",
filepath.Join("testdata", "SecureTrust_CA_Duplicate.pem"),
},
},
},
Expand All @@ -118,11 +119,11 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
contributor, ok := result.Layers[0].(*cacerts.TrustedCACerts)
Expect(ok).To(BeTrue())
Expect(len(contributor.CertPaths)).To(Equal(3))
Expect(contributor.CertPaths).To(Equal([]string{
"some/path/cert1.pem",
"some/path/cert2.pem",
"some/path/cert3.pem",
}))
Expect(contributor.CertPaths).To(ConsistOf(
ContainSubstring(filepath.Join("testdata", "SecureTrust_CA.pem")),
ContainSubstring(filepath.Join("testdata", "SecureTrust_CA_Duplicate.pem")),
ContainSubstring(filepath.Join("testdata", "Go_Daddy_Class_2_CA.pem")),
))
})
})

Expand Down
33 changes: 33 additions & 0 deletions cacerts/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,36 @@ func CanonicalString(s string) string {
s = strings.ToLower(s)
return string(regexp.MustCompile(`[[:space:]]+`).ReplaceAll([]byte(s), []byte(" ")))
}

func SplitCerts(path string, certDir string) ([]string, error) {
var paths []string
var block *pem.Block
var rest []byte

raw, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read file at path %q\n%w", path, err)
}

block, rest = pem.Decode(raw)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM data")
} else if len(rest) == 0 {
// only one cert found, use original path
paths = append(paths, path)
return paths, nil
}
for ind := 0; ; ind++ {
if block != nil {
newCertPath := filepath.Join(certDir, fmt.Sprintf("cert_%d_%s", ind, filepath.Base(path)))
if os.WriteFile(newCertPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: block.Bytes}), 0777); err != nil {
return nil, fmt.Errorf("failed to write extra certficate to file\n%w", err)
}
paths = append(paths, newCertPath)
} else {
break
}
block, rest = pem.Decode(rest)
}
return paths, nil
}
35 changes: 35 additions & 0 deletions cacerts/certs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,41 @@ func testCerts(t *testing.T, context spec.G, it spec.S) {
Expect(cacerts.CanonicalString("SOME VAL")).To(Equal("some val"))
})
})

context("SplitCerts", func() {
var dir string
it.Before(func() {
var err error
dir, err = ioutil.TempDir("", "multi-certs")
Expect(err).NotTo(HaveOccurred())
})

it.After(func() {
Expect(os.RemoveAll(dir)).To(Succeed())
})

it("splits file with X certs into X new files", func() {
paths, err := cacerts.SplitCerts(filepath.Join("testdata", "multiple-certs.pem"), dir)
Expect(err).NotTo(HaveOccurred())
Expect(paths).To(HaveLen(2))
Expect(paths[0]).To(BeARegularFile())
Expect(paths[1]).To(BeARegularFile())
Expect(paths).To(ConsistOf(
ContainSubstring("cert_0_multiple-certs.pem"),
ContainSubstring("cert_1_multiple-certs.pem"),
))
})
it("does not split file with 1 cert", func() {
paths, err := cacerts.SplitCerts(filepath.Join("testdata", "SecureTrust_CA.pem"), dir)
Expect(err).NotTo(HaveOccurred())
Expect(paths).To(HaveLen(1))
Expect(paths[0]).To(Equal(filepath.Join("testdata", "SecureTrust_CA.pem")))
})
it("returns an error when PEM data cannot be read", func() {
_, err := cacerts.SplitCerts(filepath.Join("testdata", "SecureTrust_CA-corrupt.pem"), dir)
Expect(err).To(HaveOccurred())
})
})
}

type rdnSeq []rdnSET
Expand Down
22 changes: 16 additions & 6 deletions cacerts/execd.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,28 @@ func NewExecD(bindings libcnb.Bindings) *ExecD {
// Execute adds certificates from bindings of type "ca-certificates" to the system truststore at launch time.
func (e *ExecD) Execute() (map[string]string, error) {
env := map[string]string{}
paths := getsCertsFromBindings(e.Bindings)
if len(paths) == 0 {
return env, nil
}
var splitPaths []string
certDir, err := ioutil.TempDir("", "ca-certificates")
if err != nil {
return nil, fmt.Errorf("failed to create temp dir\n%w", err)
}
if err := e.GenerateHashLinks(certDir, paths); err != nil {

paths := getsCertsFromBindings(e.Bindings)
if len(paths) == 0 || err != nil {
return env, err
}
for _, p := range paths {
if extraPaths, err := SplitCerts(p, certDir); err != nil {
return nil, fmt.Errorf("failed to split certificates at path %s \n%w", p, err)
} else {
splitPaths = append(splitPaths, extraPaths...)
}
}

if err := e.GenerateHashLinks(certDir, splitPaths); err != nil {
return nil, fmt.Errorf("failed to generate CA certficate symlinks\n%w", err)
}
e.Logger.Infof("Added %d additional CA certificate(s) to system truststore", len(paths))
e.Logger.Infof("Added %d additional CA certificate(s) to system truststore", len(splitPaths))

if v := e.GetEnv(EnvCAPath); v == "" {
env[EnvCAPath] = certDir
Expand Down
30 changes: 15 additions & 15 deletions cacerts/execd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,17 @@ func testExecD(t *testing.T, context spec.G, it spec.S) {
execd.Bindings = []libcnb.Binding{
{
Type: "ca-certificates",
Path: "some-path",
Path: "testdata",
Secret: map[string]string{
"cert1.pem": "",
"cert2.pem": "",
"SecureTrust_CA.pem": "",
"SecureTrust_CA_Duplicate.pem": "",
},
},
{
Type: "ca-certificates",
Path: "other-path",
Path: "testdata",
Secret: map[string]string{
"cert3.pem": "",
"Go_Daddy_Class_2_CA.pem": "",
},
},
}
Expand Down Expand Up @@ -102,11 +102,11 @@ func testExecD(t *testing.T, context spec.G, it spec.S) {
envFile, err := execd.Execute()
Expect(err).NotTo(HaveOccurred())
Expect(called).To(Equal(1))
Expect(certPaths).To(Equal([]string{
filepath.Join("other-path", "cert3.pem"),
filepath.Join("some-path", "cert1.pem"),
filepath.Join("some-path", "cert2.pem"),
}))
Expect(certPaths).To(ConsistOf(
ContainSubstring(filepath.Join("testdata", "SecureTrust_CA.pem")),
ContainSubstring(filepath.Join("testdata", "SecureTrust_CA_Duplicate.pem")),
ContainSubstring(filepath.Join("testdata", "Go_Daddy_Class_2_CA.pem")),
))
Expect(envFile["SSL_CERT_DIR"]).To(Equal(certDir))
})
})
Expand All @@ -119,11 +119,11 @@ func testExecD(t *testing.T, context spec.G, it spec.S) {
it("appends to SSL_CERT_DIR a dir containing hash links", func() {
envFile, err := execd.Execute()
Expect(err).NotTo(HaveOccurred())
Expect(certPaths).To(Equal([]string{
filepath.Join("other-path", "cert3.pem"),
filepath.Join("some-path", "cert1.pem"),
filepath.Join("some-path", "cert2.pem"),
}))
Expect(certPaths).To(ConsistOf(
ContainSubstring(filepath.Join("testdata", "SecureTrust_CA.pem")),
ContainSubstring(filepath.Join("testdata", "SecureTrust_CA_Duplicate.pem")),
ContainSubstring(filepath.Join("testdata", "Go_Daddy_Class_2_CA.pem")),
))
Expect(envFile["SSL_CERT_DIR"]).To(Equal("some-dir" + string(os.PathListSeparator) + certDir))
})
})
Expand Down
19 changes: 19 additions & 0 deletions cacerts/testdata/SecureTrust_CA-corrupt.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
NVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz
MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv
cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz
Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO
0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao
wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj
7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS
8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT
BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB
/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg
JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC
NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3
6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/
3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm
D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS
CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR
3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE=
-----END CERTIFICATE-----

0 comments on commit 1c84d6d

Please sign in to comment.