From aa1c5410bf194af3715f15e5fda9717b7db3eeed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Cuadrado=20Juan?= Date: Thu, 15 Feb 2024 14:50:45 +0100 Subject: [PATCH 1/3] feat: Add CEL kw.crypto.verifyCert() function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Víctor Cuadrado Juan --- internal/cel/library/crypto.go | 138 ++++++++++++++++++++++++++++ internal/cel/library/crypto_test.go | 128 ++++++++++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 internal/cel/library/crypto.go create mode 100644 internal/cel/library/crypto_test.go diff --git a/internal/cel/library/crypto.go b/internal/cel/library/crypto.go new file mode 100644 index 0000000..f9085bb --- /dev/null +++ b/internal/cel/library/crypto.go @@ -0,0 +1,138 @@ +package library + +import ( + "reflect" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/common/types/traits" + "github.com/kubewarden/policy-sdk-go/pkg/capabilities/crypto" +) + +// Crypto returns a cel.EnvOption to configure namespaced crypto host-callback +// Kubewarden functions. +// +// # Crypto.VerifyCert +// +// This CEL function accepts a certificate, a certificate chain, and an +// expiration date. +// It returns a bool on whether the provided CertificateVerificationRequest +// (containing a cert to be verified, a cert chain, and an expiration date) +// passes certificate verification. +// +// Accepts 3 arguments: +// - string, of PEM-encoded certificate to verify. +// - list of strings, of PEM-encoded certs, ordered by trust usage +// (intermediates first, root last). If empty, certificate is assumed trusted. +// - string in RFC 3339 time format, to check expiration against. +// If empty, certificate is assumed never expired. +// +// Returns a map() with 2 fields: +// - "Trusted": informing if certificate passed verification or not +// - "Reason": with reason, in case "Trusted" is false +// +// Usage in CEL: +// +// crypto.verifyCert(, list(), ) -> map(, value) +// +// Example: +// +// kw.crypto.verifyCert( +// '---BEGIN CERTIFICATE---foo---END CERTIFICATE---', +// [ +// '---BEGIN CERTIFICATE---bar---END CERTIFICATE---' +// ], +// '2030-08-15T16:23:42+00:00' +// )" +func Crypto() cel.EnvOption { + return cel.Lib(cryptoLib{}) +} + +type cryptoLib struct{} + +// LibraryName implements the SingletonLibrary interface method. +func (cryptoLib) LibraryName() string { + return "kw.crypto" +} + +// CompileOptions implements the Library interface method. +func (cryptoLib) CompileOptions() []cel.EnvOption { + return []cel.EnvOption{ + // group every binding under a container to simplify usage + cel.Container("crypto"), + + cel.Function("kw.crypto.verifyCert", + cel.Overload("kw_crypto_verify_cert", + []*cel.Type{ + cel.StringType, + cel.ListType(cel.StringType), + cel.StringType, + }, + cel.MapType(cel.StringType, cel.DynType), + cel.FunctionBinding(verifyCert), + ), + ), + } +} + +// ProgramOptions implements the Library interface method. +func (cryptoLib) ProgramOptions() []cel.ProgramOption { + return []cel.ProgramOption{} +} + +func verifyCert(args ...ref.Val) ref.Val { + cert, ok1 := args[0].Value().(string) + if !ok1 { + return types.MaybeNoSuchOverloadErr(args[0]) + } + + certChain, ok2 := args[1].(traits.Lister) + if !ok2 { + return types.MaybeNoSuchOverloadErr(args[1]) + } + + notAfter, ok3 := args[2].Value().(string) + if !ok3 { + return types.MaybeNoSuchOverloadErr(args[2]) + } + + // convert all cert.Data from string to []rune + cryptoCert := crypto.Certificate{ + Encoding: crypto.Pem, + Data: []rune(cert), + } + certChainLength, ok := certChain.Size().(types.Int) + if !ok { + return types.NewErr("cannot convert certChain length to int") + } + cryptoCertChain := make([]crypto.Certificate, 0, certChainLength) + for i := types.Int(0); i < certChainLength; i++ { + certElem, err := certChain.Get(i).ConvertToNative(reflect.TypeOf("")) + if err != nil { + return types.NewErr("cannot convert certChain: %s", err) + } + certString, ok := certElem.(string) + if !ok { + return types.NewErr("cannot convert cert into string") + } + + cryptoCertChain = append(cryptoCertChain, + crypto.Certificate{ + Encoding: crypto.Pem, + Data: []rune(certString), + }, + ) + } + + response, err := crypto.VerifyCert(&host, cryptoCert, cryptoCertChain, notAfter) + if err != nil { + return types.NewErr("cannot verify certificate: %s", err) + } + + return types.NewStringInterfaceMap(types.DefaultTypeAdapter, + map[string]any{ + "Trusted": response.Trusted, + "Reason": response.Reason, + }) +} diff --git a/internal/cel/library/crypto_test.go b/internal/cel/library/crypto_test.go new file mode 100644 index 0000000..e084cee --- /dev/null +++ b/internal/cel/library/crypto_test.go @@ -0,0 +1,128 @@ +package library + +import ( + "fmt" + "reflect" + "testing" + + "github.com/google/cel-go/cel" + "github.com/kubewarden/policy-sdk-go/pkg/capabilities" + "github.com/kubewarden/policy-sdk-go/pkg/capabilities/crypto" + "github.com/stretchr/testify/require" +) + +func TestCrypto(t *testing.T) { + tests := []struct { + name string + expression string + responseTrusted bool + responseReason string + expectedResult any + }{ + { + "kw.crypto.verifyCert", + "kw.crypto.verifyCert(" + + "'---BEGIN CERTIFICATE---foo---END CERTIFICATE---'," + + "[ '---BEGIN CERTIFICATE---bar---END CERTIFICATE---' ]," + + "'2030-08-15T16:23:42+00:00'" + + ")", + false, + "the certificate is expired", + map[string]any{ + "Trusted": false, + "Reason": "the certificate is expired", + }, + }, + { + "kw.crypto.verifyCert with empty CertChain", + "kw.crypto.verifyCert( " + + "'---BEGIN CERTIFICATE---foo2---END CERTIFICATE---'," + + "[]," + + "'0004-08-15T16:23:42+00:00'" + + ")", + true, // e.g: cert is past expiration date, yet is trusted (empty CertChain) + "", + map[string]any{ + "Trusted": true, + "Reason": "", + }, + }, + { + "kw.crypto.verifyCert return type", + "kw.crypto.verifyCert( " + + "'---BEGIN CERTIFICATE---foo2---END CERTIFICATE---'," + + "[]," + + "'0004-08-15T16:23:42+00:00'" + + ").Trusted", + true, // e.g: cert is past expiration date, yet is trusted (empty CertChain) + "", + true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var err error + host.Client, err = capabilities.NewSuccessfulMockWapcClient(crypto.CertificateVerificationResponse{ + Trusted: test.responseTrusted, + Reason: test.responseReason, + }) + require.NoError(t, err) + + env, err := cel.NewEnv( + Crypto(), + ) + require.NoError(t, err) + + ast, issues := env.Compile(test.expression) + require.Empty(t, issues) + + prog, err := env.Program(ast) + require.NoError(t, err) + + val, _, err := prog.Eval(map[string]interface{}{}) + require.NoError(t, err) + + result, err := val.ConvertToNative(reflect.TypeOf(test.expectedResult)) + require.NoError(t, err) + + require.Equal(t, test.expectedResult, result) + }) + } +} + +func TestCryptoHostFailure(t *testing.T) { + tests := []struct { + name string + expression string + }{ + { + "kw.crypto.verifyCert host failure", + "kw.crypto.verifyCert( " + + "'---BEGIN CERTIFICATE---foo3---END CERTIFICATE---'," + + "[ '---BEGIN CERTIFICATE---bar3---END CERTIFICATE---' ]," + + "'2030-08-15T16:23:42+00:00'" + + ")", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var err error + host.Client = capabilities.NewFailingMockWapcClient(fmt.Errorf("hostcallback error")) + + env, err := cel.NewEnv( + Crypto(), + ) + require.NoError(t, err) + + ast, issues := env.Compile(test.expression) + require.Empty(t, issues) + + prog, err := env.Program(ast) + require.NoError(t, err) + + _, _, err = prog.Eval(map[string]interface{}{}) + require.Error(t, err) + require.Equal(t, "cannot verify certificate: hostcallback error", err.Error()) + }) + } +} From a23549d612aad88d487bd193079dbadedac542ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Cuadrado=20Juan?= Date: Fri, 16 Feb 2024 14:28:39 +0100 Subject: [PATCH 2/3] deps: Bump policy-sdk-go to v0.8.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Víctor Cuadrado Juan --- go.mod | 2 +- go.sum | 8 ++++---- test_data/session.yaml | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index d5a8aa4..b009889 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/google/cel-go v0.17.7 github.com/hashicorp/go-multierror v1.1.1 github.com/kubewarden/k8s-objects v1.29.0-kw1 - github.com/kubewarden/policy-sdk-go v0.6.0 + github.com/kubewarden/policy-sdk-go v0.8.0 github.com/stretchr/testify v1.8.4 k8s.io/apiserver v0.29.1 ) diff --git a/go.sum b/go.sum index edcaf85..8808b67 100644 --- a/go.sum +++ b/go.sum @@ -9,16 +9,16 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/google/cel-go v0.17.7 h1:6ebJFzu1xO2n7TLtN+UBqShGBhlD85bhvglh5DpcfqQ= github.com/google/cel-go v0.17.7/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/kubewarden/k8s-objects v1.29.0-kw1 h1:bVQ2WL1ROqApYmHQJ/yxrs3tssfzzalblE2txChcHxY= github.com/kubewarden/k8s-objects v1.29.0-kw1/go.mod h1:EMF+Hr26oDR4yQkWJAQpl0M0Ek5ioNXlCswjGZO0G2U= -github.com/kubewarden/policy-sdk-go v0.6.0 h1:f7RL+hkcjt1g5/4JmUU+itzsdMNs5rFJT7ISJtSAB9g= -github.com/kubewarden/policy-sdk-go v0.6.0/go.mod h1:C8sUX4FYhbP69cvQfPLmIvAJhVHQyg1qaq9EynOn8a0= +github.com/kubewarden/policy-sdk-go v0.8.0 h1:4SR6UeKLBQ+UkwohuMqYw2lPKgqgF5Ifdw7tFNjQwiI= +github.com/kubewarden/policy-sdk-go v0.8.0/go.mod h1:gjYdcErABXti/dxoNW2PceSwy4+/X+o/wuLwWHZCoNU= github.com/kubewarden/strfmt v0.1.3 h1:bb+2rbotioROjCkziSt+hqnHXzOlumN94NxDKdV2kPI= github.com/kubewarden/strfmt v0.1.3/go.mod h1:DXoaaIYwqW1LyyRoMeyxfHUU+VUSTNFdj38juCXfRzs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/test_data/session.yaml b/test_data/session.yaml index fcc500a..d928f9e 100644 --- a/test_data/session.yaml +++ b/test_data/session.yaml @@ -4,7 +4,6 @@ api_version: v1 kind: Namespace name: default - namespace: "" disable_cache: false response: type: Success From 47740c489ff8691bb46139d9784ccd2ca86eb23c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Cuadrado=20Juan?= Date: Thu, 22 Feb 2024 10:14:16 +0100 Subject: [PATCH 3/3] chore: Lowercase map keys of crypto.verifyCert() return map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Víctor Cuadrado Juan --- internal/cel/library/crypto.go | 8 ++++---- internal/cel/library/crypto_test.go | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/cel/library/crypto.go b/internal/cel/library/crypto.go index f9085bb..72990a2 100644 --- a/internal/cel/library/crypto.go +++ b/internal/cel/library/crypto.go @@ -29,8 +29,8 @@ import ( // If empty, certificate is assumed never expired. // // Returns a map() with 2 fields: -// - "Trusted": informing if certificate passed verification or not -// - "Reason": with reason, in case "Trusted" is false +// - "trusted": informing if certificate passed verification or not +// - "reason": with reason, in case "Trusted" is false // // Usage in CEL: // @@ -132,7 +132,7 @@ func verifyCert(args ...ref.Val) ref.Val { return types.NewStringInterfaceMap(types.DefaultTypeAdapter, map[string]any{ - "Trusted": response.Trusted, - "Reason": response.Reason, + "trusted": response.Trusted, + "reason": response.Reason, }) } diff --git a/internal/cel/library/crypto_test.go b/internal/cel/library/crypto_test.go index e084cee..06374fa 100644 --- a/internal/cel/library/crypto_test.go +++ b/internal/cel/library/crypto_test.go @@ -29,8 +29,8 @@ func TestCrypto(t *testing.T) { false, "the certificate is expired", map[string]any{ - "Trusted": false, - "Reason": "the certificate is expired", + "trusted": false, + "reason": "the certificate is expired", }, }, { @@ -43,8 +43,8 @@ func TestCrypto(t *testing.T) { true, // e.g: cert is past expiration date, yet is trusted (empty CertChain) "", map[string]any{ - "Trusted": true, - "Reason": "", + "trusted": true, + "reason": "", }, }, { @@ -53,7 +53,7 @@ func TestCrypto(t *testing.T) { "'---BEGIN CERTIFICATE---foo2---END CERTIFICATE---'," + "[]," + "'0004-08-15T16:23:42+00:00'" + - ").Trusted", + ").trusted", true, // e.g: cert is past expiration date, yet is trusted (empty CertChain) "", true,