Skip to content

Commit db4471f

Browse files
Merge pull request #396 from erikgb/truststore-extract
refactor: extract truststore encoding to internal package
2 parents cd0369c + 80b78be commit db4471f

File tree

5 files changed

+228
-160
lines changed

5 files changed

+228
-160
lines changed

pkg/bundle/bundle_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
4141

4242
trustapi "github.com/cert-manager/trust-manager/pkg/apis/trust/v1alpha1"
43+
"github.com/cert-manager/trust-manager/pkg/bundle/internal/truststore"
4344
"github.com/cert-manager/trust-manager/pkg/fspkg"
4445
"github.com/cert-manager/trust-manager/test/dummy"
4546
"github.com/cert-manager/trust-manager/test/gen"
@@ -48,7 +49,7 @@ import (
4849
func testEncodeJKS(t *testing.T, data string) []byte {
4950
t.Helper()
5051

51-
encoded, err := jksEncoder{password: trustapi.DefaultJKSPassword}.encode(data)
52+
encoded, err := truststore.NewJKSEncoder(trustapi.DefaultJKSPassword).Encode(data)
5253
if err != nil {
5354
t.Error(err)
5455
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
Copyright 2021 The cert-manager Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package truststore
18+
19+
import (
20+
"bytes"
21+
"crypto/sha256"
22+
"encoding/hex"
23+
"fmt"
24+
25+
"github.com/pavlo-v-chernykh/keystore-go/v4"
26+
"software.sslmate.com/src/go-pkcs12"
27+
28+
"github.com/cert-manager/trust-manager/pkg/util"
29+
)
30+
31+
type Encoder interface {
32+
Encode(trustBundle string) ([]byte, error)
33+
}
34+
35+
func NewJKSEncoder(password string) Encoder {
36+
return &jksEncoder{password: password}
37+
}
38+
39+
type jksEncoder struct {
40+
password string
41+
}
42+
43+
// Encode creates a binary JKS file from the given PEM-encoded trust bundle and Password.
44+
// Note that the Password is not treated securely; JKS files generally seem to expect a Password
45+
// to exist and so we have the option for one.
46+
func (e jksEncoder) Encode(trustBundle string) ([]byte, error) {
47+
cas, err := util.DecodeX509CertificateChainBytes([]byte(trustBundle))
48+
if err != nil {
49+
return nil, fmt.Errorf("failed to decode trust bundle: %w", err)
50+
}
51+
52+
// WithOrderedAliases ensures that trusted certs are added to the JKS file in order,
53+
// which makes the files appear to be reliably deterministic.
54+
ks := keystore.New(keystore.WithOrderedAliases())
55+
56+
for _, c := range cas {
57+
alias := certAlias(c.Raw, c.Subject.String())
58+
59+
// Note on CreationTime:
60+
// Debian's JKS trust store sets the creation time to match the time that certs are added to the
61+
// trust store (i.e., it's effectively time.Now() at the instant the file is generated).
62+
// Using that method would make our JKS files in trust-manager non-deterministic, leaving us with
63+
// two options if we want to maintain determinism:
64+
// - Using something from the cert being added (e.g. NotBefore / NotAfter)
65+
// - Using a fixed time (i.e. unix epoch)
66+
// We use NotBefore here, arbitrarily.
67+
68+
err = ks.SetTrustedCertificateEntry(alias, keystore.TrustedCertificateEntry{
69+
CreationTime: c.NotBefore,
70+
Certificate: keystore.Certificate{
71+
Type: "X509",
72+
Content: c.Raw,
73+
},
74+
})
75+
76+
if err != nil {
77+
// this error should never happen if we set jks.Certificate correctly
78+
return nil, fmt.Errorf("failed to add cert with alias %q to trust store: %w", alias, err)
79+
}
80+
}
81+
82+
buf := &bytes.Buffer{}
83+
84+
err = ks.Store(buf, []byte(e.password))
85+
if err != nil {
86+
return nil, fmt.Errorf("failed to create JKS file: %w", err)
87+
}
88+
89+
return buf.Bytes(), nil
90+
}
91+
92+
func NewPKCS12Encoder(password string) Encoder {
93+
return &pkcs12Encoder{password: password}
94+
}
95+
96+
type pkcs12Encoder struct {
97+
password string
98+
}
99+
100+
func (e pkcs12Encoder) Encode(trustBundle string) ([]byte, error) {
101+
cas, err := util.DecodeX509CertificateChainBytes([]byte(trustBundle))
102+
if err != nil {
103+
return nil, fmt.Errorf("failed to decode trust bundle: %w", err)
104+
}
105+
106+
var entries []pkcs12.TrustStoreEntry
107+
for _, c := range cas {
108+
entries = append(entries, pkcs12.TrustStoreEntry{
109+
Cert: c,
110+
FriendlyName: certAlias(c.Raw, c.Subject.String()),
111+
})
112+
}
113+
114+
encoder := pkcs12.LegacyRC2
115+
116+
if e.password == "" {
117+
encoder = pkcs12.Passwordless
118+
}
119+
120+
return encoder.EncodeTrustStoreEntries(entries, e.password)
121+
}
122+
123+
// certAlias creates a JKS-safe alias for the given DER-encoded certificate, such that
124+
// any two certificates will have a different aliases unless they're identical in every way.
125+
// This unique alias fixes an issue where we used the Issuer field as an alias, leading to
126+
// different certs being treated as identical.
127+
// The friendlyName is included in the alias as a UX feature when examining JKS files using a
128+
// tool like `keytool`.
129+
func certAlias(derData []byte, friendlyName string) string {
130+
certHashBytes := sha256.Sum256(derData)
131+
certHash := hex.EncodeToString(certHashBytes[:])
132+
133+
// Since certHash is the part which actually distinguishes between two
134+
// certificates, put it first so that it won't be truncated if a cert
135+
// with a really long subject is added. Not sure what the upper limit
136+
// for length actually is, but it shouldn't matter here.
137+
138+
return certHash[:8] + "|" + friendlyName
139+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
Copyright 2021 The cert-manager Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package truststore
18+
19+
import (
20+
"bytes"
21+
"crypto/x509"
22+
"encoding/pem"
23+
"testing"
24+
25+
"github.com/pavlo-v-chernykh/keystore-go/v4"
26+
27+
"github.com/cert-manager/trust-manager/pkg/apis/trust/v1alpha1"
28+
"github.com/cert-manager/trust-manager/test/dummy"
29+
)
30+
31+
func Test_encodeJKSAliases(t *testing.T) {
32+
// IMPORTANT: We use TestCertificate1 and TestCertificate2 here because they're defined
33+
// to be self-signed and to also use the same Subject, while being different certs.
34+
// This test ensures that the aliases we create when adding to a JKS file is different under
35+
// these conditions (where the issuer / subject is identical).
36+
// Using different dummy certs would allow this test to pass but wouldn't actually test anything useful!
37+
bundle := dummy.JoinCerts(dummy.TestCertificate1, dummy.TestCertificate2)
38+
39+
jksFile, err := jksEncoder{password: v1alpha1.DefaultJKSPassword}.Encode(bundle)
40+
if err != nil {
41+
t.Fatalf("didn't expect an error but got: %s", err)
42+
}
43+
44+
reader := bytes.NewReader(jksFile)
45+
46+
ks := keystore.New()
47+
48+
err = ks.Load(reader, []byte(v1alpha1.DefaultJKSPassword))
49+
if err != nil {
50+
t.Fatalf("failed to parse generated JKS file: %s", err)
51+
}
52+
53+
entryNames := ks.Aliases()
54+
55+
if len(entryNames) != 2 {
56+
t.Fatalf("expected two certs in JKS file but got %d", len(entryNames))
57+
}
58+
}
59+
60+
func Test_certAlias(t *testing.T) {
61+
// We might not ever rely on aliases being stable, but this test seeks
62+
// to enforce stability for now. It'll be easy to remove.
63+
64+
// If this test starts failing after TestCertificate1 is updated, it'll
65+
// need to be updated with the new alias for the new cert.
66+
67+
block, _ := pem.Decode([]byte(dummy.TestCertificate1))
68+
if block == nil {
69+
t.Fatalf("couldn't parse a PEM block from TestCertificate1")
70+
}
71+
72+
cert, err := x509.ParseCertificate(block.Bytes)
73+
if err != nil {
74+
t.Fatalf("Dummy certificate TestCertificate1 couldn't be parsed: %s", err)
75+
}
76+
77+
alias := certAlias(cert.Raw, cert.Subject.String())
78+
79+
expectedAlias := "548b988f|CN=cmct-test-root,O=cert-manager"
80+
81+
if alias != expectedAlias {
82+
t.Fatalf("expected alias to be %q but got %q", expectedAlias, alias)
83+
}
84+
}

pkg/bundle/source.go

Lines changed: 3 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,18 @@ import (
2020
"bytes"
2121
"context"
2222
"crypto/sha256"
23-
"encoding/hex"
2423
"encoding/pem"
2524
"fmt"
2625
"slices"
2726
"strings"
2827

29-
jks "github.com/pavlo-v-chernykh/keystore-go/v4"
3028
corev1 "k8s.io/api/core/v1"
3129
apierrors "k8s.io/apimachinery/pkg/api/errors"
3230
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3331
"sigs.k8s.io/controller-runtime/pkg/client"
34-
"software.sslmate.com/src/go-pkcs12"
3532

3633
trustapi "github.com/cert-manager/trust-manager/pkg/apis/trust/v1alpha1"
34+
"github.com/cert-manager/trust-manager/pkg/bundle/internal/truststore"
3735
"github.com/cert-manager/trust-manager/pkg/util"
3836
)
3937

@@ -210,120 +208,22 @@ func (b *bundle) secretBundle(ctx context.Context, ref *trustapi.SourceObjectKey
210208
return results.String(), nil
211209
}
212210

213-
type jksEncoder struct {
214-
password string
215-
}
216-
217-
// encodeJKS creates a binary JKS file from the given PEM-encoded trust bundle and password.
218-
// Note that the password is not treated securely; JKS files generally seem to expect a password
219-
// to exist and so we have the option for one.
220-
func (e jksEncoder) encode(trustBundle string) ([]byte, error) {
221-
cas, err := util.DecodeX509CertificateChainBytes([]byte(trustBundle))
222-
if err != nil {
223-
return nil, fmt.Errorf("failed to decode trust bundle: %w", err)
224-
}
225-
226-
// WithOrderedAliases ensures that trusted certs are added to the JKS file in order,
227-
// which makes the files appear to be reliably deterministic.
228-
ks := jks.New(jks.WithOrderedAliases())
229-
230-
for _, c := range cas {
231-
alias := certAlias(c.Raw, c.Subject.String())
232-
233-
// Note on CreationTime:
234-
// Debian's JKS trust store sets the creation time to match the time that certs are added to the
235-
// trust store (i.e., it's effectively time.Now() at the instant the file is generated).
236-
// Using that method would make our JKS files in trust-manager non-deterministic, leaving us with
237-
// two options if we want to maintain determinism:
238-
// - Using something from the cert being added (e.g. NotBefore / NotAfter)
239-
// - Using a fixed time (i.e. unix epoch)
240-
// We use NotBefore here, arbitrarily.
241-
242-
err = ks.SetTrustedCertificateEntry(alias, jks.TrustedCertificateEntry{
243-
CreationTime: c.NotBefore,
244-
Certificate: jks.Certificate{
245-
Type: "X509",
246-
Content: c.Raw,
247-
},
248-
})
249-
250-
if err != nil {
251-
// this error should never happen if we set jks.Certificate correctly
252-
return nil, fmt.Errorf("failed to add cert with alias %q to trust store: %w", alias, err)
253-
}
254-
}
255-
256-
buf := &bytes.Buffer{}
257-
258-
err = ks.Store(buf, []byte(e.password))
259-
if err != nil {
260-
return nil, fmt.Errorf("failed to create JKS file: %w", err)
261-
}
262-
263-
return buf.Bytes(), nil
264-
}
265-
266-
// certAlias creates a JKS-safe alias for the given DER-encoded certificate, such that
267-
// any two certificates will have a different aliases unless they're identical in every way.
268-
// This unique alias fixes an issue where we used the Issuer field as an alias, leading to
269-
// different certs being treated as identical.
270-
// The friendlyName is included in the alias as a UX feature when examining JKS files using a
271-
// tool like `keytool`.
272-
func certAlias(derData []byte, friendlyName string) string {
273-
certHashBytes := sha256.Sum256(derData)
274-
certHash := hex.EncodeToString(certHashBytes[:])
275-
276-
// Since certHash is the part which actually distinguishes between two
277-
// certificates, put it first so that it won't be truncated if a cert
278-
// with a really long subject is added. Not sure what the upper limit
279-
// for length actually is, but it shouldn't matter here.
280-
281-
return certHash[:8] + "|" + friendlyName
282-
}
283-
284-
type pkcs12Encoder struct {
285-
password string
286-
}
287-
288-
func (e pkcs12Encoder) encode(trustBundle string) ([]byte, error) {
289-
cas, err := util.DecodeX509CertificateChainBytes([]byte(trustBundle))
290-
if err != nil {
291-
return nil, fmt.Errorf("failed to decode trust bundle: %w", err)
292-
}
293-
294-
var entries []pkcs12.TrustStoreEntry
295-
for _, c := range cas {
296-
entries = append(entries, pkcs12.TrustStoreEntry{
297-
Cert: c,
298-
FriendlyName: certAlias(c.Raw, c.Subject.String()),
299-
})
300-
}
301-
302-
encoder := pkcs12.LegacyRC2
303-
304-
if e.password == "" {
305-
encoder = pkcs12.Passwordless
306-
}
307-
308-
return encoder.EncodeTrustStoreEntries(entries, e.password)
309-
}
310-
311211
func (b *bundleData) populateData(bundles []string, formats *trustapi.AdditionalFormats) error {
312212
b.data = strings.Join(bundles, "\n") + "\n"
313213

314214
if formats != nil {
315215
b.binaryData = make(map[string][]byte)
316216

317217
if formats.JKS != nil {
318-
encoded, err := jksEncoder{password: *formats.JKS.Password}.encode(b.data)
218+
encoded, err := truststore.NewJKSEncoder(*formats.JKS.Password).Encode(b.data)
319219
if err != nil {
320220
return fmt.Errorf("failed to encode JKS: %w", err)
321221
}
322222
b.binaryData[formats.JKS.Key] = encoded
323223
}
324224

325225
if formats.PKCS12 != nil {
326-
encoded, err := pkcs12Encoder{password: *formats.PKCS12.Password}.encode(b.data)
226+
encoded, err := truststore.NewPKCS12Encoder(*formats.PKCS12.Password).Encode(b.data)
327227
if err != nil {
328228
return fmt.Errorf("failed to encode PKCS12: %w", err)
329229
}

0 commit comments

Comments
 (0)