-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support for bundle signature verification #2475
Support for bundle signature verification #2475
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good so far! Handful of nits and questions. Looking forward to more of the changes for the CLI tooling so I can play around with it 😄
bundle/bundle.go
Outdated
var value interface{} | ||
if isStructuredDoc(path) { | ||
err := util.Unmarshal(data.Bytes(), &value) | ||
if err != nil { | ||
return err | ||
} | ||
} else { | ||
value = data.Bytes() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is interesting, and maybe just a detail I missed in the design doc. Why aren't we using the hash of the source file for structured docs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added details in the docs and also a comment in the code. lmk if it clarifies this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The doc updates look good! That helps a lot 👍
bundle/bundle.go
Outdated
var buf bytes.Buffer | ||
buf.Write(payload) | ||
|
||
var jpl JWTPayload | ||
if err := util.NewJSONDecoder(&buf).Decode(&jpl); err != nil { | ||
return err | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Can use util.UnmarshalJSON
and simplify some of this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated to use util.UnmarshalJSON
}) | ||
} | ||
|
||
// add files to the bundle and reader |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Maybe split up this test into the ones using the list of cases for error checking and the case below here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Separated into 2 tests.
plugins/bundle/config.go
Outdated
Name string `json:"name"` // Deprecated: Use `Bundles` map instead | ||
Service string `json:"service"` // Deprecated: Use `Bundles` map instead | ||
Prefix *string `json:"prefix"` // Deprecated: Use `Bundles` map instead | ||
Signing *bdl.VerificationConfig `json:"signing"` // Deprecated: Use `Bundles` map instead |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO we shouldn't add it here. If someone wants to use the new feature they should update to using the non-deprecated bundles
config
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree too. Removed the signing config from the deprecated config.
plugins/plugins.go
Outdated
@@ -122,6 +123,7 @@ type Manager struct { | |||
Config *config.Config | |||
Info *ast.Term | |||
ID string | |||
Keys map[string]*bdl.KeyConfig |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably shouldn't have public exported fields using types that are internal. I think moving the config structs for the keys/signing stuff out of the internal package is OK.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unexported keys
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We still have a handful of places where we have usage of the internal types as parameters to exported methods, for example looking at:
func (r *Reader) WithBundleVerificationConfig(config *bdl.VerificationConfig) *Reader {
If I was writing a golang tool that wanted to load and verify a bundle, how could I use this API? I don't have a way to make a config to pass into this.
Please take a look at either making those types public (just merge it into the public bundles package) or double check all the API's where they are required to ensure they are usable from the outside.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved the code in internal/bundle
to the public bundle package.
be87ff4
to
30c76ea
Compare
This looks great! A few thoughts/actions that came out of manual testing.
Maybe this instead?
EDIT: re: comment above about making The
|
👍 This is really cool, finished playing around with it and have a few random notes. Lots of it is just docs or tweaks to the help output. Random notes (in no particular order):
It stands out from the others in verbosity. Maybe just saying like: "Sign Rego bundle files"
but for eval we give more useful information:
Since we have such a large help text for
I would have expected the signatures to not be changing without any changes to the bundle source files. Looking at the output I see that the subsequent "sign" operations are including the signature file:
|
My last comment there reminded me too that we should document somewhere how the paths are specified, for example in my example for the idempotency issue I was specifying
But if I were to do a
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed the implementation. Looks great. Mostly just nits.
bundle/sign.go
Outdated
// Use of this source code is governed by an Apache2 | ||
// license that can be found in the LICENSE file. | ||
|
||
// Package bundle defines structures that contain information required during the bundle signature verification process |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems out-of-sync since thes files are in the bunde package. I could imagine splitting this into it's own top-level package (keys
). This would also be nice if we end up re-using keys for something other than bundles in the future. @patrick-east WDYT?
wantErr: false, | ||
}, | ||
{ | ||
input: `{"name": "a/b/c", "decision": "query", "signing": {"keyid": "bar", "scope": "write"}}}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what happens if the key configuration that discovery depends on changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can add a check to see if the key that discovery depends on has changed like we do for services.
The keyid
could be included in the bundle signature, which we could read from the bundle itself. Since we only support one signature in the bundle, this would work.
If the keyid
was not provided in the signature, we could pick it up from discovery.signing
.
WDYT ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, except per the offline discussion yesterday, we should make the config keyid override the bundle keyid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a check to avoid updates to keys in the boot config.
cmd/sign.go
Outdated
return writeTokenToFile(string(token), params.outputFilePath) | ||
} | ||
|
||
func generatePayload(dataPaths []string, claimsFile string) ([]byte, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's figure out how to avoid this. We don't want to duplicate this code. We'll likely need to improve/refactor the loader package.
bundle/hash.go
Outdated
// object: Hash {, then each key (in alphabetical order) and digest of the value, and finally }. | ||
// | ||
// array: Hash [, then digest of the value, and finally ]. | ||
func (h *hasher) file(v interface{}) ([]byte, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per the offline discussion, we can align this algorithm to match the hashing of raw (ordered) JSON file by making the actual digested content to match the bytes matched in that case. In other words, need to remove a) type strings from hashed, b) add commas to containers, c) consider the primitive types' hashing details.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the algorithm.
bundle/hash_test.go
Outdated
"map_unequal": {`{"a": {"foo": [1, 2, 3]}, "b": "bar"}`, `{"b": "bars", "a": {"foo": [1, 2, 3]}}`, SHA384, false}, | ||
"array": {`[1, 2, 3]`, `[1, 2, 3]`, SHA512, true}, | ||
"null": {`null`, `null`, SHA512224, true}, | ||
"bool": {`false`, `false`, SHA512256, true}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd think you can transform this into test that checks the hash computed directly from the raw bytes equals to the one the alg computes. Then a few more corner cases to cover in the types of docs hashed: []byte, string with unicode characters, string that could HTML escaped (but won't).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated with more tests.
bundle/hash.go
Outdated
// Supported values for HashingAlgorithm | ||
const ( | ||
MD5 HashingAlgorithm = "MD5" | ||
SHA1 HashingAlgorithm = "SHA1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think the convention is to have dash between "SHA" and the number of bits (per RFCs at least). Once you do that, then perhaps change the underscores to dashes too, for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
248b7e5
to
cbdcf6d
Compare
Some updates to $ opa --help
An open source project to policy-enable your service.
Usage:
opa [command]
Available Commands:
bench Benchmark a Rego query
build Build an OPA bundle
check Check Rego source files
deps Analyze Rego query dependencies
eval Evaluate a Rego query
fmt Format Rego source files
help Help about any command
parse Parse Rego source file
run Start OPA in interactive or server mode
sign Sign OPA bundle files
test Execute Rego test cases
version Print the version of OPA
Flags:
-h, --help help for opa
Use "opa [command] --help" for more information about a command. $ opa sign
Error: specify atleast one path containing policy and/or data files
Usage:
opa sign <path> [<path> [...]] [flags]
Flags:
--claims-file string set path of JSON file containing optional claims (see: https://openpolicyagent.org/docs/latest/management/#bundle-signature-format)
--crypto-alg string name of the signing algorithm (default "RS256")
-h, --help help for sign
-k, --key string set the secret (HMAC) or path of the PEM file containing the private key (RSA and ECDSA)
-o, --output-file-path string set the location for the .signatures.json file (default ".")
specify atleast one path containing policy and/or data files $ opa sign .
Error: specify the secret (HMAC) or path of the PEM file containing the private key (RSA and ECDSA)
Usage:
opa sign <path> [<path> [...]] [flags]
Flags:
--claims-file string set path of JSON file containing optional claims (see: https://openpolicyagent.org/docs/latest/management/#bundle-signature-format)
--crypto-alg string name of the signing algorithm (default "RS256")
-h, --help help for sign
-k, --key string set the secret (HMAC) or path of the PEM file containing the private key (RSA and ECDSA)
-o, --output-file-path string set the location for the .signatures.json file (default ".")
specify the secret (HMAC) or path of the PEM file containing the private key (RSA and ECDSA) |
cmd/flags.go
Outdated
@@ -85,6 +85,10 @@ func addIgnoreFlag(fs *pflag.FlagSet, ignoreNames *[]string) { | |||
fs.StringSliceVarP(ignoreNames, "ignore", "", []string{}, "set file and directory names to ignore during loading (e.g., '.*' excludes hidden files)") | |||
} | |||
|
|||
func addSigningAlgFlag(fs *pflag.FlagSet, alg *string, value string) { | |||
fs.StringVarP(alg, "crypto-alg", "", value, "name of the signing algorithm") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
crypto is rather meaningless term here. how about --signature-alg?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed to --signing-alg
to match the description.
bundle/bundle.go
Outdated
func (r *Reader) checkSignaturesAndDescriptors(signatures *SignaturesConfig, descriptors []*Descriptor) error { | ||
if signatures == nil && r.verificationConfig != nil { | ||
msg := "bundle reader provided with signature verification config but .signatures.json file not included in the bundle" | ||
return fmt.Errorf("%v", msg) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: call errors.New("...")
or fmt.Errorf("...")
; not need for the intermediate variable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
bundle/bundle.go
Outdated
|
||
if signatures != nil { | ||
if r.verificationConfig == nil { | ||
msg := "reader missing signature verification config (hint: check opa configuration includes key(s))" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: same as previous (use errors.New
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
bundle/bundle.go
Outdated
} | ||
|
||
// check that number of files in the bundle signatures is equal to number of files in the bundle | ||
if len(r.files) != len(descriptors) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be better to report (1) missing and (2) extra files in the bundle. Currently we're reporting the # of files but that information is not very useful from the user's perspective (e.g., what are they going to do if they see that 7 != 6? We can just perform a diff and return something much more useful.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1, Saying which files were missing or extra would be really nice for the error message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated error message format: file(s) [a/b/c/data.json http/policy/policy.rego] specified in bundle signatures but not found in the target bundle
plugins/bundle/config.go
Outdated
// ParseBundlesConfigWithSigning validates the config and injects default values for | ||
// the defined `bundles`. This expects a map of bundle names to resource | ||
// configurations and public keys to verify a signed bundle. | ||
func ParseBundlesConfigWithSigning(config []byte, services []string, keys map[string]*bundle.KeyConfig) (*Config, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of the nice things about using the builder pattern is that you don't have to add variations every time the API needs to be extended. For example:
type ConfigBuilder struct {
raw []byte
services []string
keys map[string]*bundle.KeyConfig
}
func NewConfigBuilder() *ConfigBuilder { ... }
func (b *ConfigBuilder) WithBytes([]byte) *ConfigBuilder {...}
func (b *ConfigBuilder) WithServices([]string) *ConfigBuilder {...}
func (b *ConfigBuilder) WithKeyConfigs(map[string]*bundle.KeyConfig) {...}
func (b *ConfigBuilder) Parse() (*Config, error)
Example usage:
config, err := bundle.NewConfigBuilder().
WithBytes(bs)
WithServices(services).
WithKeyConfigs(keys).
Parse()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the bundle and discovery config code to use builder pattern.
ddd3994
to
4e3039c
Compare
bundle/bundle.go
Outdated
} | ||
|
||
// check that number of files in the bundle signatures is equal to number of files in the bundle | ||
if len(r.files) != len(descriptors) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1, Saying which files were missing or extra would be really nice for the error message.
bundle/keys.go
Outdated
// Use of this source code is governed by an Apache2 | ||
// license that can be found in the LICENSE file. | ||
|
||
// Package bundle defines structures that contain information required during the bundle signature verification process |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This description seems a little off for bundle
, is this remnants from when it was separate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the description.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if you have multiple package level godoc comments? At this point, we should probably just create a separate doc.go file and put the package level comment in there that explains what the overall bundle
package does.
docs/content/configuration.md
Outdated
The following signing algorithms are supported: | ||
|
||
ES256 "ES256" // ECDSA using P-256 and SHA-256 | ||
ES384 "ES384" // ECDSA using P-384 and SHA-384 | ||
ES512 "ES512" // ECDSA using P-521 and SHA-512 | ||
HS256 "HS256" // HMAC using SHA-256 | ||
HS384 "HS384" // HMAC using SHA-384 | ||
HS512 "HS512" // HMAC using SHA-512 | ||
PS256 "PS256" // RSASSA-PSS using SHA256 and MGF1-SHA256 | ||
PS384 "PS384" // RSASSA-PSS using SHA384 and MGF1-SHA384 | ||
PS512 "PS512" // RSASSA-PSS using SHA512 and MGF1-SHA512 | ||
RS256 "RS256" // RSASSA-PKCS-v1.5 using SHA-256 | ||
RS384 "RS384" // RSASSA-PKCS-v1.5 using SHA-384 | ||
RS512 "RS512" // RSASSA-PKCS-v1.5 using SHA-512 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make this into a table like:
Name | Description |
---|---|
ES256 | ECDSA using P-256 and SHA-256 |
... | ... |
or something? Its a little more work to maintain but I think helps make the docs a bit easier to understand. As-is its unclear what the ES256 "ES256"
means, like do I need quotes? no quotes? which one goes into the --signing-alg
parameter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated to a table.
loader/loader.go
Outdated
if signatures != nil { | ||
root.Signatures = signatures | ||
|
||
if fl.bvc == nil { | ||
return nil, fmt.Errorf("missing configuration to verify bundle signature") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if someone specifies a directory with >1 bundle in it? Say I had:
./bundles/b1
./bundles/b1/.signatures.json
./bundles/b1/policy.rego
./bundles/b2
./bundles/b2/.signatures.json
./bundles/b1/other-policy.rego
And I do something like opa run ./bundles
IIRC both bundles get handled by a single call to Filtered()
. If I'm reading this correctly we would only verify the last bundle signature file we found, right?
We could potentially say if someone wants bundle verification to happen they need to use the -b
option and specify them as bundles. Not sure how others feel about that.. but we did want to push more people to using that. I'd almost prefer having this error out saying "found a signed bundle, use --bundle instead" rather than potentially giving wrong results or having a ton of complexity in the --data
loading path.
Just kind of brainstorming here.. if we do go the "unsupported" path we could iterate later on to add support as needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if someone specifies a directory with >1 bundle in it? Say I had:
With the current implementation, I'd expect an error since we do not support multiple JWTs. OTOH, if there was only a single signature file then it would probably error because the signature file would not reference the files in othe other bundle (if it did, things may just work but that seems like a very strange edge case.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd expect an error since we do not support multiple JWTs
Not sure I follow, if we load those files as if they are bundles, eg:
./bundles/b1
./bundles/b1/.signatures.json
./bundles/b1/policy.rego
./bundles/b2
./bundles/b2/.signatures.json
./bundles/b1/other-policy.rego
should be loaded the same as
./bundles/b1.tar.gz
./bundles/b2.tar.gz
if I did opa run ./bundles
. Each bundle still has a single JWT that should verify its content. I guess the question is whether or not we want the non-bundle-mode loading option to even try to handle these or if we say a user has to specify opa run -b ./bundles/b1 ./bundles/b2
or opa run -b ./bundles/b1.tar.gz ./bundles/b2.tar.gz
instead if they want validation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I follow, if we load those files as if they are bundles, eg:
If we're passing multiple paths and interpreting them as bundles (e.g., -b
was given) then yes, but I thought the point was that one of those was not happening.
With the rest of this PR it looks like we're erring on the side of security; if the user does opa run .
without -b
and the filesystem contains a signatures.json file, it will be honored. This makes sense to some extent though it does depart from the rest of the file loading conventions.
Thinking about this a bit more, I'm a bit surprised we pickup .signatures.json
files under b1 and b2 when the load path is .
or bundles/
. I was under the impression the signatures.json file could only exist at the root of the bundle. This doesn't matter too much though since users could also do opa run ./bundles/b1 ./bundles/b2
(without -b
) and in that case the signatures files would get honored.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Closing the loop on the offline discussion. The plan is to leave non-bundle-mode loading alone for now. OPA will not verify signatures unless bundle mode is enabled. This simplifies the implementation, removes room for error, and avoids the backwards incompatibility issue of making certain files have meaning when non-bundle-mode loading is in use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Bundle signing and verification is supported only in the bundle mode.
d8b02ad
to
f203c3b
Compare
eefd530
to
f5e5389
Compare
Added changes that skip bundle verification in
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did another round of manual testing on the latest changes. I think this is quite close. I've left a few comments on error messages and help text but that's about it.
I did notice one behaviour that seems like a bug. This produces an error:
torin:~/temp$ touch .foo
torin:~/temp$ ls -al
total 8
drwxr-xr-x 2 torin torin 4096 Jul 6 15:50 .
drwxr-xr-x 16 torin torin 4096 Jul 6 15:50 ..
-rw-r--r-- 1 torin torin 0 Jul 6 15:50 .foo
torin:~/temp$ opa sign . --signing-key secret --signing-alg HS256
error: open foo: no such file or directory
This does not:
torin:~/temp$ mkdir x
torin:~/temp$ touch x/.foo
torin:~/temp$ rm .foo
torin:~/temp$ opa sign . --signing-key secret --signing-alg HS256
torin:~/temp$ ls -alR
.:
total 16
drwxr-xr-x 3 torin torin 4096 Jul 6 15:51 .
drwxr-xr-x 16 torin torin 4096 Jul 6 15:51 ..
-rw-r--r-- 1 torin torin 260 Jul 6 15:51 .signatures.json
drwxr-xr-x 2 torin torin 4096 Jul 6 15:51 x
./x:
total 8
drwxr-xr-x 2 torin torin 4096 Jul 6 15:51 .
drwxr-xr-x 3 torin torin 4096 Jul 6 15:51 ..
-rw-r--r-- 1 torin torin 0 Jul 6 15:51 .foo
$ opa eval -i .signatures.json 'io.jwt.decode(input.signatures[0])'
{
"result": [
{
"expressions": [
{
"value": [
{
"alg": "HS256"
},
{
"files": [
{
"algorithm": "SHA-256",
"hash": "12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"name": "x/.foo"
}
]
},
"a2ebc02cff48ab0f0ae551bc24ef4a84d324a8567ab101e9b160ea9f81641538"
],
"text": "io.jwt.decode(input.signatures[0])",
"location": {
"row": 1,
"col": 1
}
}
]
}
]
}
[EDIT: hit submit accidentally]
This made me realize that opa sign
is not idempotent; if you run opa sign ...
without -b
then it produces a .signatures.json file. The second time you run the same command, you get an error. However, if we were to fix this and run the command a second time then opa sign
need to ignore .signatures.json in order to be idempotent. This begs the question of how the CLI treats .signatures.json files...today, without signing, if you run commands without -b
then we don't apply any meaning to the files; OPA just loads files into memory and tries to treat them as policy or data (with directories controlling the location of raw JSON). With -b that behaviour is modified slightly; OPA only looks for data.json files and manifests can control roots. In the future we may choose to apply more meaning to bundles.
With these changes, it seems like we've moved away from that model. If you run opa run .
and there is a signatures.json file then we try to check them. From a security POV this makes sense; we don't want users running commands on file systems and assuming they're secure. OTOH, from a consistency POV this marks a change; we're now applying special meaning to certain files.
I'm not immediately sure what to takeaway from this but figured it was worth putting into text.
EDIT2:
It looks like we're treating both .signatures.json
and signatures.json
as the same?
torin:~$ opa run signatures.json
error: load error: unable to verify signature of signed bundle (hint: check --verification-key flag to provide signature verification config)
torin:~$ cat signatures.json
{"foo":1}
torin:~$ mv signatures.json .signatures.json
torin:~$ opa run .signatures.json
error: load error: unable to verify signature of signed bundle (hint: check --verification-key flag to provide signature verification config)
This seems like a bug.
cmd/build.go
Outdated
"github.com/spf13/cobra" | ||
|
||
"github.com/open-policy-agent/opa/compile" | ||
"github.com/open-policy-agent/opa/util" | ||
) | ||
|
||
const defaultPublicKeyID = "opa_default_key" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason this key ID bothers me--I think it's because opa
and key
seem redundant. Could we just use "default"
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Digging into this a bit more...what am I doing wrong?
# build a bunlde
$ opa build . --signing-key=secret --signing-alg=HS256
# serve over http
$ python3 -m http.server
Inside another terminal:
$ opa run -s --set services.default.url=http://localhost:8000 --set bundles.default.resource=bundle.tar.gz --set keys.opa_default_key.key=secret --set keys.opa_default_key.algorithm=HS256
{"addrs":[":8181"],"diagnostic-addrs":[],"insecure_addr":"","level":"info","msg":"Initializing server.","time":"2020-07-06T17:51:53-04:00"}
{"level":"info","msg":"Starting bundle downloader.","name":"default","plugin":"bundle","time":"2020-07-06T17:51:53-04:00"}
{"level":"error","msg":"Bundle download failed: verification key ID is empty","name":"default","plugin":"bundle","time":"2020-07-06T17:51:53-04:00"}
It seems the JWTs produced by opa build
do not have the key ID claim contained in them. This seems like a bug? I can specify the keyid inside the bundle config section but that does not seem correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the default key ID to default
.
The key ID can be optionally included in the JWT by providing a path to the JSON file that contains optional claims (--claims-file
). OPA will look for the key ID on the command-line first, then in the OPA config and finally in the JWT. We've documented this here.
# build a bundle
$ opa build . --signing-key=secret --signing-alg=HS256
# serve over http
$ python3 -m http.server
# specify the key ID on the command-line
$ opa run -s --set services.default.url=http://localhost:8000 --set bundles.default.resource=bundle.tar.gz --set bundles.default.signing.keyid=default --set keys.default.key=secret --set keys.default.algorithm=HS256
{"addrs":[":8181"],"diagnostic-addrs":[],"insecure_addr":"","level":"info","msg":"Initializing server.","time":"2020-07-06T16:12:50-07:00"}
{"level":"info","msg":"Starting bundle downloader.","name":"default","plugin":"bundle","time":"2020-07-06T16:12:50-07:00"}
{"level":"info","msg":"Bundle downloaded and activated successfully.","name":"default","plugin":"bundle","time":"2020-07-06T16:12:50-07:00"}
claims.json
{
"keyid": "default"
}
# build a bundle
$ opa build . --signing-key=secret --signing-alg=HS256 --claims-file claims.json
# serve over http
$ python3 -m http.server
# use key ID specified in the JWT
$ opa run -s --set services.default.url=http://localhost:8000 --set bundles.default.resource=bundle.tar.gz --set keys.default.key=secret --set keys.default.algorithm=HS256
{"addrs":[":8181"],"diagnostic-addrs":[],"insecure_addr":"","level":"info","msg":"Initializing server.","time":"2020-07-06T16:22:44-07:00"}
{"level":"info","msg":"Starting bundle downloader.","name":"default","plugin":"bundle","time":"2020-07-06T16:22:44-07:00"}
{"level":"info","msg":"Bundle downloaded and activated successfully.","name":"default","plugin":"bundle","time":"2020-07-06T16:22:44-07:00"}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ashutosh-narkar WDYT about making opa build
set the keyid claim automatically (the claim file could override it if needed).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to close the loop on the offline conversation. We reached a decision to have opa build
inject the keyid claim using the default value, by default. Users can override the keyid by passing their own claims.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
# build a bundle
opa build -b . --signing-key=secret --signing-alg=HS256
# serve over http
$ python3 -m http.server
# run OPA
opa run -s --set services.default.url=http://localhost:8000 --set bundles.default.resource=bundle.tar.gz --set keys.default.key=secret --set keys.default.algorithm=HS256
{"addrs":[":8181"],"diagnostic-addrs":[],"insecure_addr":"","level":"info","msg":"Initializing server.","time":"2020-07-09T01:33:30-07:00"}
{"level":"info","msg":"Starting bundle downloader.","name":"default","plugin":"bundle","time":"2020-07-09T01:33:30-07:00"}
{"level":"info","msg":"Bundle downloaded and activated successfully.","name":"default","plugin":"bundle","time":"2020-07-09T01:33:30-07:00"}
compile/compile.go
Outdated
@@ -243,6 +258,7 @@ func (c *Compiler) initBundle() error { | |||
result := &bundle.Bundle{} | |||
result.Manifest.Init() | |||
result.Data = load.Files.Documents | |||
result.Signatures = load.Files.Signatures |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was just playing around with the build command a bit more and noticed this line; I don't think it has any effect and we don't have any corresponding test coverage for the changes to this file to verify it. Let's at least add some tests that verify the expected behaviour (e.g., you could have two tests, one that compiles w/o optimization disables, so that it's effectively a no-op that just tar/gzips the input files and another that enables optimization so the file contents change and the resulting signatures are rewritten.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added more tests to cover this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two thoughts...
-
I was a bit surprised to see that we're modifying the bundle passed as a parameter to the
Write
call. TheWrite
call accepts a bundle by-value not by-reference. This means that writes to the bundle structure will not be seen by the caller. However, since the signature config field is a pointer, the caller will see writes to that. From a maintainability POV, I could see this being an issue down the road. -
If the signatures are being updated by the
Write
call, why do we need to initialize here?
Takeaways:
-
Is the
Signatures
field a pointer toSignaturesConfig
by design? If so, why? If not, I'd be tempted to remove the pointer so that we can continue passing the Bundle around by value and safely assume it's not being mutated by the receiver. -
If we compute a bundle signature and then write out the bundle, when we re-read the bundle into memory, can we re-compute the signature?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couple of changes:
-
Removed the pointer for the
Signatures
field in theSignaturesConfig
. -
Removed signing logic from the writer. So now it simply writes the bundle object to the underlying data stream.
If we compute a bundle signature and then write out the bundle, when we re-read the bundle into memory, can we re-compute the signature?
Kind of, you can re-compute the payload. For example:
# in directory "foo"
foo/data.json
foo/policy.rego
# generate a signature
opa build --signing-alg HS256 --signing-key secret -b foo # this creates a bundle "bundle.tar.gz"
# bundle contents
tar -tzf bundle.tar.gz
/data.json
/foo/policy.rego
/.manifest
/.signatures.json -----> A
# compute the bundle signature
opa sign --signing-key secret --signing-alg HS256 -b bundle.tar.gz
# this generates a ".signatures.json" in the current directory -----> B
The JWT payload in A
and B
will match. Although the payloads match, the token itself won't match as the files
list in the payload has the files in different orders in A
and B
. Hope this makes sense.
443eea3
to
41d28ff
Compare
|
||
h.Write([]byte("]")) | ||
default: | ||
h.Write(encodePrimitive(x)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tsandall asked about hashing []byte offline, whether it needs to be serialized to JSON first before hashing. That's unnecessary, you should hash the []byte directly here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ashutosh-narkar LGTM. Can you add that documentation note I just mentioned and squash your commits into a smaller set that makes sense (1 commit is fine or multiple, up to you!)
When OPA receives a new bundle, it checks that it has been properly signed using a (public) key that OPA has been | ||
configured with out-of-band. Only if that verification succeeds does OPA activate the new bundle; otherwise, OPA | ||
continues using its existing bundle and reports an activation failure via the status API and error logging. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a note here to warn folks that bundle signing is only supported in some modes? Something like this:
> ⚠️ Bundle signature verification is only performed by the `opa run` when the `-b`/`--bundle` flag is given or when Bundle downloading is enabled. Sub-commands primarily used in development and debug environments (such as `opa eval`, `opa test`, etc.) DO NOT verify bundle signatures at this point in time.
I just want to make sure we have something we can point to for the time being. We can work on introducing CLI support for verification in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the comment and squashed the commits.
These changes add support for digital signatures for policy bundles which can be used to verify their authenticity. Bundle signature verification involves the following steps: * Verify the JWT signature * Verify the files in the JWT payload exist in the bundle * Verify the file content of the files in bundle match with those in the payload This commit adds a new `sign` command to generate a digital signature for policy bundles. For more details, run "opa sign --help" The signatures generated by the 'sign' command can be verified by the 'build' command. The 'build' command can also sign the bundle it generates. The 'run' command can verify a signed bundle or skip verification altogether. OPA 'sign', 'build' and 'run' can be used to sign/verify bundles in bundle mode (--bundle) mode only. Verification can be also be performed when bundle downloading is enabled. Fixes: open-policy-agent#1757 Signed-off-by: Ashutosh Narkar <anarkar4387@gmail.com>
28d89d9
to
1c9b933
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🔥
These changes add support to verify the signature of a signed bundle.
At a high-level, bundle signature verification involves the following steps:
Verify the JWT signature
Verify the files in the JWT payload exist in the bundle
Verify the file content of the files in bundle match with those in the JWT payload
More details about the changes are included in the commit messages.
Fixes: #1757