mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
94a953150a
This change allows users that build their own executable or "spin" of OPA to give it a name, and have it reference itself properly in help texts. It's a vanity thing, but I think some people would appreciate it, hat tip to the international association of pedants. Signed-off-by: Stephan Renatus <stephan@styra.com> Co-authored-by: kevinstyra <83973046+kevinstyra@users.noreply.github.com>
294 lines
8.8 KiB
Go
294 lines
8.8 KiB
Go
// Copyright 2018 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/open-policy-agent/opa/cmd/internal/env"
|
|
initload "github.com/open-policy-agent/opa/internal/runtime/init"
|
|
"github.com/open-policy-agent/opa/v1/bundle"
|
|
"github.com/open-policy-agent/opa/v1/util"
|
|
)
|
|
|
|
type signCmdParams struct {
|
|
algorithm string
|
|
key string
|
|
claimsFile string
|
|
outputFilePath string
|
|
bundleMode bool
|
|
plugin string
|
|
}
|
|
|
|
const (
|
|
defaultTokenSigningAlg = "RS256"
|
|
defaultHashingAlg = "SHA-256"
|
|
signaturesFile = ".signatures.json"
|
|
)
|
|
|
|
var errSigningConfigIncomplete = errors.New("specify the secret (HMAC) or path of the PEM file containing the private key (RSA and ECDSA)")
|
|
|
|
func newSignCmdParams() signCmdParams {
|
|
return signCmdParams{}
|
|
}
|
|
|
|
func initSign(root *cobra.Command, brand string) {
|
|
executable := root.Name()
|
|
cmdParams := newSignCmdParams()
|
|
|
|
var signCommand = &cobra.Command{
|
|
Use: "sign <path> [<path> [...]]",
|
|
Short: `Generate an ` + brand + ` bundle signature`,
|
|
Long: `Generate an ` + brand + ` bundle signature.
|
|
|
|
The 'sign' command generates a digital signature for policy bundles. It generates a
|
|
".signatures.json" file that dictates which files should be included in the bundle,
|
|
what their SHA hashes are, and is cryptographically secure.
|
|
|
|
The signatures file is a JSON file with an array containing a single JSON Web Token (JWT)
|
|
that encapsulates the signature for the bundle.
|
|
|
|
The --signing-alg flag can be used to specify the algorithm to sign the token. The 'sign'
|
|
command uses RS256 (by default) as the signing algorithm.
|
|
See https://www.openpolicyagent.org/docs/latest/configuration/#keys
|
|
for a list of supported signing algorithms.
|
|
|
|
The key to be used for signing the JWT MUST be provided using the --signing-key flag.
|
|
For example, for RSA family of algorithms, the command expects a PEM file containing
|
|
the private key.
|
|
For HMAC family of algorithms (eg. HS256), the secret can be provided using
|
|
the --signing-key flag.
|
|
|
|
` + brand + ` 'sign' can ONLY be used with the --bundle flag to load paths that refer to
|
|
existing bundle files or directories following the bundle structure.
|
|
|
|
$ ` + executable + ` sign --signing-key /path/to/private_key.pem --bundle foo
|
|
|
|
Where foo has the following structure:
|
|
|
|
foo/
|
|
|
|
|
+-- bar/
|
|
| |
|
|
| +-- data.json
|
|
|
|
|
+-- policy.rego
|
|
|
|
|
+-- .manifest
|
|
|
|
This will create a ".signatures.json" file in the current directory.
|
|
The --output-file-path flag can be used to specify a different location for
|
|
the ".signatures.json" file.
|
|
|
|
The content of the ".signatures.json" file is shown below:
|
|
|
|
{
|
|
"signatures": [
|
|
"eyJhbGciOiJSUzI1NiJ9.eyJmaWxlcyI6W3sibmFtZSI6Ii5tYW5pZmVzdCIsImhhc2giOiIxODc0NWRlNzJjMDFlODBjZDlmNTIwZjQxOGMwMDlhYzRkMmMzZDAyYjE3YTUwZTJkMDQyMTU4YmMzNTJhMzJkIiwiYWxnb3JpdGhtIjoiU0hBLTI1NiJ9LHsibmFtZSI6ImJhci9kYXRhLmpzb24iLCJoYXNoIjoiOTNhMjM5NzFhOTE0ZTVlYWNiZjBhOGQyNTE1NGNkYTMwOWMzYzFjNzJmYmI5OTE0ZDQ3YzYwZjNjYjY4MTU4OCIsImFsZ29yaXRobSI6IlNIQS0yNTYifSx7Im5hbWUiOiJwb2xpY3kucmVnbyIsImhhc2giOiJkMGYyNDJhYWUzNGRiNTRlZjU2NmJlYTRkNDVmY2YxOTcwMGM1ZDhmODdhOWRiOTMyZGZhZDZkMWYwZjI5MWFjIiwiYWxnb3JpdGhtIjoiU0hBLTI1NiJ9XX0.lNsmRqrmT1JI4Z_zpY6IzHRZQAU306PyOjZ6osquixPuTtdSBxgbsdKDcp7Civw3B77BgygVsvx4k3fYr8XCDKChm0uYKScrpFr9_yS6g5mVTQws3KZncZXCQHdupRFoqMS8vXAVgJr52C83AinYWABwH2RYq_B0ZPf_GDzaMgzpep9RlDNecGs57_4zlyxmP2ESU8kjfX8jAA6rYFKeGXJHMD-j4SassoYIzYRv9YkHx8F8Y2ae5Kd5M24Ql0kkvqc_4eO_T9s4nbQ4q5qGHGE-91ND1KVn2avcUyVVPc0-XCR7EH8HnHgCl0v1c7gX1RL7ET7NJbPzfmzQAzk0ZW0dEHI4KZnXSpqy8m-3zAc8kIARm2QwoNEWpy3MWiooPeZVSa9d5iw1aLrbyumfjBP0vCQEPes-Aa6PrARwd5jR9SacO5By0-4emzskvJYRZqbfJ9tXSXDMcAFOAm6kqRPJaj8AO4CyajTC_Lt32_0OLeXqYgNpt3HDqLqGjrb-8fVeQc-hKh0aES8XehQqXj4jMwfsTyj5alsXZm08LwzcFlfQZ7s1kUtmr0_BBNJYcdZUdlu6Qio3LFSRYXNuu6edAO1VH5GKqZISvE1uvDZb2E0Z-rtH-oPp1iSpfvsX47jKJ42LVpI6OahEBri44dzHOIwwm3CIuV8gFzOwR0k"
|
|
]
|
|
}
|
|
|
|
And the decoded JWT payload has the following form:
|
|
|
|
{
|
|
"files": [
|
|
{
|
|
"name": ".manifest",
|
|
"hash": "18745de72c01e80cd9f520f418c009ac4d2c3d02b17a50e2d042158bc352a32d",
|
|
"algorithm": "SHA-256"
|
|
},
|
|
{
|
|
"name": "policy.rego",
|
|
"hash": "d0f242aae34db54ef566bea4d45fcf19700c5d8f87a9db932dfad6d1f0f291ac",
|
|
"algorithm": "SHA-256"
|
|
},
|
|
{
|
|
"name": "bar/data.json",
|
|
"hash": "93a23971a914e5eacbf0a8d25154cda309c3c1c72fbb9914d47c60f3cb681588",
|
|
"algorithm": "SHA-256"
|
|
}
|
|
]
|
|
}
|
|
|
|
The "files" field is generated from the files under the directory path(s)
|
|
provided to the 'sign' command. During bundle signature verification, ` + brand + ` will check
|
|
each file name (ex. "foo/bar/data.json") in the "files" field
|
|
exists in the actual bundle. The file content is hashed using SHA256.
|
|
|
|
To include additional claims in the payload use the --claims-file flag to provide
|
|
a JSON file containing optional claims.
|
|
|
|
For more information on the format of the ".signatures.json" file see
|
|
https://www.openpolicyagent.org/docs/latest/management-bundles/#signature-format.
|
|
`,
|
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
|
if err := validateSignParams(args, cmdParams); err != nil {
|
|
return err
|
|
}
|
|
return env.CmdFlags.CheckEnvironmentVariables(cmd)
|
|
},
|
|
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cmd.SilenceErrors = true
|
|
cmd.SilenceUsage = true
|
|
|
|
if err := doSign(args, cmdParams); err != nil {
|
|
fmt.Println("error:", err)
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
addBundleModeFlag(signCommand.Flags(), &cmdParams.bundleMode, false)
|
|
|
|
// bundle signing config
|
|
addSigningKeyFlag(signCommand.Flags(), &cmdParams.key)
|
|
addClaimsFileFlag(signCommand.Flags(), &cmdParams.claimsFile)
|
|
addSigningAlgFlag(signCommand.Flags(), &cmdParams.algorithm, defaultTokenSigningAlg)
|
|
addSigningPluginFlag(signCommand.Flags(), &cmdParams.plugin)
|
|
|
|
signCommand.Flags().StringVarP(&cmdParams.outputFilePath, "output-file-path", "o", ".", "set the location for the .signatures.json file")
|
|
|
|
root.AddCommand(signCommand)
|
|
}
|
|
|
|
func doSign(args []string, params signCmdParams) error {
|
|
load, err := initload.WalkPaths(args, nil, params.bundleMode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hash, err := bundle.NewSignatureHasher(bundle.HashingAlgorithm(defaultHashingAlg))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
files, err := readBundleFiles(load.BundlesLoader, hash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
signingConfig, err := buildSigningConfig(params.key, params.algorithm, params.claimsFile, params.plugin)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
token, err := bundle.GenerateSignedToken(files, signingConfig, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeTokenToFile(token, params.outputFilePath)
|
|
}
|
|
|
|
func readBundleFiles(loaders []initload.BundleLoader, h bundle.SignatureHasher) ([]bundle.FileInfo, error) {
|
|
files := []bundle.FileInfo{}
|
|
|
|
for _, bl := range loaders {
|
|
for {
|
|
f, err := bl.DirectoryLoader.NextFile()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
if err != nil {
|
|
return files, fmt.Errorf("bundle read failed: %w", err)
|
|
}
|
|
|
|
// skip existing signatures file
|
|
if strings.HasSuffix(f.Path(), bundle.SignaturesFile) {
|
|
continue
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
n, err := f.Read(&buf, bundle.DefaultSizeLimitBytes+1)
|
|
f.Close()
|
|
|
|
if err != nil && err != io.EOF {
|
|
return files, err
|
|
} else if err == nil && n >= bundle.DefaultSizeLimitBytes {
|
|
return files, fmt.Errorf("bundle file exceeded max size (%v bytes)", bundle.DefaultSizeLimitBytes)
|
|
}
|
|
|
|
path := f.Path()
|
|
if bl.IsDir {
|
|
path = f.URL()
|
|
}
|
|
|
|
// hash the file content
|
|
fi, err := hashFileContent(h, buf.Bytes(), path)
|
|
if err != nil {
|
|
return files, err
|
|
}
|
|
files = append(files, fi)
|
|
}
|
|
}
|
|
return files, nil
|
|
}
|
|
|
|
func hashFileContent(h bundle.SignatureHasher, data []byte, path string) (bundle.FileInfo, error) {
|
|
|
|
var fileInfo bundle.FileInfo
|
|
var value any
|
|
|
|
if bundle.IsStructuredDoc(path) {
|
|
err := util.Unmarshal(data, &value)
|
|
if err != nil {
|
|
return fileInfo, err
|
|
}
|
|
} else {
|
|
value = data
|
|
}
|
|
|
|
bytes, err := h.HashFile(value)
|
|
if err != nil {
|
|
return fileInfo, err
|
|
}
|
|
|
|
return bundle.NewFile(strings.TrimPrefix(path, "/"), hex.EncodeToString(bytes), defaultHashingAlg), nil
|
|
}
|
|
|
|
func writeTokenToFile(token, fileLoc string) error {
|
|
content := make(map[string]any)
|
|
content["signatures"] = []string{token}
|
|
|
|
bs, err := json.MarshalIndent(content, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
path := signaturesFile
|
|
if fileLoc != "" {
|
|
path = filepath.Join(fileLoc, path)
|
|
}
|
|
return os.WriteFile(path, bs, 0644)
|
|
}
|
|
|
|
func validateSignParams(args []string, params signCmdParams) error {
|
|
if len(args) == 0 {
|
|
return errors.New("specify atleast one path containing policy and/or data files")
|
|
}
|
|
|
|
if params.key == "" {
|
|
return errSigningConfigIncomplete
|
|
}
|
|
|
|
if !params.bundleMode {
|
|
return errors.New("enable bundle mode (ie. --bundle) to sign bundle files or directories")
|
|
}
|
|
return nil
|
|
}
|