Browse Source

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

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Moritz Johner 4 years ago
parent
commit
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
 
 You can use templates to inject your secrets into a configuration file that you mount into your pod:
+
 ``` 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
 {% 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
-{% include 'template-v2-from-secret.yaml' %}
+{% include 'jwk-template-v2-external-secret.yaml' %}
 ```
 
 ## Helper functions
+
 !!! info inline end
 
     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).
 
 <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
 
 You have to opt-in to use the new engine version by specifying `template.engineVersion=v2`:
+
 ```yaml
 apiVersion: external-secrets.io/v1alpha1
 kind: ExternalSecret
@@ -74,17 +99,18 @@ spec:
       data:
         # this used to be {{ .foobar | toString }}
         egg: "new: {{ .foobar }}"
-
-        #
-        mycert: "{{ .mysecret | pkcs12cert | pemCertificate }}"
 {% endraw %}
 ```
 
 ##### 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:
   name: template
 spec:
-  refreshInterval: 1h
-  secretStoreRef:
-    name: secretstore-sample
-    kind: SecretStore
+  # ...
   target:
     name: secret-to-be-created
     # 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:
   name: template
 spec:
-  refreshInterval: 1h
-  secretStoreRef:
-    name: secretstore-sample
-    kind: SecretStore
+  # ...
   target:
-    name: secret-to-be-created
-    # this is how the Kind=Secret will look like
     template:
       type: kubernetes.io/tls
       engineVersion: v2
       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 %}

+ 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/lestrrat-go/jwx/jwk"
-	"github.com/youmark/pkcs8"
 	"golang.org/x/crypto/pkcs12"
 	corev1 "k8s.io/api/core/v1"
 )
@@ -33,9 +32,6 @@ var tplFuncs = tpl.FuncMap{
 	"pkcs12cert":     pkcs12cert,
 	"pkcs12certPass": pkcs12certPass,
 
-	"pemPrivateKey":  pemPrivateKey,
-	"pemCertificate": pemCertificate,
-
 	"jwkPublicKeyPem":  jwkPublicKeyPem,
 	"jwkPrivateKeyPem": jwkPrivateKeyPem,
 }
@@ -49,20 +45,18 @@ const (
 	errParse                = "unable to parse template at key %s: %s"
 	errExecute              = "unable to execute template at key %s: %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"
-	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() {
-	fmt.Printf("calling init in v2 pkg")
 	sprigFuncs := sprig.TxtFuncMap()
 	delete(sprigFuncs, "env")
 	delete(sprigFuncs, "expandenv")
 
 	for k, v := range sprigFuncs {
-		fmt.Printf("adding func %s\n", k)
 		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) {
-	key, _, err := pkcs12.Decode([]byte(input), pass)
+	blocks, err := pkcs12.ToPEM([]byte(input), pass)
 	if err != nil {
 		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) {
@@ -119,11 +147,35 @@ func pkcs12key(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 {
 		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) {
@@ -170,19 +222,3 @@ func pemEncode(thing, kind string) (string, error) {
 	err := pem.Encode(buf, &pem.Block{Type: kind, Bytes: []byte(thing)})
 	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
 limitations under the License.
 */
-package template_test
+package template
 
 import (
+	"os"
 	"strings"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
-
-	"github.com/external-secrets/external-secrets/pkg/template/v2"
 )
 
 const (
@@ -234,8 +234,8 @@ func TestExecute(t *testing.T) {
 		{
 			name: "base64 pkcs12 extract",
 			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{
 				"secret": []byte(pkcs12ContentNoPass),
@@ -248,8 +248,8 @@ func TestExecute(t *testing.T) {
 		{
 			name: "base64 pkcs12 extract with password",
 			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{
 				"secret": []byte(pkcs12ContentWithPass),
@@ -272,7 +272,7 @@ func TestExecute(t *testing.T) {
 		{
 			name: "pkcs12 key wrong password",
 			tpl: map[string][]byte{
-				"key": []byte(`{{ .secret | b64dec | pkcs12keyPass "wrong" | pemPrivateKey }}`),
+				"key": []byte(`{{ .secret | b64dec | pkcs12keyPass "wrong" }}`),
 			},
 			data: map[string][]byte{
 				"secret": []byte(pkcs12ContentWithPass),
@@ -282,7 +282,7 @@ func TestExecute(t *testing.T) {
 		{
 			name: "pkcs12 cert wrong password",
 			tpl: map[string][]byte{
-				"cert": []byte(`{{ .secret | b64dec | pkcs12certPass "wrong" | pemCertificate }}`),
+				"cert": []byte(`{{ .secret | b64dec | pkcs12certPass "wrong" }}`),
 			},
 			data: map[string][]byte{
 				"secret": []byte(pkcs12ContentWithPass),
@@ -361,7 +361,7 @@ func TestExecute(t *testing.T) {
 			sec := &corev1.Secret{
 				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) {
 				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)
 }
+
+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)
+		})
+	}
+}