Support computing MAC only over values which end up encrypted

Signed-off-by: Mitar <mitar.git@tnode.com>
This commit is contained in:
Mitar 2021-12-18 21:50:57 +01:00
Родитель 73ec51f982
Коммит 051ce028c9
9 изменённых файлов: 165 добавлений и 16 удалений

Просмотреть файл

@ -1427,6 +1427,9 @@ to any key of a file. When set, all values underneath the key that set the
Note that, while in cleartext, unencrypted content is still added to the Note that, while in cleartext, unencrypted content is still added to the
checksum of the file, and thus cannot be modified outside of SOPS without checksum of the file, and thus cannot be modified outside of SOPS without
breaking the file integrity check. breaking the file integrity check.
This behavior can be modified using ``--mac-only-encrypted`` flag or ``.sops.yaml``
config file which makes SOPS compute a MAC only over values it encrypted and
not all values.
The unencrypted suffix can be set to a different value using the The unencrypted suffix can be set to a different value using the
``--unencrypted-suffix`` option. ``--unencrypted-suffix`` option.
@ -1539,6 +1542,9 @@ In addition to authenticating branches of the tree using keys as additional
data, SOPS computes a MAC on all the values to ensure that no value has been data, SOPS computes a MAC on all the values to ensure that no value has been
added or removed fraudulently. The MAC is stored encrypted with AES_GCM and added or removed fraudulently. The MAC is stored encrypted with AES_GCM and
the data key under tree -> ``sops`` -> ``mac``. the data key under tree -> ``sops`` -> ``mac``.
This behavior can be modified using ``--mac-only-encrypted`` flag or ``.sops.yaml``
config file which makes SOPS compute a MAC only over values it encrypted and
not all values.
Motivation Motivation
---------- ----------

Просмотреть файл

@ -35,6 +35,7 @@ type editExampleOpts struct {
EncryptedSuffix string EncryptedSuffix string
UnencryptedRegex string UnencryptedRegex string
EncryptedRegex string EncryptedRegex string
MACOnlyEncrypted bool
KeyGroups []sops.KeyGroup KeyGroups []sops.KeyGroup
GroupThreshold int GroupThreshold int
} }
@ -65,6 +66,7 @@ func editExample(opts editExampleOpts) ([]byte, error) {
EncryptedSuffix: opts.EncryptedSuffix, EncryptedSuffix: opts.EncryptedSuffix,
UnencryptedRegex: opts.UnencryptedRegex, UnencryptedRegex: opts.UnencryptedRegex,
EncryptedRegex: opts.EncryptedRegex, EncryptedRegex: opts.EncryptedRegex,
MACOnlyEncrypted: opts.MACOnlyEncrypted,
Version: version.Version, Version: version.Version,
ShamirThreshold: opts.GroupThreshold, ShamirThreshold: opts.GroupThreshold,
}, },

Просмотреть файл

@ -23,6 +23,7 @@ type encryptOpts struct {
EncryptedSuffix string EncryptedSuffix string
UnencryptedRegex string UnencryptedRegex string
EncryptedRegex string EncryptedRegex string
MACOnlyEncrypted bool
KeyGroups []sops.KeyGroup KeyGroups []sops.KeyGroup
GroupThreshold int GroupThreshold int
} }
@ -82,6 +83,7 @@ func encrypt(opts encryptOpts) (encryptedFile []byte, err error) {
EncryptedSuffix: opts.EncryptedSuffix, EncryptedSuffix: opts.EncryptedSuffix,
UnencryptedRegex: opts.UnencryptedRegex, UnencryptedRegex: opts.UnencryptedRegex,
EncryptedRegex: opts.EncryptedRegex, EncryptedRegex: opts.EncryptedRegex,
MACOnlyEncrypted: opts.MACOnlyEncrypted,
Version: version.Version, Version: version.Version,
ShamirThreshold: opts.GroupThreshold, ShamirThreshold: opts.GroupThreshold,
}, },

Просмотреть файл

@ -668,6 +668,10 @@ func main() {
Name: "ignore-mac", Name: "ignore-mac",
Usage: "ignore Message Authentication Code during decryption", Usage: "ignore Message Authentication Code during decryption",
}, },
cli.BoolFlag{
Name: "mac-only-encrypted",
Usage: "compute MAC only over values which end up encrypted",
},
cli.StringFlag{ cli.StringFlag{
Name: "unencrypted-suffix", Name: "unencrypted-suffix",
Usage: "override the unencrypted key suffix.", Usage: "override the unencrypted key suffix.",
@ -738,6 +742,7 @@ func main() {
encryptedSuffix := c.String("encrypted-suffix") encryptedSuffix := c.String("encrypted-suffix")
encryptedRegex := c.String("encrypted-regex") encryptedRegex := c.String("encrypted-regex")
unencryptedRegex := c.String("unencrypted-regex") unencryptedRegex := c.String("unencrypted-regex")
macOnlyEncrypted := c.Bool("mac-only-encrypted")
conf, err := loadConfig(c, fileName, nil) conf, err := loadConfig(c, fileName, nil)
if err != nil { if err != nil {
return toExitError(err) return toExitError(err)
@ -756,6 +761,9 @@ func main() {
if unencryptedRegex == "" { if unencryptedRegex == "" {
unencryptedRegex = conf.UnencryptedRegex unencryptedRegex = conf.UnencryptedRegex
} }
if !macOnlyEncrypted {
macOnlyEncrypted = conf.MACOnlyEncrypted
}
} }
cryptRuleCount := 0 cryptRuleCount := 0
@ -806,6 +814,7 @@ func main() {
EncryptedSuffix: encryptedSuffix, EncryptedSuffix: encryptedSuffix,
UnencryptedRegex: unencryptedRegex, UnencryptedRegex: unencryptedRegex,
EncryptedRegex: encryptedRegex, EncryptedRegex: encryptedRegex,
MACOnlyEncrypted: macOnlyEncrypted,
KeyServices: svcs, KeyServices: svcs,
KeyGroups: groups, KeyGroups: groups,
GroupThreshold: threshold, GroupThreshold: threshold,
@ -963,6 +972,7 @@ func main() {
EncryptedSuffix: encryptedSuffix, EncryptedSuffix: encryptedSuffix,
UnencryptedRegex: unencryptedRegex, UnencryptedRegex: unencryptedRegex,
EncryptedRegex: encryptedRegex, EncryptedRegex: encryptedRegex,
MACOnlyEncrypted: macOnlyEncrypted,
KeyGroups: groups, KeyGroups: groups,
GroupThreshold: threshold, GroupThreshold: threshold,
}) })

Просмотреть файл

@ -123,6 +123,7 @@ type creationRule struct {
EncryptedSuffix string `yaml:"encrypted_suffix"` EncryptedSuffix string `yaml:"encrypted_suffix"`
UnencryptedRegex string `yaml:"unencrypted_regex"` UnencryptedRegex string `yaml:"unencrypted_regex"`
EncryptedRegex string `yaml:"encrypted_regex"` EncryptedRegex string `yaml:"encrypted_regex"`
MACOnlyEncrypted bool `yaml:"mac_only_encrypted"`
} }
// Load loads a sops config file into a temporary struct // Load loads a sops config file into a temporary struct
@ -142,6 +143,7 @@ type Config struct {
EncryptedSuffix string EncryptedSuffix string
UnencryptedRegex string UnencryptedRegex string
EncryptedRegex string EncryptedRegex string
MACOnlyEncrypted bool
Destination publish.Destination Destination publish.Destination
OmitExtensions bool OmitExtensions bool
} }
@ -265,6 +267,7 @@ func configFromRule(rule *creationRule, kmsEncryptionContext map[string]*string)
EncryptedSuffix: rule.EncryptedSuffix, EncryptedSuffix: rule.EncryptedSuffix,
UnencryptedRegex: rule.UnencryptedRegex, UnencryptedRegex: rule.UnencryptedRegex,
EncryptedRegex: rule.EncryptedRegex, EncryptedRegex: rule.EncryptedRegex,
MACOnlyEncrypted: rule.MACOnlyEncrypted,
}, nil }, nil
} }

Просмотреть файл

@ -158,6 +158,14 @@ creation_rules:
unencrypted_regex: "^dec:" unencrypted_regex: "^dec:"
`) `)
var sampleConfigWithMACOnlyEncrypted = []byte(`
creation_rules:
- path_regex: barbar*
kms: "1"
pgp: "2"
mac_only_encrypted: true
`)
var sampleConfigWithInvalidParameters = []byte(` var sampleConfigWithInvalidParameters = []byte(`
creation_rules: creation_rules:
- path_regex: foobar* - path_regex: foobar*
@ -416,6 +424,12 @@ func TestLoadConfigFileWithEncryptedRegex(t *testing.T) {
assert.Equal(t, "^enc:", conf.EncryptedRegex) assert.Equal(t, "^enc:", conf.EncryptedRegex)
} }
func TestLoadConfigFileWithMACOnlyEncrypted(t *testing.T) {
conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithMACOnlyEncrypted, t), "/conf/path", "barbar", nil)
assert.Equal(t, nil, err)
assert.Equal(t, true, conf.MACOnlyEncrypted)
}
func TestLoadConfigFileWithInvalidParameters(t *testing.T) { func TestLoadConfigFileWithInvalidParameters(t *testing.T) {
_, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithInvalidParameters, t), "/conf/path", "foobar", nil) _, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithInvalidParameters, t), "/conf/path", "foobar", nil)
assert.NotNil(t, err) assert.NotNil(t, err)

57
sops.go
Просмотреть файл

@ -70,6 +70,12 @@ const MacMismatch = sopsError("MAC mismatch")
// MetadataNotFound occurs when the input file is malformed and doesn't have sops metadata in it // MetadataNotFound occurs when the input file is malformed and doesn't have sops metadata in it
const MetadataNotFound = sopsError("sops metadata not found") const MetadataNotFound = sopsError("sops metadata not found")
// MACOnlyEncryptedInitialization is a constant and known sequence of 32 bytes used to initialize
// MAC which is computed only over values which end up encrypted. That assures that a MAC with the
// setting enabled is always different from a MAC with this setting disabled.
// The following numbers are taken from the output of `echo -n sops | sha256sum` (shell) or `hashlib.sha256(b'sops').hexdigest()` (Python).
var MACOnlyEncryptedInitialization = []byte{0x8a, 0x3f, 0xd2, 0xad, 0x54, 0xce, 0x66, 0x52, 0x7b, 0x10, 0x34, 0xf3, 0xd1, 0x47, 0xbe, 0xb, 0xb, 0x97, 0x5b, 0x3b, 0xf4, 0x4f, 0x72, 0xc6, 0xfd, 0xad, 0xec, 0x81, 0x76, 0xf2, 0x7d, 0x69}
var log *logrus.Logger var log *logrus.Logger
func init() { func init() {
@ -291,22 +297,21 @@ func (branch TreeBranch) walkBranch(in TreeBranch, path []string, onLeaves func(
// is provided (by default it is not), those not matching EncryptedRegex, // is provided (by default it is not), those not matching EncryptedRegex,
// if EncryptedRegex is provided (by default it is not) or those matching // if EncryptedRegex is provided (by default it is not) or those matching
// UnencryptedRegex, if UnencryptedRegex is provided (by default it is not). // UnencryptedRegex, if UnencryptedRegex is provided (by default it is not).
// If encryption is successful, it returns the MAC for the encrypted tree. // If encryption is successful, it returns the MAC for the encrypted tree
// (all values if MACOnlyEncrypted is false, or only over values which end
// up encrypted if MACOnlyEncrypted is true).
func (tree Tree) Encrypt(key []byte, cipher Cipher) (string, error) { func (tree Tree) Encrypt(key []byte, cipher Cipher) (string, error) {
audit.SubmitEvent(audit.EncryptEvent{ audit.SubmitEvent(audit.EncryptEvent{
File: tree.FilePath, File: tree.FilePath,
}) })
hash := sha512.New() hash := sha512.New()
if tree.Metadata.MACOnlyEncrypted {
// We initialize with known set of bytes so that a MAC with this setting
// enabled is always different from a MAC with this setting disabled.
hash.Write(MACOnlyEncryptedInitialization)
}
walk := func(branch TreeBranch) error { walk := func(branch TreeBranch) error {
_, err := branch.walkBranch(branch, make([]string, 0), func(in interface{}, path []string) (interface{}, error) { _, err := branch.walkBranch(branch, make([]string, 0), func(in interface{}, path []string) (interface{}, error) {
// Only add to MAC if not a comment
if _, ok := in.(Comment); !ok {
bytes, err := ToBytes(in)
if err != nil {
return nil, fmt.Errorf("Could not convert %s to bytes: %s", in, err)
}
hash.Write(bytes)
}
encrypted := true encrypted := true
if tree.Metadata.UnencryptedSuffix != "" { if tree.Metadata.UnencryptedSuffix != "" {
for _, v := range path { for _, v := range path {
@ -344,6 +349,16 @@ func (tree Tree) Encrypt(key []byte, cipher Cipher) (string, error) {
} }
} }
} }
if !tree.Metadata.MACOnlyEncrypted || encrypted {
// Only add to MAC if not a comment
if _, ok := in.(Comment); !ok {
bytes, err := ToBytes(in)
if err != nil {
return nil, fmt.Errorf("Could not convert %s to bytes: %s", in, err)
}
hash.Write(bytes)
}
}
if encrypted { if encrypted {
var err error var err error
pathString := strings.Join(path, ":") + ":" pathString := strings.Join(path, ":") + ":"
@ -371,13 +386,20 @@ func (tree Tree) Encrypt(key []byte, cipher Cipher) (string, error) {
// those not ending with EncryptedSuffix, if EncryptedSuffix is provided (by default it is not), // those not ending with EncryptedSuffix, if EncryptedSuffix is provided (by default it is not),
// those not matching EncryptedRegex, if EncryptedRegex is provided (by default it is not), // those not matching EncryptedRegex, if EncryptedRegex is provided (by default it is not),
// or those matching UnencryptedRegex, if UnencryptedRegex is provided (by default it is not). // or those matching UnencryptedRegex, if UnencryptedRegex is provided (by default it is not).
// If decryption is successful, it returns the MAC for the decrypted tree. // If decryption is successful, it returns the MAC for the decrypted tree
// (all values if MACOnlyEncrypted is false, or only over values which end
// up decrypted if MACOnlyEncrypted is true).
func (tree Tree) Decrypt(key []byte, cipher Cipher) (string, error) { func (tree Tree) Decrypt(key []byte, cipher Cipher) (string, error) {
log.Debug("Decrypting tree") log.Debug("Decrypting tree")
audit.SubmitEvent(audit.DecryptEvent{ audit.SubmitEvent(audit.DecryptEvent{
File: tree.FilePath, File: tree.FilePath,
}) })
hash := sha512.New() hash := sha512.New()
if tree.Metadata.MACOnlyEncrypted {
// We initialize with known set of bytes so that a MAC with this setting
// enabled is always different from a MAC with this setting disabled.
hash.Write(MACOnlyEncryptedInitialization)
}
walk := func(branch TreeBranch) error { walk := func(branch TreeBranch) error {
_, err := branch.walkBranch(branch, make([]string, 0), func(in interface{}, path []string) (interface{}, error) { _, err := branch.walkBranch(branch, make([]string, 0), func(in interface{}, path []string) (interface{}, error) {
encrypted := true encrypted := true
@ -441,13 +463,15 @@ func (tree Tree) Decrypt(key []byte, cipher Cipher) (string, error) {
} else { } else {
v = in v = in
} }
// Only add to MAC if not a comment if !tree.Metadata.MACOnlyEncrypted || encrypted {
if _, ok := v.(Comment); !ok { // Only add to MAC if not a comment
bytes, err := ToBytes(v) if _, ok := v.(Comment); !ok {
if err != nil { bytes, err := ToBytes(v)
return nil, fmt.Errorf("Could not convert %s to bytes: %s", in, err) if err != nil {
return nil, fmt.Errorf("Could not convert %s to bytes: %s", in, err)
}
hash.Write(bytes)
} }
hash.Write(bytes)
} }
return v, nil return v, nil
}) })
@ -490,6 +514,7 @@ type Metadata struct {
UnencryptedRegex string UnencryptedRegex string
EncryptedRegex string EncryptedRegex string
MessageAuthenticationCode string MessageAuthenticationCode string
MACOnlyEncrypted bool
Version string Version string
KeyGroups []KeyGroup KeyGroups []KeyGroup
// ShamirThreshold is the number of key groups required to recover the // ShamirThreshold is the number of key groups required to recover the

Просмотреть файл

@ -242,6 +242,90 @@ func TestUnencryptedRegex(t *testing.T) {
} }
} }
func TestMACOnlyEncrypted(t *testing.T) {
branches := TreeBranches{
TreeBranch{
TreeItem{
Key: "foo_encrypted",
Value: "bar",
},
TreeItem{
Key: "bar",
Value: TreeBranch{
TreeItem{
Key: "foo",
Value: "bar",
},
},
},
},
}
tree := Tree{Branches: branches, Metadata: Metadata{EncryptedSuffix: "_encrypted", MACOnlyEncrypted: true}}
onlyEncrypted := TreeBranches{
TreeBranch{
TreeItem{
Key: "foo_encrypted",
Value: "bar",
},
},
}
treeOnlyEncrypted := Tree{Branches: onlyEncrypted, Metadata: Metadata{EncryptedSuffix: "_encrypted", MACOnlyEncrypted: true}}
cipher := reverseCipher{}
mac, err := tree.Encrypt(bytes.Repeat([]byte("f"), 32), cipher)
if err != nil {
t.Errorf("Encrypting the tree failed: %s", err)
}
macOnlyEncrypted, err := treeOnlyEncrypted.Encrypt(bytes.Repeat([]byte("f"), 32), cipher)
if err != nil {
t.Errorf("Encrypting the treeOnlyEncrypted failed: %s", err)
}
if mac != macOnlyEncrypted {
t.Errorf("MACs don't match:\ngot \t\t%+v,\nexpected \t\t%+v", mac, macOnlyEncrypted)
}
}
func TestMACOnlyEncryptedNoConfusion(t *testing.T) {
branches := TreeBranches{
TreeBranch{
TreeItem{
Key: "foo_encrypted",
Value: "bar",
},
TreeItem{
Key: "bar",
Value: TreeBranch{
TreeItem{
Key: "foo",
Value: "bar",
},
},
},
},
}
tree := Tree{Branches: branches, Metadata: Metadata{EncryptedSuffix: "_encrypted", MACOnlyEncrypted: true}}
onlyEncrypted := TreeBranches{
TreeBranch{
TreeItem{
Key: "foo_encrypted",
Value: "bar",
},
},
}
treeOnlyEncrypted := Tree{Branches: onlyEncrypted, Metadata: Metadata{EncryptedSuffix: "_encrypted"}}
cipher := reverseCipher{}
mac, err := tree.Encrypt(bytes.Repeat([]byte("f"), 32), cipher)
if err != nil {
t.Errorf("Encrypting the tree failed: %s", err)
}
macOnlyEncrypted, err := treeOnlyEncrypted.Encrypt(bytes.Repeat([]byte("f"), 32), cipher)
if err != nil {
t.Errorf("Encrypting the treeOnlyEncrypted failed: %s", err)
}
if mac == macOnlyEncrypted {
t.Errorf("MACs match but they should not")
}
}
type MockCipher struct{} type MockCipher struct{}
func (m MockCipher) Encrypt(value interface{}, key []byte, path string) (string, error) { func (m MockCipher) Encrypt(value interface{}, key []byte, path string) (string, error) {

Просмотреть файл

@ -51,6 +51,7 @@ type Metadata struct {
EncryptedSuffix string `yaml:"encrypted_suffix,omitempty" json:"encrypted_suffix,omitempty"` EncryptedSuffix string `yaml:"encrypted_suffix,omitempty" json:"encrypted_suffix,omitempty"`
UnencryptedRegex string `yaml:"unencrypted_regex,omitempty" json:"unencrypted_regex,omitempty"` UnencryptedRegex string `yaml:"unencrypted_regex,omitempty" json:"unencrypted_regex,omitempty"`
EncryptedRegex string `yaml:"encrypted_regex,omitempty" json:"encrypted_regex,omitempty"` EncryptedRegex string `yaml:"encrypted_regex,omitempty" json:"encrypted_regex,omitempty"`
MACOnlyEncrypted bool `yaml:"mac_only_encrypted,omitempty" json:"mac_only_encrypted,omitempty"`
Version string `yaml:"version" json:"version"` Version string `yaml:"version" json:"version"`
} }
@ -114,6 +115,7 @@ func MetadataFromInternal(sopsMetadata sops.Metadata) Metadata {
m.UnencryptedRegex = sopsMetadata.UnencryptedRegex m.UnencryptedRegex = sopsMetadata.UnencryptedRegex
m.EncryptedRegex = sopsMetadata.EncryptedRegex m.EncryptedRegex = sopsMetadata.EncryptedRegex
m.MessageAuthenticationCode = sopsMetadata.MessageAuthenticationCode m.MessageAuthenticationCode = sopsMetadata.MessageAuthenticationCode
m.MACOnlyEncrypted = sopsMetadata.MACOnlyEncrypted
m.Version = sopsMetadata.Version m.Version = sopsMetadata.Version
m.ShamirThreshold = sopsMetadata.ShamirThreshold m.ShamirThreshold = sopsMetadata.ShamirThreshold
if len(sopsMetadata.KeyGroups) == 1 { if len(sopsMetadata.KeyGroups) == 1 {
@ -270,6 +272,7 @@ func (m *Metadata) ToInternal() (sops.Metadata, error) {
EncryptedSuffix: m.EncryptedSuffix, EncryptedSuffix: m.EncryptedSuffix,
UnencryptedRegex: m.UnencryptedRegex, UnencryptedRegex: m.UnencryptedRegex,
EncryptedRegex: m.EncryptedRegex, EncryptedRegex: m.EncryptedRegex,
MACOnlyEncrypted: m.MACOnlyEncrypted,
LastModified: lastModified, LastModified: lastModified,
}, nil }, nil
} }