diff --git a/ast/builtins.go b/ast/builtins.go index f386778118..ee57ed4413 100644 --- a/ast/builtins.go +++ b/ast/builtins.go @@ -200,6 +200,10 @@ var DefaultBuiltins = [...]*Builtin{ CryptoSha256, CryptoX509ParseCertificateRequest, CryptoX509ParseRSAPrivateKey, + CryptoHmacMd5, + CryptoHmacSha1, + CryptoHmacSha256, + CryptoHmacSha512, // Graphs WalkBuiltin, @@ -1873,6 +1877,58 @@ var CryptoSha256 = &Builtin{ ), } +// CryptoHmacMd5 returns a string representing the MD-5 HMAC of the input message using the input key +// Inputs are message, key +var CryptoHmacMd5 = &Builtin{ + Name: "crypto.hmac.md5", + Decl: types.NewFunction( + types.Args( + types.S, + types.S, + ), + types.S, + ), +} + +// CryptoHmacSha1 returns a string representing the SHA-1 HMAC of the input message using the input key +// Inputs are message, key +var CryptoHmacSha1 = &Builtin{ + Name: "crypto.hmac.sha1", + Decl: types.NewFunction( + types.Args( + types.S, + types.S, + ), + types.S, + ), +} + +// CryptoHmacSha256 returns a string representing the SHA-256 HMAC of the input message using the input key +// Inputs are message, key +var CryptoHmacSha256 = &Builtin{ + Name: "crypto.hmac.sha256", + Decl: types.NewFunction( + types.Args( + types.S, + types.S, + ), + types.S, + ), +} + +// CryptoHmacSha512 returns a string representing the SHA-512 HMAC of the input message using the input key +// Inputs are message, key +var CryptoHmacSha512 = &Builtin{ + Name: "crypto.hmac.sha512", + Decl: types.NewFunction( + types.Args( + types.S, + types.S, + ), + types.S, + ), +} + /** * Graphs. */ diff --git a/capabilities.json b/capabilities.json index 612db5adbb..d9a04bf152 100644 --- a/capabilities.json +++ b/capabilities.json @@ -552,6 +552,74 @@ "type": "function" } }, + { + "name": "crypto.hmac.md5", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.sha1", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.sha256", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.sha512", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, { "name": "crypto.md5", "decl": { diff --git a/docs/README.md b/docs/README.md index 565e4c33b2..7277c79b03 100644 --- a/docs/README.md +++ b/docs/README.md @@ -47,7 +47,7 @@ re-deployed. ### Preview Markdown `content` (*.md) The majority of this can be done with any markdown renderer (typically built into or -a via plug-in for IDE's and editors). The rendered output will be very similar to what Hugo will +via a plug-in for IDEs and editors). The rendered output will be very similar to what Hugo will generate. > This excludes the Hugo shortcodes (places with `{{< SHORT_CODE >}}` in the markdown. diff --git a/docs/content/contrib-adding-builtin-functions.md b/docs/content/contrib-adding-builtin-functions.md new file mode 100644 index 0000000000..7e10440eb4 --- /dev/null +++ b/docs/content/contrib-adding-builtin-functions.md @@ -0,0 +1,161 @@ +--- +title: Adding Built-in Functions +kind: contrib +weight: 5 +--- + +[Built-in Functions](../policy-reference/#built-in-functions) +can be added inside the `topdown` package. + +Built-in functions may be upstreamed if they are generally useful and provide functionality that would be +impractical to implement natively in Rego (e.g., CIDR arithmetic). Implementations should avoid thirdparty +dependencies. If absolutely necessary, consider importing the code manually into the `internal` package. + +{{< info >}} +Read more about extending OPA with custom built-in functions in go [here](../extensions#custom-built-in-functions-in-go). +{{< /info >}} + +Adding a new built-in function involves the following steps: + +1. [Declare and register](#declare-and-register) the function +2. [Implementation](#implement) the function +3. [Test](#test) the function +4. [Document](#document) the function + +## Example + +The following example adds a simple built-in function, `repeat(string, int)`, that returns a given string repeated a given number of times. + +### Declare and Register + +In `ast/builtins.go`, we declare the structure of our built-in function with a `Builtin` struct instance: + +```go +// Repeat returns, as a string, the given string repeated the given number of times. +var Repeat = &Builtin{ + Name: "repeat", // The name of the function + Decl: types.NewFunction( + types.Args( // The built-in takes two arguments, where .. + types.S, // .. the first is a string, and .. + types.N, // .. the second is a number. + ), + types.S, // The return type is a string. + ), +} +``` + +To register the new built-in function, we locate the `DefaultBuiltins` array in `ast/builtins.go`, and add the `Builtin` instance to it: + +```go +var DefaultBuiltins = [...]*Builtin{ + ... + Repeat, + ... +} +``` + +### Implement + +In the `topdown` package, we locate a suitable source file for our new built-in function, or add a new file, as appropriate. + +In this example, we introduce a new source file, `topdown/repeat.go`: + +```go +package topdown + +import ( + "fmt" + "strings" + + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/topdown/builtins" +) + +// implements topdown.BuiltinFunc +func builtinRepeat(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + // Get the first argument as a string, returning an error if it's not the correct type. + str, err := builtins.StringOperand(operands[0].Value, 1) + if err != nil { + return err + } + + // Get the first argument as an int, returning an error if it's not the correct type or not a positive value. + count, err := builtins.IntOperand(operands[1].Value, 2) + if err != nil { + return err + } else if count < 0 { + // Defensive check, strings.Repeat(...) will panic for count<0 + return fmt.Errorf("count must be a positive integer") + } + + // Return a string by invoking the given iterator function + return iter(ast.StringTerm(strings.Repeat(string(str), count))) +} + +func init() { + RegisterBuiltinFunc(ast.Repeat.Name, builtinRepeat) +} +``` + +In the above code, `builtinRepeat` implements the `topdown.BuiltinFunc` function type. +The call to `RegisterBuiltinFunc(...)` in `init()` adds the built-in function to the evaluation engine; binding the implementation to `ast.Repeat` that was registered in [an earlier step](#register-the-function). + +### Test + +All built-in function implementations must include a test suite. +Test cases for built-in functions are written in YAML and located under `test/cases/testdata`. + +We create two new test cases (one positive, expecting a string output; and one negative, expecting an error) for our built-in function: + +```yaml +cases: + - note: repeat/positive + query: data.test.p = x + modules: + - | + package test + + p = repeated { + repeated := repeat(input.str, input.count) + } + input: {"str": "Foo", "count": 3} + want_result: + - x: FooFooFoo + - note: repeat/negative + query: data.test.p = x + modules: + - | + package test + + p = repeated { + repeated := repeat(input.str, input.count) + } + input: { "str": "Foo", "count": -3 } + strict_error: true + want_error_code: eval_builtin_error + want_error: 'repeat: count must be a positive integer' +``` + +The above test cases can be run separate from all other tests through: `go test ./topdown -v -run 'TestRego/repeat'` + +See [test/cases/testdata/helloworld](https://github.com/open-policy-agent/opa/blob/main/test/cases/testdata/helloworld) +for a more detailed example of how to implement tests for your built-in functions. + +> Note: We can manually test our new built-in function by [building](../contrib-development#getting-started) +> and running the `eval` command. E.g.: `$./opa__ eval 'repeat("Foo", 3)'` + +### Document + +All built-in functions must be documented in `docs/content/policy-reference.md` under an appropriate subsection. + +For this example, we add an entry for our new function under the `Strings` section: + +```markdown +### Strings + +| Built-in | Description | Wasm Support | +| ------- |-------------|---------------| +... +| ``output := repeat(string, count)`` | ``output`` is ``string`` repeated ``count``times | ``SDK-dependent`` | +... +``` diff --git a/docs/content/contrib-development.md b/docs/content/contrib-development.md index b3f59ac21e..42fe3aec17 100644 --- a/docs/content/contrib-development.md +++ b/docs/content/contrib-development.md @@ -96,18 +96,6 @@ Pull Request, please mention it in the discussion. > If you are not familiar with squashing commits, see [the following blog post for a good overview](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html). -## Built-in Functions - -[Built-in Functions](../policy-reference/#built-in-functions) -can be added inside the `topdown` package in this repository. - -Built-in functions may be upstreamed if they are generally useful and provide functionality that would be -impractical to implement natively in Rego (e.g., CIDR arithmetic). Implementations should avoid thirdparty -dependencies. If absolutely necessary, consider importing the code manually into the `internal` package. - -All built-in function implementations must include a test suite. See [test/cases/testdata/helloworld](https://github.com/open-policy-agent/opa/blob/main/test/cases/testdata/helloworld) -in this repository for an example of how to implement tests for your built-in functions. - ## Benchmarks Several packages in this repository implement benchmark tests. To execute the diff --git a/docs/content/policy-reference.md b/docs/content/policy-reference.md index 106c8e53ae..ec0f19b08e 100644 --- a/docs/content/policy-reference.md +++ b/docs/content/policy-reference.md @@ -831,6 +831,10 @@ Note that the opa executable will need access to the timezone files in the envir | ``output := crypto.md5(string)`` | ``output`` is ``string`` md5 hashed. | ``SDK-dependent`` | | ``output := crypto.sha1(string)`` | ``output`` is ``string`` sha1 hashed. | ``SDK-dependent`` | | ``output := crypto.sha256(string)`` | ``output`` is ``string`` sha256 hashed. | ``SDK-dependent`` | +| ``output := crypto.hmac.md5(string, key)`` | ``output`` is HMAC-MD5 of ``string`` using ``key`` | ``SDK-dependent`` | +| ``output := crypto.hmac.sha1(string, key)`` | ``output`` is HMAC-SHA-1 of ``string`` using ``key`` | ``SDK-dependent`` | +| ``output := crypto.hmac.sha256(string, key)`` | ``output`` is HMAC-SHA-256 of ``string`` using ``key`` | ``SDK-dependent`` | +| ``output := crypto.hmac.sha512(string, key)`` | ``output`` is HMAC-SHA-512 of ``string`` using ``key`` | ``SDK-dependent`` | ### Graphs diff --git a/test/cases/testdata/cryptohmacmd5/test-cryptohmacmd5.yaml b/test/cases/testdata/cryptohmacmd5/test-cryptohmacmd5.yaml new file mode 100644 index 0000000000..2823ff1bf2 --- /dev/null +++ b/test/cases/testdata/cryptohmacmd5/test-cryptohmacmd5.yaml @@ -0,0 +1,28 @@ +cases: + - note: cryptohmacmd5/crypto.hmac.md5 + query: data.test.p = x + modules: + - | + package test + + p[mac] { + mac := crypto.hmac.md5(input.message, input.key) + } + input: {"message": "foo", "key": "bar"} + want_result: + - x: + - 31b6db9e5eb4addb42f1a6ca07367adc + - note: cryptohmacmd5/crypto.hmac.md5_unicode + query: data.test.p = x + modules: + - | + package test + + p[mac] { + mac := crypto.hmac.md5(input.message, input.key) + } + input: {"message": "åäöçß🥲♙Ω", "key": "秘密の"} + want_result: + - x: + - 20a8743c2157ac60b7e8b79c83651b8d + strict_error: true \ No newline at end of file diff --git a/test/cases/testdata/cryptohmacsha1/test-cryptohmacsha1.yaml b/test/cases/testdata/cryptohmacsha1/test-cryptohmacsha1.yaml new file mode 100644 index 0000000000..dc0ae1d1f7 --- /dev/null +++ b/test/cases/testdata/cryptohmacsha1/test-cryptohmacsha1.yaml @@ -0,0 +1,28 @@ +cases: + - note: cryptohmacsha1/crypto.hmac.sha1 + query: data.test.p = x + modules: + - | + package test + + p[mac] { + mac := crypto.hmac.sha1(input.message, input.key) + } + input: {"message": "foo", "key": "bar"} + want_result: + - x: + - 85d155c55ed286a300bd1cf124de08d87e914f3a + - note: cryptohmacsha1/crypto.hmac.sha1_unicode + query: data.test.p = x + modules: + - | + package test + + p[mac] { + mac := crypto.hmac.sha1(input.message, input.key) + } + input: {"message": "åäöçß🥲♙Ω", "key": "秘密の"} + want_result: + - x: + - 81759c39013935fcf0de833d44c8018d7c1455dd + strict_error: true diff --git a/test/cases/testdata/cryptohmacsha256/test-cryptohmacsha256.yaml b/test/cases/testdata/cryptohmacsha256/test-cryptohmacsha256.yaml new file mode 100644 index 0000000000..5e55ac2f54 --- /dev/null +++ b/test/cases/testdata/cryptohmacsha256/test-cryptohmacsha256.yaml @@ -0,0 +1,28 @@ +cases: + - note: cryptohmacsha256/crypto.hmac.sha256 + query: data.test.p = x + modules: + - | + package test + + p[mac] { + mac := crypto.hmac.sha256(input.message, input.key) + } + input: {"message": "foo", "key": "bar"} + want_result: + - x: + - 147933218aaabc0b8b10a2b3a5c34684c8d94341bcf10a4736dc7270f7741851 + - note: cryptohmacsha256/crypto.hmac.sha256_unicode + query: data.test.p = x + modules: + - | + package test + + p[mac] { + mac := crypto.hmac.sha256(input.message, input.key) + } + input: {"message": "åäöçß🥲♙Ω", "key": "秘密の"} + want_result: + - x: + - eb90daeb76d4b2571fbdaf94bbb240809faa8fed93ec0c260dd38c3fdf8d963a + strict_error: true \ No newline at end of file diff --git a/test/cases/testdata/cryptohmacsha512/test-cryptohmacsha512.yaml b/test/cases/testdata/cryptohmacsha512/test-cryptohmacsha512.yaml new file mode 100644 index 0000000000..db5915fa90 --- /dev/null +++ b/test/cases/testdata/cryptohmacsha512/test-cryptohmacsha512.yaml @@ -0,0 +1,28 @@ +cases: + - note: cryptohmacsha512/crypto.hmac.sha512 + query: data.test.p = x + modules: + - | + package test + + p[mac] { + mac := crypto.hmac.sha512(input.message, input.key) + } + input: {"message": "foo", "key": "bar"} + want_result: + - x: + - 24257d7210582a65c731ec55159c8184cc24c02489453e58587f71f44c23a2d61b4b72154a89d17b2d49448a8452ea066f4fc56a2bcead45c088572ffccdb3d8 + - note: cryptohmacsha512/crypto.hmac.sha512_unicode + query: data.test.p = x + modules: + - | + package test + + p[mac] { + mac := crypto.hmac.sha512(input.message, input.key) + } + input: {"message": "åäöçß🥲♙Ω", "key": "秘密の"} + want_result: + - x: + - 192f5afded233d6e21427aa26ed267ac118cfa2971013d91cbed530c0b208d78138b83dfe1d6cc3553d7bd518f22a481402c723028e1279d1ffbe8f11ea6b125 + strict_error: true \ No newline at end of file diff --git a/topdown/crypto.go b/topdown/crypto.go index 502eb26484..2b5a96eaff 100644 --- a/topdown/crypto.go +++ b/topdown/crypto.go @@ -6,14 +6,17 @@ package topdown import ( "bytes" + "crypto/hmac" "crypto/md5" "crypto/sha1" "crypto/sha256" + "crypto/sha512" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "fmt" + "hash" "io/ioutil" "os" "strings" @@ -190,6 +193,42 @@ func builtinCryptoSha256(a ast.Value) (ast.Value, error) { return hashHelper(a, func(s ast.String) string { return fmt.Sprintf("%x", sha256.Sum256([]byte(s))) }) } +func hmacHelper(args []*ast.Term, iter func(*ast.Term) error, h func() hash.Hash) error { + a1 := args[0].Value + message, err := builtins.StringOperand(a1, 1) + if err != nil { + return err + } + + a2 := args[1].Value + key, err := builtins.StringOperand(a2, 2) + if err != nil { + return err + } + + mac := hmac.New(h, []byte(key)) + mac.Write([]byte(message)) + messageDigest := mac.Sum(nil) + + return iter(ast.StringTerm(fmt.Sprintf("%x", messageDigest))) +} + +func builtinCryptoHmacMd5(_ BuiltinContext, args []*ast.Term, iter func(*ast.Term) error) error { + return hmacHelper(args, iter, md5.New) +} + +func builtinCryptoHmacSha1(_ BuiltinContext, args []*ast.Term, iter func(*ast.Term) error) error { + return hmacHelper(args, iter, sha1.New) +} + +func builtinCryptoHmacSha256(_ BuiltinContext, args []*ast.Term, iter func(*ast.Term) error) error { + return hmacHelper(args, iter, sha256.New) +} + +func builtinCryptoHmacSha512(_ BuiltinContext, args []*ast.Term, iter func(*ast.Term) error) error { + return hmacHelper(args, iter, sha512.New) +} + func init() { RegisterFunctionalBuiltin1(ast.CryptoX509ParseCertificates.Name, builtinCryptoX509ParseCertificates) RegisterBuiltinFunc(ast.CryptoX509ParseAndVerifyCertificates.Name, builtinCryptoX509ParseAndVerifyCertificates) @@ -198,6 +237,10 @@ func init() { RegisterFunctionalBuiltin1(ast.CryptoSha256.Name, builtinCryptoSha256) RegisterFunctionalBuiltin1(ast.CryptoX509ParseCertificateRequest.Name, builtinCryptoX509ParseCertificateRequest) RegisterBuiltinFunc(ast.CryptoX509ParseRSAPrivateKey.Name, builtinCryptoX509ParseRSAPrivateKey) + RegisterBuiltinFunc(ast.CryptoHmacMd5.Name, builtinCryptoHmacMd5) + RegisterBuiltinFunc(ast.CryptoHmacSha1.Name, builtinCryptoHmacSha1) + RegisterBuiltinFunc(ast.CryptoHmacSha256.Name, builtinCryptoHmacSha256) + RegisterBuiltinFunc(ast.CryptoHmacSha512.Name, builtinCryptoHmacSha512) } func verifyX509CertificateChain(certs []*x509.Certificate) ([]*x509.Certificate, error) {