Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+514
View File
@@ -0,0 +1,514 @@
package bundle
import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/open-policy-agent/opa/v1/loader/filter"
"github.com/open-policy-agent/opa/v1/storage"
)
const maxSizeLimitBytesErrMsg = "bundle file %s size (%d bytes) exceeds configured size_limit_bytes (%d bytes)"
// Descriptor contains information about a file and
// can be used to read the file contents.
type Descriptor struct {
url string
path string
reader io.Reader
closer io.Closer
closeOnce *sync.Once
}
// lazyFile defers reading the file until the first call of Read
type lazyFile struct {
path string
file *os.File
}
// newLazyFile creates a new instance of lazyFile
func newLazyFile(path string) *lazyFile {
return &lazyFile{path: path}
}
// Read implements io.Reader. It will check if the file has been opened
// and open it if it has not before attempting to read using the file's
// read method
func (f *lazyFile) Read(b []byte) (int, error) {
var err error
if f.file == nil {
if f.file, err = os.Open(f.path); err != nil {
return 0, fmt.Errorf("failed to open file %s: %w", f.path, err)
}
}
return f.file.Read(b)
}
// Close closes the lazy file if it has been opened using the file's
// close method
func (f *lazyFile) Close() error {
if f.file != nil {
return f.file.Close()
}
return nil
}
func NewDescriptor(url, path string, reader io.Reader) *Descriptor {
return &Descriptor{
url: url,
path: path,
reader: reader,
}
}
func (d *Descriptor) WithCloser(closer io.Closer) *Descriptor {
d.closer = closer
d.closeOnce = new(sync.Once)
return d
}
// Path returns the path of the file.
func (d *Descriptor) Path() string {
return d.path
}
// URL returns the url of the file.
func (d *Descriptor) URL() string {
return d.url
}
// Read will read all the contents from the file the Descriptor refers to
// into the dest writer up n bytes. Will return an io.EOF error
// if EOF is encountered before n bytes are read.
func (d *Descriptor) Read(dest io.Writer, n int64) (int64, error) {
n, err := io.CopyN(dest, d.reader, n)
return n, err
}
// Close the file, on some Loader implementations this might be a no-op.
// It should *always* be called regardless of file.
func (d *Descriptor) Close() error {
var err error
if d.closer != nil {
d.closeOnce.Do(func() {
err = d.closer.Close()
})
}
return err
}
type PathFormat int64
const (
Chrooted PathFormat = iota
SlashRooted
Passthrough
)
// DirectoryLoader defines an interface which can be used to load
// files from a directory by iterating over each one in the tree.
type DirectoryLoader interface {
// NextFile must return io.EOF if there is no next value. The returned
// descriptor should *always* be closed when no longer needed.
NextFile() (*Descriptor, error)
WithFilter(filter filter.LoaderFilter) DirectoryLoader
WithPathFormat(PathFormat) DirectoryLoader
WithSizeLimitBytes(sizeLimitBytes int64) DirectoryLoader
WithFollowSymlinks(followSymlinks bool) DirectoryLoader
}
type dirLoader struct {
root string
files []string
idx int
filter filter.LoaderFilter
pathFormat PathFormat
maxSizeLimitBytes int64
followSymlinks bool
}
// Normalize root directory, ex "./src/bundle" -> "src/bundle"
// We don't need an absolute path, but this makes the joined/trimmed
// paths more uniform.
func normalizeRootDirectory(root string) string {
if len(root) > 1 {
if root[0] == '.' && root[1] == filepath.Separator {
if len(root) == 2 {
root = root[:1] // "./" -> "."
} else {
root = root[2:] // remove leading "./"
}
}
}
return root
}
// NewDirectoryLoader returns a basic DirectoryLoader implementation
// that will load files from a given root directory path.
func NewDirectoryLoader(root string) DirectoryLoader {
d := dirLoader{
root: normalizeRootDirectory(root),
pathFormat: Chrooted,
}
return &d
}
// WithFilter specifies the filter object to use to filter files while loading bundles
func (d *dirLoader) WithFilter(filter filter.LoaderFilter) DirectoryLoader {
d.filter = filter
return d
}
// WithPathFormat specifies how a path is formatted in a Descriptor
func (d *dirLoader) WithPathFormat(pathFormat PathFormat) DirectoryLoader {
d.pathFormat = pathFormat
return d
}
// WithSizeLimitBytes specifies the maximum size of any file in the directory to read
func (d *dirLoader) WithSizeLimitBytes(sizeLimitBytes int64) DirectoryLoader {
d.maxSizeLimitBytes = sizeLimitBytes
return d
}
// WithFollowSymlinks specifies whether to follow symlinks when loading files from the directory
func (d *dirLoader) WithFollowSymlinks(followSymlinks bool) DirectoryLoader {
d.followSymlinks = followSymlinks
return d
}
func formatPath(fileName string, root string, pathFormat PathFormat) string {
switch pathFormat {
case SlashRooted:
if !strings.HasPrefix(fileName, string(filepath.Separator)) {
return string(filepath.Separator) + fileName
}
return fileName
case Chrooted:
// Trim off the root directory and return path as if chrooted
result := strings.TrimPrefix(fileName, filepath.FromSlash(root))
if root == "." && filepath.Base(fileName) == ManifestExt {
result = fileName
}
if !strings.HasPrefix(result, string(filepath.Separator)) {
result = string(filepath.Separator) + result
}
return result
case Passthrough:
fallthrough
default:
return fileName
}
}
// NextFile iterates to the next file in the directory tree
// and returns a file Descriptor for the file.
func (d *dirLoader) NextFile() (*Descriptor, error) {
// build a list of all files we will iterate over and read, but only one time
if d.files == nil {
d.files = []string{}
err := filepath.Walk(d.root, func(path string, info os.FileInfo, _ error) error {
if info == nil {
return nil
}
if info.Mode().IsRegular() {
if d.filter != nil && d.filter(filepath.ToSlash(path), info, getdepth(path, false)) {
return nil
}
if d.maxSizeLimitBytes > 0 && info.Size() > d.maxSizeLimitBytes {
return fmt.Errorf(maxSizeLimitBytesErrMsg, strings.TrimPrefix(path, "/"), info.Size(), d.maxSizeLimitBytes)
}
d.files = append(d.files, path)
} else if d.followSymlinks && info.Mode().Type()&fs.ModeSymlink == fs.ModeSymlink {
if d.filter != nil && d.filter(filepath.ToSlash(path), info, getdepth(path, false)) {
return nil
}
if d.maxSizeLimitBytes > 0 && info.Size() > d.maxSizeLimitBytes {
return fmt.Errorf(maxSizeLimitBytesErrMsg, strings.TrimPrefix(path, "/"), info.Size(), d.maxSizeLimitBytes)
}
d.files = append(d.files, path)
} else if info.Mode().IsDir() {
if d.filter != nil && d.filter(filepath.ToSlash(path), info, getdepth(path, true)) {
return filepath.SkipDir
}
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to list files: %w", err)
}
}
// If done reading files then just return io.EOF
// errors for each NextFile() call
if d.idx >= len(d.files) {
return nil, io.EOF
}
fileName := d.files[d.idx]
d.idx++
fh := newLazyFile(fileName)
cleanedPath := formatPath(fileName, d.root, d.pathFormat)
f := NewDescriptor(filepath.Join(d.root, cleanedPath), cleanedPath, fh).WithCloser(fh)
return f, nil
}
type tarballLoader struct {
baseURL string
r io.Reader
tr *tar.Reader
files []file
idx int
filter filter.LoaderFilter
skipDir map[string]struct{}
pathFormat PathFormat
maxSizeLimitBytes int64
}
type file struct {
name string
reader io.Reader
path storage.Path
raw []byte
}
// NewTarballLoader is deprecated. Use NewTarballLoaderWithBaseURL instead.
func NewTarballLoader(r io.Reader) DirectoryLoader {
l := tarballLoader{
r: r,
pathFormat: Passthrough,
}
return &l
}
// NewTarballLoaderWithBaseURL returns a new DirectoryLoader that reads
// files out of a gzipped tar archive. The file URLs will be prefixed
// with the baseURL.
func NewTarballLoaderWithBaseURL(r io.Reader, baseURL string) DirectoryLoader {
l := tarballLoader{
baseURL: strings.TrimSuffix(baseURL, "/"),
r: r,
pathFormat: Passthrough,
}
return &l
}
// WithFilter specifies the filter object to use to filter files while loading bundles
func (t *tarballLoader) WithFilter(filter filter.LoaderFilter) DirectoryLoader {
t.filter = filter
return t
}
// WithPathFormat specifies how a path is formatted in a Descriptor
func (t *tarballLoader) WithPathFormat(pathFormat PathFormat) DirectoryLoader {
t.pathFormat = pathFormat
return t
}
// WithSizeLimitBytes specifies the maximum size of any file in the tarball to read
func (t *tarballLoader) WithSizeLimitBytes(sizeLimitBytes int64) DirectoryLoader {
t.maxSizeLimitBytes = sizeLimitBytes
return t
}
// WithFollowSymlinks is a no-op for tarballLoader
func (t *tarballLoader) WithFollowSymlinks(_ bool) DirectoryLoader {
return t
}
// NextFile iterates to the next file in the directory tree
// and returns a file Descriptor for the file.
func (t *tarballLoader) NextFile() (*Descriptor, error) {
if t.tr == nil {
gr, err := gzip.NewReader(t.r)
if err != nil {
return nil, fmt.Errorf("archive read failed: %w", err)
}
t.tr = tar.NewReader(gr)
}
if t.files == nil {
t.files = []file{}
if t.skipDir == nil {
t.skipDir = map[string]struct{}{}
}
for {
header, err := t.tr.Next()
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
// Keep iterating on the archive until we find a normal file
if header.Typeflag == tar.TypeReg {
if t.filter != nil {
if t.filter(filepath.ToSlash(header.Name), header.FileInfo(), getdepth(header.Name, false)) {
continue
}
basePath := strings.Trim(filepath.Dir(filepath.ToSlash(header.Name)), "/")
// check if the directory is to be skipped
if _, ok := t.skipDir[basePath]; ok {
continue
}
match := false
for p := range t.skipDir {
if strings.HasPrefix(basePath, p) {
match = true
break
}
}
if match {
continue
}
}
if t.maxSizeLimitBytes > 0 && header.Size > t.maxSizeLimitBytes {
return nil, fmt.Errorf(maxSizeLimitBytesErrMsg, header.Name, header.Size, t.maxSizeLimitBytes)
}
f := file{name: header.Name}
// Note(philipc): We rely on the previous size check in this loop for safety.
buf := bytes.NewBuffer(make([]byte, 0, header.Size))
if _, err := io.Copy(buf, t.tr); err != nil {
return nil, fmt.Errorf("failed to copy file %s: %w", header.Name, err)
}
f.reader = buf
t.files = append(t.files, f)
} else if header.Typeflag == tar.TypeDir {
cleanedPath := filepath.ToSlash(header.Name)
if t.filter != nil && t.filter(cleanedPath, header.FileInfo(), getdepth(header.Name, true)) {
t.skipDir[strings.Trim(cleanedPath, "/")] = struct{}{}
}
}
}
}
// If done reading files then just return io.EOF
// errors for each NextFile() call
if t.idx >= len(t.files) {
return nil, io.EOF
}
f := t.files[t.idx]
t.idx++
cleanedPath := formatPath(f.name, "", t.pathFormat)
d := NewDescriptor(filepath.Join(t.baseURL, cleanedPath), cleanedPath, f.reader)
return d, nil
}
// Next implements the storage.Iterator interface.
// It iterates to the next policy or data file in the directory tree
// and returns a storage.Update for the file.
func (it *iterator) Next() (*storage.Update, error) {
if it.files == nil {
it.files = []file{}
for _, item := range it.raw {
f := file{name: item.Path}
p, err := getFileStoragePath(f.name)
if err != nil {
return nil, err
}
f.path = p
f.raw = item.Value
it.files = append(it.files, f)
}
sortFilePathAscend(it.files)
}
// If done reading files then just return io.EOF
// errors for each NextFile() call
if it.idx >= len(it.files) {
return nil, io.EOF
}
f := it.files[it.idx]
it.idx++
var isPolicy bool
if strings.HasSuffix(f.name, RegoExt) {
isPolicy = true
}
return &storage.Update{
Path: f.path,
Value: f.raw,
IsPolicy: isPolicy,
}, nil
}
type iterator struct {
raw []Raw
files []file
idx int
}
func NewIterator(raw []Raw) storage.Iterator {
it := iterator{
raw: raw,
}
return &it
}
func sortFilePathAscend(files []file) {
sort.Slice(files, func(i, j int) bool {
return len(files[i].path) < len(files[j].path)
})
}
func getdepth(path string, isDir bool) int {
if isDir {
cleanedPath := strings.Trim(filepath.ToSlash(path), "/")
return len(strings.Split(cleanedPath, "/"))
}
basePath := strings.Trim(filepath.Dir(filepath.ToSlash(path)), "/")
return len(strings.Split(basePath, "/"))
}
func getFileStoragePath(path string) (storage.Path, error) {
fpath := strings.TrimLeft(filepath.ToSlash(filepath.Dir(path)), "/.")
if strings.HasSuffix(path, RegoExt) {
fpath = strings.Trim(filepath.ToSlash(path), "/")
}
p, ok := storage.ParsePathEscaped("/" + fpath)
if !ok {
return nil, fmt.Errorf("storage path invalid: %v", path)
}
return p, nil
}
+140
View File
@@ -0,0 +1,140 @@
package bundle
import (
"fmt"
"io"
"io/fs"
"path/filepath"
"sync"
"github.com/open-policy-agent/opa/v1/loader/filter"
)
const (
defaultFSLoaderRoot = "."
)
type dirLoaderFS struct {
sync.Mutex
filesystem fs.FS
files []string
idx int
filter filter.LoaderFilter
root string
pathFormat PathFormat
maxSizeLimitBytes int64
followSymlinks bool
}
// NewFSLoader returns a basic DirectoryLoader implementation
// that will load files from a fs.FS interface
func NewFSLoader(filesystem fs.FS) (DirectoryLoader, error) {
return NewFSLoaderWithRoot(filesystem, defaultFSLoaderRoot), nil
}
// NewFSLoaderWithRoot returns a basic DirectoryLoader implementation
// that will load files from a fs.FS interface at the supplied root
func NewFSLoaderWithRoot(filesystem fs.FS, root string) DirectoryLoader {
d := dirLoaderFS{
filesystem: filesystem,
root: normalizeRootDirectory(root),
pathFormat: Chrooted,
}
return &d
}
func (d *dirLoaderFS) walkDir(path string, dirEntry fs.DirEntry, err error) error {
if err != nil {
return err
}
if dirEntry != nil {
info, err := dirEntry.Info()
if err != nil {
return err
}
if dirEntry.Type().IsRegular() {
if d.filter != nil && d.filter(filepath.ToSlash(path), info, getdepth(path, false)) {
return nil
}
if d.maxSizeLimitBytes > 0 && info.Size() > d.maxSizeLimitBytes {
return fmt.Errorf("file %s size %d exceeds limit of %d", path, info.Size(), d.maxSizeLimitBytes)
}
d.files = append(d.files, path)
} else if dirEntry.Type()&fs.ModeSymlink != 0 && d.followSymlinks {
if d.filter != nil && d.filter(filepath.ToSlash(path), info, getdepth(path, false)) {
return nil
}
if d.maxSizeLimitBytes > 0 && info.Size() > d.maxSizeLimitBytes {
return fmt.Errorf("file %s size %d exceeds limit of %d", path, info.Size(), d.maxSizeLimitBytes)
}
d.files = append(d.files, path)
} else if dirEntry.Type().IsDir() {
if d.filter != nil && d.filter(filepath.ToSlash(path), info, getdepth(path, true)) {
return fs.SkipDir
}
}
}
return nil
}
// WithFilter specifies the filter object to use to filter files while loading bundles
func (d *dirLoaderFS) WithFilter(filter filter.LoaderFilter) DirectoryLoader {
d.filter = filter
return d
}
// WithPathFormat specifies how a path is formatted in a Descriptor
func (d *dirLoaderFS) WithPathFormat(pathFormat PathFormat) DirectoryLoader {
d.pathFormat = pathFormat
return d
}
// WithSizeLimitBytes specifies the maximum size of any file in the filesystem directory to read
func (d *dirLoaderFS) WithSizeLimitBytes(sizeLimitBytes int64) DirectoryLoader {
d.maxSizeLimitBytes = sizeLimitBytes
return d
}
func (d *dirLoaderFS) WithFollowSymlinks(followSymlinks bool) DirectoryLoader {
d.followSymlinks = followSymlinks
return d
}
// NextFile iterates to the next file in the directory tree
// and returns a file Descriptor for the file.
func (d *dirLoaderFS) NextFile() (*Descriptor, error) {
d.Lock()
defer d.Unlock()
if d.files == nil {
err := fs.WalkDir(d.filesystem, d.root, d.walkDir)
if err != nil {
return nil, fmt.Errorf("failed to list files: %w", err)
}
}
// If done reading files then just return io.EOF
// errors for each NextFile() call
if d.idx >= len(d.files) {
return nil, io.EOF
}
fileName := d.files[d.idx]
d.idx++
fh, err := d.filesystem.Open(fileName)
if err != nil {
return nil, fmt.Errorf("failed to open file %s: %w", fileName, err)
}
cleanedPath := formatPath(fileName, d.root, d.pathFormat)
f := NewDescriptor(cleanedPath, cleanedPath, fh).WithCloser(fh)
return f, nil
}
+135
View File
@@ -0,0 +1,135 @@
// Copyright 2020 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 bundle
import (
"bytes"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/json"
"fmt"
"hash"
"io"
"github.com/open-policy-agent/opa/v1/util"
)
// HashingAlgorithm represents a subset of hashing algorithms implemented in Go
type HashingAlgorithm string
// Supported values for HashingAlgorithm
const (
MD5 HashingAlgorithm = "MD5"
SHA1 HashingAlgorithm = "SHA-1"
SHA224 HashingAlgorithm = "SHA-224"
SHA256 HashingAlgorithm = "SHA-256"
SHA384 HashingAlgorithm = "SHA-384"
SHA512 HashingAlgorithm = "SHA-512"
SHA512224 HashingAlgorithm = "SHA-512-224"
SHA512256 HashingAlgorithm = "SHA-512-256"
)
// String returns the string representation of a HashingAlgorithm
func (alg HashingAlgorithm) String() string {
return string(alg)
}
// SignatureHasher computes a signature digest for a file with (structured or unstructured) data and policy
type SignatureHasher interface {
HashFile(v any) ([]byte, error)
}
type hasher struct {
h func() hash.Hash // hash function factory
}
// NewSignatureHasher returns a signature hasher suitable for a particular hashing algorithm
func NewSignatureHasher(alg HashingAlgorithm) (SignatureHasher, error) {
h := &hasher{}
switch alg {
case MD5:
h.h = md5.New
case SHA1:
h.h = sha1.New
case SHA224:
h.h = sha256.New224
case SHA256:
h.h = sha256.New
case SHA384:
h.h = sha512.New384
case SHA512:
h.h = sha512.New
case SHA512224:
h.h = sha512.New512_224
case SHA512256:
h.h = sha512.New512_256
default:
return nil, fmt.Errorf("unsupported hashing algorithm: %s", alg)
}
return h, nil
}
// HashFile hashes the file content, JSON or binary, both in golang native format.
func (h *hasher) HashFile(v any) ([]byte, error) {
hf := h.h()
walk(v, hf)
return hf.Sum(nil), nil
}
// walk hashes the file content, JSON or binary, both in golang native format.
//
// Computation for unstructured documents is a hash of the document.
//
// Computation for the types of structured JSON document is as follows:
//
// object: Hash {, then each key (in alphabetical order) and digest of the value, then comma (between items) and finally }.
//
// array: Hash [, then digest of the value, then comma (between items) and finally ].
func walk(v any, h io.Writer) {
switch x := v.(type) {
case map[string]any:
_, _ = h.Write([]byte("{"))
for i, key := range util.KeysSorted(x) {
if i > 0 {
_, _ = h.Write([]byte(","))
}
_, _ = h.Write(encodePrimitive(key))
_, _ = h.Write([]byte(":"))
walk(x[key], h)
}
_, _ = h.Write([]byte("}"))
case []any:
_, _ = h.Write([]byte("["))
for i, e := range x {
if i > 0 {
_, _ = h.Write([]byte(","))
}
walk(e, h)
}
_, _ = h.Write([]byte("]"))
case []byte:
_, _ = h.Write(x)
default:
_, _ = h.Write(encodePrimitive(x))
}
}
func encodePrimitive(v any) []byte {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
_ = encoder.Encode(v)
return bytes.Trim(buf.Bytes(), "\n")
}
+173
View File
@@ -0,0 +1,173 @@
// Copyright 2020 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 bundle provide helpers that assist in creating the verification and signing key configuration
package bundle
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"os"
"strings"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/open-policy-agent/opa/v1/keys"
"github.com/open-policy-agent/opa/v1/util"
)
const (
defaultTokenSigningAlg = "RS256"
)
// KeyConfig holds the keys used to sign or verify bundles and tokens
// Moved to own package, alias kept for backwards compatibility
type KeyConfig = keys.Config
// VerificationConfig represents the key configuration used to verify a signed bundle
type VerificationConfig struct {
PublicKeys map[string]*KeyConfig
KeyID string `json:"keyid"`
Scope string `json:"scope"`
Exclude []string `json:"exclude_files"`
}
// NewVerificationConfig return a new VerificationConfig
func NewVerificationConfig(keys map[string]*KeyConfig, id, scope string, exclude []string) *VerificationConfig {
return &VerificationConfig{
PublicKeys: keys,
KeyID: id,
Scope: scope,
Exclude: exclude,
}
}
// ValidateAndInjectDefaults validates the config and inserts default values
func (vc *VerificationConfig) ValidateAndInjectDefaults(keys map[string]*KeyConfig) error {
vc.PublicKeys = keys
if vc.KeyID != "" {
found := false
for key := range keys {
if key == vc.KeyID {
found = true
break
}
}
if !found {
return fmt.Errorf("key id %s not found", vc.KeyID)
}
}
return nil
}
// GetPublicKey returns the public key corresponding to the given key id
func (vc *VerificationConfig) GetPublicKey(id string) (*KeyConfig, error) {
var kc *KeyConfig
var ok bool
if kc, ok = vc.PublicKeys[id]; !ok {
return nil, fmt.Errorf("verification key corresponding to ID %v not found", id)
}
return kc, nil
}
// SigningConfig represents the key configuration used to generate a signed bundle
type SigningConfig struct {
Plugin string
Key string
Algorithm string
ClaimsPath string
}
// NewSigningConfig return a new SigningConfig
func NewSigningConfig(key, alg, claimsPath string) *SigningConfig {
if alg == "" {
alg = defaultTokenSigningAlg
}
return &SigningConfig{
Plugin: defaultSignerID,
Key: key,
Algorithm: alg,
ClaimsPath: claimsPath,
}
}
// WithPlugin sets the signing plugin in the signing config
func (s *SigningConfig) WithPlugin(plugin string) *SigningConfig {
if plugin != "" {
s.Plugin = plugin
}
return s
}
// GetPrivateKey returns the private key or secret from the signing config
func (s *SigningConfig) GetPrivateKey() (any, error) {
var keyData string
alg, ok := jwa.LookupSignatureAlgorithm(s.Algorithm)
if !ok {
return nil, fmt.Errorf("unknown signature algorithm: %s", s.Algorithm)
}
// Check if the key looks like PEM data first (starts with -----BEGIN)
if strings.HasPrefix(s.Key, "-----BEGIN") {
keyData = s.Key
} else {
// Try to read as a file path
if _, err := os.Stat(s.Key); err == nil {
bs, err := os.ReadFile(s.Key)
if err != nil {
return nil, err
}
keyData = string(bs)
} else if os.IsNotExist(err) {
// Not a file, treat as raw key data
keyData = s.Key
} else {
return nil, err
}
}
// For HMAC algorithms, return the key as bytes
if alg == jwa.HS256() || alg == jwa.HS384() || alg == jwa.HS512() {
return []byte(keyData), nil
}
// For RSA/ECDSA algorithms, parse the PEM-encoded key
block, _ := pem.Decode([]byte(keyData))
if block == nil {
return nil, errors.New("failed to parse PEM block containing the key")
}
switch block.Type {
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
case "PRIVATE KEY":
return x509.ParsePKCS8PrivateKey(block.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
default:
return nil, fmt.Errorf("unsupported key type: %s", block.Type)
}
}
// GetClaims returns the claims by reading the file specified in the signing config
func (s *SigningConfig) GetClaims() (map[string]any, error) {
var claims map[string]any
bs, err := os.ReadFile(s.ClaimsPath)
if err != nil {
return claims, err
}
if err := util.UnmarshalJSON(bs, &claims); err != nil {
return claims, err
}
return claims, nil
}
+130
View File
@@ -0,0 +1,130 @@
// Copyright 2020 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 bundle provide helpers that assist in the creating a signed bundle
package bundle
import (
"fmt"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jwt"
)
const defaultSignerID = "_default"
var signers map[string]Signer
// Signer is the interface expected for implementations that generate bundle signatures.
type Signer interface {
GenerateSignedToken([]FileInfo, *SigningConfig, string) (string, error)
}
// GenerateSignedToken will retrieve the Signer implementation based on the Plugin specified
// in SigningConfig, and call its implementation of GenerateSignedToken. The signer generates
// a signed token given the list of files to be included in the payload and the bundle
// signing config. The keyID if non-empty, represents the value for the "keyid" claim in the token.
func GenerateSignedToken(files []FileInfo, sc *SigningConfig, keyID string) (string, error) {
var plugin string
// for backwards compatibility, check if there is no plugin specified, and use default
if sc.Plugin == "" {
plugin = defaultSignerID
} else {
plugin = sc.Plugin
}
signer, err := GetSigner(plugin)
if err != nil {
return "", err
}
return signer.GenerateSignedToken(files, sc, keyID)
}
// DefaultSigner is the default bundle signing implementation. It signs bundles by generating
// a JWT and signing it using a locally-accessible private key.
type DefaultSigner struct{}
// GenerateSignedToken generates a signed token given the list of files to be
// included in the payload and the bundle signing config. The keyID if non-empty,
// represents the value for the "keyid" claim in the token
func (*DefaultSigner) GenerateSignedToken(files []FileInfo, sc *SigningConfig, keyID string) (string, error) {
token, err := generateToken(files, sc, keyID)
if err != nil {
return "", err
}
privateKey, err := sc.GetPrivateKey()
if err != nil {
return "", err
}
// Parse the algorithm string to jwa.SignatureAlgorithm
alg, ok := jwa.LookupSignatureAlgorithm(sc.Algorithm)
if !ok {
return "", fmt.Errorf("unknown signature algorithm: %s", sc.Algorithm)
}
// In order to sign the token with a kid, we need a key ID _on_ the key
// (note: we might be able to make this more efficient if we just load
// the key as a JWK from the start)
jwkKey, err := jwk.Import(privateKey)
if err != nil {
return "", fmt.Errorf("failed to import private key: %w", err)
}
if err := jwkKey.Set(jwk.KeyIDKey, keyID); err != nil {
return "", fmt.Errorf("failed to set key ID on JWK: %w", err)
}
// Since v3.0.6, jwx will take the fast path for signing the token if
// there's exactly one WithKey in the options with no sub-options
signed, err := jwt.Sign(token, jwt.WithKey(alg, jwkKey))
if err != nil {
return "", err
}
return string(signed), nil
}
func generateToken(files []FileInfo, sc *SigningConfig, keyID string) (jwt.Token, error) {
tb := jwt.NewBuilder()
tb.Claim("files", files)
if sc.ClaimsPath != "" {
claims, err := sc.GetClaims()
if err != nil {
return nil, err
}
for k, v := range claims {
tb.Claim(k, v)
}
} else if keyID != "" {
// keyid claim is deprecated but include it for backwards compatibility.
tb.Claim("keyid", keyID)
}
return tb.Build()
}
// GetSigner returns the Signer registered under the given id
func GetSigner(id string) (Signer, error) {
signer, ok := signers[id]
if !ok {
return nil, fmt.Errorf("no signer exists under id %s", id)
}
return signer, nil
}
// RegisterSigner registers a Signer under the given id
func RegisterSigner(id string, s Signer) error {
if id == defaultSignerID {
return fmt.Errorf("signer id %s is reserved, use a different id", id)
}
signers[id] = s
return nil
}
func init() {
signers = map[string]Signer{
defaultSignerID: &DefaultSigner{},
}
}
File diff suppressed because it is too large Load Diff
+286
View File
@@ -0,0 +1,286 @@
// Copyright 2020 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 bundle provide helpers that assist in the bundle signature verification process
package bundle
import (
"bytes"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jws/jwsbb"
"github.com/open-policy-agent/opa/v1/util"
)
// parseVerificationKey converts a string key to the appropriate type for jws.Verify
func parseVerificationKey(keyData string, alg jwa.SignatureAlgorithm) (any, error) {
// For HMAC algorithms, return the key as bytes
if alg == jwa.HS256() || alg == jwa.HS384() || alg == jwa.HS512() {
return []byte(keyData), nil
}
// For RSA/ECDSA algorithms, try to parse as PEM first
block, _ := pem.Decode([]byte(keyData))
if block != nil {
switch block.Type {
case "RSA PUBLIC KEY":
return x509.ParsePKCS1PublicKey(block.Bytes)
case "PUBLIC KEY":
return x509.ParsePKIXPublicKey(block.Bytes)
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
case "PRIVATE KEY":
return x509.ParsePKCS8PrivateKey(block.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
case "CERTIFICATE":
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
return cert.PublicKey, nil
}
}
return nil, errors.New("failed to parse PEM block containing the key")
}
const defaultVerifierID = "_default"
var verifiers map[string]Verifier
// Verifier is the interface expected for implementations that verify bundle signatures.
type Verifier interface {
VerifyBundleSignature(SignaturesConfig, *VerificationConfig) (map[string]FileInfo, error)
}
// VerifyBundleSignature will retrieve the Verifier implementation based
// on the Plugin specified in SignaturesConfig, and call its implementation
// of VerifyBundleSignature. VerifyBundleSignature verifies the bundle signature
// using the given public keys or secret. If a signature is verified, it keeps
// track of the files specified in the JWT payload
func VerifyBundleSignature(sc SignaturesConfig, bvc *VerificationConfig) (map[string]FileInfo, error) {
// default implementation does not return a nil for map, so don't
// do it here either
files := make(map[string]FileInfo)
var plugin string
// for backwards compatibility, check if there is no plugin specified, and use default
if sc.Plugin == "" {
plugin = defaultVerifierID
} else {
plugin = sc.Plugin
}
verifier, err := GetVerifier(plugin)
if err != nil {
return files, err
}
return verifier.VerifyBundleSignature(sc, bvc)
}
// DefaultVerifier is the default bundle verification implementation. It verifies bundles by checking
// the JWT signature using a locally-accessible public key.
type DefaultVerifier struct{}
// VerifyBundleSignature verifies the bundle signature using the given public keys or secret.
// If a signature is verified, it keeps track of the files specified in the JWT payload
func (*DefaultVerifier) VerifyBundleSignature(sc SignaturesConfig, bvc *VerificationConfig) (map[string]FileInfo, error) {
files := make(map[string]FileInfo)
if len(sc.Signatures) == 0 {
return files, errors.New(".signatures.json: missing JWT (expected exactly one)")
}
if len(sc.Signatures) > 1 {
return files, errors.New(".signatures.json: multiple JWTs not supported (expected exactly one)")
}
for _, token := range sc.Signatures {
payload, err := verifyJWTSignature(token, bvc)
if err != nil {
return files, err
}
for _, file := range payload.Files {
files[file.Name] = file
}
}
return files, nil
}
func verifyJWTSignature(token string, bvc *VerificationConfig) (*DecodedSignature, error) {
tokbytes := []byte(token)
hdrb64, payloadb64, signatureb64, err := jwsbb.SplitCompact(tokbytes)
if err != nil {
return nil, fmt.Errorf("failed to split compact JWT: %w", err)
}
// check for the id of the key to use for JWT signature verification
// first in the OPA config. If not found, then check the JWT kid.
keyID := bvc.KeyID
if keyID == "" {
// Use jwsbb.Header to access into the "kid" header field, which we will
// use to determine the key to use for verification.
hdr := jwsbb.HeaderParseCompact(hdrb64)
v, err := jwsbb.HeaderGetString(hdr, "kid")
switch {
case err == nil:
// err == nils means we found the key ID in the header
keyID = v
case errors.Is(err, jwsbb.ErrHeaderNotFound()):
// no "kid" in the header. no op.
default:
// some other error occurred while trying to extract the key ID
return nil, fmt.Errorf("failed to extract key ID from headers: %w", err)
}
}
// Because we want to fallback to ds.KeyID when we can't find the
// keyID, we need to parse the payload here already.
decoder := base64.RawURLEncoding
payload := make([]byte, decoder.DecodedLen(len(payloadb64)))
if _, err := decoder.Decode(payload, payloadb64); err != nil {
return nil, fmt.Errorf("failed to base64 decode JWT payload: %w", err)
}
var ds DecodedSignature
if err := json.Unmarshal(payload, &ds); err != nil {
return nil, err
}
// If header has no key id, check the deprecated key claim.
if keyID == "" {
keyID = ds.KeyID
}
// If we still don't have a keyID, we cannot proceed
if keyID == "" {
return nil, errors.New("verification key ID is empty")
}
// now that we have the keyID, fetch the actual key
keyConfig, err := bvc.GetPublicKey(keyID)
if err != nil {
return nil, err
}
alg, ok := jwa.LookupSignatureAlgorithm(keyConfig.Algorithm)
if !ok {
return nil, fmt.Errorf("unknown signature algorithm: %s", keyConfig.Algorithm)
}
// Parse the key into the appropriate type
parsedKey, err := parseVerificationKey(keyConfig.Key, alg)
if err != nil {
return nil, err
}
signature := make([]byte, decoder.DecodedLen(len(signatureb64)))
if _, err = decoder.Decode(signature, signatureb64); err != nil {
return nil, fmt.Errorf("failed to base64 decode JWT signature: %w", err)
}
signbuf := make([]byte, len(hdrb64)+1+len(payloadb64))
copy(signbuf, hdrb64)
signbuf[len(hdrb64)] = '.'
copy(signbuf[len(hdrb64)+1:], payloadb64)
if err := jwsbb.Verify(parsedKey, alg.String(), signbuf, signature); err != nil {
return nil, fmt.Errorf("failed to verify JWT signature: %w", err)
}
// verify the scope
scope := bvc.Scope
if scope == "" {
scope = keyConfig.Scope
}
if ds.Scope != scope {
return nil, errors.New("scope mismatch")
}
return &ds, nil
}
// VerifyBundleFile verifies the hash of a file in the bundle matches to that provided in the bundle's signature
func VerifyBundleFile(path string, data bytes.Buffer, files map[string]FileInfo) error {
var file FileInfo
var ok bool
if file, ok = files[path]; !ok {
return fmt.Errorf("file %v not included in bundle signature", path)
}
if file.Algorithm == "" {
return fmt.Errorf("no hashing algorithm provided for file %v", path)
}
hash, err := NewSignatureHasher(HashingAlgorithm(file.Algorithm))
if err != nil {
return err
}
// hash the file content
// For unstructured files, hash the byte stream of the file
// For structured files, read the byte stream and parse into a JSON structure;
// then recursively order the fields of all objects alphabetically and then apply
// the hash function to result to compute the hash. This ensures that the digital signature is
// independent of whitespace and other non-semantic JSON features.
var value any
if IsStructuredDoc(path) {
err := util.Unmarshal(data.Bytes(), &value)
if err != nil {
return err
}
} else {
value = data.Bytes()
}
bs, err := hash.HashFile(value)
if err != nil {
return err
}
// compare file hash with same file in the JWT payloads
fb, err := hex.DecodeString(file.Hash)
if err != nil {
return err
}
if !bytes.Equal(fb, bs) {
return fmt.Errorf("%v: digest mismatch (want: %x, got: %x)", path, fb, bs)
}
delete(files, path)
return nil
}
// GetVerifier returns the Verifier registered under the given id
func GetVerifier(id string) (Verifier, error) {
verifier, ok := verifiers[id]
if !ok {
return nil, fmt.Errorf("no verifier exists under id %s", id)
}
return verifier, nil
}
// RegisterVerifier registers a Verifier under the given id
func RegisterVerifier(id string, v Verifier) error {
if id == defaultVerifierID {
return fmt.Errorf("verifier id %s is reserved, use a different id", id)
}
verifiers[id] = v
return nil
}
func init() {
verifiers = map[string]Verifier{
defaultVerifierID: &DefaultVerifier{},
}
}