Skip to content

Commit

Permalink
Support computing MAC only over values which end up encrypted
Browse files Browse the repository at this point in the history
Signed-off-by: Mitar <mitar.git@tnode.com>
  • Loading branch information
mitar committed Sep 25, 2023
1 parent d24fe0c commit e198aa3
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 16 deletions.
6 changes: 6 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1409,6 +1409,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
checksum of the file, and thus cannot be modified outside of sops without
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
``--unencrypted-suffix`` option.
Expand Down Expand Up @@ -1521,6 +1524,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
added or removed fraudulently. The MAC is stored encrypted with AES_GCM and
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
----------
Expand Down
2 changes: 2 additions & 0 deletions cmd/sops/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type editExampleOpts struct {
EncryptedSuffix string
UnencryptedRegex string
EncryptedRegex string
MACOnlyEncrypted bool
KeyGroups []sops.KeyGroup
GroupThreshold int
}
Expand Down Expand Up @@ -65,6 +66,7 @@ func editExample(opts editExampleOpts) ([]byte, error) {
EncryptedSuffix: opts.EncryptedSuffix,
UnencryptedRegex: opts.UnencryptedRegex,
EncryptedRegex: opts.EncryptedRegex,
MACOnlyEncrypted: opts.MACOnlyEncrypted,
Version: version.Version,
ShamirThreshold: opts.GroupThreshold,
},
Expand Down
2 changes: 2 additions & 0 deletions cmd/sops/encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type encryptOpts struct {
EncryptedSuffix string
UnencryptedRegex string
EncryptedRegex string
MACOnlyEncrypted bool
KeyGroups []sops.KeyGroup
GroupThreshold int
}
Expand Down Expand Up @@ -82,6 +83,7 @@ func encrypt(opts encryptOpts) (encryptedFile []byte, err error) {
EncryptedSuffix: opts.EncryptedSuffix,
UnencryptedRegex: opts.UnencryptedRegex,
EncryptedRegex: opts.EncryptedRegex,
MACOnlyEncrypted: opts.MACOnlyEncrypted,
Version: version.Version,
ShamirThreshold: opts.GroupThreshold,
},
Expand Down
10 changes: 10 additions & 0 deletions cmd/sops/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,10 @@ func main() {
Name: "ignore-mac",
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{
Name: "unencrypted-suffix",
Usage: "override the unencrypted key suffix.",
Expand Down Expand Up @@ -738,6 +742,7 @@ func main() {
encryptedSuffix := c.String("encrypted-suffix")
encryptedRegex := c.String("encrypted-regex")
unencryptedRegex := c.String("unencrypted-regex")
macOnlyEncrypted := c.Bool("mac-only-encrypted")
conf, err := loadConfig(c, fileName, nil)
if err != nil {
return toExitError(err)
Expand All @@ -756,6 +761,9 @@ func main() {
if unencryptedRegex == "" {
unencryptedRegex = conf.UnencryptedRegex
}
if !macOnlyEncrypted {
macOnlyEncrypted = conf.MACOnlyEncrypted
}
}

cryptRuleCount := 0
Expand Down Expand Up @@ -806,6 +814,7 @@ func main() {
EncryptedSuffix: encryptedSuffix,
UnencryptedRegex: unencryptedRegex,
EncryptedRegex: encryptedRegex,
MACOnlyEncrypted: macOnlyEncrypted,
KeyServices: svcs,
KeyGroups: groups,
GroupThreshold: threshold,
Expand Down Expand Up @@ -958,6 +967,7 @@ func main() {
EncryptedSuffix: encryptedSuffix,
UnencryptedRegex: unencryptedRegex,
EncryptedRegex: encryptedRegex,
MACOnlyEncrypted: macOnlyEncrypted,
KeyGroups: groups,
GroupThreshold: threshold,
})
Expand Down
3 changes: 3 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ type creationRule struct {
EncryptedSuffix string `yaml:"encrypted_suffix"`
UnencryptedRegex string `yaml:"unencrypted_regex"`
EncryptedRegex string `yaml:"encrypted_regex"`
MACOnlyEncrypted bool `yaml:"mac_only_encrypted"`
}

// Load loads a sops config file into a temporary struct
Expand All @@ -142,6 +143,7 @@ type Config struct {
EncryptedSuffix string
UnencryptedRegex string
EncryptedRegex string
MACOnlyEncrypted bool
Destination publish.Destination
OmitExtensions bool
}
Expand Down Expand Up @@ -265,6 +267,7 @@ func configFromRule(rule *creationRule, kmsEncryptionContext map[string]*string)
EncryptedSuffix: rule.EncryptedSuffix,
UnencryptedRegex: rule.UnencryptedRegex,
EncryptedRegex: rule.EncryptedRegex,
MACOnlyEncrypted: rule.MACOnlyEncrypted,
}, nil
}

Expand Down
14 changes: 14 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ creation_rules:
unencrypted_regex: "^dec:"
`)

var sampleConfigWithMACOnlyEncrypted = []byte(`
creation_rules:
- path_regex: barbar*
kms: "1"
pgp: "2"
mac_only_encrypted: true
`)

var sampleConfigWithInvalidParameters = []byte(`
creation_rules:
- path_regex: foobar*
Expand Down Expand Up @@ -414,6 +422,12 @@ func TestLoadConfigFileWithEncryptedRegex(t *testing.T) {
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) {
_, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithInvalidParameters, t), "/conf/path", "foobar", nil)
assert.NotNil(t, err)
Expand Down
57 changes: 41 additions & 16 deletions sops.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
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

func init() {
Expand Down Expand Up @@ -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,
// if EncryptedRegex is provided (by default it is not) or those matching
// 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) {
audit.SubmitEvent(audit.EncryptEvent{
File: tree.FilePath,
})
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 {
_, 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
if tree.Metadata.UnencryptedSuffix != "" {
for _, v := range path {
Expand Down Expand Up @@ -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 {
var err error
pathString := strings.Join(path, ":") + ":"
Expand Down Expand Up @@ -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 matching EncryptedRegex, if EncryptedRegex 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) {
log.Debug("Decrypting tree")
audit.SubmitEvent(audit.DecryptEvent{
File: tree.FilePath,
})
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 {
_, err := branch.walkBranch(branch, make([]string, 0), func(in interface{}, path []string) (interface{}, error) {
encrypted := true
Expand Down Expand Up @@ -441,13 +463,15 @@ func (tree Tree) Decrypt(key []byte, cipher Cipher) (string, error) {
} else {
v = in
}
// Only add to MAC if not a comment
if _, ok := v.(Comment); !ok {
bytes, err := ToBytes(v)
if err != nil {
return nil, fmt.Errorf("Could not convert %s to bytes: %s", in, err)
if !tree.Metadata.MACOnlyEncrypted || encrypted {
// Only add to MAC if not a comment
if _, ok := v.(Comment); !ok {
bytes, err := ToBytes(v)
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
})
Expand Down Expand Up @@ -490,6 +514,7 @@ type Metadata struct {
UnencryptedRegex string
EncryptedRegex string
MessageAuthenticationCode string
MACOnlyEncrypted bool
Version string
KeyGroups []KeyGroup
// ShamirThreshold is the number of key groups required to recover the
Expand Down
84 changes: 84 additions & 0 deletions sops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}

func (m MockCipher) Encrypt(value interface{}, key []byte, path string) (string, error) {
Expand Down
3 changes: 3 additions & 0 deletions stores/stores.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type Metadata struct {
EncryptedSuffix string `yaml:"encrypted_suffix,omitempty" json:"encrypted_suffix,omitempty"`
UnencryptedRegex string `yaml:"unencrypted_regex,omitempty" json:"unencrypted_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"`
}

Expand Down Expand Up @@ -114,6 +115,7 @@ func MetadataFromInternal(sopsMetadata sops.Metadata) Metadata {
m.UnencryptedRegex = sopsMetadata.UnencryptedRegex
m.EncryptedRegex = sopsMetadata.EncryptedRegex
m.MessageAuthenticationCode = sopsMetadata.MessageAuthenticationCode
m.MACOnlyEncrypted = sopsMetadata.MACOnlyEncrypted
m.Version = sopsMetadata.Version
m.ShamirThreshold = sopsMetadata.ShamirThreshold
if len(sopsMetadata.KeyGroups) == 1 {
Expand Down Expand Up @@ -270,6 +272,7 @@ func (m *Metadata) ToInternal() (sops.Metadata, error) {
EncryptedSuffix: m.EncryptedSuffix,
UnencryptedRegex: m.UnencryptedRegex,
EncryptedRegex: m.EncryptedRegex,
MACOnlyEncrypted: m.MACOnlyEncrypted,
LastModified: lastModified,
}, nil
}
Expand Down

0 comments on commit e198aa3

Please sign in to comment.