Skip to content

Commit

Permalink
FIPS: Use nop keystore implementation
Browse files Browse the repository at this point in the history
Keystore.Factory and other constuctor functions will return a
NopKeystore if the requirefips build tag is set. The NopKeystore will
will return ErrNotWritable for any retrievals, and
ErrKeystoreUnsupported for create/save/store operations. The NopKeystore
uses the same existing constructor name as the regular file keystore so
it can be used in-place by callers.
  • Loading branch information
michel-laterman committed Feb 12, 2025
1 parent 977d131 commit 05cba0c
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 80 deletions.
84 changes: 84 additions & 0 deletions keystore/file_keystore_fips.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

//go:build requirefips

package keystore

import (
"github.com/elastic/elastic-agent-libs/config"
)

// NopKeystore is used in FIPS mode to a beats keystore.
type NopKeystore struct{}

// NewFileKeystoreWithPasswordAndStrictPerms returns a NopKeystore.
//
// It is using an exising, exported function call name so callers do not need to change the function call, just the build tag.
func NewFileKeystoreWithPasswordAndStrictPerms(keystoreFile string, password *SecureString, strictPerms bool) (Keystore, error) {
return &NopKeystore{}, nil
}

// Retrieve returns ErrKeyDoesntExists
func (*NopKeystore) Retrieve(key string) (*SecureString, error) {
return nil, ErrKeyDoesntExists
}

// Store returns ErrKeystoreDisabled
func (*NopKeystore) Store(key string, value []byte) error {
return nil, ErrKeystoreDisabled
}

// Delete is a nop that returns nil
func (*NopKeystore) Delete(key string) error {
return nil
}

// Save returns ErrKeystoreDisabled
func (*NopKeystore) Save() error {
return ErrKeystoreDisabled
}

// List is a nop that returns a 0 length list of strings
func (*NopKeystore) List() ([]string, error) {
return []string{}, nil
}

// GetConfig returns is a nop that returns a new config.C
func (*NopKeystore) GetConfig() (*config.C, error) {
return config.NewConfig(), nil
}

// Create returns ErrKeystoreDisabled
func (*NopKeystore) Create(override bool) error {
return ErrKeystoreDisabled
}

// IsPersisted always retuns false
func (*NopKeystore) IsPersisted() bool {
return false
}

// Package is a nop that always returns nil
func (*NopKeystore) Package() ([]byte, error) {
return nil, nil
}

// ConfiguredPath retuns an empty string
func (*NopKeystore) ConfiguredPath() string {
return ""
}
83 changes: 3 additions & 80 deletions keystore/file_keystore.go → keystore/file_keystore_nofips.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
// specific language governing permissions and limitations
// under the License.

//go:build !requirefips

package keystore

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha512"
"encoding/base64"
"encoding/json"
Expand All @@ -38,25 +39,6 @@ import (
"github.com/elastic/elastic-agent-libs/file"
)

const (
filePermission = 0600

// Encryption Related constants
iVLength = 12
saltLength = 64
iterationsCount = 10000
keyLength = 32
)

// Version of the keystore format, will be added at the beginning of the file.
var version = []byte("v1")

// Packager defines a keystore that we can read the raw bytes and be packaged in an artifact.
type Packager interface {
Package() ([]byte, error)
ConfiguredPath() string
}

// FileKeystore Allows to store key / secrets pair securely into an encrypted local file.
type FileKeystore struct {
sync.RWMutex
Expand All @@ -67,48 +49,7 @@ type FileKeystore struct {
isStrictPerms bool
}

// Allow the original SecureString type to be correctly serialized to json.
type serializableSecureString struct {
*SecureString
Value []byte `json:"value"`
}

// Factory Create the right keystore with the configured options.
func Factory(c *config.C, defaultPath string, strictPerms bool) (Keystore, error) {
cfg := defaultConfig()

if c == nil {
c = config.NewConfig()
}
err := c.Unpack(&cfg)

if err != nil {
return nil, fmt.Errorf("could not read keystore configuration, err: %w", err)
}

if cfg.Path == "" {
cfg.Path = defaultPath
}

keystore, err := NewFileKeystoreWithStrictPerms(cfg.Path, strictPerms)
return keystore, err
}

// NewFileKeystore returns an new File based keystore or an error, currently users cannot set their
// own password on the keystore, the default password will be an empty string. When the keystore
// is initialized the secrets are automatically loaded into memory.
func NewFileKeystore(keystoreFile string) (Keystore, error) {
return NewFileKeystoreWithStrictPerms(keystoreFile, false)
}

// NewFileKeystore returns an new File based keystore or an error, currently users cannot set their
// own password on the keystore, the default password will be an empty string. When the keystore
// is initialized the secrets are automatically loaded into memory.
func NewFileKeystoreWithStrictPerms(keystoreFile string, strictPerms bool) (Keystore, error) {
return NewFileKeystoreWithPasswordAndStrictPerms(keystoreFile, NewSecureString([]byte("")), strictPerms)
}

// NewFileKeystoreWithPassword return a new File based keystore or an error, allow to define what
// NewFileKeystoreWithPasswordAndStrictPerms return a new File based keystore or an error, allow to define what
// password to use to create the keystore.
func NewFileKeystoreWithPasswordAndStrictPerms(keystoreFile string, password *SecureString, strictPerms bool) (Keystore, error) {
keystore := FileKeystore{
Expand All @@ -127,12 +68,6 @@ func NewFileKeystoreWithPasswordAndStrictPerms(keystoreFile string, password *Se
return &keystore, nil
}

// NewFileKeystoreWithPassword return a new File based keystore or an error, allow to define what
// password to use to create the keystore.
func NewFileKeystoreWithPassword(keystoreFile string, password *SecureString) (Keystore, error) {
return NewFileKeystoreWithPasswordAndStrictPerms(keystoreFile, password, false)
}

// Retrieve return a SecureString instance that will contains both the key and the secret.
func (k *FileKeystore) Retrieve(key string) (*SecureString, error) {
k.RLock()
Expand Down Expand Up @@ -459,15 +394,3 @@ func (k *FileKeystore) ConfiguredPath() string {
func (k *FileKeystore) hashPassword(password, salt []byte) []byte {
return pbkdf2.Key(password, salt, iterationsCount, keyLength, sha512.New)
}

// randomBytes return a slice of random bytes of the defined length
func randomBytes(length int) ([]byte, error) {
r := make([]byte, length)
_, err := rand.Read(r)

if err != nil {
return nil, err
}

return r, nil
}
File renamed without changes.
77 changes: 77 additions & 0 deletions keystore/keystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
package keystore

import (
"crypto/rand"
"errors"
"fmt"

"github.com/elastic/elastic-agent-libs/config"
"github.com/elastic/go-ucfg"
Expand All @@ -37,8 +39,24 @@ var (

// ErrNotWritable is returned when the keystore is not writable
ErrNotListing = errors.New("the configured keystore is not listing")

// ErrKeystoreDisabled is returned on create, save, and store operations if built with the requirefips flag
ErrKeystoreDisabled = fmt.Errorf("keystore disabled in FIPS mode: %w", errors.ErrUnsupported)
)

const (
filePermission = 0600

// Encryption Related constants
iVLength = 12
saltLength = 64
iterationsCount = 10000
keyLength = 32
)

// Version of the keystore format, will be added at the beginning of the file.
var version = []byte("v1")

// Keystore implement a way to securely saves and retrieves secrets to be used in the configuration
// Currently all credentials are loaded upfront and are not lazy retrieved, we will eventually move
// to that concept, so we can deal with tokens that has a limited duration or can be revoked by a
Expand Down Expand Up @@ -73,6 +91,12 @@ type ListingKeystore interface {
List() ([]string, error)
}

// Packager defines a keystore that we can read the raw bytes and be packaged in an artifact.
type Packager interface {
Package() ([]byte, error)
ConfiguredPath() string
}

// Use parse.NoopConfig to disable interpreting all parser characters when loading secrets.
var parseConfig = parse.NoopConfig

Expand Down Expand Up @@ -118,3 +142,56 @@ func AsListingKeystore(store Keystore) (ListingKeystore, error) {
}
return w, nil
}

// Factory Create the right keystore with the configured options.
func Factory(c *config.C, defaultPath string, strictPerms bool) (Keystore, error) {
cfg := defaultConfig()

if c == nil {
c = config.NewConfig()
}
err := c.Unpack(&cfg)

if err != nil {
return nil, fmt.Errorf("could not read keystore configuration, err: %w", err)
}

if cfg.Path == "" {
cfg.Path = defaultPath
}

keystore, err := NewFileKeystoreWithStrictPerms(cfg.Path, strictPerms)
return keystore, err
}

// NewFileKeystore returns an new File based keystore or an error, currently users cannot set their
// own password on the keystore, the default password will be an empty string. When the keystore
// is initialized the secrets are automatically loaded into memory.
func NewFileKeystore(keystoreFile string) (Keystore, error) {
return NewFileKeystoreWithStrictPerms(keystoreFile, false)
}

// NewFileKeystore returns an new File based keystore or an error, currently users cannot set their
// own password on the keystore, the default password will be an empty string. When the keystore
// is initialized the secrets are automatically loaded into memory.
func NewFileKeystoreWithStrictPerms(keystoreFile string, strictPerms bool) (Keystore, error) {
return NewFileKeystoreWithPasswordAndStrictPerms(keystoreFile, NewSecureString([]byte("")), strictPerms)
}

// NewFileKeystoreWithPassword return a new File based keystore or an error, allow to define what
// password to use to create the keystore.
func NewFileKeystoreWithPassword(keystoreFile string, password *SecureString) (Keystore, error) {
return NewFileKeystoreWithPasswordAndStrictPerms(keystoreFile, password, false)
}

// randomBytes return a slice of random bytes of the defined length
func randomBytes(length int) ([]byte, error) {
r := make([]byte, length)
_, err := rand.Read(r)

if err != nil {
return nil, err
}

return r, nil
}
6 changes: 6 additions & 0 deletions keystore/secure_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ type SecureString struct {
value []byte
}

// Allow the original SecureString type to be correctly serialized to json.
type serializableSecureString struct {
*SecureString
Value []byte `json:"value"`
}

// NewSecureString return a struct representing a secrets string.
func NewSecureString(value []byte) *SecureString {
return &SecureString{
Expand Down

0 comments on commit 05cba0c

Please sign in to comment.