Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e362e13
Changed the LoadPaths function.
yogisinha Jul 26, 2023
23ceed5
Adding a new function crypto.x509.parse_and_verify_certificates_with_…
yogisinha Feb 11, 2024
35d2f85
Added test case, using Go x509.VerifyOptions, fixed function descript…
yogisinha Feb 21, 2024
a1f123d
Merge branch 'main' into parse_and_verify_certificates_with_options
yogisinha Feb 21, 2024
4a472ff
Added auto generated builtins json files.
yogisinha Feb 21, 2024
290918c
Merge branch 'parse_and_verify_certificates_with_options' of https://…
yogisinha Feb 21, 2024
c91d9cd
Added changes as per suggestions.
yogisinha Feb 21, 2024
6ba2619
Added new lines
yogisinha Feb 21, 2024
a8ba1d1
Added new lines
yogisinha Feb 21, 2024
9c655c3
Added new lines
yogisinha Feb 21, 2024
6b32620
Added the new Array type for KeyUsages
yogisinha Feb 22, 2024
4f240dd
Adding a new function crypto.x509.parse_and_verify_certificates_with_…
yogisinha Feb 11, 2024
4597ffe
Added test case, using Go x509.VerifyOptions, fixed function descript…
yogisinha Feb 21, 2024
6556e4e
Added auto generated builtins json files.
yogisinha Feb 21, 2024
4562893
Added changes as per suggestions.
yogisinha Feb 21, 2024
2fe697c
Added new lines
yogisinha Feb 21, 2024
ce57d7a
Added new lines
yogisinha Feb 21, 2024
124047c
Added new lines
yogisinha Feb 21, 2024
444388d
Added the new Array type for KeyUsages
yogisinha Feb 22, 2024
8cc5838
Merge branch 'parse_and_verify_certificates_with_options' of https://…
yogisinha Feb 28, 2024
ee1819b
Merge branch 'main' into parse_and_verify_certificates_with_options
yogisinha Feb 29, 2024
797e401
Adding a new function crypto.x509.parse_and_verify_certificates_with_…
yogisinha Feb 11, 2024
4a40db9
Added test case, using Go x509.VerifyOptions, fixed function descript…
yogisinha Feb 21, 2024
d6331e8
Added auto generated builtins json files.
yogisinha Feb 21, 2024
9fffee2
Added changes as per suggestions.
yogisinha Feb 21, 2024
9bb2127
Added new lines
yogisinha Feb 21, 2024
6d47ba4
Added new lines
yogisinha Feb 21, 2024
5303e09
Added new lines
yogisinha Feb 21, 2024
6066aae
Added the new Array type for KeyUsages
yogisinha Feb 22, 2024
5eda48e
Adding a new function crypto.x509.parse_and_verify_certificates_with_…
yogisinha Mar 2, 2024
c8422fe
Changes as per suggestions.
yogisinha Mar 2, 2024
aa87a3a
Merge branch 'parse_and_verify_certificates_with_options' of https://…
yogisinha Mar 2, 2024
0436522
Changes as per suggestions.
yogisinha Mar 2, 2024
74b6730
Changes as per suggestions.
yogisinha Mar 6, 2024
458e963
Changes as per suggestions.
yogisinha Mar 6, 2024
dc6683a
Merge branch 'main' of https://github.com/open-policy-agent/opa into …
yogisinha Mar 10, 2024
a9b4bd8
Added the test case for parse cert with options.
yogisinha Mar 10, 2024
720b333
Changes as per suggestions.
yogisinha Mar 12, 2024
f992c0d
Shortening the function name.
yogisinha Mar 12, 2024
9effeab
Commenting out the function to check the deploy preview without this.
yogisinha Mar 13, 2024
ea77470
Merge branch 'main' of https://github.com/open-policy-agent/opa into …
yogisinha Mar 13, 2024
3c31aa6
Some changes.
yogisinha Mar 13, 2024
580fa87
Some changes.
yogisinha Mar 13, 2024
f9b196d
changes to test preview.
yogisinha Mar 13, 2024
5cad579
changes to test preview.
yogisinha Mar 13, 2024
49796ad
changes to test preview.
yogisinha Mar 13, 2024
8ebe57a
changes to test preview.
yogisinha Mar 13, 2024
ce4a226
Merge branch 'main' of https://github.com/open-policy-agent/opa into …
yogisinha Mar 18, 2024
3993618
Suggested changes.
yogisinha Mar 18, 2024
91e9007
Update generated code
Mar 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 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,51 @@ 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. Second argument is a option object to pass
extra configs to verify the validity of certificates.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Second argument is a option object to pass extra configs to verify the validity of certificates."

I would change this to:

"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.` +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
with all others being treated as intermediates.` +
with all others being treated as intermediates.
` +
Screenshot 2024-02-21 at 18 11 56

"Example option object: " +
"`options := {\"DNSName\" : \"example.dns.com\", \"CurrentTime\": 1708447636000000000,`" +
"`\"KeyUsages\": {\"KeyUsageServerAuth\", \"KeyUsageClientAuth\", \"KeyUsageCodeSigning\"},`" +
"`\"MaxConstraintComparisons\" : 5,`" +
"`}`" +
"Then this function call will look like: `crypto.x509.parse_and_verify_certificates_with_options(<cert base64 encoded string>, options)`" +
"This option's field has same definitions as fields in [x509.VerifyOptions struct](https://pkg.go.dev/crypto/x509#VerifyOptions)" +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screen.Recording.2024-02-21.at.18.13.21.mov

I think that we need to make some of these lines less long, as it's pushing out the rest of the table at the moment. Some strategic line breaks should be 👌

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I realized that too and was already working on that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"This option's field has same definitions as fields in [x509.VerifyOptions struct](https://pkg.go.dev/crypto/x509#VerifyOptions)" +
"This options object has the same fields as [x509.VerifyOptions](https://pkg.go.dev/crypto/x509#VerifyOptions)" +

"`CurrentTime` is nanoseconds since epoch" +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"`CurrentTime` is nanoseconds since epoch" +
"`CurrentTime` is the number of nanoseconds since the Unix Epoch as a number" +

"List of possible values for `KeyUsages` field are: `[\"KeyUsageAny\", \"KeyUsageServerAuth\",`" +
"`\"KeyUsageClientAuth\", \"KeyUsageCodeSigning\", \"KeyUsageEmailProtection\", \"KeyUsageIPSECEndSystem\",`" +
"`\"KeyUsageIPSECTunnel\", \"KeyUsageIPSECUser\", \"KeyUsageTimeStamping\", \"KeyUsageOCSPSigning\",`" +
"`\"KeyUsageMicrosoftServerGatedCrypto\", \"KeyUsageNetscapeServerGatedCrypto\",`" +
"`\"KeyUsageMicrosoftCommercialCodeSigning\", \"KeyUsageMicrosoftKernelCodeSigning\"]`",

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.A,
types.NewAny(
types.S,
types.N,
types.NewSet(types.A),
),
)),
).Description("option object to pass extra configs to verify the validity of certificates."),
),
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 @@ -4337,6 +4338,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": "option object to pass extra configs to verify the validity of certificates.",
"name": "options",
"type": "object[any: any\u003cnumber, string, set[any]\u003e]"
}
],
"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. Second argument is a option object to pass\nextra configs to verify the validity of certificates.\n\nThe first certificate is treated as the root and the last is treated as the leaf,\nwith all others being treated as intermediates.Example option object: `options := {\"DNSName\" : \"example.dns.com\", \"CurrentTime\": 1708447636000000000,``\"KeyUsages\": {\"KeyUsageServerAuth\", \"KeyUsageClientAuth\", \"KeyUsageCodeSigning\"},``\"MaxConstraintComparisons\" : 5,``}`Then this function call will look like: `crypto.x509.parse_and_verify_certificates_with_options(\u003ccert base64 encoded string\u003e, options)`This option's field has same definitions as fields in [x509.VerifyOptions struct](https://pkg.go.dev/crypto/x509#VerifyOptions)`CurrentTime` is nanoseconds since epochList of possible values for `KeyUsages` field are: `[\"KeyUsageAny\", \"KeyUsageServerAuth\",``\"KeyUsageClientAuth\", \"KeyUsageCodeSigning\", \"KeyUsageEmailProtection\", \"KeyUsageIPSECEndSystem\",``\"KeyUsageIPSECTunnel\", \"KeyUsageIPSECUser\", \"KeyUsageTimeStamping\", \"KeyUsageOCSPSigning\",``\"KeyUsageMicrosoftServerGatedCrypto\", \"KeyUsageNetscapeServerGatedCrypto\",``\"KeyUsageMicrosoftCommercialCodeSigning\", \"KeyUsageMicrosoftKernelCodeSigning\"]`",
"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
58 changes: 58 additions & 0 deletions capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,64 @@
"type": "function"
}
},
{
"name": "crypto.x509.parse_and_verify_certificates_with_options",
"decl": {
"args": [
{
"type": "string"
},
{
"dynamic": {
"key": {
"type": "any"
},
"value": {
"of": [
{
"type": "number"
},
{
"type": "string"
},
{
"of": {
"type": "any"
},
"type": "set"
}
],
"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
138 changes: 134 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,130 @@ 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() {
switch key.String() {
case "\"DNSName\"":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be nicer to clean the quote from the key at the top of the loop iteration, this might make it look a little more tidy in the cases.

dns, ok := options.Get(key).Value.(ast.String)
if ok {
verifyOpt.DNSName = dns.String()[1 : len(dns.String())-1]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about something like 👇

Suggested change
verifyOpt.DNSName = dns.String()[1 : len(dns.String())-1]
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\"":
ks, ok := options.Get(key).Value.(ast.Set)
if ok {
// Collect the x509.ExtKeyUsage values by looking up the
// mapping of key usage strings to x509.ExtKeyUsage
ks.Foreach(func(t *ast.Term) {
u, ok := t.Value.(ast.String)
if ok {
c := u.String()[1 : len(u.String())-1]
if t, ok := allowedKeyUsages[c]; ok {
verifyOpt.KeyUsages = append(verifyOpt.KeyUsages, t)
}
}
})

} else {
return verifyOpt, fmt.Errorf("'KeyUsages' should be a set")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might we want to support the case where the user supplies an array?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided on Set because it automatically dedups the Key usage entry if user has supplied same entry more than once. Array would not do that. LMK what you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just imagining that a user might provide an array by mistake, if it were coming from input or data loaded from JSON this is quite likely. I agree that a Set is what we'd like to receive in an ideal case, but I think there's merit in covering an array as input too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense. I would change that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having dup in array is ok. its not a issue. I looked into Go code and passing list with dup key usages should be fine. just extra loop executions in this part of code: https://cs.opensource.google/go/go/+/refs/tags/go1.22.0:src/crypto/x509/verify.go;l=1166

Copy link
Contributor Author

@yogisinha yogisinha Feb 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the Array type for KeyUsages.

}
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 +505,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 +520,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 +540,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