diff --git a/ast/builtins.go b/ast/builtins.go index 722e3f2ef6..47b557f104 100644 --- a/ast/builtins.go +++ b/ast/builtins.go @@ -207,6 +207,7 @@ var DefaultBuiltins = [...]*Builtin{ // Crypto CryptoX509ParseCertificates, CryptoX509ParseAndVerifyCertificates, + CryptoX509ParseAndVerifyCertificatesWithOptions, CryptoMd5, CryptoSha1, CryptoSha256, @@ -2327,6 +2328,31 @@ with all others being treated as intermediates.`, ), } +var CryptoX509ParseAndVerifyCertificatesWithOptions = &Builtin{ + Name: "crypto.x509.parse_and_verify_certificates_with_options", + Description: `Returns one or more certificates from the given string containing PEM +or base64 encoded DER certificates after verifying the supplied certificates form a complete +certificate chain back to a trusted root. A config option passed as the second argument can +be used to configure the validation options used. + +The first certificate is treated as the root and the last is treated as the leaf, +with all others being treated as intermediates.`, + + Decl: types.NewFunction( + types.Args( + types.Named("certs", types.S).Description("base64 encoded DER or PEM data containing two or more certificates where the first is a root CA, the last is a leaf certificate, and all others are intermediate CAs"), + types.Named("options", types.NewObject( + nil, + types.NewDynamicProperty(types.S, types.A), + )).Description("object containing extra configs to verify the validity of certificates. `options` object supports four fields which maps to same fields in [x509.VerifyOptions struct](https://pkg.go.dev/crypto/x509#VerifyOptions). `DNSName`, `CurrentTime`: Nanoseconds since the Unix Epoch as a number, `MaxConstraintComparisons` and `KeyUsages`. `KeyUsages` is list and can have possible values as in: `\"KeyUsageAny\"`, `\"KeyUsageServerAuth\"`, `\"KeyUsageClientAuth\"`, `\"KeyUsageCodeSigning\"`, `\"KeyUsageEmailProtection\"`, `\"KeyUsageIPSECEndSystem\"`, `\"KeyUsageIPSECTunnel\"`, `\"KeyUsageIPSECUser\"`, `\"KeyUsageTimeStamping\"`, `\"KeyUsageOCSPSigning\"`, `\"KeyUsageMicrosoftServerGatedCrypto\"`, `\"KeyUsageNetscapeServerGatedCrypto\"`, `\"KeyUsageMicrosoftCommercialCodeSigning\"`, `\"KeyUsageMicrosoftKernelCodeSigning\"` "), + ), + types.Named("output", types.NewArray([]types.Type{ + types.B, + types.NewArray(nil, types.NewObject(nil, types.NewDynamicProperty(types.S, types.A))), + }, nil)).Description("array of `[valid, certs]`: if the input certificate chain could be verified then `valid` is `true` and `certs` is an array of X.509 certificates represented as objects; if the input certificate chain could not be verified then `valid` is `false` and `certs` is `[]`"), + ), +} + var CryptoX509ParseCertificateRequest = &Builtin{ Name: "crypto.x509.parse_certificate_request", Description: "Returns a PKCS #10 certificate signing request from the given PEM-encoded PKCS#10 certificate signing request.", diff --git a/builtin_metadata.json b/builtin_metadata.json index e1123f52a1..bbe42e5811 100644 --- a/builtin_metadata.json +++ b/builtin_metadata.json @@ -43,6 +43,7 @@ "crypto.sha1", "crypto.sha256", "crypto.x509.parse_and_verify_certificates", + "crypto.x509.parse_and_verify_certificates_with_options", "crypto.x509.parse_certificate_request", "crypto.x509.parse_certificates", "crypto.x509.parse_keypair", @@ -4417,6 +4418,31 @@ }, "wasm": false }, + "crypto.x509.parse_and_verify_certificates_with_options": { + "args": [ + { + "description": "base64 encoded DER or PEM data containing two or more certificates where the first is a root CA, the last is a leaf certificate, and all others are intermediate CAs", + "name": "certs", + "type": "string" + }, + { + "description": "object containing extra configs to verify the validity of certificates. `options` object supports four fields which maps to same fields in [x509.VerifyOptions struct](https://pkg.go.dev/crypto/x509#VerifyOptions). `DNSName`, `CurrentTime`: Nanoseconds since the Unix Epoch as a number, `MaxConstraintComparisons` and `KeyUsages`. `KeyUsages` is list and can have possible values as in: `\"KeyUsageAny\"`, `\"KeyUsageServerAuth\"`, `\"KeyUsageClientAuth\"`, `\"KeyUsageCodeSigning\"`, `\"KeyUsageEmailProtection\"`, `\"KeyUsageIPSECEndSystem\"`, `\"KeyUsageIPSECTunnel\"`, `\"KeyUsageIPSECUser\"`, `\"KeyUsageTimeStamping\"`, `\"KeyUsageOCSPSigning\"`, `\"KeyUsageMicrosoftServerGatedCrypto\"`, `\"KeyUsageNetscapeServerGatedCrypto\"`, `\"KeyUsageMicrosoftCommercialCodeSigning\"`, `\"KeyUsageMicrosoftKernelCodeSigning\"` ", + "name": "options", + "type": "object[string: any]" + } + ], + "available": [ + "edge" + ], + "description": "Returns one or more certificates from the given string containing PEM\nor base64 encoded DER certificates after verifying the supplied certificates form a complete\ncertificate chain back to a trusted root. A config option passed as the second argument can\nbe used to configure the validation options used.\n\nThe first certificate is treated as the root and the last is treated as the leaf,\nwith all others being treated as intermediates.", + "introduced": "edge", + "result": { + "description": "array of `[valid, certs]`: if the input certificate chain could be verified then `valid` is `true` and `certs` is an array of X.509 certificates represented as objects; if the input certificate chain could not be verified then `valid` is `false` and `certs` is `[]`", + "name": "output", + "type": "array\u003cboolean, array[object[string: any]]\u003e" + }, + "wasm": false + }, "crypto.x509.parse_certificate_request": { "args": [ { diff --git a/capabilities.json b/capabilities.json index e38edc9482..f3d03f5f4c 100644 --- a/capabilities.json +++ b/capabilities.json @@ -757,6 +757,50 @@ "type": "function" } }, + { + "name": "crypto.x509.parse_and_verify_certificates_with_options", + "decl": { + "args": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "dynamic": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + } + ], + "type": "array" + }, + "type": "function" + } + }, { "name": "crypto.x509.parse_certificate_request", "decl": { diff --git a/topdown/crypto.go b/topdown/crypto.go index 520c051860..f24432a264 100644 --- a/topdown/crypto.go +++ b/topdown/crypto.go @@ -21,6 +21,7 @@ import ( "hash" "os" "strings" + "time" "github.com/open-policy-agent/opa/internal/jwx/jwk" @@ -104,7 +105,7 @@ func builtinCryptoX509ParseAndVerifyCertificates(_ BuiltinContext, operands []*a return iter(invalid) } - verified, err := verifyX509CertificateChain(certs) + verified, err := verifyX509CertificateChain(certs, x509.VerifyOptions{}) if err != nil { return iter(invalid) } @@ -122,6 +123,153 @@ func builtinCryptoX509ParseAndVerifyCertificates(_ BuiltinContext, operands []*a return iter(valid) } +var allowedKeyUsages = map[string]x509.ExtKeyUsage{ + "KeyUsageAny": x509.ExtKeyUsageAny, + "KeyUsageServerAuth": x509.ExtKeyUsageServerAuth, + "KeyUsageClientAuth": x509.ExtKeyUsageClientAuth, + "KeyUsageCodeSigning": x509.ExtKeyUsageCodeSigning, + "KeyUsageEmailProtection": x509.ExtKeyUsageEmailProtection, + "KeyUsageIPSECEndSystem": x509.ExtKeyUsageIPSECEndSystem, + "KeyUsageIPSECTunnel": x509.ExtKeyUsageIPSECTunnel, + "KeyUsageIPSECUser": x509.ExtKeyUsageIPSECUser, + "KeyUsageTimeStamping": x509.ExtKeyUsageTimeStamping, + "KeyUsageOCSPSigning": x509.ExtKeyUsageOCSPSigning, + "KeyUsageMicrosoftServerGatedCrypto": x509.ExtKeyUsageMicrosoftServerGatedCrypto, + "KeyUsageNetscapeServerGatedCrypto": x509.ExtKeyUsageNetscapeServerGatedCrypto, + "KeyUsageMicrosoftCommercialCodeSigning": x509.ExtKeyUsageMicrosoftCommercialCodeSigning, + "KeyUsageMicrosoftKernelCodeSigning": x509.ExtKeyUsageMicrosoftKernelCodeSigning, +} + +func builtinCryptoX509ParseAndVerifyCertificatesWithOptions(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + + input, err := builtins.StringOperand(operands[0].Value, 1) + if err != nil { + return err + } + + options, err := builtins.ObjectOperand(operands[1].Value, 2) + if err != nil { + return err + } + + invalid := ast.ArrayTerm( + ast.BooleanTerm(false), + ast.NewTerm(ast.NewArray()), + ) + + certs, err := getX509CertsFromString(string(input)) + if err != nil { + return iter(invalid) + } + + // Collect the cert verification options + verifyOpt, err := extractVerifyOpts(options) + if err != nil { + return err + } + + verified, err := verifyX509CertificateChain(certs, verifyOpt) + if err != nil { + return iter(invalid) + } + + value, err := ast.InterfaceToValue(verified) + if err != nil { + return err + } + + valid := ast.ArrayTerm( + ast.BooleanTerm(true), + ast.NewTerm(value), + ) + + return iter(valid) +} + +func extractVerifyOpts(options ast.Object) (verifyOpt x509.VerifyOptions, err error) { + + for _, key := range options.Keys() { + k, err := ast.JSON(key.Value) + if err != nil { + return verifyOpt, err + } + k, ok := k.(string) + if !ok { + continue + } + + switch k { + case "DNSName": + dns, ok := options.Get(key).Value.(ast.String) + if ok { + verifyOpt.DNSName = strings.Trim(string(dns), "\"") + } else { + return verifyOpt, fmt.Errorf("'DNSName' should be a string") + } + case "CurrentTime": + c, ok := options.Get(key).Value.(ast.Number) + if ok { + nanosecs, ok := c.Int64() + if ok { + verifyOpt.CurrentTime = time.Unix(0, nanosecs) + } else { + return verifyOpt, fmt.Errorf("'CurrentTime' should be a valid int64 number") + } + } else { + return verifyOpt, fmt.Errorf("'CurrentTime' should be a number") + } + case "MaxConstraintComparisons": + c, ok := options.Get(key).Value.(ast.Number) + if ok { + maxComparisons, ok := c.Int() + if ok { + verifyOpt.MaxConstraintComparisions = maxComparisons + } else { + return verifyOpt, fmt.Errorf("'MaxConstraintComparisons' should be a valid number") + } + } else { + return verifyOpt, fmt.Errorf("'MaxConstraintComparisons' should be a number") + } + case "KeyUsages": + type forEach interface { + Foreach(func(*ast.Term)) + } + var ks forEach + switch options.Get(key).Value.(type) { + case *ast.Array: + ks = options.Get(key).Value.(*ast.Array) + case ast.Set: + ks = options.Get(key).Value.(ast.Set) + default: + return verifyOpt, fmt.Errorf("'KeyUsages' should be an Array or Set") + } + + // Collect the x509.ExtKeyUsage values by looking up the + // mapping of key usage strings to x509.ExtKeyUsage + var invalidKUsgs []string + ks.Foreach(func(t *ast.Term) { + u, ok := t.Value.(ast.String) + if ok { + v := strings.Trim(string(u), "\"") + if k, ok := allowedKeyUsages[v]; ok { + verifyOpt.KeyUsages = append(verifyOpt.KeyUsages, k) + } else { + invalidKUsgs = append(invalidKUsgs, v) + } + } + }) + if len(invalidKUsgs) > 0 { + return x509.VerifyOptions{}, fmt.Errorf("invalid entries for 'KeyUsages' found: %s", invalidKUsgs) + } + default: + return verifyOpt, fmt.Errorf("invalid key option") + } + + } + + return verifyOpt, nil +} + func builtinCryptoX509ParseKeyPair(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { certificate, err := builtins.StringOperand(operands[0].Value, 1) if err != nil { @@ -380,6 +528,7 @@ func builtinCryptoHmacEqual(_ BuiltinContext, operands []*ast.Term, iter func(*a func init() { RegisterBuiltinFunc(ast.CryptoX509ParseCertificates.Name, builtinCryptoX509ParseCertificates) RegisterBuiltinFunc(ast.CryptoX509ParseAndVerifyCertificates.Name, builtinCryptoX509ParseAndVerifyCertificates) + RegisterBuiltinFunc(ast.CryptoX509ParseAndVerifyCertificatesWithOptions.Name, builtinCryptoX509ParseAndVerifyCertificatesWithOptions) RegisterBuiltinFunc(ast.CryptoMd5.Name, builtinCryptoMd5) RegisterBuiltinFunc(ast.CryptoSha1.Name, builtinCryptoSha1) RegisterBuiltinFunc(ast.CryptoSha256.Name, builtinCryptoSha256) @@ -394,7 +543,7 @@ func init() { RegisterBuiltinFunc(ast.CryptoHmacEqual.Name, builtinCryptoHmacEqual) } -func verifyX509CertificateChain(certs []*x509.Certificate) ([]*x509.Certificate, error) { +func verifyX509CertificateChain(certs []*x509.Certificate, vo x509.VerifyOptions) ([]*x509.Certificate, error) { if len(certs) < 2 { return nil, builtins.NewOperandErr(1, "must supply at least two certificates to be able to verify") } @@ -414,8 +563,12 @@ func verifyX509CertificateChain(certs []*x509.Certificate) ([]*x509.Certificate, // verify the cert chain back to the root verifyOpts := x509.VerifyOptions{ - Roots: roots, - Intermediates: intermediates, + Roots: roots, + Intermediates: intermediates, + DNSName: vo.DNSName, + CurrentTime: vo.CurrentTime, + KeyUsages: vo.KeyUsages, + MaxConstraintComparisions: vo.MaxConstraintComparisions, } chains, err := leaf.Verify(verifyOpts) if err != nil { diff --git a/topdown/crypto_test.go b/topdown/crypto_test.go index e70476df7e..00e067a59a 100644 --- a/topdown/crypto_test.go +++ b/topdown/crypto_test.go @@ -5,10 +5,18 @@ import ( "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" + "crypto/x509" "encoding/base64" "fmt" + "strconv" "strings" "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/topdown/builtins" ) var rootCA = `-----BEGIN CERTIFICATE----- @@ -150,7 +158,7 @@ func TestX509ParseAndVerify(t *testing.T) { t.Fatalf("failed to parse PEM cert chain: %v", err) } - if _, err := verifyX509CertificateChain(parsed); err != nil { + if _, err := verifyX509CertificateChain(parsed, x509.VerifyOptions{}); err != nil { t.Error("x509 verification failed when it was expected to succeed") } }) @@ -164,7 +172,7 @@ func TestX509ParseAndVerify(t *testing.T) { t.Fatalf("failed to parse base64 cert chain: %v", err) } - if _, err := verifyX509CertificateChain(parsed); err != nil { + if _, err := verifyX509CertificateChain(parsed, x509.VerifyOptions{}); err != nil { t.Error("x509 verification failed when it was expected to succeed") } }) @@ -177,7 +185,7 @@ func TestX509ParseAndVerify(t *testing.T) { t.Fatalf("failed to parse PEM cert chain: %v", err) } - if _, err := verifyX509CertificateChain(parsed); err == nil { + if _, err := verifyX509CertificateChain(parsed, x509.VerifyOptions{}); err == nil { t.Error("x509 verification succeeded when it was expected to fail") } }) @@ -190,7 +198,7 @@ func TestX509ParseAndVerify(t *testing.T) { t.Fatalf("failed to parse PEM cert chain: %v", err) } - if _, err := verifyX509CertificateChain(parsed); err == nil { + if _, err := verifyX509CertificateChain(parsed, x509.VerifyOptions{}); err == nil { t.Error("x509 verification succeeded when it was expected to fail") } }) @@ -201,7 +209,7 @@ func TestX509ParseAndVerify(t *testing.T) { t.Fatalf("failed to parse leaf cert: %v", err) } - if _, err := verifyX509CertificateChain(parsed); err == nil { + if _, err := verifyX509CertificateChain(parsed, x509.VerifyOptions{}); err == nil { t.Error("x509 verification succeeded when it was expected to fail") } }) @@ -647,3 +655,179 @@ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }) } } + +func TestParseAndVerifyX509CertsWithOptions(t *testing.T) { + + chain := strings.Join([]string{rootCA, intermediateCA, leaf}, "\n") + + parsed, err := getX509CertsFromString(chain) + if err != nil { + t.Fatalf("failed to parse PEM cert chain: %v", err) + } + + dnsName := parsed[2].DNSNames[0] + notBefore, notAfter := parsed[2].NotBefore, parsed[2].NotAfter + invalidBefore := notBefore.Add(-time.Minute).UnixNano() + invalidAfter := notAfter.Add(time.Minute).UnixNano() + // This certificate has "KeyUsageServerAuth", "KeyUsageClientAuth" as key usages. So these are used + // in following tests. + + tests := []struct { + jsonOption *ast.Term + expectErr bool + }{ + { + jsonOption: ast.MustParseTerm(`{"DNSName": "bad.dns.com"}`), + expectErr: true, + }, + { + jsonOption: ast.MustParseTerm(`{"CurrentTime": ` + strconv.FormatInt(invalidBefore, 10) + `}`), + expectErr: true, + }, + { + jsonOption: ast.MustParseTerm(`{"CurrentTime": ` + strconv.FormatInt(invalidAfter, 10) + `}`), + expectErr: true, + }, + { + jsonOption: ast.MustParseTerm(`{"CurrentTime": ` + strconv.FormatInt(notBefore.UnixNano(), 10) + `}`), + expectErr: false, + }, + { + jsonOption: ast.MustParseTerm(`{"DNSName": "` + dnsName + `" }`), + expectErr: false, + }, + { + jsonOption: ast.MustParseTerm(`{"KeyUsages": ["KeyUsageServerAuth", "KeyUsageClientAuth", "KeyUsageCodeSigning"] }`), + expectErr: false, + }, + { + jsonOption: ast.MustParseTerm(`{"KeyUsages": ["KeyUsageCodeSigning"] }`), + expectErr: true, + }, + { + jsonOption: ast.MustParseTerm(`{"DNSName": "` + dnsName + `", "CurrentTime": ` + strconv.FormatInt(notBefore.UnixNano(), 10) + `, "KeyUsages": ["KeyUsageServerAuth", "KeyUsageCodeSigning"] }`), + expectErr: false, + }, + } + + for _, testCase := range tests { + options, _ := builtins.ObjectOperand(testCase.jsonOption.Value, 0) + vo, err := extractVerifyOpts(options) + if err != nil { + t.Fatalf("Unexpected error in extracting options: %s", err) + } + + _, err = verifyX509CertificateChain(parsed, vo) + if testCase.expectErr { + if err == nil { + t.Fatalf("expected error in verifying cert chain, but got nil error") + } + } else { + if err != nil { + t.Fatalf("did not expect error, but got error: %s", err) + } + } + } + +} + +func TestExtractX509VerifyOptions(t *testing.T) { + tests := []struct { + jsonOption *ast.Term + expectErr error + expectVerifyOpt x509.VerifyOptions + }{ + { + jsonOption: ast.MustParseTerm(`{"DNSName": 1}`), + expectErr: fmt.Errorf("'DNSName' should be a string"), + }, + { + jsonOption: ast.MustParseTerm(`{CurrentTime: "string"}`), + expectErr: fmt.Errorf("'CurrentTime' should be a number"), + }, + { + jsonOption: ast.MustParseTerm(`{MaxConstraintComparisons: "string"}`), + expectErr: fmt.Errorf("'MaxConstraintComparisons' should be a number"), + }, + { + jsonOption: ast.MustParseTerm(`{"KeyUsages" : "true"}`), + expectErr: fmt.Errorf("'KeyUsages' should be an Array or Set"), + }, + { + jsonOption: ast.MustParseTerm(`{"DNSName": 1, CurrentTime: "string", "KeyUsages" : {1,2}}`), + expectErr: fmt.Errorf("'DNSName' should be a string"), + }, + { + jsonOption: ast.MustParseTerm(`{"InvalidKey": "test.com"}`), + expectErr: fmt.Errorf("invalid key option"), + }, + { + jsonOption: ast.MustParseTerm(`{}`), + expectVerifyOpt: x509.VerifyOptions{}, + }, + { + jsonOption: ast.MustParseTerm(`{"KeyUsages" : {"KeyUsageAny", "InvalidKeyUsage", "KeyUsageServerAuth"}}`), + expectErr: fmt.Errorf("invalid entries for 'KeyUsages' found: %s", []string{"InvalidKeyUsage"}), + }, + { + jsonOption: ast.MustParseTerm(`{"KeyUsages" : ["1","KeyUsageAny", "InvalidKeyUsage", "KeyUsageServerAuth", "2"]}`), + expectErr: fmt.Errorf("invalid entries for 'KeyUsages' found: %s", []string{"1", "InvalidKeyUsage", "2"}), + }, + { + jsonOption: ast.MustParseTerm(`{"DNSName": "test.com", "CurrentTime": 1708447636000000000, ` + + `"MaxConstraintComparisons": 5, "KeyUsages" : {"KeyUsageAny", "KeyUsageServerAuth","KeyUsageClientAuth"}}`), + expectVerifyOpt: x509.VerifyOptions{ + DNSName: "test.com", + CurrentTime: time.Unix(0, 1708447636000000000), + MaxConstraintComparisions: 5, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + }, + }, + { + jsonOption: ast.MustParseTerm(`{"DNSName": "test.com", "CurrentTime": 1708447636000000000, ` + + `"MaxConstraintComparisons": 5, "KeyUsages" : {"KeyUsageAny", "KeyUsageAny", 1, 2, "KeyUsageServerAuth","KeyUsageClientAuth"}}`), + expectVerifyOpt: x509.VerifyOptions{ + DNSName: "test.com", + CurrentTime: time.Unix(0, 1708447636000000000), + MaxConstraintComparisions: 5, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + }, + }, + { // KeyUsages as an array + jsonOption: ast.MustParseTerm(`{"DNSName": "test.com", "CurrentTime": 1708447636000000000, + "MaxConstraintComparisons": 5, + "KeyUsages" : ["KeyUsageAny", "KeyUsageAny", 1, 2, + "KeyUsageServerAuth","KeyUsageClientAuth"]}`), + expectVerifyOpt: x509.VerifyOptions{ + DNSName: "test.com", + CurrentTime: time.Unix(0, 1708447636000000000), + MaxConstraintComparisions: 5, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny, x509.ExtKeyUsageAny, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + }, + }, + } + + for _, testCase := range tests { + options, _ := builtins.ObjectOperand(testCase.jsonOption.Value, 0) + if testCase.expectErr == nil { + vo, err := extractVerifyOpts(options) + if err != nil { + t.Fatalf("did not expect error but got %s", err) + } + // ignore the order of ExtKeyUsage values + if !cmp.Equal(vo, testCase.expectVerifyOpt, cmpopts.SortSlices( + func(ku1, ku2 x509.ExtKeyUsage) bool { + return ku1 < ku2 + })) { + + t.Fatalf("expected x509.VerifyOptions: %+v \n"+ + "got: %+v", testCase.expectVerifyOpt, vo) + } + } else { + _, err := extractVerifyOpts(options) + if err == nil { + t.Fatalf("expected error: %s, got nil error", testCase.expectErr) + } + } + } +}