Skip to content

Commit ea0dc02

Browse files
authored
Adding a new function crypto.x509.parse_and_verify_certificates_with_options. Fixes #5882 (#6643)
Signed-off-by: Yogesh Sinha <[email protected]>
1 parent 5f16f4a commit ea0dc02

File tree

5 files changed

+442
-9
lines changed

5 files changed

+442
-9
lines changed

ast/builtins.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ var DefaultBuiltins = [...]*Builtin{
207207
// Crypto
208208
CryptoX509ParseCertificates,
209209
CryptoX509ParseAndVerifyCertificates,
210+
CryptoX509ParseAndVerifyCertificatesWithOptions,
210211
CryptoMd5,
211212
CryptoSha1,
212213
CryptoSha256,
@@ -2327,6 +2328,31 @@ with all others being treated as intermediates.`,
23272328
),
23282329
}
23292330

2331+
var CryptoX509ParseAndVerifyCertificatesWithOptions = &Builtin{
2332+
Name: "crypto.x509.parse_and_verify_certificates_with_options",
2333+
Description: `Returns one or more certificates from the given string containing PEM
2334+
or base64 encoded DER certificates after verifying the supplied certificates form a complete
2335+
certificate chain back to a trusted root. A config option passed as the second argument can
2336+
be used to configure the validation options used.
2337+
2338+
The first certificate is treated as the root and the last is treated as the leaf,
2339+
with all others being treated as intermediates.`,
2340+
2341+
Decl: types.NewFunction(
2342+
types.Args(
2343+
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"),
2344+
types.Named("options", types.NewObject(
2345+
nil,
2346+
types.NewDynamicProperty(types.S, types.A),
2347+
)).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\"` "),
2348+
),
2349+
types.Named("output", types.NewArray([]types.Type{
2350+
types.B,
2351+
types.NewArray(nil, types.NewObject(nil, types.NewDynamicProperty(types.S, types.A))),
2352+
}, 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 `[]`"),
2353+
),
2354+
}
2355+
23302356
var CryptoX509ParseCertificateRequest = &Builtin{
23312357
Name: "crypto.x509.parse_certificate_request",
23322358
Description: "Returns a PKCS #10 certificate signing request from the given PEM-encoded PKCS#10 certificate signing request.",

builtin_metadata.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"crypto.sha1",
4444
"crypto.sha256",
4545
"crypto.x509.parse_and_verify_certificates",
46+
"crypto.x509.parse_and_verify_certificates_with_options",
4647
"crypto.x509.parse_certificate_request",
4748
"crypto.x509.parse_certificates",
4849
"crypto.x509.parse_keypair",
@@ -4417,6 +4418,31 @@
44174418
},
44184419
"wasm": false
44194420
},
4421+
"crypto.x509.parse_and_verify_certificates_with_options": {
4422+
"args": [
4423+
{
4424+
"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",
4425+
"name": "certs",
4426+
"type": "string"
4427+
},
4428+
{
4429+
"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\"` ",
4430+
"name": "options",
4431+
"type": "object[string: any]"
4432+
}
4433+
],
4434+
"available": [
4435+
"edge"
4436+
],
4437+
"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.",
4438+
"introduced": "edge",
4439+
"result": {
4440+
"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 `[]`",
4441+
"name": "output",
4442+
"type": "array\u003cboolean, array[object[string: any]]\u003e"
4443+
},
4444+
"wasm": false
4445+
},
44204446
"crypto.x509.parse_certificate_request": {
44214447
"args": [
44224448
{

capabilities.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,50 @@
757757
"type": "function"
758758
}
759759
},
760+
{
761+
"name": "crypto.x509.parse_and_verify_certificates_with_options",
762+
"decl": {
763+
"args": [
764+
{
765+
"type": "string"
766+
},
767+
{
768+
"dynamic": {
769+
"key": {
770+
"type": "string"
771+
},
772+
"value": {
773+
"type": "any"
774+
}
775+
},
776+
"type": "object"
777+
}
778+
],
779+
"result": {
780+
"static": [
781+
{
782+
"type": "boolean"
783+
},
784+
{
785+
"dynamic": {
786+
"dynamic": {
787+
"key": {
788+
"type": "string"
789+
},
790+
"value": {
791+
"type": "any"
792+
}
793+
},
794+
"type": "object"
795+
},
796+
"type": "array"
797+
}
798+
],
799+
"type": "array"
800+
},
801+
"type": "function"
802+
}
803+
},
760804
{
761805
"name": "crypto.x509.parse_certificate_request",
762806
"decl": {

topdown/crypto.go

Lines changed: 157 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"hash"
2222
"os"
2323
"strings"
24+
"time"
2425

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

@@ -104,7 +105,7 @@ func builtinCryptoX509ParseAndVerifyCertificates(_ BuiltinContext, operands []*a
104105
return iter(invalid)
105106
}
106107

107-
verified, err := verifyX509CertificateChain(certs)
108+
verified, err := verifyX509CertificateChain(certs, x509.VerifyOptions{})
108109
if err != nil {
109110
return iter(invalid)
110111
}
@@ -122,6 +123,153 @@ func builtinCryptoX509ParseAndVerifyCertificates(_ BuiltinContext, operands []*a
122123
return iter(valid)
123124
}
124125

126+
var allowedKeyUsages = map[string]x509.ExtKeyUsage{
127+
"KeyUsageAny": x509.ExtKeyUsageAny,
128+
"KeyUsageServerAuth": x509.ExtKeyUsageServerAuth,
129+
"KeyUsageClientAuth": x509.ExtKeyUsageClientAuth,
130+
"KeyUsageCodeSigning": x509.ExtKeyUsageCodeSigning,
131+
"KeyUsageEmailProtection": x509.ExtKeyUsageEmailProtection,
132+
"KeyUsageIPSECEndSystem": x509.ExtKeyUsageIPSECEndSystem,
133+
"KeyUsageIPSECTunnel": x509.ExtKeyUsageIPSECTunnel,
134+
"KeyUsageIPSECUser": x509.ExtKeyUsageIPSECUser,
135+
"KeyUsageTimeStamping": x509.ExtKeyUsageTimeStamping,
136+
"KeyUsageOCSPSigning": x509.ExtKeyUsageOCSPSigning,
137+
"KeyUsageMicrosoftServerGatedCrypto": x509.ExtKeyUsageMicrosoftServerGatedCrypto,
138+
"KeyUsageNetscapeServerGatedCrypto": x509.ExtKeyUsageNetscapeServerGatedCrypto,
139+
"KeyUsageMicrosoftCommercialCodeSigning": x509.ExtKeyUsageMicrosoftCommercialCodeSigning,
140+
"KeyUsageMicrosoftKernelCodeSigning": x509.ExtKeyUsageMicrosoftKernelCodeSigning,
141+
}
142+
143+
func builtinCryptoX509ParseAndVerifyCertificatesWithOptions(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
144+
145+
input, err := builtins.StringOperand(operands[0].Value, 1)
146+
if err != nil {
147+
return err
148+
}
149+
150+
options, err := builtins.ObjectOperand(operands[1].Value, 2)
151+
if err != nil {
152+
return err
153+
}
154+
155+
invalid := ast.ArrayTerm(
156+
ast.BooleanTerm(false),
157+
ast.NewTerm(ast.NewArray()),
158+
)
159+
160+
certs, err := getX509CertsFromString(string(input))
161+
if err != nil {
162+
return iter(invalid)
163+
}
164+
165+
// Collect the cert verification options
166+
verifyOpt, err := extractVerifyOpts(options)
167+
if err != nil {
168+
return err
169+
}
170+
171+
verified, err := verifyX509CertificateChain(certs, verifyOpt)
172+
if err != nil {
173+
return iter(invalid)
174+
}
175+
176+
value, err := ast.InterfaceToValue(verified)
177+
if err != nil {
178+
return err
179+
}
180+
181+
valid := ast.ArrayTerm(
182+
ast.BooleanTerm(true),
183+
ast.NewTerm(value),
184+
)
185+
186+
return iter(valid)
187+
}
188+
189+
func extractVerifyOpts(options ast.Object) (verifyOpt x509.VerifyOptions, err error) {
190+
191+
for _, key := range options.Keys() {
192+
k, err := ast.JSON(key.Value)
193+
if err != nil {
194+
return verifyOpt, err
195+
}
196+
k, ok := k.(string)
197+
if !ok {
198+
continue
199+
}
200+
201+
switch k {
202+
case "DNSName":
203+
dns, ok := options.Get(key).Value.(ast.String)
204+
if ok {
205+
verifyOpt.DNSName = strings.Trim(string(dns), "\"")
206+
} else {
207+
return verifyOpt, fmt.Errorf("'DNSName' should be a string")
208+
}
209+
case "CurrentTime":
210+
c, ok := options.Get(key).Value.(ast.Number)
211+
if ok {
212+
nanosecs, ok := c.Int64()
213+
if ok {
214+
verifyOpt.CurrentTime = time.Unix(0, nanosecs)
215+
} else {
216+
return verifyOpt, fmt.Errorf("'CurrentTime' should be a valid int64 number")
217+
}
218+
} else {
219+
return verifyOpt, fmt.Errorf("'CurrentTime' should be a number")
220+
}
221+
case "MaxConstraintComparisons":
222+
c, ok := options.Get(key).Value.(ast.Number)
223+
if ok {
224+
maxComparisons, ok := c.Int()
225+
if ok {
226+
verifyOpt.MaxConstraintComparisions = maxComparisons
227+
} else {
228+
return verifyOpt, fmt.Errorf("'MaxConstraintComparisons' should be a valid number")
229+
}
230+
} else {
231+
return verifyOpt, fmt.Errorf("'MaxConstraintComparisons' should be a number")
232+
}
233+
case "KeyUsages":
234+
type forEach interface {
235+
Foreach(func(*ast.Term))
236+
}
237+
var ks forEach
238+
switch options.Get(key).Value.(type) {
239+
case *ast.Array:
240+
ks = options.Get(key).Value.(*ast.Array)
241+
case ast.Set:
242+
ks = options.Get(key).Value.(ast.Set)
243+
default:
244+
return verifyOpt, fmt.Errorf("'KeyUsages' should be an Array or Set")
245+
}
246+
247+
// Collect the x509.ExtKeyUsage values by looking up the
248+
// mapping of key usage strings to x509.ExtKeyUsage
249+
var invalidKUsgs []string
250+
ks.Foreach(func(t *ast.Term) {
251+
u, ok := t.Value.(ast.String)
252+
if ok {
253+
v := strings.Trim(string(u), "\"")
254+
if k, ok := allowedKeyUsages[v]; ok {
255+
verifyOpt.KeyUsages = append(verifyOpt.KeyUsages, k)
256+
} else {
257+
invalidKUsgs = append(invalidKUsgs, v)
258+
}
259+
}
260+
})
261+
if len(invalidKUsgs) > 0 {
262+
return x509.VerifyOptions{}, fmt.Errorf("invalid entries for 'KeyUsages' found: %s", invalidKUsgs)
263+
}
264+
default:
265+
return verifyOpt, fmt.Errorf("invalid key option")
266+
}
267+
268+
}
269+
270+
return verifyOpt, nil
271+
}
272+
125273
func builtinCryptoX509ParseKeyPair(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
126274
certificate, err := builtins.StringOperand(operands[0].Value, 1)
127275
if err != nil {
@@ -380,6 +528,7 @@ func builtinCryptoHmacEqual(_ BuiltinContext, operands []*ast.Term, iter func(*a
380528
func init() {
381529
RegisterBuiltinFunc(ast.CryptoX509ParseCertificates.Name, builtinCryptoX509ParseCertificates)
382530
RegisterBuiltinFunc(ast.CryptoX509ParseAndVerifyCertificates.Name, builtinCryptoX509ParseAndVerifyCertificates)
531+
RegisterBuiltinFunc(ast.CryptoX509ParseAndVerifyCertificatesWithOptions.Name, builtinCryptoX509ParseAndVerifyCertificatesWithOptions)
383532
RegisterBuiltinFunc(ast.CryptoMd5.Name, builtinCryptoMd5)
384533
RegisterBuiltinFunc(ast.CryptoSha1.Name, builtinCryptoSha1)
385534
RegisterBuiltinFunc(ast.CryptoSha256.Name, builtinCryptoSha256)
@@ -394,7 +543,7 @@ func init() {
394543
RegisterBuiltinFunc(ast.CryptoHmacEqual.Name, builtinCryptoHmacEqual)
395544
}
396545

397-
func verifyX509CertificateChain(certs []*x509.Certificate) ([]*x509.Certificate, error) {
546+
func verifyX509CertificateChain(certs []*x509.Certificate, vo x509.VerifyOptions) ([]*x509.Certificate, error) {
398547
if len(certs) < 2 {
399548
return nil, builtins.NewOperandErr(1, "must supply at least two certificates to be able to verify")
400549
}
@@ -414,8 +563,12 @@ func verifyX509CertificateChain(certs []*x509.Certificate) ([]*x509.Certificate,
414563

415564
// verify the cert chain back to the root
416565
verifyOpts := x509.VerifyOptions{
417-
Roots: roots,
418-
Intermediates: intermediates,
566+
Roots: roots,
567+
Intermediates: intermediates,
568+
DNSName: vo.DNSName,
569+
CurrentTime: vo.CurrentTime,
570+
KeyUsages: vo.KeyUsages,
571+
MaxConstraintComparisions: vo.MaxConstraintComparisions,
419572
}
420573
chains, err := leaf.Verify(verifyOpts)
421574
if err != nil {

0 commit comments

Comments
 (0)