Просмотр исходного кода

fix(template): extract multiple certs/keys from PKCS#12

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Moritz Johner 4 лет назад
Родитель
Сommit
9486dd85dd

+ 50 - 24
docs/guides-templating.md

@@ -5,48 +5,73 @@ With External Secrets Operator you can transform the data from the external secr
 ## Examples
 ## Examples
 
 
 You can use templates to inject your secrets into a configuration file that you mount into your pod:
 You can use templates to inject your secrets into a configuration file that you mount into your pod:
+
 ``` yaml
 ``` yaml
 {% include 'multiline-template-v2-external-secret.yaml' %}
 {% include 'multiline-template-v2-external-secret.yaml' %}
 ```
 ```
 
 
-You can also use pre-defined functions to extract data from your secrets. Here: extract key/cert from a pkcs12 archive and store it as PEM.
+### TemplateFrom
+
+You do not have to define your templates inline in an ExternalSecret but you can pull `ConfigMaps` or other Secrets that contain a template. Consider the following example:
+
+```yaml
+{% include 'template-v2-from-secret.yaml' %}
+```
+
+
+### Extract Keys and Certificates from PKCS#12 Archive
+You can use pre-defined functions to extract data from your secrets. Here: extract keys and certificates from a PKCS#12 archive and store it as PEM.
+
 ``` yaml
 ``` yaml
 {% include 'pkcs12-template-v2-external-secret.yaml' %}
 {% include 'pkcs12-template-v2-external-secret.yaml' %}
 ```
 ```
 
 
-### TemplateFrom
+### Extract from JWK
+You can extract the public or private key parts of a JWK and use them as [PKCS#8](https://pkg.go.dev/crypto/x509#ParsePKCS8PrivateKey) private key or PEM-encoded [PKIX](https://pkg.go.dev/crypto/x509#MarshalPKIXPublicKey) public key.
+
+A JWK looks similar to this:
+```json
+{
+  "kty": "RSA",
+  "kid": "cc34c0a0-bd5a-4a3c-a50d-a2a7db7643df",
+  "use": "sig",
+  "n": "pjdss...",
+  "e": "AQAB"
+  // ...
+}
+```
 
 
-You do not have to define your templates inline in an ExternalSecret but you can pull `ConfigMaps` or other Secrets that contain a template. Consider the following example:
+And what you want may be a PEM-encoded public or private key portion of it. Take a look at this example on how to transform it into the desired format:
 
 
 ``` yaml
 ``` yaml
-{% include 'template-v2-from-secret.yaml' %}
+{% include 'jwk-template-v2-external-secret.yaml' %}
 ```
 ```
 
 
 ## Helper functions
 ## Helper functions
+
 !!! info inline end
 !!! info inline end
 
 
     Note: we removed `env` and `expandenv` from sprig functions for security reasons.
     Note: we removed `env` and `expandenv` from sprig functions for security reasons.
 
 
-We provide a couple of convenience functions that help you transform your secrets. This is useful when dealing with pkcs12 or jwk encoded secrets.
+We provide a couple of convenience functions that help you transform your secrets. This is useful when dealing with PKCS#12 archives or JSON Web Keys (JWK).
 
 
 In addition to that you can use over 200+ [sprig functions](http://masterminds.github.io/sprig/). If you feel a function is missing or might be valuable feel free to open an issue and submit a [pull request](contributing-process.md#submitting-a-pull-request).
 In addition to that you can use over 200+ [sprig functions](http://masterminds.github.io/sprig/). If you feel a function is missing or might be valuable feel free to open an issue and submit a [pull request](contributing-process.md#submitting-a-pull-request).
 
 
 <br/>
 <br/>
 
 
-| Function       | Description                                                                | Input                            | Output        |
-| -------------- | -------------------------------------------------------------------------- | -------------------------------- | ------------- |
-| pkcs12key      | extracts the private key from a pkcs12 archive                             | `string`                         | `string`      |
-| pkcs12keyPass  | extracts the private key from a pkcs12 archive using the provided password | password `string`, data `string` | `string`      |
-| pkcs12cert     | extracts the certificate from a pkcs12 archive                             | `string`                         | `string`      |
-| pkcs12certPass | extracts the certificate from a pkcs12 archive using the provided password | password `string`, data `string` | `string`      |
-| pemPrivateKey  | PEM encodes the provided bytes as private key                              | `string`                         | `string`      |
-| pemCertificate | PEM encodes the provided bytes as certificate                              | `string`                         | `string`      |
-| jwkPublicKeyPem | takes an json-serialized JWK as `string` and returns an PEM block of type `PUBLIC KEY` that contains the public key ([see here](https://golang.org/pkg/crypto/x509/#MarshalPKIXPublicKey)) for details | `string`                         | `string`      |
-| jwkPrivateKeyPem | takes an json-serialized JWK as `string` and returns an PEM block of type `PRIVATE KEY` that contains the private key in PKCS #8 format ([see here](https://golang.org/pkg/crypto/x509/#MarshalPKCS8PrivateKey)) for details | `string`                         | `string`      |
+| Function         | Description                                                                                                                                                                                                                  |
+| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| pkcs12key        | Extracts all private keys from a PKCS#12 archive and encodes them in **PKCS#8 PEM** format.                                                                                                                                            |
+| pkcs12keyPass    | Same as `pkcs12key`. Uses the provided password to decrypt the PKCS#12 archive.                                                                                                                                                   |
+| pkcs12cert       | Extracts all certificates from a PKCS#12 archive and orders them if possible. If disjunct or multiple leaf certs are provided they are returned as-is. <br/> Sort order: `leaf / intermediate(s) / root`.                     |
+| pkcs12certPass   | Same as `pkcs12cert`. Uses the provided password to decrypt the PKCS#12 archive.                                                                                                                                                   |
+| jwkPublicKeyPem  | Takes an json-serialized JWK and returns an PEM block of type `PUBLIC KEY` that contains the public key. [See here](https://golang.org/pkg/crypto/x509/#MarshalPKIXPublicKey) for details.                     |
+| jwkPrivateKeyPem | Takes an json-serialized JWK as `string` and returns an PEM block of type `PRIVATE KEY` that contains the private key in PKCS #8 format. [See here](https://golang.org/pkg/crypto/x509/#MarshalPKCS8PrivateKey) for details. |
 
 
 ## Migrating from v1
 ## Migrating from v1
 
 
 You have to opt-in to use the new engine version by specifying `template.engineVersion=v2`:
 You have to opt-in to use the new engine version by specifying `template.engineVersion=v2`:
+
 ```yaml
 ```yaml
 apiVersion: external-secrets.io/v1alpha1
 apiVersion: external-secrets.io/v1alpha1
 kind: ExternalSecret
 kind: ExternalSecret
@@ -74,17 +99,18 @@ spec:
       data:
       data:
         # this used to be {{ .foobar | toString }}
         # this used to be {{ .foobar | toString }}
         egg: "new: {{ .foobar }}"
         egg: "new: {{ .foobar }}"
-
-        #
-        mycert: "{{ .mysecret | pkcs12cert | pemCertificate }}"
 {% endraw %}
 {% endraw %}
 ```
 ```
 
 
 ##### Functions removed/replaced
 ##### Functions removed/replaced
 
 
-* `base64encode` was renamed to `b64enc`.
-* `base64decode` was renamed to `b64dec`. Any errors that occurr during decoding are silenced.
-* `fromJSON` was renamed to `fromJson`. Any errors that occurr during unmarshalling are silenced.
-* `toJSON` was renamed to `toJson`. Any errors that occurr during marshalling are silenced.
-* `toString` implementation was replaced by the `sprig` implementation and should be api-compatible.
-* `toBytes` was removed.
+- `base64encode` was renamed to `b64enc`.
+- `base64decode` was renamed to `b64dec`. Any errors that occurr during decoding are silenced.
+- `fromJSON` was renamed to `fromJson`. Any errors that occurr during unmarshalling are silenced.
+- `toJSON` was renamed to `toJson`. Any errors that occurr during marshalling are silenced.
+- `pkcs12key` and `pkcs12keyPass` encode the PKCS#8 key directly into PEM format. There is no need to call `pemPrivateKey` anymore. Also, these functions do extract all private keys from the PKCS#12 archive not just the first one.
+- `pkcs12cert` and `pkcs12certPass` encode the certs directly into PEM format. There is no need to call `pemCertificate` anymore. These functions now **extract all certificates** from the PKCS#12 archive not just the first one.
+- `toString` implementation was replaced by the `sprig` implementation and should be api-compatible.
+- `toBytes` was removed.
+- `pemPrivateKey` was removed. It's now implemented within the `pkcs12*` functions.
+- `pemCertificate` was removed. It's now implemented within the `pkcs12*` functions.

+ 25 - 0
docs/snippets/jwk-template-v2-external-secret.yaml

@@ -0,0 +1,25 @@
+{% raw %}
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: template
+spec:
+  # ...
+  target:
+    template:
+      engineVersion: v2
+      data:
+        # .myjwk is a json-encoded JWK string.
+        #
+        # this template will produce for jwk_pub a PEM encoded public key:
+        # -----BEGIN PUBLIC KEY-----
+        # MIIBI...
+        # ...
+        # ...AQAB
+        # -----END PUBLIC KEY-----
+        jwk_pub: "{{ .myjwk | jwkPublicKeyPem }}"
+        # private key is a pem-encoded PKCS#8 private key
+        jwk_priv: "{{ .myjwk | jwkPrivateKeyPem }}"
+
+
+{% endraw %}

+ 1 - 4
docs/snippets/multiline-template-v2-external-secret.yaml

@@ -4,10 +4,7 @@ kind: ExternalSecret
 metadata:
 metadata:
   name: template
   name: template
 spec:
 spec:
-  refreshInterval: 1h
-  secretStoreRef:
-    name: secretstore-sample
-    kind: SecretStore
+  # ...
   target:
   target:
     name: secret-to-be-created
     name: secret-to-be-created
     # this is how the Kind=Secret will look like
     # this is how the Kind=Secret will look like

+ 6 - 14
docs/snippets/pkcs12-template-v2-external-secret.yaml

@@ -4,24 +4,16 @@ kind: ExternalSecret
 metadata:
 metadata:
   name: template
   name: template
 spec:
 spec:
-  refreshInterval: 1h
-  secretStoreRef:
-    name: secretstore-sample
-    kind: SecretStore
+  # ...
   target:
   target:
-    name: secret-to-be-created
-    # this is how the Kind=Secret will look like
     template:
     template:
       type: kubernetes.io/tls
       type: kubernetes.io/tls
       engineVersion: v2
       engineVersion: v2
       data:
       data:
-        tls.crt: "{{ .mysecret | pkcs12cert | pemCertificate }}"
-        tls.key: "{{ .mysecret | pkcs12key | pemPrivateKey }}"
+        tls.crt: "{{ .mysecret | pkcs12cert }}"
+        tls.key: "{{ .mysecret | pkcs12key }}"
+
+        # if needed unlock the pkcs12 with the password
+        tls.crt: "{{ .mysecret | pkcs12certPass "my-password" }}"
 
 
-  data:
-  # this is a pkcs12 archive that contains
-  # a cert and a private key
-  - secretKey: mysecret
-    remoteRef:
-      key: example
 {% endraw %}
 {% endraw %}

+ 80 - 0
pkg/template/v2/_testdata/Makefile

@@ -0,0 +1,80 @@
+# prerequisite:
+# install step cli
+# from: https://github.com/smallstep/cli
+
+all: ca disjunct-ca intermediate leaf  \
+	pkcs12-nopass pkcs12-disjunct pkcs12-multibag pkcs12-withpass-1234
+
+clean:
+	rm *.{pfx,crt,key,pem}
+
+ca:
+	step certificate create root-ca \
+		root-ca.crt root-ca.key \
+		--profile root-ca --kty OKP --curve Ed25519 \
+		--no-password --insecure -f
+
+disjunct-ca:
+	step certificate create disjunct-root-ca \
+		disjunct-root-ca.crt disjunct-root-ca.key \
+		--profile root-ca --kty OKP --curve Ed25519 \
+		--no-password --insecure -f
+
+intermediate:
+	step certificate create intermediate-ca \
+		intermediate-ca.crt intermediate-ca.key \
+            --profile intermediate-ca \
+			--ca ./root-ca.crt \
+			--ca-key ./root-ca.key \
+			--kty EC --curve P-256 \
+			--no-password --insecure -f
+
+leaf:
+	step certificate create foo \
+		foo.crt foo.key --profile leaf \
+		--ca ./intermediate-ca.crt \
+		--ca-key ./intermediate-ca.key \
+		--no-password --insecure -f
+
+pkcs12-nopass: ca intermediate leaf
+	# deliberately in wrong order
+	cat foo.crt root-ca.crt intermediate-ca.crt > chain.pem
+
+	# create pkcs12
+	openssl pkcs12 -export \
+		-in chain.pem \
+		-inkey foo.key \
+		-out foo-nopass.pfx \
+		-password pass:
+
+pkcs12-disjunct: ca intermediate disjunct-ca leaf
+	cat root-ca.crt intermediate-ca.crt disjunct-root-ca.crt > disjunct-chain.pem
+
+	openssl pkcs12 -export \
+		-in foo.crt \
+		-certfile disjunct-chain.pem \
+		-inkey foo.key \
+		-out foo-disjunct-nopass.pfx \
+		-password pass:
+
+pkcs12-multibag: ca intermediate leaf
+	# deliberately in wrong order, we're missing the leaf cert here
+	cat root-ca.crt intermediate-ca.crt > intermediate-chain.pem
+
+	openssl pkcs12 -export \
+		-in foo.crt \
+		-certfile intermediate-chain.pem \
+		-inkey foo.key \
+		-out foo-multibag-nopass.pfx \
+		-password pass:
+
+pkcs12-withpass-1234: ca intermediate leaf
+	# deliberately in the wrong order
+	cat foo.crt root-ca.crt intermediate-ca.crt > chain.pem
+
+	# create pkcs12
+	openssl pkcs12 -export \
+		-in chain.pem \
+		-inkey foo.key \
+		-out foo-withpass-1234.pfx \
+		-password pass:1234

+ 31 - 0
pkg/template/v2/_testdata/chain.pem

@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIBqjCCAU+gAwIBAgIRAPnGGsBUMbZhmh5QdnYdBmUwCgYIKoZIzj0EAwIwGjEY
+MBYGA1UEAxMPaW50ZXJtZWRpYXRlLWNhMB4XDTIyMDIwOTEwMjUzMVoXDTIyMDIx
+MDEwMjUzMVowDjEMMAoGA1UEAxMDZm9vMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
+QgAEqnxdeInykx8JZsLi13rZLekoG2cosQ3F+2InVNy7hCQ7soMqdaJsGQ6LFtov
+ogUFtOOTRWrunblqNWGZsowHbKOBgTB/MA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUE
+FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFLtundVbuKd73OWzo6SY
+by0Ajeb2MB8GA1UdIwQYMBaAFCLg80J/bZBbOd+Y8+V94l5xM2zEMA4GA1UdEQQH
+MAWCA2ZvbzAKBggqhkjOPQQDAgNJADBGAiEA4K4SbVNqrEtl7RfwBfJFMnWI+X8D
+zMPMc4Xqzp2qTxcCIQDsySgtiakypZfWakpB49zJph0kLwGK8xhWvGMUw1N1/w==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIBJzCB2qADAgECAhEArvunrLoYXTmwMROkmbAlBTAFBgMrZXAwEjEQMA4GA1UE
+AxMHcm9vdC1jYTAeFw0yMjAyMDkxMDI1MzBaFw0zMjAyMDcxMDI1MzBaMBIxEDAO
+BgNVBAMTB3Jvb3QtY2EwKjAFBgMrZXADIQDSw5uQ1io+jcKevCH0sl+tGTB6/BQs
+Bu84ibw13QoP36NFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8C
+AQEwHQYDVR0OBBYEFGtn1GnNjmd6cxOhMnVau1L8IkGtMAUGAytlcANBAOHSAS4z
+/6ctcvRwlGr9Hyt7vVLROImD2t3rFdDDHLLL1znikK3JZvVbETyMFOMbOMQS33C/
+4FtLGenZFXySjQw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIBgDCCATKgAwIBAgIRAOzjpCdp42oW5MoccLpRXpAwBQYDK2VwMBIxEDAOBgNV
+BAMTB3Jvb3QtY2EwHhcNMjIwMjA5MTAyNTMxWhcNMzIwMjA3MTAyNTMxWjAaMRgw
+FgYDVQQDEw9pbnRlcm1lZGlhdGUtY2EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
+AATekdyX6cZe0Ajmme363TQoWnrQwXnARzeWEf4FRQE8BGWgf8z7wljjpb4M4S4f
++CJAYYY/6x38UnlsxXEeBTofo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/
+BAgwBgEB/wIBADAdBgNVHQ4EFgQUIuDzQn9tkFs535jz5X3iXnEzbMQwHwYDVR0j
+BBgwFoAUa2fUac2OZ3pzE6EydVq7UvwiQa0wBQYDK2VwA0EA4gntaGs/3ME6q1y9
+gO4ntri2qwoC25l3q7q9BiFBmeBmvS6I1w9HCZHtB3JnVC/IYDTCYDNTbpGWEOjl
+aCKLCA==
+-----END CERTIFICATE-----

+ 29 - 0
pkg/template/v2/_testdata/disjunct-chain.pem

@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIBJzCB2qADAgECAhEArvunrLoYXTmwMROkmbAlBTAFBgMrZXAwEjEQMA4GA1UE
+AxMHcm9vdC1jYTAeFw0yMjAyMDkxMDI1MzBaFw0zMjAyMDcxMDI1MzBaMBIxEDAO
+BgNVBAMTB3Jvb3QtY2EwKjAFBgMrZXADIQDSw5uQ1io+jcKevCH0sl+tGTB6/BQs
+Bu84ibw13QoP36NFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8C
+AQEwHQYDVR0OBBYEFGtn1GnNjmd6cxOhMnVau1L8IkGtMAUGAytlcANBAOHSAS4z
+/6ctcvRwlGr9Hyt7vVLROImD2t3rFdDDHLLL1znikK3JZvVbETyMFOMbOMQS33C/
+4FtLGenZFXySjQw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIBgDCCATKgAwIBAgIRAOzjpCdp42oW5MoccLpRXpAwBQYDK2VwMBIxEDAOBgNV
+BAMTB3Jvb3QtY2EwHhcNMjIwMjA5MTAyNTMxWhcNMzIwMjA3MTAyNTMxWjAaMRgw
+FgYDVQQDEw9pbnRlcm1lZGlhdGUtY2EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
+AATekdyX6cZe0Ajmme363TQoWnrQwXnARzeWEf4FRQE8BGWgf8z7wljjpb4M4S4f
++CJAYYY/6x38UnlsxXEeBTofo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/
+BAgwBgEB/wIBADAdBgNVHQ4EFgQUIuDzQn9tkFs535jz5X3iXnEzbMQwHwYDVR0j
+BBgwFoAUa2fUac2OZ3pzE6EydVq7UvwiQa0wBQYDK2VwA0EA4gntaGs/3ME6q1y9
+gO4ntri2qwoC25l3q7q9BiFBmeBmvS6I1w9HCZHtB3JnVC/IYDTCYDNTbpGWEOjl
+aCKLCA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIBOTCB7KADAgECAhEA0cArEY0s5JoP8JfqIDv2BTAFBgMrZXAwGzEZMBcGA1UE
+AxMQZGlzanVuY3Qtcm9vdC1jYTAeFw0yMjAyMDkxMDI1MzBaFw0zMjAyMDcxMDI1
+MzBaMBsxGTAXBgNVBAMTEGRpc2p1bmN0LXJvb3QtY2EwKjAFBgMrZXADIQAJER3w
+QFZH1DBmxZm9IkaZ5noangcg/CYNP+GtcyQPL6NFMEMwDgYDVR0PAQH/BAQDAgEG
+MBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMddJG4lWXTN/x9InsiJUTxu
+d4+HMAUGAytlcANBABcyLV4om9LPV0TGDf0jiM+JxH1R+ATvAE8FHDtd8L66BrzA
+Id656nPz3fz9ZMB9VZr7iGcghXYlTHxu6NkQEgA=
+-----END CERTIFICATE-----

+ 9 - 0
pkg/template/v2/_testdata/disjunct-root-ca.crt

@@ -0,0 +1,9 @@
+-----BEGIN CERTIFICATE-----
+MIIBOTCB7KADAgECAhEA0cArEY0s5JoP8JfqIDv2BTAFBgMrZXAwGzEZMBcGA1UE
+AxMQZGlzanVuY3Qtcm9vdC1jYTAeFw0yMjAyMDkxMDI1MzBaFw0zMjAyMDcxMDI1
+MzBaMBsxGTAXBgNVBAMTEGRpc2p1bmN0LXJvb3QtY2EwKjAFBgMrZXADIQAJER3w
+QFZH1DBmxZm9IkaZ5noangcg/CYNP+GtcyQPL6NFMEMwDgYDVR0PAQH/BAQDAgEG
+MBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMddJG4lWXTN/x9InsiJUTxu
+d4+HMAUGAytlcANBABcyLV4om9LPV0TGDf0jiM+JxH1R+ATvAE8FHDtd8L66BrzA
+Id656nPz3fz9ZMB9VZr7iGcghXYlTHxu6NkQEgA=
+-----END CERTIFICATE-----

+ 3 - 0
pkg/template/v2/_testdata/disjunct-root-ca.key

@@ -0,0 +1,3 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIDWjPU0WTbgryudXkq5qduRP5utcq8yEsmYPizQ0J1vW
+-----END PRIVATE KEY-----

BIN
pkg/template/v2/_testdata/foo-disjunct-nopass.pfx


BIN
pkg/template/v2/_testdata/foo-multibag-nopass.pfx


BIN
pkg/template/v2/_testdata/foo-nopass.pfx


BIN
pkg/template/v2/_testdata/foo-withpass-1234.pfx


+ 11 - 0
pkg/template/v2/_testdata/foo.crt

@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE-----
+MIIBqjCCAU+gAwIBAgIRAPnGGsBUMbZhmh5QdnYdBmUwCgYIKoZIzj0EAwIwGjEY
+MBYGA1UEAxMPaW50ZXJtZWRpYXRlLWNhMB4XDTIyMDIwOTEwMjUzMVoXDTIyMDIx
+MDEwMjUzMVowDjEMMAoGA1UEAxMDZm9vMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
+QgAEqnxdeInykx8JZsLi13rZLekoG2cosQ3F+2InVNy7hCQ7soMqdaJsGQ6LFtov
+ogUFtOOTRWrunblqNWGZsowHbKOBgTB/MA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUE
+FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFLtundVbuKd73OWzo6SY
+by0Ajeb2MB8GA1UdIwQYMBaAFCLg80J/bZBbOd+Y8+V94l5xM2zEMA4GA1UdEQQH
+MAWCA2ZvbzAKBggqhkjOPQQDAgNJADBGAiEA4K4SbVNqrEtl7RfwBfJFMnWI+X8D
+zMPMc4Xqzp2qTxcCIQDsySgtiakypZfWakpB49zJph0kLwGK8xhWvGMUw1N1/w==
+-----END CERTIFICATE-----

+ 5 - 0
pkg/template/v2/_testdata/foo.key

@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIMuAjEwBeXznOjx3V7viagAznflfL+p64CXkm++xlXhkoAoGCCqGSM49
+AwEHoUQDQgAEqnxdeInykx8JZsLi13rZLekoG2cosQ3F+2InVNy7hCQ7soMqdaJs
+GQ6LFtovogUFtOOTRWrunblqNWGZsowHbA==
+-----END EC PRIVATE KEY-----

+ 11 - 0
pkg/template/v2/_testdata/intermediate-ca.crt

@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE-----
+MIIBgDCCATKgAwIBAgIRAOzjpCdp42oW5MoccLpRXpAwBQYDK2VwMBIxEDAOBgNV
+BAMTB3Jvb3QtY2EwHhcNMjIwMjA5MTAyNTMxWhcNMzIwMjA3MTAyNTMxWjAaMRgw
+FgYDVQQDEw9pbnRlcm1lZGlhdGUtY2EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
+AATekdyX6cZe0Ajmme363TQoWnrQwXnARzeWEf4FRQE8BGWgf8z7wljjpb4M4S4f
++CJAYYY/6x38UnlsxXEeBTofo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/
+BAgwBgEB/wIBADAdBgNVHQ4EFgQUIuDzQn9tkFs535jz5X3iXnEzbMQwHwYDVR0j
+BBgwFoAUa2fUac2OZ3pzE6EydVq7UvwiQa0wBQYDK2VwA0EA4gntaGs/3ME6q1y9
+gO4ntri2qwoC25l3q7q9BiFBmeBmvS6I1w9HCZHtB3JnVC/IYDTCYDNTbpGWEOjl
+aCKLCA==
+-----END CERTIFICATE-----

+ 5 - 0
pkg/template/v2/_testdata/intermediate-ca.key

@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIIYmlQRczt7PR7VN9jjKf5MGcgKHgohcBbrPbikYVWNqoAoGCCqGSM49
+AwEHoUQDQgAE3pHcl+nGXtAI5pnt+t00KFp60MF5wEc3lhH+BUUBPARloH/M+8JY
+46W+DOEuH/giQGGGP+sd/FJ5bMVxHgU6Hw==
+-----END EC PRIVATE KEY-----

+ 20 - 0
pkg/template/v2/_testdata/intermediate-chain.pem

@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIBJzCB2qADAgECAhEArvunrLoYXTmwMROkmbAlBTAFBgMrZXAwEjEQMA4GA1UE
+AxMHcm9vdC1jYTAeFw0yMjAyMDkxMDI1MzBaFw0zMjAyMDcxMDI1MzBaMBIxEDAO
+BgNVBAMTB3Jvb3QtY2EwKjAFBgMrZXADIQDSw5uQ1io+jcKevCH0sl+tGTB6/BQs
+Bu84ibw13QoP36NFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8C
+AQEwHQYDVR0OBBYEFGtn1GnNjmd6cxOhMnVau1L8IkGtMAUGAytlcANBAOHSAS4z
+/6ctcvRwlGr9Hyt7vVLROImD2t3rFdDDHLLL1znikK3JZvVbETyMFOMbOMQS33C/
+4FtLGenZFXySjQw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIBgDCCATKgAwIBAgIRAOzjpCdp42oW5MoccLpRXpAwBQYDK2VwMBIxEDAOBgNV
+BAMTB3Jvb3QtY2EwHhcNMjIwMjA5MTAyNTMxWhcNMzIwMjA3MTAyNTMxWjAaMRgw
+FgYDVQQDEw9pbnRlcm1lZGlhdGUtY2EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
+AATekdyX6cZe0Ajmme363TQoWnrQwXnARzeWEf4FRQE8BGWgf8z7wljjpb4M4S4f
++CJAYYY/6x38UnlsxXEeBTofo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/
+BAgwBgEB/wIBADAdBgNVHQ4EFgQUIuDzQn9tkFs535jz5X3iXnEzbMQwHwYDVR0j
+BBgwFoAUa2fUac2OZ3pzE6EydVq7UvwiQa0wBQYDK2VwA0EA4gntaGs/3ME6q1y9
+gO4ntri2qwoC25l3q7q9BiFBmeBmvS6I1w9HCZHtB3JnVC/IYDTCYDNTbpGWEOjl
+aCKLCA==
+-----END CERTIFICATE-----

+ 9 - 0
pkg/template/v2/_testdata/root-ca.crt

@@ -0,0 +1,9 @@
+-----BEGIN CERTIFICATE-----
+MIIBJzCB2qADAgECAhEArvunrLoYXTmwMROkmbAlBTAFBgMrZXAwEjEQMA4GA1UE
+AxMHcm9vdC1jYTAeFw0yMjAyMDkxMDI1MzBaFw0zMjAyMDcxMDI1MzBaMBIxEDAO
+BgNVBAMTB3Jvb3QtY2EwKjAFBgMrZXADIQDSw5uQ1io+jcKevCH0sl+tGTB6/BQs
+Bu84ibw13QoP36NFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8C
+AQEwHQYDVR0OBBYEFGtn1GnNjmd6cxOhMnVau1L8IkGtMAUGAytlcANBAOHSAS4z
+/6ctcvRwlGr9Hyt7vVLROImD2t3rFdDDHLLL1znikK3JZvVbETyMFOMbOMQS33C/
+4FtLGenZFXySjQw=
+-----END CERTIFICATE-----

+ 3 - 0
pkg/template/v2/_testdata/root-ca.key

@@ -0,0 +1,3 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEICJ7FAxZXElbLQe/yrvr+ZqYQHKf9oGtzsasBZ8a32nk
+-----END PRIVATE KEY-----

+ 141 - 0
pkg/template/v2/pem_chain.go

@@ -0,0 +1,141 @@
+/*
+MIT License
+
+Copyright (c) Microsoft Corporation.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE
+
+Original Author: Anish Ramasekar https://github.com/aramase
+In: https://github.com/Azure/secrets-store-csi-driver-provider-azure/pull/332
+*/
+package template
+
+import (
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+)
+
+const (
+	errNilCert           = "certificate is nil"
+	errFoundDisjunctCert = "found multiple leaf or disjunct certificates"
+	errNoLeafFound       = "no leaf certificate found"
+	errChainCycle        = "constructing chain resulted in cycle"
+)
+
+type node struct {
+	cert     *x509.Certificate
+	parent   *node
+	isParent bool
+}
+
+func fetchCertChains(data []byte) ([]byte, error) {
+	var newCertChain []*x509.Certificate
+	var pemData []byte
+	nodes, err := pemToNodes(data)
+	if err != nil {
+		return nil, err
+	}
+
+	// at the end of this computation, the output will be a single linked list
+	// the tail of the list will be the root node (which has no parents)
+	// the head of the list will be the leaf node (whose parent will be intermediate certs)
+	// (head) leaf -> intermediates -> root (tail)
+	for i := range nodes {
+		for j := range nodes {
+			// ignore same node to prevent generating a cycle
+			if i == j {
+				continue
+			}
+			// if ith node AuthorityKeyId is same as jth node SubjectKeyId, jth node was used
+			// to sign the ith certificate
+			if string(nodes[i].cert.AuthorityKeyId) == string(nodes[j].cert.SubjectKeyId) {
+				nodes[j].isParent = true
+				nodes[i].parent = nodes[j]
+				break
+			}
+		}
+	}
+
+	var foundLeaf bool
+	var leaf *node
+	for i := range nodes {
+		if !nodes[i].isParent {
+			if foundLeaf {
+				return nil, fmt.Errorf(errFoundDisjunctCert)
+			}
+			// this is the leaf node as it's not a parent for any other node
+			leaf = nodes[i]
+			foundLeaf = true
+		}
+	}
+
+	if leaf == nil {
+		return nil, fmt.Errorf(errNoLeafFound)
+	}
+
+	processedNodes := 0
+	// iterate through the directed list and append the nodes to new cert chain
+	for leaf != nil {
+		processedNodes++
+		// ensure we aren't stuck in a cyclic loop
+		if processedNodes > len(nodes) {
+			return pemData, fmt.Errorf(errChainCycle)
+		}
+		newCertChain = append(newCertChain, leaf.cert)
+		leaf = leaf.parent
+	}
+
+	for _, cert := range newCertChain {
+		b := &pem.Block{
+			Type:  pemTypeCertificate,
+			Bytes: cert.Raw,
+		}
+		pemData = append(pemData, pem.EncodeToMemory(b)...)
+	}
+	return pemData, nil
+}
+
+func pemToNodes(data []byte) ([]*node, error) {
+	nodes := make([]*node, 0)
+	for {
+		// decode pem to der first
+		block, rest := pem.Decode(data)
+		data = rest
+
+		if block == nil {
+			break
+		}
+		cert, err := x509.ParseCertificate(block.Bytes)
+		if err != nil {
+			return nil, err
+		}
+		// this should not be the case because ParseCertificate should return a non nil
+		// certificate when there is no error.
+		if cert == nil {
+			return nil, fmt.Errorf(errNilCert)
+		}
+		nodes = append(nodes, &node{
+			cert:     cert,
+			parent:   nil,
+			isParent: false,
+		})
+	}
+	return nodes, nil
+}

+ 68 - 32
pkg/template/v2/template.go

@@ -22,7 +22,6 @@ import (
 
 
 	"github.com/Masterminds/sprig/v3"
 	"github.com/Masterminds/sprig/v3"
 	"github.com/lestrrat-go/jwx/jwk"
 	"github.com/lestrrat-go/jwx/jwk"
-	"github.com/youmark/pkcs8"
 	"golang.org/x/crypto/pkcs12"
 	"golang.org/x/crypto/pkcs12"
 	corev1 "k8s.io/api/core/v1"
 	corev1 "k8s.io/api/core/v1"
 )
 )
@@ -33,9 +32,6 @@ var tplFuncs = tpl.FuncMap{
 	"pkcs12cert":     pkcs12cert,
 	"pkcs12cert":     pkcs12cert,
 	"pkcs12certPass": pkcs12certPass,
 	"pkcs12certPass": pkcs12certPass,
 
 
-	"pemPrivateKey":  pemPrivateKey,
-	"pemCertificate": pemCertificate,
-
 	"jwkPublicKeyPem":  jwkPublicKeyPem,
 	"jwkPublicKeyPem":  jwkPublicKeyPem,
 	"jwkPrivateKeyPem": jwkPrivateKeyPem,
 	"jwkPrivateKeyPem": jwkPrivateKeyPem,
 }
 }
@@ -49,20 +45,18 @@ const (
 	errParse                = "unable to parse template at key %s: %s"
 	errParse                = "unable to parse template at key %s: %s"
 	errExecute              = "unable to execute template at key %s: %s"
 	errExecute              = "unable to execute template at key %s: %s"
 	errDecodePKCS12WithPass = "unable to decode pkcs12 with password: %s"
 	errDecodePKCS12WithPass = "unable to decode pkcs12 with password: %s"
-	errConvertPrivKey       = "unable to convert pkcs12 private key: %s"
 	errDecodeCertWithPass   = "unable to decode pkcs12 certificate with password: %s"
 	errDecodeCertWithPass   = "unable to decode pkcs12 certificate with password: %s"
-	errEncodePEMKey         = "unable to encode pem private key: %s"
-	errEncodePEMCert        = "unable to encode pem certificate: %s"
+	errParsePrivKey         = "unable to parse private key type"
+
+	pemTypeCertificate = "CERTIFICATE"
 )
 )
 
 
 func init() {
 func init() {
-	fmt.Printf("calling init in v2 pkg")
 	sprigFuncs := sprig.TxtFuncMap()
 	sprigFuncs := sprig.TxtFuncMap()
 	delete(sprigFuncs, "env")
 	delete(sprigFuncs, "env")
 	delete(sprigFuncs, "expandenv")
 	delete(sprigFuncs, "expandenv")
 
 
 	for k, v := range sprigFuncs {
 	for k, v := range sprigFuncs {
-		fmt.Printf("adding func %s\n", k)
 		tplFuncs[k] = v
 		tplFuncs[k] = v
 	}
 	}
 }
 }
@@ -103,15 +97,49 @@ func execute(k, val string, data map[string][]byte) ([]byte, error) {
 }
 }
 
 
 func pkcs12keyPass(pass, input string) (string, error) {
 func pkcs12keyPass(pass, input string) (string, error) {
-	key, _, err := pkcs12.Decode([]byte(input), pass)
+	blocks, err := pkcs12.ToPEM([]byte(input), pass)
 	if err != nil {
 	if err != nil {
 		return "", fmt.Errorf(errDecodePKCS12WithPass, err)
 		return "", fmt.Errorf(errDecodePKCS12WithPass, err)
 	}
 	}
-	kb, err := pkcs8.ConvertPrivateKeyToPKCS8(key)
-	if err != nil {
-		return "", fmt.Errorf(errConvertPrivKey, err)
+
+	var pemData []byte
+	for _, block := range blocks {
+		// remove bag attributes like localKeyID, friendlyName
+		block.Headers = nil
+		if block.Type == pemTypeCertificate {
+			continue
+		}
+		key, err := parsePrivateKey(block.Bytes)
+		if err != nil {
+			return "", err
+		}
+		// we use pkcs8 because it supports more key types (ecdsa, ed25519), not just RSA
+		block.Bytes, err = x509.MarshalPKCS8PrivateKey(key)
+		if err != nil {
+			return "", err
+		}
+		// report error if encode fails
+		var buf bytes.Buffer
+		if err := pem.Encode(&buf, block); err != nil {
+			return "", err
+		}
+		pemData = append(pemData, buf.Bytes()...)
+	}
+
+	return string(pemData), nil
+}
+
+func parsePrivateKey(block []byte) (interface{}, error) {
+	if k, err := x509.ParsePKCS1PrivateKey(block); err == nil {
+		return k, nil
+	}
+	if k, err := x509.ParsePKCS8PrivateKey(block); err == nil {
+		return k, nil
+	}
+	if k, err := x509.ParseECPrivateKey(block); err == nil {
+		return k, nil
 	}
 	}
-	return string(kb), nil
+	return nil, fmt.Errorf(errParsePrivKey)
 }
 }
 
 
 func pkcs12key(input string) (string, error) {
 func pkcs12key(input string) (string, error) {
@@ -119,11 +147,35 @@ func pkcs12key(input string) (string, error) {
 }
 }
 
 
 func pkcs12certPass(pass, input string) (string, error) {
 func pkcs12certPass(pass, input string) (string, error) {
-	_, cert, err := pkcs12.Decode([]byte(input), pass)
+	blocks, err := pkcs12.ToPEM([]byte(input), pass)
 	if err != nil {
 	if err != nil {
 		return "", fmt.Errorf(errDecodeCertWithPass, err)
 		return "", fmt.Errorf(errDecodeCertWithPass, err)
 	}
 	}
-	return string(cert.Raw), nil
+
+	var pemData []byte
+	for _, block := range blocks {
+		if block.Type != pemTypeCertificate {
+			continue
+		}
+		// remove bag attributes like localKeyID, friendlyName
+		block.Headers = nil
+		// report error if encode fails
+		var buf bytes.Buffer
+		if err := pem.Encode(&buf, block); err != nil {
+			return "", err
+		}
+		pemData = append(pemData, buf.Bytes()...)
+	}
+
+	// try to order certificate chain. If it fails we return
+	// the unordered raw pem data.
+	// This fails if multiple leaf or disjunct certs are provided.
+	ordered, err := fetchCertChains(pemData)
+	if err != nil {
+		return string(pemData), nil
+	}
+
+	return string(ordered), nil
 }
 }
 
 
 func pkcs12cert(input string) (string, error) {
 func pkcs12cert(input string) (string, error) {
@@ -170,19 +222,3 @@ func pemEncode(thing, kind string) (string, error) {
 	err := pem.Encode(buf, &pem.Block{Type: kind, Bytes: []byte(thing)})
 	err := pem.Encode(buf, &pem.Block{Type: kind, Bytes: []byte(thing)})
 	return buf.String(), err
 	return buf.String(), err
 }
 }
-
-func pemPrivateKey(key string) (string, error) {
-	res, err := pemEncode(key, "PRIVATE KEY")
-	if err != nil {
-		return res, fmt.Errorf(errEncodePEMKey, err)
-	}
-	return res, nil
-}
-
-func pemCertificate(cert string) (string, error) {
-	res, err := pemEncode(cert, "CERTIFICATE")
-	if err != nil {
-		return res, fmt.Errorf(errEncodePEMCert, err)
-	}
-	return res, nil
-}

+ 134 - 10
pkg/template/v2/template_test.go

@@ -11,16 +11,16 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 See the License for the specific language governing permissions and
 limitations under the License.
 limitations under the License.
 */
 */
-package template_test
+package template
 
 
 import (
 import (
+	"os"
 	"strings"
 	"strings"
 	"testing"
 	"testing"
 
 
+	"github.com/google/go-cmp/cmp"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
 	corev1 "k8s.io/api/core/v1"
-
-	"github.com/external-secrets/external-secrets/pkg/template/v2"
 )
 )
 
 
 const (
 const (
@@ -234,8 +234,8 @@ func TestExecute(t *testing.T) {
 		{
 		{
 			name: "base64 pkcs12 extract",
 			name: "base64 pkcs12 extract",
 			tpl: map[string][]byte{
 			tpl: map[string][]byte{
-				"key":  []byte(`{{ .secret | b64dec | pkcs12key | pemPrivateKey }}`),
-				"cert": []byte(`{{ .secret | b64dec | pkcs12cert | pemCertificate }}`),
+				"key":  []byte(`{{ .secret | b64dec | pkcs12key }}`),
+				"cert": []byte(`{{ .secret | b64dec | pkcs12cert }}`),
 			},
 			},
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"secret": []byte(pkcs12ContentNoPass),
 				"secret": []byte(pkcs12ContentNoPass),
@@ -248,8 +248,8 @@ func TestExecute(t *testing.T) {
 		{
 		{
 			name: "base64 pkcs12 extract with password",
 			name: "base64 pkcs12 extract with password",
 			tpl: map[string][]byte{
 			tpl: map[string][]byte{
-				"key":  []byte(`{{ .secret | b64dec | pkcs12keyPass "123456" | pemPrivateKey }}`),
-				"cert": []byte(`{{ .secret | b64dec | pkcs12certPass "123456" | pemCertificate }}`),
+				"key":  []byte(`{{ .secret | b64dec | pkcs12keyPass "123456" }}`),
+				"cert": []byte(`{{ .secret | b64dec | pkcs12certPass "123456" }}`),
 			},
 			},
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"secret": []byte(pkcs12ContentWithPass),
 				"secret": []byte(pkcs12ContentWithPass),
@@ -272,7 +272,7 @@ func TestExecute(t *testing.T) {
 		{
 		{
 			name: "pkcs12 key wrong password",
 			name: "pkcs12 key wrong password",
 			tpl: map[string][]byte{
 			tpl: map[string][]byte{
-				"key": []byte(`{{ .secret | b64dec | pkcs12keyPass "wrong" | pemPrivateKey }}`),
+				"key": []byte(`{{ .secret | b64dec | pkcs12keyPass "wrong" }}`),
 			},
 			},
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"secret": []byte(pkcs12ContentWithPass),
 				"secret": []byte(pkcs12ContentWithPass),
@@ -282,7 +282,7 @@ func TestExecute(t *testing.T) {
 		{
 		{
 			name: "pkcs12 cert wrong password",
 			name: "pkcs12 cert wrong password",
 			tpl: map[string][]byte{
 			tpl: map[string][]byte{
-				"cert": []byte(`{{ .secret | b64dec | pkcs12certPass "wrong" | pemCertificate }}`),
+				"cert": []byte(`{{ .secret | b64dec | pkcs12certPass "wrong" }}`),
 			},
 			},
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"secret": []byte(pkcs12ContentWithPass),
 				"secret": []byte(pkcs12ContentWithPass),
@@ -361,7 +361,7 @@ func TestExecute(t *testing.T) {
 			sec := &corev1.Secret{
 			sec := &corev1.Secret{
 				Data: make(map[string][]byte),
 				Data: make(map[string][]byte),
 			}
 			}
-			err := template.Execute(row.tpl, row.data, sec)
+			err := Execute(row.tpl, row.data, sec)
 			if !ErrorContains(err, row.expErr) {
 			if !ErrorContains(err, row.expErr) {
 				t.Errorf("unexpected error: %s, expected: %s", err, row.expErr)
 				t.Errorf("unexpected error: %s, expected: %s", err, row.expErr)
 			}
 			}
@@ -382,3 +382,127 @@ func ErrorContains(out error, want string) bool {
 	}
 	}
 	return strings.Contains(out.Error(), want)
 	return strings.Contains(out.Error(), want)
 }
 }
+
+func TestPkcs12certPass(t *testing.T) {
+	const (
+		leafCertPath         = "_testdata/foo.crt"
+		intermediateCertPath = "_testdata/intermediate-ca.crt"
+		rootCertPath         = "_testdata/root-ca.crt"
+		disjunctCertPath     = "_testdata/disjunct-root-ca.crt"
+	)
+	type args struct {
+		pass     string
+		filename string
+	}
+	type testCase struct {
+		name    string
+		args    args
+		want    []string
+		wantErr bool
+	}
+	tests := []testCase{
+		{
+			// this case expects the whole chain to be stored
+			// in a single bag.
+			// bag(1): leaf/root/intermediate cert
+			// bag(2): private key
+			name: "read file without password",
+			args: args{
+				pass:     "",
+				filename: "_testdata/foo-nopass.pfx",
+			},
+			want: []string{
+				// this order is important
+				leafCertPath,
+				intermediateCertPath,
+				rootCertPath,
+			},
+		},
+		{
+			// same as above but with password
+			name: "read file with password",
+			args: args{
+				pass:     "1234",
+				filename: "_testdata/foo-withpass-1234.pfx",
+			},
+			want: []string{
+				// this order is important
+				leafCertPath,
+				intermediateCertPath,
+				rootCertPath,
+			},
+		},
+		{
+			// cert chain may be stored in different bags
+			// this test case uses a pfx that has the following structure:
+			// bag(1): leaf certificate
+			// bag(2): root + intermediate cert
+			// bag(3): private key
+			name: "read multibag cert chain",
+			args: args{
+				pass:     "",
+				filename: "_testdata/foo-multibag-nopass.pfx",
+			},
+			want: []string{
+				// this order is important
+				leafCertPath,
+				intermediateCertPath,
+				rootCertPath,
+			},
+		},
+		{
+			// cert chain may contain a disjunct cert
+			// bag(1): leaf/root/intermediate/disjunct
+			// bag(2): private key
+			name: "read disjunct cert chain",
+			args: args{
+				pass:     "",
+				filename: "_testdata/foo-disjunct-nopass.pfx",
+			},
+			want: []string{
+				// this order is important
+				leafCertPath,
+				rootCertPath,
+				intermediateCertPath,
+				disjunctCertPath,
+			},
+		},
+		{
+			name: "read file wrong password",
+			args: args{
+				pass:     "wrongpass",
+				filename: "_testdata/foo-withpass-1234.pfx",
+			},
+			wantErr: true,
+		},
+	}
+
+	testFunc := func(t *testing.T, tc testCase) {
+		archive, err := os.ReadFile(tc.args.filename)
+		if err != nil {
+			t.Error(err)
+		}
+		var expOut []byte
+		for _, w := range tc.want {
+			c, err := os.ReadFile(w)
+			if err != nil {
+				t.Error(err)
+			}
+			expOut = append(expOut, c...)
+		}
+		got, err := pkcs12certPass(tc.args.pass, string(archive))
+		if (err != nil) != tc.wantErr {
+			t.Errorf("pkcs12certPass() error = %v, wantErr %v", err, tc.wantErr)
+			return
+		}
+		if diff := cmp.Diff(string(expOut), got); diff != "" {
+			t.Errorf("pkcs12certPass() = diff:\n%s", diff)
+		}
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			testFunc(t, tt)
+		})
+	}
+}