-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #18 from viccuad/hostcap-crypto
feat: Add CEL kw.crypto.verifyCert() function
- Loading branch information
Showing
5 changed files
with
271 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters