Skip to content

Commit

Permalink
Merge pull request #18 from viccuad/hostcap-crypto
Browse files Browse the repository at this point in the history
feat: Add CEL kw.crypto.verifyCert() function
  • Loading branch information
viccuad authored Feb 22, 2024
2 parents 1897b56 + 47740c4 commit b41a56f
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 6 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
138 changes: 138 additions & 0 deletions internal/cel/library/crypto.go
Original file line number Diff line number Diff line change
@@ -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(<string>) with 2 fields:
// - "trusted": <bool> informing if certificate passed verification or not
// - "reason": <string> with reason, in case "Trusted" is false
//
// Usage in CEL:
//
// crypto.verifyCert(<string>, list(<string>), <string>) -> map(<string>, 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,
})
}
128 changes: 128 additions & 0 deletions internal/cel/library/crypto_test.go
Original file line number Diff line number Diff line change
@@ -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())
})
}
}
1 change: 0 additions & 1 deletion test_data/session.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
api_version: v1
kind: Namespace
name: default
namespace: ""
disable_cache: false
response:
type: Success
Expand Down

0 comments on commit b41a56f

Please sign in to comment.