Skip to content

Commit

Permalink
Adding a new function crypto.x509.parse_and_verify_certificates_with_…
Browse files Browse the repository at this point in the history
…options. Fixes #5882

Signed-off-by: Yogesh Sinha <sinhayogi@gmail.com>
  • Loading branch information
yogisinha committed Mar 23, 2024
1 parent 143a8e6 commit 159cad8
Show file tree
Hide file tree
Showing 5 changed files with 442 additions and 9 deletions.
26 changes: 26 additions & 0 deletions ast/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ var DefaultBuiltins = [...]*Builtin{
// Crypto
CryptoX509ParseCertificates,
CryptoX509ParseAndVerifyCertificates,
CryptoX509ParseAndVerifyCertificatesWithOptions,
CryptoMd5,
CryptoSha1,
CryptoSha256,
Expand Down Expand Up @@ -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.",
Expand Down
26 changes: 26 additions & 0 deletions builtin_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": [
{
Expand Down
44 changes: 44 additions & 0 deletions capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
161 changes: 157 additions & 4 deletions topdown/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"hash"
"os"
"strings"
"time"

"github.com/open-policy-agent/opa/internal/jwx/jwk"

Expand Down Expand Up @@ -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)
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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")
}
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit 159cad8

Please sign in to comment.