summaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
Diffstat (limited to 'pkg')
-rw-r--r--pkg/integrity/clearsign.go105
-rw-r--r--pkg/integrity/clearsign_test.go158
-rw-r--r--pkg/integrity/digest.go26
-rw-r--r--pkg/integrity/digest_test.go76
-rw-r--r--pkg/integrity/doc.go6
-rw-r--r--pkg/integrity/metadata_test.go26
-rw-r--r--pkg/integrity/select.go31
-rw-r--r--pkg/integrity/sign.go94
-rw-r--r--pkg/integrity/sign_test.go260
-rw-r--r--pkg/integrity/testdata/TestDigest_MarshalJSON/SHA1.golden1
-rw-r--r--pkg/integrity/testdata/TestDigest_MarshalJSON/SHA512_224.golden1
-rw-r--r--pkg/integrity/testdata/TestDigest_MarshalJSON/SHA512_256.golden1
-rw-r--r--pkg/integrity/testdata/TestGetHeaderMetadata/SHA1.golden1
-rw-r--r--pkg/integrity/testdata/TestGetHeaderMetadata/SHA512_224.golden1
-rw-r--r--pkg/integrity/testdata/TestGetHeaderMetadata/SHA512_256.golden1
-rw-r--r--pkg/integrity/testdata/TestGetImageMetadata/Object1.golden2
-rw-r--r--pkg/integrity/testdata/TestGetImageMetadata/Object2.golden2
-rw-r--r--pkg/integrity/testdata/TestGetImageMetadata/SHA1.golden1
-rw-r--r--pkg/integrity/testdata/TestGetImageMetadata/SHA224.golden2
-rw-r--r--pkg/integrity/testdata/TestGetImageMetadata/SHA256.golden2
-rw-r--r--pkg/integrity/testdata/TestGetImageMetadata/SHA384.golden2
-rw-r--r--pkg/integrity/testdata/TestGetImageMetadata/SHA512.golden2
-rw-r--r--pkg/integrity/testdata/TestGetObjectMetadata/RelativeID.golden2
-rw-r--r--pkg/integrity/testdata/TestGetObjectMetadata/SHA1.golden1
-rw-r--r--pkg/integrity/testdata/TestGetObjectMetadata/SHA512_224.golden1
-rw-r--r--pkg/integrity/testdata/TestGetObjectMetadata/SHA512_256.golden1
-rw-r--r--pkg/integrity/testdata/TestGroupSigner_Sign/Group1.golden (renamed from pkg/integrity/testdata/TestGroupSigner_SignWithEntity/Group1.golden)bin33692 -> 33230 bytes
-rw-r--r--pkg/integrity/testdata/TestGroupSigner_Sign/Group2.golden (renamed from pkg/integrity/testdata/TestGroupSigner_SignWithEntity/Group2.golden)bin33545 -> 33031 bytes
-rw-r--r--pkg/integrity/testdata/TestGroupSigner_Sign/Object1.golden (renamed from pkg/integrity/testdata/TestGroupSigner_SignWithEntity/Object1.golden)bin33545 -> 33031 bytes
-rw-r--r--pkg/integrity/testdata/TestGroupSigner_Sign/Object2.golden (renamed from pkg/integrity/testdata/TestGroupSigner_SignWithEntity/Object2.golden)bin33545 -> 33031 bytes
-rw-r--r--pkg/integrity/testdata/TestGroupSigner_SignWithEntity/SignatureConfigSHA256.goldenbin33692 -> 0 bytes
-rw-r--r--pkg/integrity/testdata/TestGroupSigner_SignWithEntity/SignatureConfigSHA384.goldenbin33692 -> 0 bytes
-rw-r--r--pkg/integrity/testdata/TestGroupSigner_SignWithEntity/SignatureConfigSHA512.goldenbin33692 -> 0 bytes
-rw-r--r--pkg/integrity/testdata/TestSignAndEncodeJSON/DefaultHash.golden15
-rw-r--r--pkg/integrity/testdata/TestSignAndEncodeJSON/SHA1.golden15
-rw-r--r--pkg/integrity/testdata/TestSignAndEncodeJSON/SHA224.golden15
-rw-r--r--pkg/integrity/testdata/TestSignAndEncodeJSON/SHA256.golden15
-rw-r--r--pkg/integrity/testdata/TestSignAndEncodeJSON/SHA384.golden15
-rw-r--r--pkg/integrity/testdata/TestSignAndEncodeJSON/SHA512.golden15
-rw-r--r--pkg/integrity/testdata/TestSigner_Sign/EncryptedKey.goldenbin36868 -> 40960 bytes
-rw-r--r--pkg/integrity/testdata/TestSigner_Sign/NoKeyMaterial.goldenbin36868 -> 0 bytes
-rw-r--r--pkg/integrity/testdata/TestSigner_Sign/OneGroup.goldenbin42014 -> 42014 bytes
-rw-r--r--pkg/integrity/testdata/TestSigner_Sign/OptSignDeterministic.goldenbin42014 -> 42014 bytes
-rw-r--r--pkg/integrity/testdata/TestSigner_Sign/OptSignGroup1.goldenbin46110 -> 304158 bytes
-rw-r--r--pkg/integrity/testdata/TestSigner_Sign/OptSignGroup2.goldenbin45911 -> 303959 bytes
-rw-r--r--pkg/integrity/testdata/TestSigner_Sign/OptSignObject1.goldenbin45911 -> 303959 bytes
-rw-r--r--pkg/integrity/testdata/TestSigner_Sign/OptSignObject2.goldenbin45911 -> 303959 bytes
-rw-r--r--pkg/integrity/testdata/TestSigner_Sign/OptSignObject3.goldenbin45911 -> 303959 bytes
-rw-r--r--pkg/integrity/testdata/TestSigner_Sign/OptSignObjects.goldenbin50007 -> 305013 bytes
-rw-r--r--pkg/integrity/testdata/TestSigner_Sign/TwoGroups.goldenbin50007 -> 305013 bytes
-rw-r--r--pkg/integrity/testdata/Test_clearsignEncoder_signMessage/OK.golden15
-rw-r--r--pkg/integrity/verify.go356
-rw-r--r--pkg/integrity/verify_test.go854
-rw-r--r--pkg/sif/arch.go5
-rw-r--r--pkg/sif/buffer.go14
-rw-r--r--pkg/sif/create.go4
-rw-r--r--pkg/sif/descriptor.go36
-rw-r--r--pkg/sif/descriptor_input.go35
-rw-r--r--pkg/sif/descriptor_input_test.go17
-rw-r--r--pkg/sif/descriptor_test.go111
-rw-r--r--pkg/sif/sif.go168
-rw-r--r--pkg/sif/testdata/TestAddObject/Empty.goldenbin32770 -> 32178 bytes
-rw-r--r--pkg/sif/testdata/TestAddObject/NotEmpty.goldenbin36866 -> 32770 bytes
-rw-r--r--pkg/sif/testdata/TestAddObject/NotEmptyAligned.goldenbin32898 -> 32258 bytes
-rw-r--r--pkg/sif/testdata/TestAddObject/NotEmptyNotAligned.goldenbin32772 -> 32180 bytes
-rw-r--r--pkg/sif/testdata/TestAddObject/WithTime.goldenbin32770 -> 32178 bytes
-rw-r--r--pkg/sif/testdata/TestCreateContainer/OneDescriptor.goldenbin32770 -> 32178 bytes
-rw-r--r--pkg/sif/testdata/TestCreateContainer/TwoDescriptors.goldenbin36866 -> 32770 bytes
-rw-r--r--pkg/sif/testdata/TestCreateContainerAtPath/OneDescriptor.goldenbin32770 -> 32178 bytes
-rw-r--r--pkg/sif/testdata/TestCreateContainerAtPath/TwoDescriptors.goldenbin36866 -> 32770 bytes
-rw-r--r--pkg/sif/testdata/TestDeleteObject/WithTime.goldenbin32770 -> 32178 bytes
-rw-r--r--pkg/sif/testdata/TestDeleteObject/Zero.goldenbin32770 -> 32178 bytes
-rw-r--r--pkg/sif/testdata/TestNewDescriptorInput/OptSBOMMetadata.goldenbin0 -> 585 bytes
-rw-r--r--pkg/siftool/add.go65
-rw-r--r--pkg/siftool/add_test.go4
-rw-r--r--pkg/siftool/del_test.go4
-rw-r--r--pkg/siftool/dump_test.go4
-rw-r--r--pkg/siftool/header_test.go4
-rw-r--r--pkg/siftool/info_test.go4
-rw-r--r--pkg/siftool/list_test.go4
-rw-r--r--pkg/siftool/mount.go27
-rw-r--r--pkg/siftool/mount_test.go61
-rw-r--r--pkg/siftool/new_test.go4
-rw-r--r--pkg/siftool/setprim_test.go4
-rw-r--r--pkg/siftool/siftool.go19
-rw-r--r--pkg/siftool/siftool_test.go21
-rw-r--r--pkg/siftool/testdata/TestAddCommands/Add/out.golden14
-rw-r--r--pkg/siftool/testdata/TestAddCommands/Mount/err.golden0
-rw-r--r--pkg/siftool/testdata/TestAddCommands/Mount/out.golden10
-rw-r--r--pkg/siftool/testdata/TestAddCommands/SifToolExperimental/err.golden0
-rw-r--r--pkg/siftool/testdata/TestAddCommands/SifToolExperimental/out.golden19
-rw-r--r--pkg/siftool/testdata/Test_command_getDump/Three/out.golden18
-rw-r--r--pkg/siftool/testdata/Test_command_getDump/Two/out.goldenbin4 -> 4096 bytes
-rw-r--r--pkg/siftool/testdata/Test_command_getHeader/OneGroup/out.golden2
-rw-r--r--pkg/siftool/testdata/Test_command_getHeader/TwoGroups/out.golden2
-rw-r--r--pkg/siftool/testdata/Test_command_getHeader/TwoGroupsSigned/out.golden2
-rw-r--r--pkg/siftool/testdata/Test_command_getInfo/Two/out.golden2
-rw-r--r--pkg/siftool/testdata/Test_command_getList/OneGroup/out.golden2
-rw-r--r--pkg/siftool/testdata/Test_command_getList/OneGroupSigned/out.golden2
-rw-r--r--pkg/siftool/testdata/Test_command_getList/TwoGroups/out.golden4
-rw-r--r--pkg/siftool/testdata/Test_command_getList/TwoGroupsSigned/out.golden8
-rw-r--r--pkg/siftool/testdata/Test_command_getMount/Empty/err.golden1
-rw-r--r--pkg/siftool/testdata/Test_command_getMount/Empty/out.golden9
-rw-r--r--pkg/siftool/testdata/Test_command_getMount/OneGroup/err.golden0
-rw-r--r--pkg/siftool/testdata/Test_command_getMount/OneGroup/out.golden0
-rw-r--r--pkg/siftool/testdata/Test_command_getUnmount/err.golden0
-rw-r--r--pkg/siftool/testdata/Test_command_getUnmount/out.golden0
-rw-r--r--pkg/siftool/unmount.go27
-rw-r--r--pkg/siftool/unmount_test.go42
-rw-r--r--pkg/user/mount.go122
-rw-r--r--pkg/user/unmount.go93
-rw-r--r--pkg/user/unmount_test.go139
112 files changed, 2028 insertions, 1226 deletions
diff --git a/pkg/integrity/clearsign.go b/pkg/integrity/clearsign.go
index e2985cb..2a99d98 100644
--- a/pkg/integrity/clearsign.go
+++ b/pkg/integrity/clearsign.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2020, Sylabs Inc. All rights reserved.
+// Copyright (c) 2020-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
// distributed with the sources of this project regarding your rights to use or distribute this
// software.
@@ -7,9 +7,10 @@ package integrity
import (
"bytes"
- "encoding/json"
+ "crypto"
"errors"
"io"
+ "time"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
@@ -18,61 +19,81 @@ import (
var errClearsignedMsgNotFound = errors.New("clearsigned message not found")
-// signAndEncodeJSON encodes v, clear-signs it with privateKey, and writes it to w. If config is
-// nil, sensible defaults are used.
-func signAndEncodeJSON(w io.Writer, v interface{}, privateKey *packet.PrivateKey, config *packet.Config) error {
- // Get clearsign encoder.
- plaintext, err := clearsign.Encode(w, privateKey, config)
+type clearsignEncoder struct {
+ e *openpgp.Entity
+ config *packet.Config
+}
+
+// newClearsignEncoder returns an encoder that signs messages in clear-sign format using entity e.
+// If timeFunc is not nil, it is used to generate signature timestamps.
+func newClearsignEncoder(e *openpgp.Entity, timeFunc func() time.Time) *clearsignEncoder {
+ return &clearsignEncoder{
+ e: e,
+ config: &packet.Config{
+ Time: timeFunc,
+ },
+ }
+}
+
+// signMessage signs the message from r in clear-sign format, and writes the result to w. On
+// success, the hash function is returned.
+func (en *clearsignEncoder) signMessage(w io.Writer, r io.Reader) (crypto.Hash, error) {
+ plaintext, err := clearsign.Encode(w, en.e.PrivateKey, en.config)
if err != nil {
- return err
+ return 0, err
}
defer plaintext.Close()
- // Wrap clearsign encoder with JSON encoder.
- return json.NewEncoder(plaintext).Encode(v)
+ _, err = io.Copy(plaintext, r)
+ return en.config.Hash(), err
}
-// verifyAndDecodeJSON reads the first clearsigned message in data, verifies its signature, and
-// returns the signing entity any suffix of data which follows the message. The plaintext is
-// unmarshalled to v (if not nil).
-func verifyAndDecodeJSON(data []byte, v interface{}, kr openpgp.KeyRing) (*openpgp.Entity, []byte, error) {
- // Decode clearsign block and check signature.
- e, plaintext, rest, err := verifyAndDecode(data, kr)
- if err != nil {
- return e, rest, err
- }
+type clearsignDecoder struct {
+ kr openpgp.KeyRing
+}
- // Unmarshal plaintext, if requested.
- if v != nil {
- err = json.Unmarshal(plaintext, v)
+// newClearsignDecoder returns a decoder that verifies messages in clear-sign format using key
+// material from kr.
+func newClearsignDecoder(kr openpgp.KeyRing) *clearsignDecoder {
+ return &clearsignDecoder{
+ kr: kr,
}
- return e, rest, err
}
-// verifyAndDecode reads the first clearsigned message in data, verifies its signature, and returns
-// the signing entity, plaintext and suffix of data which follows the message.
-func verifyAndDecode(data []byte, kr openpgp.KeyRing) (*openpgp.Entity, []byte, []byte, error) {
- // Decode clearsign block.
- b, rest := clearsign.Decode(data)
- if b == nil {
- return nil, nil, rest, errClearsignedMsgNotFound
+// verifyMessage reads a message from r, verifies its signature, and returns the message contents.
+// On success, the signing entity is set in vr.
+func (de *clearsignDecoder) verifyMessage(r io.Reader, h crypto.Hash, vr *VerifyResult) ([]byte, error) {
+ data, err := io.ReadAll(r)
+ if err != nil {
+ return nil, err
}
- // Check signature.
- e, err := openpgp.CheckDetachedSignature(kr, bytes.NewReader(b.Bytes), b.ArmoredSignature.Body, nil)
- return e, b.Plaintext, rest, err
-}
-
-// isLegacySignature reads the first clearsigned message in data, and returns true if the plaintext
-// contains a legacy signature.
-func isLegacySignature(data []byte) (bool, error) {
// Decode clearsign block.
b, _ := clearsign.Decode(data)
if b == nil {
- return false, errClearsignedMsgNotFound
+ return nil, errClearsignedMsgNotFound
+ }
+
+ // Hash functions specified for OpenPGP in RFC4880, excluding those that are not currently
+ // recommended by NIST.
+ expectedHashes := []crypto.Hash{
+ crypto.SHA224,
+ crypto.SHA256,
+ crypto.SHA384,
+ crypto.SHA512,
+ }
+
+ // Check signature.
+ vr.e, err = openpgp.CheckDetachedSignatureAndHash(
+ de.kr,
+ bytes.NewReader(b.Bytes),
+ b.ArmoredSignature.Body,
+ expectedHashes,
+ nil,
+ )
+ if err != nil {
+ return nil, err
}
- // The plaintext of legacy signatures always begins with "SIFHASH", and non-legacy signatures
- // never do, as they are JSON.
- return bytes.HasPrefix(b.Plaintext, []byte("SIFHASH:\n")), nil
+ return b.Plaintext, err
}
diff --git a/pkg/integrity/clearsign_test.go b/pkg/integrity/clearsign_test.go
index 6620884..1131424 100644
--- a/pkg/integrity/clearsign_test.go
+++ b/pkg/integrity/clearsign_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2020-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
// distributed with the sources of this project regarding your rights to use or distribute this
// software.
@@ -21,31 +21,31 @@ import (
"github.com/sebdah/goldie/v2"
)
-type testType struct {
- One int
- Two int
-}
+var testMessage = `{"One":1,"Two":2}
+`
-func TestSignAndEncodeJSON(t *testing.T) {
+func Test_clearsignEncoder_signMessage(t *testing.T) {
e := getTestEntity(t)
- // Fake an encrypted key.
- encryptedKey := *e.PrivateKey
- encryptedKey.Encrypted = true
+ encrypted := getTestEntity(t)
+ encrypted.PrivateKey.Encrypted = true
tests := []struct {
- name string
- key *packet.PrivateKey
- hash crypto.Hash
- wantErr bool
+ name string
+ en *clearsignEncoder
+ wantErr bool
+ wantHash crypto.Hash
}{
- {name: "EncryptedKey", key: &encryptedKey, wantErr: true},
- {name: "DefaultHash", key: e.PrivateKey},
- {name: "SHA1", key: e.PrivateKey, hash: crypto.SHA1},
- {name: "SHA224", key: e.PrivateKey, hash: crypto.SHA224},
- {name: "SHA256", key: e.PrivateKey, hash: crypto.SHA256},
- {name: "SHA384", key: e.PrivateKey, hash: crypto.SHA384},
- {name: "SHA512", key: e.PrivateKey, hash: crypto.SHA512},
+ {
+ name: "EncryptedKey",
+ en: newClearsignEncoder(encrypted, fixedTime),
+ wantErr: true,
+ },
+ {
+ name: "OK",
+ en: newClearsignEncoder(e, fixedTime),
+ wantHash: crypto.SHA256,
+ },
}
for _, tt := range tests {
@@ -53,17 +53,16 @@ func TestSignAndEncodeJSON(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
b := bytes.Buffer{}
- config := packet.Config{
- DefaultHash: tt.hash,
- Time: fixedTime,
- }
-
- err := signAndEncodeJSON(&b, testType{1, 2}, tt.key, &config)
+ ht, err := tt.en.signMessage(&b, strings.NewReader(testMessage))
if got, want := err, tt.wantErr; (got != nil) != want {
t.Fatalf("got error %v, wantErr %v", got, want)
}
if err == nil {
+ if got, want := ht, tt.wantHash; got != want {
+ t.Errorf("got hash %v, want %v", got, want)
+ }
+
g := goldie.New(t, goldie.WithTestNameForDir(true))
g.Assert(t, tt.name, b.Bytes())
}
@@ -71,11 +70,9 @@ func TestSignAndEncodeJSON(t *testing.T) {
}
}
-func TestVerifyAndDecodeJSON(t *testing.T) {
+func Test_clearsignDecoder_verifyMessage(t *testing.T) {
e := getTestEntity(t)
- testValue := testType{1, 2}
-
// This is used to corrupt the plaintext.
corruptClearsign := func(w io.Writer, s string) error {
_, err := strings.NewReplacer(`{"One":1,"Two":2}`, `{"One":2,"Two":4}`).WriteString(w, s)
@@ -110,22 +107,63 @@ func TestVerifyAndDecodeJSON(t *testing.T) {
tests := []struct {
name string
hash crypto.Hash
- el openpgp.EntityList
corrupter func(w io.Writer, s string) error
- output interface{}
+ de *clearsignDecoder
wantErr error
wantEntity *openpgp.Entity
}{
- {name: "ErrUnknownIssuer", el: openpgp.EntityList{}, wantErr: pgperrors.ErrUnknownIssuer},
- {name: "CorruptedClearsign", el: openpgp.EntityList{e}, corrupter: corruptClearsign},
- {name: "CorruptedSignature", el: openpgp.EntityList{e}, corrupter: corruptSignature},
- {name: "VerifyOnly", el: openpgp.EntityList{e}, wantEntity: e},
- {name: "DefaultHash", el: openpgp.EntityList{e}, output: &testType{}, wantEntity: e},
- {name: "SHA1", hash: crypto.SHA1, el: openpgp.EntityList{e}, output: &testType{}, wantEntity: e},
- {name: "SHA224", hash: crypto.SHA224, el: openpgp.EntityList{e}, output: &testType{}, wantEntity: e},
- {name: "SHA256", hash: crypto.SHA256, el: openpgp.EntityList{e}, output: &testType{}, wantEntity: e},
- {name: "SHA384", hash: crypto.SHA384, el: openpgp.EntityList{e}, output: &testType{}, wantEntity: e},
- {name: "SHA512", hash: crypto.SHA512, el: openpgp.EntityList{e}, output: &testType{}, wantEntity: e},
+ {
+ name: "UnknownIssuer",
+ de: newClearsignDecoder(openpgp.EntityList{}),
+ wantErr: pgperrors.ErrUnknownIssuer,
+ },
+ {
+ name: "CorruptedClearsign",
+ corrupter: corruptClearsign,
+ de: newClearsignDecoder(openpgp.EntityList{e}),
+ wantErr: pgperrors.SignatureError("hash tag doesn't match"),
+ },
+ {
+ name: "CorruptedSignature",
+ corrupter: corruptSignature,
+ de: newClearsignDecoder(openpgp.EntityList{e}),
+ wantErr: pgperrors.StructuralError("signature subpacket truncated"),
+ },
+ {
+ name: "DefaultHash",
+ de: newClearsignDecoder(openpgp.EntityList{e}),
+ wantEntity: e,
+ },
+ {
+ name: "SHA1",
+ hash: crypto.SHA1,
+ de: newClearsignDecoder(openpgp.EntityList{e}),
+ wantErr: pgperrors.StructuralError("hash algorithm mismatch with cleartext message headers"),
+ },
+ {
+ name: "SHA224",
+ hash: crypto.SHA224,
+ de: newClearsignDecoder(openpgp.EntityList{e}),
+ wantEntity: e,
+ },
+ {
+ name: "SHA256",
+ hash: crypto.SHA256,
+ de: newClearsignDecoder(openpgp.EntityList{e}),
+ wantEntity: e,
+ },
+ {
+ name: "SHA384",
+ hash: crypto.SHA384,
+ de: newClearsignDecoder(openpgp.EntityList{e}),
+ wantEntity: e,
+ },
+ {
+ name: "SHA512",
+ hash: crypto.SHA512,
+ de: newClearsignDecoder(openpgp.EntityList{e}),
+ wantEntity: e,
+ },
}
for _, tt := range tests {
@@ -133,10 +171,15 @@ func TestVerifyAndDecodeJSON(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
b := bytes.Buffer{}
- config := packet.Config{
- DefaultHash: tt.hash,
+ // Sign and encode message.
+ en := clearsignEncoder{
+ e: e,
+ config: &packet.Config{
+ DefaultHash: tt.hash,
+ Time: fixedTime,
+ },
}
- err := signAndEncodeJSON(&b, testValue, e.PrivateKey, &config)
+ h, err := en.signMessage(&b, strings.NewReader(testMessage))
if err != nil {
t.Fatal(err)
}
@@ -150,31 +193,20 @@ func TestVerifyAndDecodeJSON(t *testing.T) {
}
}
- // Verify and decode.
- e, rest, err := verifyAndDecodeJSON(b.Bytes(), tt.output, tt.el)
-
- // Shouldn't be any trailing bytes.
- if n := len(rest); n != 0 {
- t.Errorf("%v trailing bytes", n)
- }
+ // Decode and verify message.
+ var vr VerifyResult
+ message, err := tt.de.verifyMessage(bytes.NewReader(b.Bytes()), h, &vr)
- // Verify the error (if any) is appropriate.
- if tt.corrupter == nil {
- if got, want := err, tt.wantErr; !errors.Is(got, want) {
- t.Fatalf("got error %v, want %v", got, want)
- }
- } else if err == nil {
- t.Errorf("got nil error despite corruption")
+ if got, want := err, tt.wantErr; !errors.Is(got, want) {
+ t.Fatalf("got error %v, want %v", got, want)
}
if err == nil {
- if tt.output != nil {
- if got, want := tt.output, &testValue; !reflect.DeepEqual(got, want) {
- t.Errorf("got value %v, want %v", got, want)
- }
+ if got, want := string(message), testMessage; got != want {
+ t.Errorf("got message %v, want %v", got, want)
}
- if got, want := e, tt.wantEntity; !reflect.DeepEqual(got, want) {
+ if got, want := vr.e, tt.wantEntity; !reflect.DeepEqual(got, want) {
t.Errorf("got entity %+v, want %+v", got, want)
}
}
diff --git a/pkg/integrity/digest.go b/pkg/integrity/digest.go
index 5b1ff2c..0783ed6 100644
--- a/pkg/integrity/digest.go
+++ b/pkg/integrity/digest.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2020-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
// distributed with the sources of this project regarding your rights to use or distribute this
// software.
@@ -22,12 +22,14 @@ var (
errDigestMalformed = errors.New("digest malformed")
)
-var supportedAlgorithms = map[crypto.Hash]string{
- crypto.SHA1: "sha1",
- crypto.SHA224: "sha224",
- crypto.SHA256: "sha256",
- crypto.SHA384: "sha384",
- crypto.SHA512: "sha512",
+// Hash functions supported for digests.
+var supportedDigestAlgorithms = map[crypto.Hash]string{
+ crypto.SHA224: "sha224",
+ crypto.SHA256: "sha256",
+ crypto.SHA384: "sha384",
+ crypto.SHA512: "sha512",
+ crypto.SHA512_224: "sha512_224",
+ crypto.SHA512_256: "sha512_256",
}
// hashValue calculates a digest by applying hash function h to the contents read from r. If h is
@@ -52,7 +54,7 @@ type digest struct {
// newDigest returns a new digest. If h is not supported, errHashUnsupported is returned. If digest
// is malformed, errDigestMalformed is returned.
func newDigest(h crypto.Hash, value []byte) (digest, error) {
- if _, ok := supportedAlgorithms[h]; !ok {
+ if _, ok := supportedDigestAlgorithms[h]; !ok {
return digest{}, errHashUnsupported
}
@@ -78,8 +80,8 @@ func newDigestReader(h crypto.Hash, r io.Reader) (digest, error) {
// For reference, the plaintext of legacy signatures is comprised of the string "SIFHASH:\n",
// followed by a digest value. For example:
//
-// SIFHASH:
-// 2f0b3dca0ec42683d306338f68689aba29cdb83625b8cc0b8a789f8de92342495a6264b0c134e706630636bf90c6f331
+// SIFHASH:
+// 2f0b3dca0ec42683d306338f68689aba29cdb83625b8cc0b8a789f8de92342495a6264b0c134e706630636bf90c6f331
func newLegacyDigest(ht crypto.Hash, b []byte) (digest, error) {
b = bytes.TrimPrefix(b, []byte("SIFHASH:\n"))
b = bytes.TrimSuffix(b, []byte("\n"))
@@ -104,7 +106,7 @@ func (d digest) matches(r io.Reader) (bool, error) {
// MarshalJSON marshals d into string of format "alg:value".
func (d digest) MarshalJSON() ([]byte, error) {
- n, ok := supportedAlgorithms[d.hash]
+ n, ok := supportedDigestAlgorithms[d.hash]
if !ok {
return nil, errHashUnsupported
}
@@ -130,7 +132,7 @@ func (d *digest) UnmarshalJSON(data []byte) error {
return fmt.Errorf("%w: %v", errDigestMalformed, err)
}
- for h, n := range supportedAlgorithms {
+ for h, n := range supportedDigestAlgorithms {
if n == name {
digest, err := newDigest(h, v)
if err != nil {
diff --git a/pkg/integrity/digest_test.go b/pkg/integrity/digest_test.go
index f6d2f45..128e10c 100644
--- a/pkg/integrity/digest_test.go
+++ b/pkg/integrity/digest_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2020-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
// distributed with the sources of this project regarding your rights to use or distribute this
// software.
@@ -74,7 +74,7 @@ func TestNewLegacyDigest(t *testing.T) {
{
name: "SHA512",
ht: crypto.SHA512,
- text: "SIFHASH:\nee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff\n", // nolint:lll
+ text: "SIFHASH:\nee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff\n", //nolint:lll
wantDigest: digest{
hash: crypto.SHA512,
value: []byte{
@@ -114,14 +114,14 @@ func TestDigest_MarshalJSON(t *testing.T) {
wantErr error
}{
{
- name: "UnsupportedHash",
+ name: "HashUnsupportedMD5",
hash: crypto.MD5,
wantErr: errHashUnsupported,
},
{
- name: "SHA1",
- hash: crypto.SHA1,
- value: "597f6a540010f94c15d71806a99a2c8710e747bd",
+ name: "HashUnsupportedSHA1",
+ hash: crypto.SHA1,
+ wantErr: errHashUnsupported,
},
{
name: "SHA224",
@@ -141,7 +141,17 @@ func TestDigest_MarshalJSON(t *testing.T) {
{
name: "SHA512",
hash: crypto.SHA512,
- value: "db3974a97f2407b7cae1ae637c0030687a11913274d578492558e39c16c017de84eacdc8c62fe34ee4e12b4b1428817f09b6a2760c3f8a664ceae94d2434a593", // nolint:lll
+ value: "db3974a97f2407b7cae1ae637c0030687a11913274d578492558e39c16c017de84eacdc8c62fe34ee4e12b4b1428817f09b6a2760c3f8a664ceae94d2434a593", //nolint:lll
+ },
+ {
+ name: "SHA512_224",
+ hash: crypto.SHA512_224,
+ value: "06001bf08dfb17d2b54925116823be230e98b5c6c278303bc4909a8c",
+ },
+ {
+ name: "SHA512_256",
+ hash: crypto.SHA512_256,
+ value: "3d37fe58435e0d87323dee4a2c1b339ef954de63716ee79f5747f94d974f913f",
},
}
@@ -193,49 +203,60 @@ func TestDigest_UnmarshalJSON(t *testing.T) {
wantErr: errDigestMalformed,
},
{
- name: "UnsupportedHash",
+ name: "HashUnsupportedMD5",
r: strings.NewReader(`"md5:b0804ec967f48520697662a204f5fe72"`),
wantErr: errHashUnsupported,
},
{
+ name: "HashUnsupportedSHA1",
+ r: strings.NewReader(`"sha1:597f6a540010f94c15d71806a99a2c8710e747bd"`),
+ wantErr: errHashUnsupported,
+ },
+ {
name: "DigestMalformedNotHex",
- r: strings.NewReader(`"sha1:oops"`),
+ r: strings.NewReader(`"sha256:oops"`),
wantErr: errDigestMalformed,
},
{
name: "DigestMalformedIncorrectLen",
- r: strings.NewReader(`"sha1:597f"`),
+ r: strings.NewReader(`"sha256:597f"`),
wantErr: errDigestMalformed,
},
{
- name: "SHA1",
- r: strings.NewReader(`"sha1:597f6a540010f94c15d71806a99a2c8710e747bd"`),
- wantHash: crypto.SHA1,
- wantValue: "597f6a540010f94c15d71806a99a2c8710e747bd",
- },
- {
name: "SHA224",
r: strings.NewReader(`"sha224:95041dd60ab08c0bf5636d50be85fe9790300f39eb84602858a9b430"`),
- wantHash: crypto.SHA1,
+ wantHash: crypto.SHA224,
wantValue: "95041dd60ab08c0bf5636d50be85fe9790300f39eb84602858a9b430",
},
{
name: "SHA256",
r: strings.NewReader(`"sha256:a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447"`),
- wantHash: crypto.SHA1,
+ wantHash: crypto.SHA256,
wantValue: "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447",
},
{
name: "SHA384",
- r: strings.NewReader(`"sha384:6b3b69ff0a404f28d75e98a066d3fc64fffd9940870cc68bece28545b9a75086b343d7a1366838083e4b8f3ca6fd3c80"`), // nolint:lll
- wantHash: crypto.SHA1,
+ r: strings.NewReader(`"sha384:6b3b69ff0a404f28d75e98a066d3fc64fffd9940870cc68bece28545b9a75086b343d7a1366838083e4b8f3ca6fd3c80"`), //nolint:lll
+ wantHash: crypto.SHA384,
wantValue: "6b3b69ff0a404f28d75e98a066d3fc64fffd9940870cc68bece28545b9a75086b343d7a1366838083e4b8f3ca6fd3c80",
},
{
name: "SHA512",
- r: strings.NewReader(`"sha512:db3974a97f2407b7cae1ae637c0030687a11913274d578492558e39c16c017de84eacdc8c62fe34ee4e12b4b1428817f09b6a2760c3f8a664ceae94d2434a593"`), // nolint:lll
- wantHash: crypto.SHA1,
- wantValue: "db3974a97f2407b7cae1ae637c0030687a11913274d578492558e39c16c017de84eacdc8c62fe34ee4e12b4b1428817f09b6a2760c3f8a664ceae94d2434a593", // nolint:lll
+ r: strings.NewReader(`"sha512:db3974a97f2407b7cae1ae637c0030687a11913274d578492558e39c16c017de84eacdc8c62fe34ee4e12b4b1428817f09b6a2760c3f8a664ceae94d2434a593"`), //nolint:lll
+ wantHash: crypto.SHA512,
+ wantValue: "db3974a97f2407b7cae1ae637c0030687a11913274d578492558e39c16c017de84eacdc8c62fe34ee4e12b4b1428817f09b6a2760c3f8a664ceae94d2434a593", //nolint:lll
+ },
+ {
+ name: "SHA512_224",
+ r: strings.NewReader(`"sha512_224:06001bf08dfb17d2b54925116823be230e98b5c6c278303bc4909a8c"`),
+ wantHash: crypto.SHA512_224,
+ wantValue: "06001bf08dfb17d2b54925116823be230e98b5c6c278303bc4909a8c",
+ },
+ {
+ name: "SHA512_256",
+ r: strings.NewReader(`"sha512_256:3d37fe58435e0d87323dee4a2c1b339ef954de63716ee79f5747f94d974f913f"`),
+ wantHash: crypto.SHA512_256,
+ wantValue: "3d37fe58435e0d87323dee4a2c1b339ef954de63716ee79f5747f94d974f913f",
},
}
@@ -243,10 +264,19 @@ func TestDigest_UnmarshalJSON(t *testing.T) {
tt := tt
t.Run(tt.name, func(t *testing.T) {
var d digest
+
err := json.NewDecoder(tt.r).Decode(&d)
if got, want := err, tt.wantErr; !errors.Is(got, want) {
t.Fatalf("got error %v, want %v", got, want)
}
+
+ if got, want := d.hash, tt.wantHash; got != want {
+ t.Errorf("got hash %v, want %v", got, want)
+ }
+
+ if got, want := hex.EncodeToString(d.value), tt.wantValue; got != want {
+ t.Errorf("got value %v, want %v", got, want)
+ }
})
}
}
diff --git a/pkg/integrity/doc.go b/pkg/integrity/doc.go
index 19739fd..37850a5 100644
--- a/pkg/integrity/doc.go
+++ b/pkg/integrity/doc.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2020, Sylabs Inc. All rights reserved.
+// Copyright (c) 2020-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
// distributed with the sources of this project regarding your rights to use or distribute this
// software.
@@ -7,7 +7,7 @@
Package integrity implements functions to add, examine, and verify digital signatures in a SIF
image.
-Sign
+# Sign
To add one or more digital signatures to a SIF, create a Signer, and supply a signing PGP entity:
@@ -23,7 +23,7 @@ Finally, to apply the signature(s):
err := s.Sign()
-Verify
+# Verify
To examine and/or verify digital signatures in a SIF, create a Verifier:
diff --git a/pkg/integrity/metadata_test.go b/pkg/integrity/metadata_test.go
index bc85985..b78ec2f 100644
--- a/pkg/integrity/metadata_test.go
+++ b/pkg/integrity/metadata_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2020-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
// distributed with the sources of this project regarding your rights to use or distribute this
// software.
@@ -35,12 +35,14 @@ func TestGetHeaderMetadata(t *testing.T) {
wantErr error
}{
{name: "HashUnavailable", header: bytes.NewReader(b), hash: crypto.MD4, wantErr: errHashUnavailable},
- {name: "HashUnsupported", header: bytes.NewReader(b), hash: crypto.MD5, wantErr: errHashUnsupported},
- {name: "SHA1", header: bytes.NewReader(b), hash: crypto.SHA1},
+ {name: "HashUnsupportedMD5", header: bytes.NewReader(b), hash: crypto.MD5, wantErr: errHashUnsupported},
+ {name: "HashUnsupportedSHA1", header: bytes.NewReader(b), hash: crypto.SHA1, wantErr: errHashUnsupported},
{name: "SHA224", header: bytes.NewReader(b), hash: crypto.SHA224},
{name: "SHA256", header: bytes.NewReader(b), hash: crypto.SHA256},
{name: "SHA384", header: bytes.NewReader(b), hash: crypto.SHA384},
{name: "SHA512", header: bytes.NewReader(b), hash: crypto.SHA512},
+ {name: "SHA512_224", header: bytes.NewReader(b), hash: crypto.SHA512_224},
+ {name: "SHA512_256", header: bytes.NewReader(b), hash: crypto.SHA512_256},
}
for _, tt := range tests {
@@ -88,13 +90,15 @@ func TestGetObjectMetadata(t *testing.T) {
wantErr error
}{
{name: "HashUnavailable", descr: bytes.NewReader(rid0), hash: crypto.MD4, wantErr: errHashUnavailable},
- {name: "HashUnsupported", descr: bytes.NewReader(rid0), hash: crypto.MD5, wantErr: errHashUnsupported},
- {name: "RelativeID", relativeID: 1, descr: bytes.NewReader(rid1), data: strings.NewReader("blah"), hash: crypto.SHA1},
- {name: "SHA1", descr: bytes.NewReader(rid0), data: strings.NewReader("blah"), hash: crypto.SHA1},
+ {name: "HashUnsupportedMD5", descr: bytes.NewReader(rid0), hash: crypto.MD5, wantErr: errHashUnsupported},
+ {name: "HashUnsupportedSHA1", descr: bytes.NewReader(rid0), hash: crypto.SHA1, wantErr: errHashUnsupported},
+ {name: "RelativeID", relativeID: 1, descr: bytes.NewReader(rid1), data: strings.NewReader("blah"), hash: crypto.SHA256}, //nolint:lll
{name: "SHA224", descr: bytes.NewReader(rid0), data: strings.NewReader("blah"), hash: crypto.SHA224},
{name: "SHA256", descr: bytes.NewReader(rid0), data: strings.NewReader("blah"), hash: crypto.SHA256},
{name: "SHA384", descr: bytes.NewReader(rid0), data: strings.NewReader("blah"), hash: crypto.SHA384},
{name: "SHA512", descr: bytes.NewReader(rid0), data: strings.NewReader("blah"), hash: crypto.SHA512},
+ {name: "SHA512_224", descr: bytes.NewReader(rid0), data: strings.NewReader("blah"), hash: crypto.SHA512_224},
+ {name: "SHA512_256", descr: bytes.NewReader(rid0), data: strings.NewReader("blah"), hash: crypto.SHA512_256},
}
for _, tt := range tests {
@@ -139,11 +143,11 @@ func TestGetImageMetadata(t *testing.T) {
wantErr error
}{
{name: "HashUnavailable", hash: crypto.MD4, wantErr: errHashUnavailable},
- {name: "HashUnsupported", hash: crypto.MD5, wantErr: errHashUnsupported},
- {name: "MinimumIDInvalid", minID: 2, ods: []sif.Descriptor{od1}, hash: crypto.SHA1, wantErr: errMinimumIDInvalid},
- {name: "Object1", minID: 1, ods: []sif.Descriptor{od1}, hash: crypto.SHA1},
- {name: "Object2", minID: 1, ods: []sif.Descriptor{od2}, hash: crypto.SHA1},
- {name: "SHA1", minID: 1, ods: []sif.Descriptor{od1, od2}, hash: crypto.SHA1},
+ {name: "HashUnsupportedMD5", hash: crypto.MD5, wantErr: errHashUnsupported},
+ {name: "HashUnsupportedSHA1", hash: crypto.SHA1, wantErr: errHashUnsupported},
+ {name: "MinimumIDInvalid", minID: 2, ods: []sif.Descriptor{od1}, hash: crypto.SHA256, wantErr: errMinimumIDInvalid},
+ {name: "Object1", minID: 1, ods: []sif.Descriptor{od1}, hash: crypto.SHA256},
+ {name: "Object2", minID: 1, ods: []sif.Descriptor{od2}, hash: crypto.SHA256},
{name: "SHA224", minID: 1, ods: []sif.Descriptor{od1, od2}, hash: crypto.SHA224},
{name: "SHA256", minID: 1, ods: []sif.Descriptor{od1, od2}, hash: crypto.SHA256},
{name: "SHA384", minID: 1, ods: []sif.Descriptor{od1, od2}, hash: crypto.SHA384},
diff --git a/pkg/integrity/select.go b/pkg/integrity/select.go
index 3f2e794..5fde05b 100644
--- a/pkg/integrity/select.go
+++ b/pkg/integrity/select.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2020-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
// distributed with the sources of this project regarding your rights to use or distribute this
// software.
@@ -11,6 +11,7 @@ import (
"fmt"
"sort"
+ "github.com/ProtonMail/go-crypto/openpgp/clearsign"
"github.com/sylabs/sif/v2/pkg/sif"
)
@@ -97,6 +98,19 @@ func getObjectSignatures(f *sif.FileImage, id uint32) ([]sif.Descriptor, error)
return sigs, nil
}
+// isLegacySignature returns true if data contains a legacy signature.
+func isLegacySignature(data []byte) bool {
+ // Legacy signatures always encoded in clear-sign format.
+ b, _ := clearsign.Decode(data)
+ if b == nil {
+ return false
+ }
+
+ // The plaintext of legacy signatures always begins with "SIFHASH", and non-legacy signatures
+ // never do, as they are JSON.
+ return bytes.HasPrefix(b.Plaintext, []byte("SIFHASH:\n"))
+}
+
// getGroupSignatures returns descriptors in f that contain signature objects linked to the object
// group with identifier groupID. If legacy is true, only legacy signatures are considered.
// Otherwise, only non-legacy signatures are considered. If no such signatures are found, a
@@ -112,8 +126,7 @@ func getGroupSignatures(f *sif.FileImage, groupID uint32, legacy bool) ([]sif.De
return false, err
}
- isLegacy, err := isLegacySignature(b)
- return isLegacy == legacy, err
+ return isLegacySignature(b) == legacy, err
},
)
if err != nil {
@@ -152,7 +165,9 @@ func getGroupMinObjectID(f *sif.FileImage, groupID uint32) (uint32, error) {
// getGroupIDs returns all identifiers for the groups contained in f, sorted by ID. If no groups
// are present, errNoGroupsFound is returned.
-func getGroupIDs(f *sif.FileImage) (groupIDs []uint32, err error) {
+func getGroupIDs(f *sif.FileImage) ([]uint32, error) {
+ var groupIDs []uint32
+
f.WithDescriptors(func(od sif.Descriptor) bool {
if groupID := od.GroupID(); groupID != 0 {
groupIDs = insertSorted(groupIDs, groupID)
@@ -161,10 +176,10 @@ func getGroupIDs(f *sif.FileImage) (groupIDs []uint32, err error) {
})
if len(groupIDs) == 0 {
- err = errNoGroupsFound
+ return nil, errNoGroupsFound
}
- return groupIDs, err
+ return groupIDs, nil
}
// getFingerprints returns a sorted list of unique fingerprints contained in sigs.
@@ -177,6 +192,10 @@ func getFingerprints(sigs []sif.Descriptor) ([][]byte, error) {
return nil, err
}
+ if len(fp) == 0 {
+ continue
+ }
+
// Check if fingerprint is already in list.
i := sort.Search(len(fps), func(i int) bool {
return bytes.Compare(fps[i], fp) >= 0
diff --git a/pkg/integrity/sign.go b/pkg/integrity/sign.go
index 9d3cb99..46a14e7 100644
--- a/pkg/integrity/sign.go
+++ b/pkg/integrity/sign.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2020-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
// distributed with the sources of this project regarding your rights to use or distribute this
// software.
@@ -8,13 +8,14 @@ package integrity
import (
"bytes"
"crypto"
+ "encoding/json"
"errors"
"fmt"
+ "io"
"sort"
"time"
"github.com/ProtonMail/go-crypto/openpgp"
- "github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/sylabs/sif/v2/pkg/sif"
)
@@ -27,12 +28,19 @@ var (
// ErrNoKeyMaterial is the error returned when no key material was provided.
var ErrNoKeyMaterial = errors.New("key material not provided")
+type encoder interface {
+ // signMessage signs the message from r, and writes the result to w. On success, the signature
+ // hash function is returned.
+ signMessage(w io.Writer, r io.Reader) (ht crypto.Hash, err error)
+}
+
type groupSigner struct {
- f *sif.FileImage // SIF image to sign.
- id uint32 // Group ID.
- ods []sif.Descriptor // Descriptors of object(s) to sign.
- mdHash crypto.Hash // Hash type for metadata.
- sigConfig *packet.Config // Configuration for signature.
+ en encoder // Message encoder.
+ f *sif.FileImage // SIF image to sign.
+ id uint32 // Group ID.
+ ods []sif.Descriptor // Descriptors of object(s) to sign.
+ mdHash crypto.Hash // Hash type for metadata.
+ fp []byte // Fingerprint of signing entity.
}
// groupSignerOpt are used to configure gs.
@@ -68,27 +76,30 @@ func optSignGroupMetadataHash(h crypto.Hash) groupSignerOpt {
}
}
-// optSignGroupSignatureConfig sets c as the configuration used for signature generation.
-func optSignGroupSignatureConfig(c *packet.Config) groupSignerOpt {
+// optSignGroupFingerprint sets fp as the fingerprint of the signing entity.
+func optSignGroupFingerprint(fp []byte) groupSignerOpt {
return func(gs *groupSigner) error {
- gs.sigConfig = c
+ gs.fp = fp
return nil
}
}
-// newGroupSigner returns a new groupSigner to add a digital signature for the specified group to
-// f, according to opts.
+// newGroupSigner returns a new groupSigner to add a digital signature using en for the specified
+// group to f, according to opts.
//
// By default, all data objects in the group will be signed. To override this behavior, use
// optSignGroupObjects(). To override the default metadata hash algorithm, use
-// optSignGroupMetadataHash(). To override the default PGP configuration for signature generation,
-// use optSignGroupSignatureConfig().
-func newGroupSigner(f *sif.FileImage, groupID uint32, opts ...groupSignerOpt) (*groupSigner, error) {
+// optSignGroupMetadataHash().
+//
+// By default, the fingerprint of the signing entity is not set. To override this behavior, use
+// optSignGroupFingerprint.
+func newGroupSigner(en encoder, f *sif.FileImage, groupID uint32, opts ...groupSignerOpt) (*groupSigner, error) {
if groupID == 0 {
return nil, sif.ErrInvalidGroupID
}
gs := groupSigner{
+ en: en,
f: f,
id: groupID,
mdHash: crypto.SHA256,
@@ -136,8 +147,8 @@ func (gs *groupSigner) addObject(od sif.Descriptor) error {
return nil
}
-// signWithEntity signs the objects specified by gs with e.
-func (gs *groupSigner) signWithEntity(e *openpgp.Entity) (sif.DescriptorInput, error) {
+// sign creates a digital signature as specified by gs.
+func (gs *groupSigner) sign() (sif.DescriptorInput, error) {
// Get minimum object ID in group. Object IDs in the image metadata will be relative to this.
minID, err := getGroupMinObjectID(gs.f, gs.id)
if err != nil {
@@ -150,17 +161,24 @@ func (gs *groupSigner) signWithEntity(e *openpgp.Entity) (sif.DescriptorInput, e
return sif.DescriptorInput{}, fmt.Errorf("failed to get image metadata: %w", err)
}
- // Sign and encode image metadata.
+ // Encode image metadata.
+ enc, err := json.Marshal(md)
+ if err != nil {
+ return sif.DescriptorInput{}, fmt.Errorf("failed to encode image metadata: %w", err)
+ }
+
+ // Sign image metadata.
b := bytes.Buffer{}
- if err := signAndEncodeJSON(&b, md, e.PrivateKey, gs.sigConfig); err != nil {
- return sif.DescriptorInput{}, fmt.Errorf("failed to encode signature: %w", err)
+ ht, err := gs.en.signMessage(&b, bytes.NewReader(enc))
+ if err != nil {
+ return sif.DescriptorInput{}, fmt.Errorf("failed to sign message: %w", err)
}
// Prepare SIF data object descriptor.
return sif.NewDescriptorInput(sif.DataSignature, &b,
sif.OptNoGroup(),
sif.OptLinkedGroupID(gs.id),
- sif.OptSignatureMetadata(gs.sigConfig.Hash(), e.PrimaryKey.Fingerprint),
+ sif.OptSignatureMetadata(ht, gs.fp),
)
}
@@ -264,9 +282,10 @@ type Signer struct {
signers []*groupSigner
}
-// NewSigner returns a Signer to add digital signature(s) to f, according to opts.
+// NewSigner returns a Signer to add digital signature(s) to f, according to opts. Key material
+// must be provided, or an error wrapping ErrNoKeyMaterial is returned.
//
-// Sign requires key material be provided. OptSignWithEntity can be used for this purpose.
+// To use key material from an OpenPGP entity, use OptSignWithEntity.
//
// By default, one digital signature is added per object group in f. To override this behavior,
// consider using OptSignGroup and/or OptSignObjects.
@@ -294,15 +313,21 @@ func NewSigner(f *sif.FileImage, opts ...SignerOpt) (*Signer, error) {
opts: so,
}
- commonOpts := []groupSignerOpt{
- optSignGroupSignatureConfig(&packet.Config{
- Time: so.timeFunc,
- }),
+ var commonOpts []groupSignerOpt
+
+ // Get message encoder.
+ var en encoder
+ switch {
+ case so.e != nil:
+ en = newClearsignEncoder(so.e, so.timeFunc)
+ commonOpts = append(commonOpts, optSignGroupFingerprint(so.e.PrimaryKey.Fingerprint))
+ default:
+ return nil, fmt.Errorf("integrity: %w", ErrNoKeyMaterial)
}
// Add signer for each groupID.
for _, groupID := range so.groupIDs {
- gs, err := newGroupSigner(f, groupID, commonOpts...)
+ gs, err := newGroupSigner(en, f, groupID, commonOpts...)
if err != nil {
return nil, fmt.Errorf("integrity: %w", err)
}
@@ -315,7 +340,7 @@ func NewSigner(f *sif.FileImage, opts ...SignerOpt) (*Signer, error) {
opts := commonOpts
opts = append(opts, optSignGroupObjects(ids...))
- gs, err := newGroupSigner(f, groupID, opts...)
+ gs, err := newGroupSigner(en, f, groupID, opts...)
if err != nil {
return err
}
@@ -336,7 +361,7 @@ func NewSigner(f *sif.FileImage, opts ...SignerOpt) (*Signer, error) {
}
for _, id := range ids {
- gs, err := newGroupSigner(f, id, commonOpts...)
+ gs, err := newGroupSigner(en, f, id, commonOpts...)
if err != nil {
return nil, fmt.Errorf("integrity: %w", err)
}
@@ -348,16 +373,9 @@ func NewSigner(f *sif.FileImage, opts ...SignerOpt) (*Signer, error) {
}
// Sign adds digital signatures as specified by s.
-//
-// If key material was not provided when s was created, Sign returns an error wrapping
-// ErrNoKeyMaterial.
func (s *Signer) Sign() error {
- if s.opts.e == nil {
- return fmt.Errorf("integrity: %w", ErrNoKeyMaterial)
- }
-
for _, gs := range s.signers {
- di, err := gs.signWithEntity(s.opts.e)
+ di, err := gs.sign()
if err != nil {
return fmt.Errorf("integrity: %w", err)
}
diff --git a/pkg/integrity/sign_test.go b/pkg/integrity/sign_test.go
index 7eeb86e..a550c89 100644
--- a/pkg/integrity/sign_test.go
+++ b/pkg/integrity/sign_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2020-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
// distributed with the sources of this project regarding your rights to use or distribute this
// software.
@@ -6,6 +6,7 @@
package integrity
import (
+ "bytes"
"crypto"
"errors"
"os"
@@ -14,7 +15,6 @@ import (
"testing"
"github.com/ProtonMail/go-crypto/openpgp"
- "github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/sebdah/goldie/v2"
"github.com/sylabs/sif/v2/pkg/sif"
)
@@ -102,6 +102,7 @@ func TestNewGroupSigner(t *testing.T) {
wantErr error
wantObjects []uint32
wantMDHash crypto.Hash
+ wantFP []byte
}{
{
name: "InvalidGroupID",
@@ -174,17 +175,40 @@ func TestNewGroupSigner(t *testing.T) {
wantObjects: []uint32{1, 2},
wantMDHash: crypto.SHA1,
},
+ {
+ name: "OptSignGroupMetadataHash",
+ fi: twoGroupImage,
+ groupID: 1,
+ opts: []groupSignerOpt{
+ optSignGroupFingerprint([]byte{
+ 0x12, 0x04, 0x5c, 0x8c, 0x0b, 0x10, 0x04, 0xd0, 0x58, 0xde,
+ 0x4b, 0xed, 0xa2, 0x0c, 0x27, 0xee, 0x7f, 0xf7, 0xba, 0x84,
+ }),
+ },
+ wantObjects: []uint32{1, 2},
+ wantMDHash: crypto.SHA256,
+ wantFP: []byte{
+ 0x12, 0x04, 0x5c, 0x8c, 0x0b, 0x10, 0x04, 0xd0, 0x58, 0xde,
+ 0x4b, 0xed, 0xa2, 0x0c, 0x27, 0xee, 0x7f, 0xf7, 0xba, 0x84,
+ },
+ },
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
- s, err := newGroupSigner(tt.fi, tt.groupID, tt.opts...)
+ en := newClearsignEncoder(getTestEntity(t), fixedTime)
+
+ s, err := newGroupSigner(en, tt.fi, tt.groupID, tt.opts...)
if got, want := err, tt.wantErr; !errors.Is(got, want) {
t.Fatalf("got error %v, want %v", got, want)
}
if err == nil {
+ if got, want := s.en, en; got != want {
+ t.Errorf("got encoder %v, want %v", got, want)
+ }
+
if got, want := s.f, tt.fi; got != want {
t.Errorf("got FileImage %v, want %v", got, want)
}
@@ -204,12 +228,16 @@ func TestNewGroupSigner(t *testing.T) {
if got, want := s.mdHash, tt.wantMDHash; got != want {
t.Errorf("got metadata hash %v, want %v", got, want)
}
+
+ if got, want := s.fp, tt.wantFP; !bytes.Equal(got, want) {
+ t.Errorf("got fingerprint %v, want %v", got, want)
+ }
}
})
}
}
-func TestGroupSigner_SignWithEntity(t *testing.T) {
+func TestGroupSigner_Sign(t *testing.T) {
twoGroups := loadContainer(t, filepath.Join(corpus, "two-groups.sif"))
d1, err := twoGroups.GetDescriptor(sif.WithID(1))
@@ -228,144 +256,92 @@ func TestGroupSigner_SignWithEntity(t *testing.T) {
}
e := getTestEntity(t)
+ clearsign := newClearsignEncoder(e, fixedTime)
encrypted := getTestEntity(t)
encrypted.PrivateKey.Encrypted = true
+ clearsignEncrypted := newClearsignEncoder(encrypted, fixedTime)
+
tests := []struct {
name string
gs groupSigner
- e *openpgp.Entity
wantErr bool
}{
{
name: "HashUnavailable",
gs: groupSigner{
+ en: clearsign,
f: twoGroups,
id: 1,
ods: []sif.Descriptor{d1},
mdHash: crypto.MD4,
- sigConfig: &packet.Config{
- Time: fixedTime,
- },
+ fp: e.PrimaryKey.Fingerprint,
},
- e: e,
wantErr: true,
},
{
name: "EncryptedKey",
gs: groupSigner{
+ en: clearsignEncrypted,
f: twoGroups,
id: 1,
ods: []sif.Descriptor{d1},
mdHash: crypto.SHA1,
- sigConfig: &packet.Config{
- Time: fixedTime,
- },
+ fp: encrypted.PrimaryKey.Fingerprint,
},
- e: encrypted,
wantErr: true,
},
{
name: "Object1",
gs: groupSigner{
+ en: clearsign,
f: twoGroups,
id: 1,
ods: []sif.Descriptor{d1},
- mdHash: crypto.SHA1,
- sigConfig: &packet.Config{
- Time: fixedTime,
- },
+ mdHash: crypto.SHA256,
+ fp: e.PrimaryKey.Fingerprint,
},
- e: e,
},
{
name: "Object2",
gs: groupSigner{
+ en: clearsign,
f: twoGroups,
id: 1,
ods: []sif.Descriptor{d2},
- mdHash: crypto.SHA1,
- sigConfig: &packet.Config{
- Time: fixedTime,
- },
+ mdHash: crypto.SHA256,
+ fp: e.PrimaryKey.Fingerprint,
},
- e: e,
},
{
name: "Group1",
gs: groupSigner{
+ en: clearsign,
f: twoGroups,
id: 1,
ods: []sif.Descriptor{d1, d2},
- mdHash: crypto.SHA1,
- sigConfig: &packet.Config{
- Time: fixedTime,
- },
+ mdHash: crypto.SHA256,
+ fp: e.PrimaryKey.Fingerprint,
},
- e: e,
},
{
name: "Group2",
gs: groupSigner{
+ en: clearsign,
f: twoGroups,
id: 2,
ods: []sif.Descriptor{d3},
- mdHash: crypto.SHA1,
- sigConfig: &packet.Config{
- Time: fixedTime,
- },
+ mdHash: crypto.SHA256,
+ fp: e.PrimaryKey.Fingerprint,
},
- e: e,
- },
- {
- name: "SignatureConfigSHA256",
- gs: groupSigner{
- f: twoGroups,
- id: 1,
- ods: []sif.Descriptor{d1, d2},
- mdHash: crypto.SHA1,
- sigConfig: &packet.Config{
- DefaultHash: crypto.SHA256,
- Time: fixedTime,
- },
- },
- e: e,
- },
- {
- name: "SignatureConfigSHA384",
- gs: groupSigner{
- f: twoGroups,
- id: 1,
- ods: []sif.Descriptor{d1, d2},
- mdHash: crypto.SHA1,
- sigConfig: &packet.Config{
- DefaultHash: crypto.SHA384,
- Time: fixedTime,
- },
- },
- e: e,
- },
- {
- name: "SignatureConfigSHA512",
- gs: groupSigner{
- f: twoGroups,
- id: 1,
- ods: []sif.Descriptor{d1, d2},
- mdHash: crypto.SHA1,
- sigConfig: &packet.Config{
- DefaultHash: crypto.SHA512,
- Time: fixedTime,
- },
- },
- e: e,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
- di, err := tt.gs.signWithEntity(tt.e)
+ di, err := tt.gs.sign()
if (err != nil) != tt.wantErr {
t.Fatalf("got error %v, want %v", err, tt.wantErr)
}
@@ -413,92 +389,125 @@ func TestNewSigner(t *testing.T) {
wantEntity *openpgp.Entity
}{
{
- name: "NilFileImage",
- fi: nil,
+ name: "NilFileImage",
+ fi: nil,
+ opts: []SignerOpt{
+ OptSignWithEntity(e),
+ },
wantErr: errNilFileImage,
},
{
- name: "NoGroupsFound",
- fi: emptyImage,
+ name: "NoGroupsFound",
+ fi: emptyImage,
+ opts: []SignerOpt{
+ OptSignWithEntity(e),
+ },
wantErr: errNoGroupsFound,
},
{
- name: "InvalidGroupID",
- fi: emptyImage,
- opts: []SignerOpt{OptSignGroup(0)},
+ name: "InvalidGroupID",
+ fi: emptyImage,
+ opts: []SignerOpt{
+ OptSignWithEntity(e),
+ OptSignGroup(0),
+ },
wantErr: sif.ErrInvalidGroupID,
},
{
- name: "NoObjectsSpecified",
- fi: emptyImage,
- opts: []SignerOpt{OptSignObjects()},
+ name: "NoObjectsSpecified",
+ fi: emptyImage,
+ opts: []SignerOpt{
+ OptSignWithEntity(e),
+ OptSignObjects(),
+ },
wantErr: errNoObjectsSpecified,
},
{
- name: "NoObjects",
- fi: emptyImage,
- opts: []SignerOpt{OptSignObjects(1)},
+ name: "NoObjects",
+ fi: emptyImage,
+ opts: []SignerOpt{
+ OptSignWithEntity(e),
+ OptSignObjects(1),
+ },
wantErr: sif.ErrNoObjects,
},
{
- name: "InvalidObjectID",
- fi: oneGroupImage,
- opts: []SignerOpt{OptSignObjects(0)},
+ name: "InvalidObjectID",
+ fi: oneGroupImage,
+ opts: []SignerOpt{
+ OptSignWithEntity(e),
+ OptSignObjects(0),
+ },
wantErr: sif.ErrInvalidObjectID,
},
{
- name: "OneGroupDefaultObjects",
- fi: oneGroupImage,
- opts: []SignerOpt{},
+ name: "OneGroupDefaultObjects",
+ fi: oneGroupImage,
+ opts: []SignerOpt{
+ OptSignWithEntity(e),
+ },
wantGroupObjects: map[uint32][]uint32{1: {1, 2}},
},
{
- name: "TwoGroupDefaultObjects",
- fi: twoGroupImage,
- opts: []SignerOpt{},
- wantGroupObjects: map[uint32][]uint32{1: {1, 2}, 2: {3}},
- },
- {
- name: "OptSignWithEntity",
- fi: twoGroupImage,
- opts: []SignerOpt{OptSignWithEntity(e)},
+ name: "TwoGroupDefaultObjects",
+ fi: twoGroupImage,
+ opts: []SignerOpt{
+ OptSignWithEntity(e),
+ },
wantGroupObjects: map[uint32][]uint32{1: {1, 2}, 2: {3}},
- wantEntity: e,
},
{
- name: "OptSignGroup1",
- fi: twoGroupImage,
- opts: []SignerOpt{OptSignGroup(1)},
+ name: "OptSignGroup1",
+ fi: twoGroupImage,
+ opts: []SignerOpt{
+ OptSignWithEntity(e),
+ OptSignGroup(1),
+ },
wantGroupObjects: map[uint32][]uint32{1: {1, 2}},
},
{
- name: "OptSignGroup2",
- fi: twoGroupImage,
- opts: []SignerOpt{OptSignGroup(2)},
+ name: "OptSignGroup2",
+ fi: twoGroupImage,
+ opts: []SignerOpt{
+ OptSignWithEntity(e),
+ OptSignGroup(2),
+ },
wantGroupObjects: map[uint32][]uint32{2: {3}},
},
{
- name: "OptSignObject1",
- fi: twoGroupImage,
- opts: []SignerOpt{OptSignObjects(1)},
+ name: "OptSignObject1",
+ fi: twoGroupImage,
+ opts: []SignerOpt{
+ OptSignWithEntity(e),
+ OptSignObjects(1),
+ },
wantGroupObjects: map[uint32][]uint32{1: {1}},
},
{
- name: "OptSignObject2",
- fi: twoGroupImage,
- opts: []SignerOpt{OptSignObjects(2)},
+ name: "OptSignObject2",
+ fi: twoGroupImage,
+ opts: []SignerOpt{
+ OptSignWithEntity(e),
+ OptSignObjects(2),
+ },
wantGroupObjects: map[uint32][]uint32{1: {2}},
},
{
- name: "OptSignObject3",
- fi: twoGroupImage,
- opts: []SignerOpt{OptSignObjects(3)},
+ name: "OptSignObject3",
+ fi: twoGroupImage,
+ opts: []SignerOpt{
+ OptSignWithEntity(e),
+ OptSignObjects(3),
+ },
wantGroupObjects: map[uint32][]uint32{2: {3}},
},
{
- name: "OptSignObjects",
- fi: twoGroupImage,
- opts: []SignerOpt{OptSignObjects(1, 2, 3)},
+ name: "OptSignObjects",
+ fi: twoGroupImage,
+ opts: []SignerOpt{
+ OptSignWithEntity(e),
+ OptSignObjects(1, 2, 3),
+ },
wantGroupObjects: map[uint32][]uint32{1: {1, 2}, 2: {3}},
},
}
@@ -554,11 +563,6 @@ func TestSigner_Sign(t *testing.T) {
wantErr bool
}{
{
- name: "NoKeyMaterial",
- inputFile: "one-group.sif",
- wantErr: true,
- },
- {
name: "EncryptedKey",
inputFile: "one-group.sif",
opts: []SignerOpt{OptSignWithEntity(encrypted)},
diff --git a/pkg/integrity/testdata/TestDigest_MarshalJSON/SHA1.golden b/pkg/integrity/testdata/TestDigest_MarshalJSON/SHA1.golden
deleted file mode 100644
index c39798c..0000000
--- a/pkg/integrity/testdata/TestDigest_MarshalJSON/SHA1.golden
+++ /dev/null
@@ -1 +0,0 @@
-"sha1:597f6a540010f94c15d71806a99a2c8710e747bd"
diff --git a/pkg/integrity/testdata/TestDigest_MarshalJSON/SHA512_224.golden b/pkg/integrity/testdata/TestDigest_MarshalJSON/SHA512_224.golden
new file mode 100644
index 0000000..96aff7b
--- /dev/null
+++ b/pkg/integrity/testdata/TestDigest_MarshalJSON/SHA512_224.golden
@@ -0,0 +1 @@
+"sha512_224:06001bf08dfb17d2b54925116823be230e98b5c6c278303bc4909a8c"
diff --git a/pkg/integrity/testdata/TestDigest_MarshalJSON/SHA512_256.golden b/pkg/integrity/testdata/TestDigest_MarshalJSON/SHA512_256.golden
new file mode 100644
index 0000000..c4a1fbc
--- /dev/null
+++ b/pkg/integrity/testdata/TestDigest_MarshalJSON/SHA512_256.golden
@@ -0,0 +1 @@
+"sha512_256:3d37fe58435e0d87323dee4a2c1b339ef954de63716ee79f5747f94d974f913f"
diff --git a/pkg/integrity/testdata/TestGetHeaderMetadata/SHA1.golden b/pkg/integrity/testdata/TestGetHeaderMetadata/SHA1.golden
deleted file mode 100644
index 51232ca..0000000
--- a/pkg/integrity/testdata/TestGetHeaderMetadata/SHA1.golden
+++ /dev/null
@@ -1 +0,0 @@
-{"digest":"sha1:bd6b562b49ff04470f641ad3c971822303049826"}
diff --git a/pkg/integrity/testdata/TestGetHeaderMetadata/SHA512_224.golden b/pkg/integrity/testdata/TestGetHeaderMetadata/SHA512_224.golden
new file mode 100644
index 0000000..9848911
--- /dev/null
+++ b/pkg/integrity/testdata/TestGetHeaderMetadata/SHA512_224.golden
@@ -0,0 +1 @@
+{"digest":"sha512_224:d5f9767e096056fcf381b801e2b0b80b33acdc09b7a7e3a1c504231e"}
diff --git a/pkg/integrity/testdata/TestGetHeaderMetadata/SHA512_256.golden b/pkg/integrity/testdata/TestGetHeaderMetadata/SHA512_256.golden
new file mode 100644
index 0000000..449166a
--- /dev/null
+++ b/pkg/integrity/testdata/TestGetHeaderMetadata/SHA512_256.golden
@@ -0,0 +1 @@
+{"digest":"sha512_256:eb199aeab4047ca6430890372769681045c20b1a0a4a78b595ab62dbdfc9285f"}
diff --git a/pkg/integrity/testdata/TestGetImageMetadata/Object1.golden b/pkg/integrity/testdata/TestGetImageMetadata/Object1.golden
index cbddd5d..142d7f0 100644
--- a/pkg/integrity/testdata/TestGetImageMetadata/Object1.golden
+++ b/pkg/integrity/testdata/TestGetImageMetadata/Object1.golden
@@ -1 +1 @@
-{"version":1,"header":{"digest":"sha1:86696357e7806b51baf75fc0bf9b8fc677e5cdd0"},"objects":[{"relativeId":0,"descriptorDigest":"sha1:1406a1a9c75a332fc50cb8519a9a7f9f2531480e","objectDigest":"sha1:15146b9bf4f1f5f9bf176a398d8c4f0321c63064"}]}
+{"version":1,"header":{"digest":"sha256:635fa0a14a8ef0c0351ed3e985799ed1d4f75ce973dea3cc76c99710795cc3f1"},"objects":[{"relativeId":0,"descriptorDigest":"sha256:3634ad01db0dd5482ecf685267b53d6201690438ca27c3d7ea91c971a1f41f92","objectDigest":"sha256:004dfc8da678c309de28b5386a1e9efd57f536b150c40d29b31506aa0fb17ec2"}]}
diff --git a/pkg/integrity/testdata/TestGetImageMetadata/Object2.golden b/pkg/integrity/testdata/TestGetImageMetadata/Object2.golden
index 9485be4..e130e64 100644
--- a/pkg/integrity/testdata/TestGetImageMetadata/Object2.golden
+++ b/pkg/integrity/testdata/TestGetImageMetadata/Object2.golden
@@ -1 +1 @@
-{"version":1,"header":{"digest":"sha1:86696357e7806b51baf75fc0bf9b8fc677e5cdd0"},"objects":[{"relativeId":1,"descriptorDigest":"sha1:ddf7e6609fec1f565545207f28d4b39f499d78c5","objectDigest":"sha1:d78f8bb992a56a597f6c7a1fb918bb78271367eb"}]}
+{"version":1,"header":{"digest":"sha256:635fa0a14a8ef0c0351ed3e985799ed1d4f75ce973dea3cc76c99710795cc3f1"},"objects":[{"relativeId":1,"descriptorDigest":"sha256:04b5f87c9692a54f80d10fb6af00c779763aeca29d610348854bd97cd8bf66fd","objectDigest":"sha256:9f9c4e5e131934969b4ac8f495691c70b8c6c8e3f489c2c9ab5f1af82bce0604"}]}
diff --git a/pkg/integrity/testdata/TestGetImageMetadata/SHA1.golden b/pkg/integrity/testdata/TestGetImageMetadata/SHA1.golden
deleted file mode 100644
index 7d86f1a..0000000
--- a/pkg/integrity/testdata/TestGetImageMetadata/SHA1.golden
+++ /dev/null
@@ -1 +0,0 @@
-{"version":1,"header":{"digest":"sha1:86696357e7806b51baf75fc0bf9b8fc677e5cdd0"},"objects":[{"relativeId":0,"descriptorDigest":"sha1:1406a1a9c75a332fc50cb8519a9a7f9f2531480e","objectDigest":"sha1:15146b9bf4f1f5f9bf176a398d8c4f0321c63064"},{"relativeId":1,"descriptorDigest":"sha1:ddf7e6609fec1f565545207f28d4b39f499d78c5","objectDigest":"sha1:d78f8bb992a56a597f6c7a1fb918bb78271367eb"}]}
diff --git a/pkg/integrity/testdata/TestGetImageMetadata/SHA224.golden b/pkg/integrity/testdata/TestGetImageMetadata/SHA224.golden
index 82dbc77..457318d 100644
--- a/pkg/integrity/testdata/TestGetImageMetadata/SHA224.golden
+++ b/pkg/integrity/testdata/TestGetImageMetadata/SHA224.golden
@@ -1 +1 @@
-{"version":1,"header":{"digest":"sha224:88ecbdbaa9bf8410c9362213ddae5e6771fd2525da3eb390300fb666"},"objects":[{"relativeId":0,"descriptorDigest":"sha224:8ac2ffbf24282ce5f49fc591eee1e0a879b0ae2a9fa813b897b94113","objectDigest":"sha224:071bce5faa03c2016d3e1e086ccb60b6ea3cabc493c9aa1013594efd"},{"relativeId":1,"descriptorDigest":"sha224:fc5b7f5b3622982bcebfbb8b3a0d81b6f850228f2ff3631d03deaab6","objectDigest":"sha224:55b9eee5f60cc362ddc07676f620372611e22272f60fdbec94f243f8"}]}
+{"version":1,"header":{"digest":"sha224:88ecbdbaa9bf8410c9362213ddae5e6771fd2525da3eb390300fb666"},"objects":[{"relativeId":0,"descriptorDigest":"sha224:8ac2ffbf24282ce5f49fc591eee1e0a879b0ae2a9fa813b897b94113","objectDigest":"sha224:071bce5faa03c2016d3e1e086ccb60b6ea3cabc493c9aa1013594efd"},{"relativeId":1,"descriptorDigest":"sha224:15f2307c74c24b5aff01556df7642009a968ad717514da242821b1cb","objectDigest":"sha224:1f26e23245e830c5aa90735377d43535b0a080d03981ea58e4b94372"}]}
diff --git a/pkg/integrity/testdata/TestGetImageMetadata/SHA256.golden b/pkg/integrity/testdata/TestGetImageMetadata/SHA256.golden
index 7286dc4..60eba6b 100644
--- a/pkg/integrity/testdata/TestGetImageMetadata/SHA256.golden
+++ b/pkg/integrity/testdata/TestGetImageMetadata/SHA256.golden
@@ -1 +1 @@
-{"version":1,"header":{"digest":"sha256:635fa0a14a8ef0c0351ed3e985799ed1d4f75ce973dea3cc76c99710795cc3f1"},"objects":[{"relativeId":0,"descriptorDigest":"sha256:3634ad01db0dd5482ecf685267b53d6201690438ca27c3d7ea91c971a1f41f92","objectDigest":"sha256:004dfc8da678c309de28b5386a1e9efd57f536b150c40d29b31506aa0fb17ec2"},{"relativeId":1,"descriptorDigest":"sha256:db74cb63348414def73535c9f0f83e8ad7df61229ed2806f4da8b69d6d7464d6","objectDigest":"sha256:5f78c33274e43fa9de5659265c1d917e25c03722dcb0b8d27db8d5feaa813953"}]}
+{"version":1,"header":{"digest":"sha256:635fa0a14a8ef0c0351ed3e985799ed1d4f75ce973dea3cc76c99710795cc3f1"},"objects":[{"relativeId":0,"descriptorDigest":"sha256:3634ad01db0dd5482ecf685267b53d6201690438ca27c3d7ea91c971a1f41f92","objectDigest":"sha256:004dfc8da678c309de28b5386a1e9efd57f536b150c40d29b31506aa0fb17ec2"},{"relativeId":1,"descriptorDigest":"sha256:04b5f87c9692a54f80d10fb6af00c779763aeca29d610348854bd97cd8bf66fd","objectDigest":"sha256:9f9c4e5e131934969b4ac8f495691c70b8c6c8e3f489c2c9ab5f1af82bce0604"}]}
diff --git a/pkg/integrity/testdata/TestGetImageMetadata/SHA384.golden b/pkg/integrity/testdata/TestGetImageMetadata/SHA384.golden
index a324654..9e3b6de 100644
--- a/pkg/integrity/testdata/TestGetImageMetadata/SHA384.golden
+++ b/pkg/integrity/testdata/TestGetImageMetadata/SHA384.golden
@@ -1 +1 @@
-{"version":1,"header":{"digest":"sha384:92bdcff06f2cde591d8af1f0ab80687e5eccf1dfe8fe7a8a8ef4b80d28bd2c7a77bcc4abfbcfdb3fc84ec7992ed54334"},"objects":[{"relativeId":0,"descriptorDigest":"sha384:ed532e8496b916182a4185a3d12f3a6d4c59d204965ef1a732013d1c05050291ec29b48b06ba100a948468868023fb82","objectDigest":"sha384:f8722c6694c4997334525090678b2148f6263502c3eb144a44e8be0d2bfd039f4067a3f8152f94ab3af7c63acfe78ce6"},{"relativeId":1,"descriptorDigest":"sha384:d06c5144e805d18f8ebbce1eef073dd711ab9a6550e76e1fb9b82e5a822dd83273069d258d099a437461e674f519ef9c","objectDigest":"sha384:0b7e0522460767c74abb4245bc0d3a27209a5aed111059faead54ffc74a93759160ac9642d7a7df3038ece62f2fa9815"}]}
+{"version":1,"header":{"digest":"sha384:92bdcff06f2cde591d8af1f0ab80687e5eccf1dfe8fe7a8a8ef4b80d28bd2c7a77bcc4abfbcfdb3fc84ec7992ed54334"},"objects":[{"relativeId":0,"descriptorDigest":"sha384:ed532e8496b916182a4185a3d12f3a6d4c59d204965ef1a732013d1c05050291ec29b48b06ba100a948468868023fb82","objectDigest":"sha384:f8722c6694c4997334525090678b2148f6263502c3eb144a44e8be0d2bfd039f4067a3f8152f94ab3af7c63acfe78ce6"},{"relativeId":1,"descriptorDigest":"sha384:de7f8e386d3c1679711711f31812d12a913b0f5202503e46e9e1c5fd36c49908c5e288a3c08e6b72285dba177596668d","objectDigest":"sha384:da6cf2d305a04a53623df94d5e74bc22aee7961a5a62b289f99db693e5a980ee276526f254f6504f9e66621ce821b977"}]}
diff --git a/pkg/integrity/testdata/TestGetImageMetadata/SHA512.golden b/pkg/integrity/testdata/TestGetImageMetadata/SHA512.golden
index d067015..1028bd3 100644
--- a/pkg/integrity/testdata/TestGetImageMetadata/SHA512.golden
+++ b/pkg/integrity/testdata/TestGetImageMetadata/SHA512.golden
@@ -1 +1 @@
-{"version":1,"header":{"digest":"sha512:503a1101d5a7f66e440f157576597c5ab9a3517b025259da985402f3b7de7c90c6034b8b5d3da992a9cae5b47dd355fce3f9932e92bc47422134cc5b7347e1e7"},"objects":[{"relativeId":0,"descriptorDigest":"sha512:4ccbff33c9be45cf2ba25412de4a87bd623fd48bf00598b756ea5a12a4eddb83aa93176a91a875b595750837b3ba77b5dfbbbd90e2126ca4d4828763db6dc591","objectDigest":"sha512:808e1f67ffbdbdae30946529b920a1ad6d49c0c50423bc0c9d41ece566e291b6c3e6b6839f3095fbab6bc15a5b971b07d4b8b2f22b982ce3c2b8fd05eef7e1b3"},{"relativeId":1,"descriptorDigest":"sha512:4759070398e2e62d47fc166051918ae99493c31e0cb533ad6b041941c69a81a3cd3141a665fb5d1adef21a5ab9ac3247ab2bd4b1551b9377728acf87e56d9870","objectDigest":"sha512:1284b2d521535196f22175d5f558104220a6ad7680e78b49fa6f20e57ea7b185d71ec1edb137e70eba528dedb141f5d2f8bb53149d262932b27cf41fed96aa7f"}]}
+{"version":1,"header":{"digest":"sha512:503a1101d5a7f66e440f157576597c5ab9a3517b025259da985402f3b7de7c90c6034b8b5d3da992a9cae5b47dd355fce3f9932e92bc47422134cc5b7347e1e7"},"objects":[{"relativeId":0,"descriptorDigest":"sha512:4ccbff33c9be45cf2ba25412de4a87bd623fd48bf00598b756ea5a12a4eddb83aa93176a91a875b595750837b3ba77b5dfbbbd90e2126ca4d4828763db6dc591","objectDigest":"sha512:808e1f67ffbdbdae30946529b920a1ad6d49c0c50423bc0c9d41ece566e291b6c3e6b6839f3095fbab6bc15a5b971b07d4b8b2f22b982ce3c2b8fd05eef7e1b3"},{"relativeId":1,"descriptorDigest":"sha512:39bc9e8ecf3192e0656f0b4de529b7e6b1b0d892a6b19fd6f619bf476558bc15255ead250e440c3fe69bbf061b49c0ca0d7de18616c0b3172b2ca2d0753ef331","objectDigest":"sha512:c948c053d5494e944dc251ba774882c58c6b18dae241caa84123c779170d1ed0a22ced3af76d67c7090b668fa7d80531e5b9e3f4677b1b5aae64e8d0d24999bf"}]}
diff --git a/pkg/integrity/testdata/TestGetObjectMetadata/RelativeID.golden b/pkg/integrity/testdata/TestGetObjectMetadata/RelativeID.golden
index dc7a4fc..409c57d 100644
--- a/pkg/integrity/testdata/TestGetObjectMetadata/RelativeID.golden
+++ b/pkg/integrity/testdata/TestGetObjectMetadata/RelativeID.golden
@@ -1 +1 @@
-{"relativeId":1,"descriptorDigest":"sha1:f3681c97de35ea124cd2e3687ed62988c7138f3a","objectDigest":"sha1:5bf1fd927dfb8679496a2e6cf00cbe50c1c87145"}
+{"relativeId":1,"descriptorDigest":"sha256:a1e6ca1d0cce1fbd71b186ac7a5c5a805c833ecc419a78d017558e79c0862790","objectDigest":"sha256:8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52"}
diff --git a/pkg/integrity/testdata/TestGetObjectMetadata/SHA1.golden b/pkg/integrity/testdata/TestGetObjectMetadata/SHA1.golden
deleted file mode 100644
index c1265fc..0000000
--- a/pkg/integrity/testdata/TestGetObjectMetadata/SHA1.golden
+++ /dev/null
@@ -1 +0,0 @@
-{"relativeId":0,"descriptorDigest":"sha1:042874d3fd63a516c5abe45b221ed8db1e5cfd84","objectDigest":"sha1:5bf1fd927dfb8679496a2e6cf00cbe50c1c87145"}
diff --git a/pkg/integrity/testdata/TestGetObjectMetadata/SHA512_224.golden b/pkg/integrity/testdata/TestGetObjectMetadata/SHA512_224.golden
new file mode 100644
index 0000000..856a115
--- /dev/null
+++ b/pkg/integrity/testdata/TestGetObjectMetadata/SHA512_224.golden
@@ -0,0 +1 @@
+{"relativeId":0,"descriptorDigest":"sha512_224:ba5b52f4337756f9efb0c7d35f16e0365ba5845b0dd9df5e9edfce3a","objectDigest":"sha512_224:b1d15ae18bb05265b44e9e0137f08078f53f5b239a78c49c2cfc2c9c"}
diff --git a/pkg/integrity/testdata/TestGetObjectMetadata/SHA512_256.golden b/pkg/integrity/testdata/TestGetObjectMetadata/SHA512_256.golden
new file mode 100644
index 0000000..3271f6d
--- /dev/null
+++ b/pkg/integrity/testdata/TestGetObjectMetadata/SHA512_256.golden
@@ -0,0 +1 @@
+{"relativeId":0,"descriptorDigest":"sha512_256:aef151cf86aaab28a4e086c9e1f9d19c8f85e4eb794336d909a6844ce7fb52ef","objectDigest":"sha512_256:9a801762c512490303535d35c221e2dc1d24f5094d038041dc4303ba7ac04f0e"}
diff --git a/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/Group1.golden b/pkg/integrity/testdata/TestGroupSigner_Sign/Group1.golden
index 53cded0..108a9da 100644
--- a/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/Group1.golden
+++ b/pkg/integrity/testdata/TestGroupSigner_Sign/Group1.golden
Binary files differ
diff --git a/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/Group2.golden b/pkg/integrity/testdata/TestGroupSigner_Sign/Group2.golden
index acd896c..9e5737b 100644
--- a/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/Group2.golden
+++ b/pkg/integrity/testdata/TestGroupSigner_Sign/Group2.golden
Binary files differ
diff --git a/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/Object1.golden b/pkg/integrity/testdata/TestGroupSigner_Sign/Object1.golden
index 4acca9d..a559012 100644
--- a/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/Object1.golden
+++ b/pkg/integrity/testdata/TestGroupSigner_Sign/Object1.golden
Binary files differ
diff --git a/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/Object2.golden b/pkg/integrity/testdata/TestGroupSigner_Sign/Object2.golden
index 69c540d..f57bd02 100644
--- a/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/Object2.golden
+++ b/pkg/integrity/testdata/TestGroupSigner_Sign/Object2.golden
Binary files differ
diff --git a/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/SignatureConfigSHA256.golden b/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/SignatureConfigSHA256.golden
deleted file mode 100644
index 53cded0..0000000
--- a/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/SignatureConfigSHA256.golden
+++ /dev/null
Binary files differ
diff --git a/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/SignatureConfigSHA384.golden b/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/SignatureConfigSHA384.golden
deleted file mode 100644
index 717dd0e..0000000
--- a/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/SignatureConfigSHA384.golden
+++ /dev/null
Binary files differ
diff --git a/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/SignatureConfigSHA512.golden b/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/SignatureConfigSHA512.golden
deleted file mode 100644
index c231fbf..0000000
--- a/pkg/integrity/testdata/TestGroupSigner_SignWithEntity/SignatureConfigSHA512.golden
+++ /dev/null
Binary files differ
diff --git a/pkg/integrity/testdata/TestSignAndEncodeJSON/DefaultHash.golden b/pkg/integrity/testdata/TestSignAndEncodeJSON/DefaultHash.golden
deleted file mode 100644
index 94cf19f..0000000
--- a/pkg/integrity/testdata/TestSignAndEncodeJSON/DefaultHash.golden
+++ /dev/null
@@ -1,15 +0,0 @@
------BEGIN PGP SIGNED MESSAGE-----
-Hash: SHA256
-
-{"One":1,"Two":2}
------BEGIN PGP SIGNATURE-----
-
-wsBzBAEBCAAnBQJZr0CRCZCiDCfuf/e6hBahBBIEXIwLEATQWN5L7aIMJ+5/97qE
-AABw3Af+JOwZe9vRSBAUbuv0akGte0vmiyBAOOJgMgP8G6PtSVMOuyk87hMs9WyB
-Gfgm8st0jkYsCP60lSziNd24cevrqSW+8CnbcWuyhSpl9DBtKnjt/BXc0AturkQA
-/8Yn/9XImSYGrcqjJeCWhVBrJTlPaYu8swbXhvDoHvrVG/t6POO4xYumN3Nf8uoY
-zP8TtK8L1c2H1sY1MbngL/rVaFapdChQw8XCc280JMqnnjixnqSdjaeVPoQJItjG
-m6k2nTUBn7OS9lHYRc9batc38ty13tVSCSd62nvfQILL5YOPqfRtGJzLU0LGMrIO
-aJv5oAPI8SaPaFGuFOPxc7oTckNCIw==
-=FYE/
------END PGP SIGNATURE----- \ No newline at end of file
diff --git a/pkg/integrity/testdata/TestSignAndEncodeJSON/SHA1.golden b/pkg/integrity/testdata/TestSignAndEncodeJSON/SHA1.golden
deleted file mode 100644
index 872a24a..0000000
--- a/pkg/integrity/testdata/TestSignAndEncodeJSON/SHA1.golden
+++ /dev/null
@@ -1,15 +0,0 @@
------BEGIN PGP SIGNED MESSAGE-----
-Hash: SHA1
-
-{"One":1,"Two":2}
------BEGIN PGP SIGNATURE-----
-
-wsBzBAEBAgAnBQJZr0CRCZCiDCfuf/e6hBahBBIEXIwLEATQWN5L7aIMJ+5/97qE
-AACnHAf+J7rou/ZbPfY+fodtzyIBTrCKyYv4R37Kiu7DQclLfBDvEiRp8ISo6VqE
-siGSUapkKCyAzgKcSs3W/A2PFdeI+w5qZ4+Vb/SMjtLCBraKdukNVbsXw8JcE9Mb
-mJGFkVvZu5afpNDQqiNAOJnpV4J1+jpnAZAMu7TQs1FmN6/hCwo4GLCtFDwbI8fO
-dPna3k1lNB/M0UDiPMhn2qdF6vMWF1m41aQ5CwvCAsMOFZi+3sBA8yZngXJ29fjL
-Wng0xaFZPfR/3dEFhlMcqCpM4OFpsVTP5b99GPXUEwaZpwPJuGVUSk7Ck8hfbndQ
-pCYg/sAXojQj69Ymc88utJq4GfR7eg==
-=EY0t
------END PGP SIGNATURE----- \ No newline at end of file
diff --git a/pkg/integrity/testdata/TestSignAndEncodeJSON/SHA224.golden b/pkg/integrity/testdata/TestSignAndEncodeJSON/SHA224.golden
deleted file mode 100644
index e01869b..0000000
--- a/pkg/integrity/testdata/TestSignAndEncodeJSON/SHA224.golden
+++ /dev/null
@@ -1,15 +0,0 @@
------BEGIN PGP SIGNED MESSAGE-----
-Hash: SHA224
-
-{"One":1,"Two":2}
------BEGIN PGP SIGNATURE-----
-
-wsBzBAEBCwAnBQJZr0CRCZCiDCfuf/e6hBahBBIEXIwLEATQWN5L7aIMJ+5/97qE
-AADZhQf8DDXI6Ac4RQwpb6dWEhSOKiKC/voPS1+NGEvFeZ0XE9aqdXgPxQkT5RSJ
-sAG7Hoe8tRRrcagXgcHSGVOnE6tqpL6CITFs6g5MtvlZRxRftpNEHo4xqbUMM1ce
-JPBKrg0u4nCARdq3XCQbuUjVJ2R24XojBjfBXsmNzDsiyekYyllpK/mmWouzQSdC
-IU1sa6/P0JyA0vYzuYPOAqJgFO/cRzXSDEuqW6I07kIbKCHi1joGYb5xVUofJ+2B
-Rt3QnlNrTzMCUxLYE6Rzy0JyzjpjhozY5ldVRkE2NrFkxrgLrItCIMNPjqud7n88
-/DPHuLBdTK6+g6qVMUBLF8QKB5X3yA==
-=Npht
------END PGP SIGNATURE----- \ No newline at end of file
diff --git a/pkg/integrity/testdata/TestSignAndEncodeJSON/SHA256.golden b/pkg/integrity/testdata/TestSignAndEncodeJSON/SHA256.golden
deleted file mode 100644
index 94cf19f..0000000
--- a/pkg/integrity/testdata/TestSignAndEncodeJSON/SHA256.golden
+++ /dev/null
@@ -1,15 +0,0 @@
------BEGIN PGP SIGNED MESSAGE-----
-Hash: SHA256
-
-{"One":1,"Two":2}
------BEGIN PGP SIGNATURE-----
-
-wsBzBAEBCAAnBQJZr0CRCZCiDCfuf/e6hBahBBIEXIwLEATQWN5L7aIMJ+5/97qE
-AABw3Af+JOwZe9vRSBAUbuv0akGte0vmiyBAOOJgMgP8G6PtSVMOuyk87hMs9WyB
-Gfgm8st0jkYsCP60lSziNd24cevrqSW+8CnbcWuyhSpl9DBtKnjt/BXc0AturkQA
-/8Yn/9XImSYGrcqjJeCWhVBrJTlPaYu8swbXhvDoHvrVG/t6POO4xYumN3Nf8uoY
-zP8TtK8L1c2H1sY1MbngL/rVaFapdChQw8XCc280JMqnnjixnqSdjaeVPoQJItjG
-m6k2nTUBn7OS9lHYRc9batc38ty13tVSCSd62nvfQILL5YOPqfRtGJzLU0LGMrIO
-aJv5oAPI8SaPaFGuFOPxc7oTckNCIw==
-=FYE/
------END PGP SIGNATURE----- \ No newline at end of file
diff --git a/pkg/integrity/testdata/TestSignAndEncodeJSON/SHA384.golden b/pkg/integrity/testdata/TestSignAndEncodeJSON/SHA384.golden
deleted file mode 100644
index 7269fb4..0000000
--- a/pkg/integrity/testdata/TestSignAndEncodeJSON/SHA384.golden
+++ /dev/null
@@ -1,15 +0,0 @@
------BEGIN PGP SIGNED MESSAGE-----
-Hash: SHA384
-
-{"One":1,"Two":2}
------BEGIN PGP SIGNATURE-----
-
-wsBzBAEBCQAnBQJZr0CRCZCiDCfuf/e6hBahBBIEXIwLEATQWN5L7aIMJ+5/97qE
-AACoHQgAlRlRxWaxCkiQPFFC8DtsV7GSw6dN4ELFxzMXd6slWhxFOJYnHX2VH1uC
-/9/1Wqzvvrv5uha5Fy3dRQT1Zpa802sJU0GorPxv6LIvOEcJ8UMUj2dTQ4traSSW
-aPb7+Dyvfsosai2it1dFU25+IFScsCk6xaf8l2mSTZxj/0jjx8lxwKUZaEjKTNNf
-uL9U46UmP3w7lN6g+Nk4lz+dqU6lzMCcyJmgekGNl7/1mmOdFxqR/QKgiK5etHzZ
-WxIy1yHpV7FLtwGYA6O7vXpw3uktl8Muoc78pU1E1YMibLfygj+VO5RINMyuYb/4
-uGn6I+LJczqFpgSKcnP7/+yRVTOHQg==
-=SYuD
------END PGP SIGNATURE----- \ No newline at end of file
diff --git a/pkg/integrity/testdata/TestSignAndEncodeJSON/SHA512.golden b/pkg/integrity/testdata/TestSignAndEncodeJSON/SHA512.golden
deleted file mode 100644
index 2bbc680..0000000
--- a/pkg/integrity/testdata/TestSignAndEncodeJSON/SHA512.golden
+++ /dev/null
@@ -1,15 +0,0 @@
------BEGIN PGP SIGNED MESSAGE-----
-Hash: SHA512
-
-{"One":1,"Two":2}
------BEGIN PGP SIGNATURE-----
-
-wsBzBAEBCgAnBQJZr0CRCZCiDCfuf/e6hBahBBIEXIwLEATQWN5L7aIMJ+5/97qE
-AADw9QgAl3ZD0vCmvxyPHiz3PuQpgEUM17z6msOtYJ8ZxL+uNe1niTeBYShvPxm1
-6VULiYnBEJ4npkHMKqIXUAnHIN9wUD/FO7/JkEW1ttxZSoQKIiJeL7m/C5E/qEG8
-ikFLlhYproo5lN18xf64h9oi2UaZGlF4jLufWflRNemSm/d5hPpns2gvP47L3jF2
-vDZO7VGi4Q5P6zO2Zg4E4tLBWZkH5PxQls5QC13VCu36zOXmEt2TRLeSQ47Y60du
-Sp+LFuSpnIC9qJmj6OWabfIvOzLyuoZQpcCjDmYOpxSftrF8b0pFXN/tXdbJzWI5
-dH8EBvdCJ15EWL41hgxyOps9uGAVaw==
-=aBQa
------END PGP SIGNATURE----- \ No newline at end of file
diff --git a/pkg/integrity/testdata/TestSigner_Sign/EncryptedKey.golden b/pkg/integrity/testdata/TestSigner_Sign/EncryptedKey.golden
index ea68b45..fb72e4a 100644
--- a/pkg/integrity/testdata/TestSigner_Sign/EncryptedKey.golden
+++ b/pkg/integrity/testdata/TestSigner_Sign/EncryptedKey.golden
Binary files differ
diff --git a/pkg/integrity/testdata/TestSigner_Sign/NoKeyMaterial.golden b/pkg/integrity/testdata/TestSigner_Sign/NoKeyMaterial.golden
deleted file mode 100644
index ea68b45..0000000
--- a/pkg/integrity/testdata/TestSigner_Sign/NoKeyMaterial.golden
+++ /dev/null
Binary files differ
diff --git a/pkg/integrity/testdata/TestSigner_Sign/OneGroup.golden b/pkg/integrity/testdata/TestSigner_Sign/OneGroup.golden
index a8694c2..94225e7 100644
--- a/pkg/integrity/testdata/TestSigner_Sign/OneGroup.golden
+++ b/pkg/integrity/testdata/TestSigner_Sign/OneGroup.golden
Binary files differ
diff --git a/pkg/integrity/testdata/TestSigner_Sign/OptSignDeterministic.golden b/pkg/integrity/testdata/TestSigner_Sign/OptSignDeterministic.golden
index ddd0767..6a497c8 100644
--- a/pkg/integrity/testdata/TestSigner_Sign/OptSignDeterministic.golden
+++ b/pkg/integrity/testdata/TestSigner_Sign/OptSignDeterministic.golden
Binary files differ
diff --git a/pkg/integrity/testdata/TestSigner_Sign/OptSignGroup1.golden b/pkg/integrity/testdata/TestSigner_Sign/OptSignGroup1.golden
index 7ab0bc4..838cbf2 100644
--- a/pkg/integrity/testdata/TestSigner_Sign/OptSignGroup1.golden
+++ b/pkg/integrity/testdata/TestSigner_Sign/OptSignGroup1.golden
Binary files differ
diff --git a/pkg/integrity/testdata/TestSigner_Sign/OptSignGroup2.golden b/pkg/integrity/testdata/TestSigner_Sign/OptSignGroup2.golden
index 0deb841..8a0f4fa 100644
--- a/pkg/integrity/testdata/TestSigner_Sign/OptSignGroup2.golden
+++ b/pkg/integrity/testdata/TestSigner_Sign/OptSignGroup2.golden
Binary files differ
diff --git a/pkg/integrity/testdata/TestSigner_Sign/OptSignObject1.golden b/pkg/integrity/testdata/TestSigner_Sign/OptSignObject1.golden
index bfc3afe..87aaa13 100644
--- a/pkg/integrity/testdata/TestSigner_Sign/OptSignObject1.golden
+++ b/pkg/integrity/testdata/TestSigner_Sign/OptSignObject1.golden
Binary files differ
diff --git a/pkg/integrity/testdata/TestSigner_Sign/OptSignObject2.golden b/pkg/integrity/testdata/TestSigner_Sign/OptSignObject2.golden
index 58b9acc..b5d103c 100644
--- a/pkg/integrity/testdata/TestSigner_Sign/OptSignObject2.golden
+++ b/pkg/integrity/testdata/TestSigner_Sign/OptSignObject2.golden
Binary files differ
diff --git a/pkg/integrity/testdata/TestSigner_Sign/OptSignObject3.golden b/pkg/integrity/testdata/TestSigner_Sign/OptSignObject3.golden
index 0deb841..8a0f4fa 100644
--- a/pkg/integrity/testdata/TestSigner_Sign/OptSignObject3.golden
+++ b/pkg/integrity/testdata/TestSigner_Sign/OptSignObject3.golden
Binary files differ
diff --git a/pkg/integrity/testdata/TestSigner_Sign/OptSignObjects.golden b/pkg/integrity/testdata/TestSigner_Sign/OptSignObjects.golden
index fd37b0d..a496b2e 100644
--- a/pkg/integrity/testdata/TestSigner_Sign/OptSignObjects.golden
+++ b/pkg/integrity/testdata/TestSigner_Sign/OptSignObjects.golden
Binary files differ
diff --git a/pkg/integrity/testdata/TestSigner_Sign/TwoGroups.golden b/pkg/integrity/testdata/TestSigner_Sign/TwoGroups.golden
index fd37b0d..a496b2e 100644
--- a/pkg/integrity/testdata/TestSigner_Sign/TwoGroups.golden
+++ b/pkg/integrity/testdata/TestSigner_Sign/TwoGroups.golden
Binary files differ
diff --git a/pkg/integrity/testdata/Test_clearsignEncoder_signMessage/OK.golden b/pkg/integrity/testdata/Test_clearsignEncoder_signMessage/OK.golden
new file mode 100644
index 0000000..b8682b3
--- /dev/null
+++ b/pkg/integrity/testdata/Test_clearsignEncoder_signMessage/OK.golden
@@ -0,0 +1,15 @@
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA256
+
+{"One":1,"Two":2}
+-----BEGIN PGP SIGNATURE-----
+
+wsBzBAEBCAAnBQJZr0CRCZCiDCfuf/e6hBYhBBIEXIwLEATQWN5L7aIMJ+5/97qE
+AACREgf8DNQbgGAyNKBnKtjx5pPBu0XUNNM0tEsaBM4RbWA68W0KzS6XefIeuRs4
+73JQh+YcLUcb3kffyezb3VFn4wUjZmTJEwi6uPldSJYrex4mLSzKDukj7D4ZaAvc
+qk1rFfi/rwFTAPGA10j1UAwfHEcv+dNZc0LdIEcKNxL2SxACMMrs5LjncQcNBKLe
+ATlaMLV4fQ3MKDRdopi7U7tkH3vaF9QyrD2pxI0yCdRyRy0Xlc4i60zbE7wWPGJx
+E3EWFIylZyKzAbZlz4pSUWqts08+wAbWbwg0EPrsLd3ZV5LkO2aFlyqS1/A73sYb
+4QRf6OiFm24EfTVcfbKeiTG/obwv/w==
+=OJk4
+-----END PGP SIGNATURE----- \ No newline at end of file
diff --git a/pkg/integrity/verify.go b/pkg/integrity/verify.go
index ef9e3e5..0d7e6b0 100644
--- a/pkg/integrity/verify.go
+++ b/pkg/integrity/verify.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2020-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
// distributed with the sources of this project regarding your rights to use or distribute this
// software.
@@ -7,7 +7,9 @@ package integrity
import (
"bytes"
+ "crypto"
"encoding/hex"
+ "encoding/json"
"errors"
"fmt"
"io"
@@ -67,7 +69,6 @@ type VerifyCallback func(r VerifyResult) (ignoreError bool)
type groupVerifier struct {
f *sif.FileImage // SIF image to verify.
- cb VerifyCallback // Verification callback.
groupID uint32 // Object group ID.
ods []sif.Descriptor // Object descriptors.
subsetOK bool // If true, permit ods to be a subset of the objects in signatures.
@@ -75,8 +76,8 @@ type groupVerifier struct {
// newGroupVerifier constructs a new group verifier, optionally limited to objects described by
// ods. If no descriptors are supplied, verify all objects in group.
-func newGroupVerifier(f *sif.FileImage, cb VerifyCallback, groupID uint32, ods ...sif.Descriptor) (*groupVerifier, error) { // nolint:lll
- v := groupVerifier{f: f, cb: cb, groupID: groupID, ods: ods}
+func newGroupVerifier(f *sif.FileImage, groupID uint32, ods ...sif.Descriptor) (*groupVerifier, error) {
+ v := groupVerifier{f: f, groupID: groupID, ods: ods}
if len(ods) == 0 {
ods, err := getGroupObjects(f, groupID)
@@ -91,163 +92,114 @@ func newGroupVerifier(f *sif.FileImage, cb VerifyCallback, groupID uint32, ods .
return &v, nil
}
-// fingerprints returns a sorted list of unique fingerprints of entities that have signed the
-// objects specified by v.
-func (v *groupVerifier) fingerprints() ([][]byte, error) {
- sigs, err := getGroupSignatures(v.f, v.groupID, false)
- if errors.Is(err, &SignatureNotFoundError{}) {
- return nil, nil
- } else if err != nil {
- return nil, err
- }
- return getFingerprints(sigs)
+// signatures returns descriptors in f that contain signature objects linked to the objects
+// specified by v. If no such signatures are found, a SignatureNotFoundError is returned.
+func (v *groupVerifier) signatures() ([]sif.Descriptor, error) {
+ return getGroupSignatures(v.f, v.groupID, false)
}
-// verifySignature verifies the objects specified by v against signature sig using keyring kr.
+// verifySignature performs cryptographic validation of the digital signature contained in sig
+// using decoder de, populating vr as appropriate.
//
// If an invalid signature is encountered, a SignatureNotValidError is returned.
//
// If verification of the SIF global header fails, ErrHeaderIntegrity is returned. If verification
// of a data object descriptor fails, a DescriptorIntegrityError is returned. If verification of a
// data object fails, a ObjectIntegrityError is returned.
-func (v *groupVerifier) verifySignature(sig sif.Descriptor, kr openpgp.KeyRing) ([]sif.Descriptor, *openpgp.Entity, error) { // nolint:lll
- b, err := sig.GetData()
+func (v *groupVerifier) verifySignature(sig sif.Descriptor, de decoder, vr *VerifyResult) error {
+ ht, fp, err := sig.SignatureMetadata()
+ if err != nil {
+ return err
+ }
+
+ // Verify signature and decode message.
+ b, err := de.verifyMessage(sig.GetReader(), ht, vr)
if err != nil {
- return nil, nil, err
+ return &SignatureNotValidError{ID: sig.ID(), Err: err}
}
- // Verify signature and decode image metadata.
+ // Unmarshal image metadata.
var im imageMetadata
- e, _, err := verifyAndDecodeJSON(b, &im, kr)
- if err != nil {
- return nil, e, &SignatureNotValidError{ID: sig.ID(), Err: err}
+ if err = json.Unmarshal(b, &im); err != nil {
+ return &SignatureNotValidError{ID: sig.ID(), Err: err}
}
// Get minimum object ID in group, and use this to populate absolute object IDs in im.
minID, err := getGroupMinObjectID(v.f, v.groupID)
if err != nil {
- return nil, e, err
+ return err
}
im.populateAbsoluteObjectIDs(minID)
// Ensure signing entity matches fingerprint in descriptor.
- _, fp, err := sig.SignatureMetadata()
- if err != nil {
- return nil, e, err
- }
- if !bytes.Equal(e.PrimaryKey.Fingerprint, fp) {
- return nil, e, errFingerprintMismatch
+ if e := vr.e; e != nil && !bytes.Equal(e.PrimaryKey.Fingerprint, fp) {
+ return errFingerprintMismatch
}
// If an object subset is not permitted, verify our set of IDs match exactly what is in the
// image metadata.
if !v.subsetOK {
if err := im.objectIDsMatch(v.ods); err != nil {
- return nil, e, err
- }
- }
-
- // Verify header and object integrity.
- verified, err := im.matches(v.f, v.ods)
- if err != nil {
- return verified, e, err
- }
-
- return verified, e, nil
-}
-
-// verifyWithKeyRing performs verification of the objects specified by v using keyring kr.
-//
-// If no signatures are found for the object group specified by v, a SignatureNotFoundError is
-// returned. If an invalid signature is encountered, a SignatureNotValidError is returned.
-//
-// If verification of the SIF global header fails, ErrHeaderIntegrity is returned. If verification
-// of a data object descriptor fails, a DescriptorIntegrityError is returned. If verification of a
-// data object fails, a ObjectIntegrityError is returned.
-func (v *groupVerifier) verifyWithKeyRing(kr openpgp.KeyRing) error {
- // Obtain all signatures related to group.
- sigs, err := getGroupSignatures(v.f, v.groupID, false)
- if err != nil {
- return err
- }
-
- for _, sig := range sigs {
- verified, e, err := v.verifySignature(sig, kr)
-
- // Call verify callback, if applicable.
- if v.cb != nil {
- r := VerifyResult{sig: sig, verified: verified, e: e, err: err}
- if ignoreError := v.cb(r); ignoreError {
- err = nil
- }
- }
-
- if err != nil {
return err
}
}
- return nil
+ // Verify header and object integrity.
+ vr.verified, err = im.matches(v.f, v.ods)
+ return err
}
type legacyGroupVerifier struct {
f *sif.FileImage // SIF image to verify.
- cb VerifyCallback // Verification callback.
groupID uint32 // Object group ID.
ods []sif.Descriptor // Object descriptors.
}
// newLegacyGroupVerifier constructs a new legacy group verifier.
-func newLegacyGroupVerifier(f *sif.FileImage, cb VerifyCallback, groupID uint32) (*legacyGroupVerifier, error) {
+func newLegacyGroupVerifier(f *sif.FileImage, groupID uint32) (*legacyGroupVerifier, error) {
ods, err := getGroupObjects(f, groupID)
if err != nil {
return nil, err
}
- return &legacyGroupVerifier{f: f, cb: cb, groupID: groupID, ods: ods}, nil
+
+ return &legacyGroupVerifier{f: f, groupID: groupID, ods: ods}, nil
}
-// fingerprints returns a sorted list of unique fingerprints of entities that have signed the
-// objects specified by v.
-func (v *legacyGroupVerifier) fingerprints() ([][]byte, error) {
- sigs, err := getGroupSignatures(v.f, v.groupID, true)
- if errors.Is(err, &SignatureNotFoundError{}) {
- return nil, nil
- } else if err != nil {
- return nil, err
- }
- return getFingerprints(sigs)
+// signatures returns descriptors in f that contain signature objects linked to the objects
+// specified by v. If no such signatures are found, a SignatureNotFoundError is returned.
+func (v *legacyGroupVerifier) signatures() ([]sif.Descriptor, error) {
+ return getGroupSignatures(v.f, v.groupID, true)
}
-// verifySignature verifies the objects specified by v against signature sig using keyring kr.
+// verifySignature performs cryptographic validation of the digital signature contained in sig
+// using decoder de, populating vr as appropriate.
//
// If an invalid signature is encountered, a SignatureNotValidError is returned.
//
// If verification of a data object fails, a ObjectIntegrityError is returned.
-func (v *legacyGroupVerifier) verifySignature(sig sif.Descriptor, kr openpgp.KeyRing) (*openpgp.Entity, error) {
- b, err := sig.GetData()
+func (v *legacyGroupVerifier) verifySignature(sig sif.Descriptor, de decoder, vr *VerifyResult) error {
+ // Verify signature and decode message.
+ b, err := de.verifyMessage(sig.GetReader(), crypto.SHA256, vr)
if err != nil {
- return nil, err
+ return &SignatureNotValidError{ID: sig.ID(), Err: err}
}
- // Verify signature and decode plaintext.
- e, b, _, err := verifyAndDecode(b, kr)
+ ht, fp, err := sig.SignatureMetadata()
if err != nil {
- return e, &SignatureNotValidError{ID: sig.ID(), Err: err}
+ return err
}
// Ensure signing entity matches fingerprint in descriptor.
- ht, fp, err := sig.SignatureMetadata()
- if err != nil {
- return e, err
- }
- if !bytes.Equal(e.PrimaryKey.Fingerprint, fp) {
- return e, errFingerprintMismatch
+ if e := vr.e; e != nil {
+ if !bytes.Equal(e.PrimaryKey.Fingerprint, fp) {
+ return errFingerprintMismatch
+ }
}
// Obtain digest from plaintext.
d, err := newLegacyDigest(ht, b)
if err != nil {
- return e, err
+ return err
}
// Get reader covering all non-signature objects.
@@ -259,156 +211,93 @@ func (v *legacyGroupVerifier) verifySignature(sig sif.Descriptor, kr openpgp.Key
// Verify integrity of objects.
if ok, err := d.matches(r); err != nil {
- return e, err
- } else if !ok {
- return e, &ObjectIntegrityError{}
- }
-
- return e, nil
-}
-
-// verifyWithKeyRing performs verification of the objects specified by v using keyring kr.
-//
-// If no signatures are found for the object group specified by v, a SignatureNotFoundError is
-// returned. If an invalid signature is encountered, a SignatureNotValidError is returned.
-//
-// If verification of the data object group fails, a ObjectIntegrityError is returned.
-func (v *legacyGroupVerifier) verifyWithKeyRing(kr openpgp.KeyRing) error {
- // Obtain all signatures related to object.
- sigs, err := getGroupSignatures(v.f, v.groupID, true)
- if err != nil {
return err
+ } else if !ok {
+ return &ObjectIntegrityError{}
}
- for _, sig := range sigs {
- e, err := v.verifySignature(sig, kr)
-
- // Call verify callback, if applicable.
- if v.cb != nil {
- r := VerifyResult{sig: sig, e: e, err: err}
- if err == nil {
- r.verified = v.ods
- }
- if ignoreError := v.cb(r); ignoreError {
- err = nil
- }
- }
-
- if err != nil {
- return err
- }
- }
-
+ vr.verified = v.ods
return nil
}
type legacyObjectVerifier struct {
f *sif.FileImage // SIF image to verify.
- cb VerifyCallback // Verification callback.
od sif.Descriptor // Object descriptor.
}
// newLegacyObjectVerifier constructs a new legacy object verifier.
-func newLegacyObjectVerifier(f *sif.FileImage, cb VerifyCallback, id uint32) (*legacyObjectVerifier, error) {
- od, err := f.GetDescriptor(sif.WithID(id))
- if err != nil {
- return nil, err
- }
- return &legacyObjectVerifier{f: f, cb: cb, od: od}, nil
+func newLegacyObjectVerifier(f *sif.FileImage, od sif.Descriptor) *legacyObjectVerifier {
+ return &legacyObjectVerifier{f: f, od: od}
}
-// fingerprints returns a sorted list of unique fingerprints of entities that have signed the
-// objects specified by v.
-func (v *legacyObjectVerifier) fingerprints() ([][]byte, error) {
- sigs, err := getObjectSignatures(v.f, v.od.ID())
- if errors.Is(err, &SignatureNotFoundError{}) {
- return nil, nil
- } else if err != nil {
- return nil, err
- }
- return getFingerprints(sigs)
+// signatures returns descriptors in f that contain signature objects linked to the objects
+// specified by v. If no such signatures are found, a SignatureNotFoundError is returned.
+func (v *legacyObjectVerifier) signatures() ([]sif.Descriptor, error) {
+ return getObjectSignatures(v.f, v.od.ID())
}
-// verifySignature verifies the objects specified by v against signature sig using keyring kr.
+// verifySignature performs cryptographic validation of the digital signature contained in sig
+// using decoder de, populating vr as appropriate.
//
// If an invalid signature is encountered, a SignatureNotValidError is returned.
//
// If verification of a data object fails, a ObjectIntegrityError is returned.
-func (v *legacyObjectVerifier) verifySignature(sig sif.Descriptor, kr openpgp.KeyRing) (*openpgp.Entity, error) {
- b, err := sig.GetData()
+func (v *legacyObjectVerifier) verifySignature(sig sif.Descriptor, de decoder, vr *VerifyResult) error {
+ // Verify signature and decode message.
+ b, err := de.verifyMessage(sig.GetReader(), crypto.SHA256, vr)
if err != nil {
- return nil, err
+ return &SignatureNotValidError{ID: sig.ID(), Err: err}
}
- // Verify signature and decode plaintext.
- e, b, _, err := verifyAndDecode(b, kr)
+ ht, fp, err := sig.SignatureMetadata()
if err != nil {
- return e, &SignatureNotValidError{ID: sig.ID(), Err: err}
+ return err
}
// Ensure signing entity matches fingerprint in descriptor.
- ht, fp, err := sig.SignatureMetadata()
- if err != nil {
- return e, err
- }
- if !bytes.Equal(e.PrimaryKey.Fingerprint, fp) {
- return e, errFingerprintMismatch
+ if e := vr.e; e != nil {
+ if !bytes.Equal(e.PrimaryKey.Fingerprint, fp) {
+ return errFingerprintMismatch
+ }
}
// Obtain digest from plaintext.
d, err := newLegacyDigest(ht, b)
if err != nil {
- return e, err
+ return err
}
// Verify object integrity.
if ok, err := d.matches(v.od.GetReader()); err != nil {
- return e, err
+ return err
} else if !ok {
- return e, &ObjectIntegrityError{ID: v.od.ID()}
+ return &ObjectIntegrityError{ID: v.od.ID()}
}
- return e, nil
+ vr.verified = []sif.Descriptor{v.od}
+ return nil
}
-// verifyWithKeyRing performs verification of the objects specified by v using keyring kr.
-//
-// If no signatures are found for the object specified by v, a SignatureNotFoundError is returned.
-// If an invalid signature is encountered, a SignatureNotValidError is returned.
-//
-// If verification of the data object fails, a ObjectIntegrityError is returned.
-func (v *legacyObjectVerifier) verifyWithKeyRing(kr openpgp.KeyRing) error {
- // Obtain all signatures related to object.
- sigs, err := getObjectSignatures(v.f, v.od.ID())
- if err != nil {
- return err
- }
-
- for _, sig := range sigs {
- e, err := v.verifySignature(sig, kr)
-
- // Call verify callback, if applicable.
- if v.cb != nil {
- r := VerifyResult{sig: sig, e: e, err: err}
- if err == nil {
- r.verified = []sif.Descriptor{v.od}
- }
- if ignoreError := v.cb(r); ignoreError {
- err = nil
- }
- }
-
- if err != nil {
- return err
- }
- }
-
- return nil
+type decoder interface {
+ // verifyMessage reads a message from r, verifies its signature, and returns the message
+ // contents.
+ verifyMessage(r io.Reader, h crypto.Hash, vr *VerifyResult) ([]byte, error)
}
type verifyTask interface {
- fingerprints() ([][]byte, error)
- verifyWithKeyRing(kr openpgp.KeyRing) error
+ // signatures returns descriptors that contain signature objects linked to the task. If no such
+ // signatures are found, a SignatureNotFoundError is returned.
+ signatures() ([]sif.Descriptor, error)
+
+ // verifySignature performs cryptographic validation of the digital signature contained in sig
+ // using decoder de, populating vr as appropriate.
+ //
+ // If an invalid signature is encountered, a SignatureNotValidError is returned.
+ //
+ // If verification of the SIF global header fails, ErrHeaderIntegrity is returned. If
+ // verification of a data object descriptor fails, a DescriptorIntegrityError is returned. If
+ // verification of a data object fails, a ObjectIntegrityError is returned.
+ verifySignature(sig sif.Descriptor, de decoder, vr *VerifyResult) error
}
type verifyOpts struct {
@@ -493,11 +382,11 @@ func OptVerifyCallback(cb VerifyCallback) VerifierOpt {
}
// getTasks returns verification tasks corresponding to groupIDs and objectIDs.
-func getTasks(f *sif.FileImage, cb VerifyCallback, groupIDs, objectIDs []uint32) ([]verifyTask, error) {
+func getTasks(f *sif.FileImage, groupIDs, objectIDs []uint32) ([]verifyTask, error) {
t := make([]verifyTask, 0, len(groupIDs)+len(objectIDs))
for _, groupID := range groupIDs {
- v, err := newGroupVerifier(f, cb, groupID)
+ v, err := newGroupVerifier(f, groupID)
if err != nil {
return nil, err
}
@@ -510,7 +399,7 @@ func getTasks(f *sif.FileImage, cb VerifyCallback, groupIDs, objectIDs []uint32)
return nil, err
}
- v, err := newGroupVerifier(f, cb, od.GroupID(), od)
+ v, err := newGroupVerifier(f, od.GroupID(), od)
if err != nil {
return nil, err
}
@@ -521,11 +410,11 @@ func getTasks(f *sif.FileImage, cb VerifyCallback, groupIDs, objectIDs []uint32)
}
// getLegacyTasks returns legacy verification tasks corresponding to groupIDs and objectIDs.
-func getLegacyTasks(f *sif.FileImage, cb VerifyCallback, groupIDs, objectIDs []uint32) ([]verifyTask, error) {
+func getLegacyTasks(f *sif.FileImage, groupIDs, objectIDs []uint32) ([]verifyTask, error) {
t := make([]verifyTask, 0, len(groupIDs)+len(objectIDs))
for _, groupID := range groupIDs {
- v, err := newLegacyGroupVerifier(f, cb, groupID)
+ v, err := newLegacyGroupVerifier(f, groupID)
if err != nil {
return nil, err
}
@@ -533,11 +422,12 @@ func getLegacyTasks(f *sif.FileImage, cb VerifyCallback, groupIDs, objectIDs []u
}
for _, id := range objectIDs {
- v, err := newLegacyObjectVerifier(f, cb, id)
+ od, err := f.GetDescriptor(sif.WithID(id))
if err != nil {
return nil, err
}
- t = append(t, v)
+
+ t = append(t, newLegacyObjectVerifier(f, od))
}
return t, nil
@@ -546,8 +436,9 @@ func getLegacyTasks(f *sif.FileImage, cb VerifyCallback, groupIDs, objectIDs []u
// Verifier describes a SIF image verifier.
type Verifier struct {
f *sif.FileImage
- kr openpgp.KeyRing
tasks []verifyTask
+ kr openpgp.KeyRing
+ cb VerifyCallback
}
// NewVerifier returns a Verifier to examine and/or verify digital signatures(s) in f according to
@@ -598,15 +489,16 @@ func NewVerifier(f *sif.FileImage, opts ...VerifierOpt) (*Verifier, error) {
if vo.isLegacy {
getTasksFunc = getLegacyTasks
}
- t, err := getTasksFunc(f, vo.cb, vo.groups, vo.objects)
+ t, err := getTasksFunc(f, vo.groups, vo.objects)
if err != nil {
return nil, fmt.Errorf("integrity: %w", err)
}
v := Verifier{
f: f,
- kr: vo.kr,
tasks: t,
+ kr: vo.kr,
+ cb: vo.cb,
}
return &v, nil
}
@@ -619,7 +511,12 @@ func (v *Verifier) fingerprints(any bool) ([][]byte, error) {
// Build up a map containing fingerprints, and the number of tasks they are participating in.
for _, t := range v.tasks {
- fps, err := t.fingerprints()
+ sigs, err := t.signatures()
+ if err != nil && !errors.Is(err, &SignatureNotFoundError{}) {
+ return nil, err
+ }
+
+ fps, err := getFingerprints(sigs)
if err != nil {
return nil, err
}
@@ -688,7 +585,12 @@ func (v *Verifier) AllSignedBy() ([][]byte, error) {
// DescriptorIntegrityError is returned. If verification of a data object fails, an error wrapping
// a ObjectIntegrityError is returned.
func (v *Verifier) Verify() error {
- if v.kr == nil {
+ // Get message decoder.
+ var de decoder
+ switch {
+ case v.kr != nil:
+ de = newClearsignDecoder(v.kr)
+ default:
return fmt.Errorf("integrity: %w", ErrNoKeyMaterial)
}
@@ -703,10 +605,32 @@ func (v *Verifier) Verify() error {
}
}
+ // Verify signature(s) associated with each task.
for _, t := range v.tasks {
- if err := t.verifyWithKeyRing(v.kr); err != nil {
+ sigs, err := t.signatures()
+ if err != nil {
return fmt.Errorf("integrity: %w", err)
}
+
+ for _, sig := range sigs {
+ vr := VerifyResult{sig: sig}
+
+ // Verify signature.
+ err := t.verifySignature(sig, de, &vr)
+
+ // Call verify callback, if applicable.
+ if v.cb != nil {
+ vr.err = err
+ if ignoreError := v.cb(vr); ignoreError {
+ err = nil
+ }
+ }
+
+ if err != nil {
+ return fmt.Errorf("integrity: %w", err)
+ }
+ }
}
+
return nil
}
diff --git a/pkg/integrity/verify_test.go b/pkg/integrity/verify_test.go
index e32b5fd..8c29782 100644
--- a/pkg/integrity/verify_test.go
+++ b/pkg/integrity/verify_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2020-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
// distributed with the sources of this project regarding your rights to use or distribute this
// software.
@@ -6,10 +6,12 @@
package integrity
import (
+ "crypto"
"errors"
"io"
"path/filepath"
"reflect"
+ "strings"
"testing"
"github.com/ProtonMail/go-crypto/openpgp"
@@ -17,29 +19,33 @@ import (
"github.com/sylabs/sif/v2/pkg/sif"
)
-func TestGroupVerifier_fingerprints(t *testing.T) {
+func TestGroupVerifier_signatures(t *testing.T) {
oneGroupImage := loadContainer(t, filepath.Join(corpus, "one-group.sif"))
oneGroupSignedImage := loadContainer(t, filepath.Join(corpus, "one-group-signed.sif"))
- e := getTestEntity(t)
+ sigs, err := oneGroupSignedImage.GetDescriptors(sif.WithDataType(sif.DataSignature))
+ if err != nil {
+ t.Fatal(err)
+ }
tests := []struct {
- name string
- f *sif.FileImage
- groupID uint32
- wantFPs [][]byte
- wantErr error
+ name string
+ f *sif.FileImage
+ groupID uint32
+ wantSigs []sif.Descriptor
+ wantErr error
}{
{
name: "Unsigned",
f: oneGroupImage,
groupID: 1,
+ wantErr: &SignatureNotFoundError{},
},
{
- name: "Signed",
- f: oneGroupSignedImage,
- groupID: 1,
- wantFPs: [][]byte{e.PrimaryKey.Fingerprint},
+ name: "Signed",
+ f: oneGroupSignedImage,
+ groupID: 1,
+ wantSigs: sigs,
},
}
@@ -51,114 +57,88 @@ func TestGroupVerifier_fingerprints(t *testing.T) {
groupID: tt.groupID,
}
- got, err := v.fingerprints()
+ sigs, err := v.signatures()
- if !errors.Is(err, tt.wantErr) {
- t.Errorf("got error %v, want %v", err, tt.wantErr)
+ if got, want := err, tt.wantErr; !errors.Is(got, want) {
+ t.Fatalf("got error %v, want %v", got, want)
}
- if !reflect.DeepEqual(got, tt.wantFPs) {
- t.Errorf("got fingerprints %v, want %v", got, tt.wantFPs)
+ if got, want := sigs, tt.wantSigs; !reflect.DeepEqual(got, want) {
+ t.Errorf("got signatures %v, want %v", got, want)
}
})
}
}
-func TestGroupVerifier_verifyWithKeyRing(t *testing.T) {
- oneGroupImage := loadContainer(t, filepath.Join(corpus, "one-group.sif"))
+func TestGroupVerifier_verify(t *testing.T) {
oneGroupSignedImage := loadContainer(t, filepath.Join(corpus, "one-group-signed.sif"))
+ sig, err := oneGroupSignedImage.GetDescriptor(sif.WithDataType(sif.DataSignature))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ verified, err := oneGroupSignedImage.GetDescriptors(sif.WithGroupID(1))
+ if err != nil {
+ t.Fatal(err)
+ }
+
e := getTestEntity(t)
- kr := openpgp.EntityList{e}
tests := []struct {
- name string
- f *sif.FileImage
- testCallback bool
- ignoreError bool
- groupID uint32
- objectIDs []uint32
- subsetOK bool
- kr openpgp.KeyRing
- wantCBSignature uint32
- wantCBVerified []uint32
- wantCBEntity *openpgp.Entity
- wantCBErr error
- wantErr error
+ name string
+ f *sif.FileImage
+ groupID uint32
+ objectIDs []uint32
+ subsetOK bool
+ sig sif.Descriptor
+ de decoder
+ wantErr error
+ wantVerified []sif.Descriptor
+ wantEntity *openpgp.Entity
}{
{
- name: "SignatureNotFound",
- f: oneGroupImage,
- groupID: 1,
- objectIDs: []uint32{1, 2},
- kr: kr,
- wantErr: &SignatureNotFoundError{},
- },
- {
- name: "SignedObjectNotFound",
- f: oneGroupSignedImage,
- groupID: 1,
- objectIDs: []uint32{1},
- kr: kr,
- wantErr: errSignedObjectNotFound,
+ name: "SignedObjectNotFound",
+ f: oneGroupSignedImage,
+ groupID: 1,
+ objectIDs: []uint32{1},
+ sig: sig,
+ de: newClearsignDecoder(openpgp.EntityList{e}),
+ wantErr: errSignedObjectNotFound,
+ wantEntity: e,
},
{
name: "UnknownIssuer",
f: oneGroupSignedImage,
groupID: 1,
objectIDs: []uint32{1, 2},
- kr: openpgp.EntityList{},
- wantErr: &SignatureNotValidError{ID: 3, Err: pgperrors.ErrUnknownIssuer},
- },
- {
- name: "IgnoreError",
- f: oneGroupSignedImage,
- testCallback: true,
- ignoreError: true,
- groupID: 1,
- objectIDs: []uint32{1, 2},
- kr: openpgp.EntityList{},
- wantCBSignature: 3,
- wantCBErr: &SignatureNotValidError{ID: 3, Err: pgperrors.ErrUnknownIssuer},
- wantErr: nil,
- },
- {
- name: "OneGroupSigned",
- f: oneGroupSignedImage,
- groupID: 1,
- objectIDs: []uint32{1, 2},
- kr: kr,
- },
- {
- name: "OneGroupSignedWithCallback",
- f: oneGroupSignedImage,
- testCallback: true,
- groupID: 1,
- objectIDs: []uint32{1, 2},
- kr: kr,
- wantCBSignature: 3,
- wantCBVerified: []uint32{1, 2},
- wantCBEntity: e,
+ sig: sig,
+ de: newClearsignDecoder(openpgp.EntityList{}),
+ wantErr: &SignatureNotValidError{
+ ID: 3,
+ Err: pgperrors.ErrUnknownIssuer,
+ },
},
{
- name: "OneGroupSignedSubset",
- f: oneGroupSignedImage,
- groupID: 1,
- objectIDs: []uint32{1},
- subsetOK: true,
- kr: kr,
+ name: "OneGroupSigned",
+ f: oneGroupSignedImage,
+ groupID: 1,
+ objectIDs: []uint32{1, 2},
+ sig: sig,
+ de: newClearsignDecoder(openpgp.EntityList{e}),
+ wantVerified: verified,
+ wantEntity: e,
},
{
- name: "OneGroupSignedSubsetWithCallback",
- f: oneGroupSignedImage,
- testCallback: true,
- groupID: 1,
- objectIDs: []uint32{1},
- subsetOK: true,
- kr: kr,
- wantCBSignature: 3,
- wantCBVerified: []uint32{1},
- wantCBEntity: e,
+ name: "OneGroupSignedSubset",
+ f: oneGroupSignedImage,
+ groupID: 1,
+ objectIDs: []uint32{1},
+ subsetOK: true,
+ sig: sig,
+ de: newClearsignDecoder(openpgp.EntityList{e}),
+ wantVerified: verified[:1],
+ wantEntity: e,
},
}
@@ -174,75 +154,58 @@ func TestGroupVerifier_verifyWithKeyRing(t *testing.T) {
ods[i] = od
}
- // Test callback functionality, if requested.
- var cb VerifyCallback
-
- //nolint:dupl
- if tt.testCallback {
- cb = func(r VerifyResult) bool {
- if got, want := r.Signature().ID(), tt.wantCBSignature; got != want {
- t.Errorf("got signature %v, want %v", got, want)
- }
-
- if got, want := len(r.Verified()), len(tt.wantCBVerified); got != want {
- t.Fatalf("got %v verified objects, want %v", got, want)
- }
- for i, od := range r.Verified() {
- if got, want := od.ID(), tt.wantCBVerified[i]; got != want {
- t.Errorf("got verified ID %v, want %v", got, want)
- }
- }
-
- if got, want := r.Entity(), tt.wantCBEntity; got != want {
- t.Errorf("got entity %v, want %v", got, want)
- }
-
- if got, want := r.Error(), tt.wantCBErr; !errors.Is(got, want) {
- t.Errorf("got error %v, want %v", got, want)
- }
-
- return tt.ignoreError
- }
- }
-
v := &groupVerifier{
f: tt.f,
- cb: cb,
groupID: tt.groupID,
ods: ods,
subsetOK: tt.subsetOK,
}
- if got, want := v.verifyWithKeyRing(tt.kr), tt.wantErr; !errors.Is(got, want) {
+ var vr VerifyResult
+ err := v.verifySignature(tt.sig, tt.de, &vr)
+
+ if got, want := err, tt.wantErr; !errors.Is(got, want) {
t.Errorf("got error %v, want %v", got, want)
}
+
+ if got, want := vr.Verified(), tt.wantVerified; !reflect.DeepEqual(got, want) {
+ t.Errorf("got verified %v, want %v", got, want)
+ }
+
+ if got, want := vr.Entity(), tt.wantEntity; !reflect.DeepEqual(got, want) {
+ t.Errorf("got entity %v, want %v", got, want)
+ }
})
}
}
-func TestLegacyGroupVerifier_fingerprints(t *testing.T) {
+func TestLegacyGroupVerifier_signatures(t *testing.T) {
oneGroupImage := loadContainer(t, filepath.Join(corpus, "one-group.sif"))
- oneGroupImageSigned := loadContainer(t, filepath.Join(corpus, "one-group-signed-legacy-group.sif"))
+ oneGroupSignedImage := loadContainer(t, filepath.Join(corpus, "one-group-signed-legacy-group.sif"))
- e := getTestEntity(t)
+ sigs, err := oneGroupSignedImage.GetDescriptors(sif.WithDataType(sif.DataSignature))
+ if err != nil {
+ t.Fatal(err)
+ }
tests := []struct {
- name string
- f *sif.FileImage
- id uint32
- wantFPs [][]byte
- wantErr error
+ name string
+ f *sif.FileImage
+ id uint32
+ wantSigs []sif.Descriptor
+ wantErr error
}{
{
- name: "Unsigned",
- f: oneGroupImage,
- id: 1,
+ name: "Unsigned",
+ f: oneGroupImage,
+ id: 1,
+ wantErr: &SignatureNotFoundError{},
},
{
- name: "Signed",
- f: oneGroupImageSigned,
- id: 1,
- wantFPs: [][]byte{e.PrimaryKey.Fingerprint},
+ name: "Signed",
+ f: oneGroupSignedImage,
+ id: 1,
+ wantSigs: sigs,
},
}
@@ -254,132 +217,69 @@ func TestLegacyGroupVerifier_fingerprints(t *testing.T) {
groupID: 1,
}
- got, err := v.fingerprints()
+ sigs, err := v.signatures()
- if !errors.Is(err, tt.wantErr) {
- t.Errorf("got error %v, want %v", err, tt.wantErr)
+ if got, want := err, tt.wantErr; !errors.Is(got, want) {
+ t.Fatalf("got error %v, want %v", got, want)
}
- if !reflect.DeepEqual(got, tt.wantFPs) {
- t.Errorf("got fingerprints %v, want %v", got, tt.wantFPs)
+ if got, want := sigs, tt.wantSigs; !reflect.DeepEqual(got, want) {
+ t.Errorf("got signatures %v, want %v", got, want)
}
})
}
}
-func TestLegacyGroupVerifier_verifyWithKeyRing(t *testing.T) {
- oneGroupImage := loadContainer(t, filepath.Join(corpus, "one-group.sif"))
+func TestLegacyGroupVerifier_verify(t *testing.T) {
oneGroupSignedImage := loadContainer(t, filepath.Join(corpus, "one-group-signed-legacy-group.sif"))
+ sig, err := oneGroupSignedImage.GetDescriptor(sif.WithDataType(sif.DataSignature))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ verified, err := oneGroupSignedImage.GetDescriptors(sif.WithGroupID(1))
+ if err != nil {
+ t.Fatal(err)
+ }
+
e := getTestEntity(t)
- kr := openpgp.EntityList{e}
tests := []struct {
- name string
- f *sif.FileImage
- testCallback bool
- ignoreError bool
- groupID uint32
- kr openpgp.KeyRing
- wantCBSignature uint32
- wantCBVerified []uint32
- wantCBEntity *openpgp.Entity
- wantCBErr error
- wantErr error
+ name string
+ f *sif.FileImage
+ groupID uint32
+ sig sif.Descriptor
+ de decoder
+ wantErr error
+ wantVerified []sif.Descriptor
+ wantEntity *openpgp.Entity
}{
{
- name: "SignatureNotFound",
- f: oneGroupImage,
- groupID: 1,
- kr: kr,
- wantErr: &SignatureNotFoundError{},
- },
- {
name: "UnknownIssuer",
f: oneGroupSignedImage,
groupID: 1,
- kr: openpgp.EntityList{},
- wantErr: pgperrors.ErrUnknownIssuer,
- },
- {
- name: "IgnoreError",
- f: oneGroupSignedImage,
- testCallback: true,
- ignoreError: true,
- groupID: 1,
- kr: openpgp.EntityList{},
- wantCBSignature: 3,
- wantCBErr: pgperrors.ErrUnknownIssuer,
- wantErr: nil,
- },
- {
- name: "OneGroupSigned",
- f: oneGroupSignedImage,
- groupID: 1,
- kr: kr,
- },
- {
- name: "OneGroupSignedWithCallback",
- f: oneGroupSignedImage,
- testCallback: true,
- groupID: 1,
- kr: kr,
- wantCBSignature: 3,
- wantCBVerified: []uint32{1, 2},
- wantCBEntity: e,
- },
- {
- name: "OneGroupSignedSubset",
- f: oneGroupSignedImage,
- groupID: 1,
- kr: kr,
+ sig: sig,
+ de: newClearsignDecoder(openpgp.EntityList{}),
+ wantErr: &SignatureNotValidError{
+ ID: 3,
+ Err: pgperrors.ErrUnknownIssuer,
+ },
},
{
- name: "OneGroupSignedSubsetWithCallback",
- f: oneGroupSignedImage,
- testCallback: true,
- groupID: 1,
- kr: kr,
- wantCBSignature: 3,
- wantCBVerified: []uint32{1, 2},
- wantCBEntity: e,
+ name: "OneGroupSigned",
+ f: oneGroupSignedImage,
+ groupID: 1,
+ sig: sig,
+ de: newClearsignDecoder(openpgp.EntityList{e}),
+ wantVerified: verified,
+ wantEntity: e,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
- // Test callback functionality, if requested.
- var cb VerifyCallback
-
- //nolint:dupl
- if tt.testCallback {
- cb = func(r VerifyResult) bool {
- if got, want := r.Signature().ID(), tt.wantCBSignature; got != want {
- t.Errorf("got signature %v, want %v", got, want)
- }
-
- if got, want := len(r.Verified()), len(tt.wantCBVerified); got != want {
- t.Fatalf("got %v verified objects, want %v", got, want)
- }
- for i, od := range r.Verified() {
- if got, want := od.ID(), tt.wantCBVerified[i]; got != want {
- t.Errorf("got verified ID %v, want %v", got, want)
- }
- }
-
- if got, want := r.Entity(), tt.wantCBEntity; got != want {
- t.Errorf("got entity %v, want %v", got, want)
- }
-
- if got, want := r.Error(), tt.wantCBErr; !errors.Is(got, want) {
- t.Errorf("got error %v, want %v", got, want)
- }
-
- return tt.ignoreError
- }
- }
-
ods, err := getGroupObjects(tt.f, tt.groupID)
if err != nil {
t.Fatal(err)
@@ -387,41 +287,58 @@ func TestLegacyGroupVerifier_verifyWithKeyRing(t *testing.T) {
v := &legacyGroupVerifier{
f: tt.f,
- cb: cb,
groupID: tt.groupID,
ods: ods,
}
- if got, want := v.verifyWithKeyRing(tt.kr), tt.wantErr; !errors.Is(got, want) {
+ var vr VerifyResult
+ err = v.verifySignature(tt.sig, tt.de, &vr)
+
+ if got, want := err, tt.wantErr; !errors.Is(got, want) {
t.Errorf("got error %v, want %v", got, want)
}
+
+ if got, want := vr.Verified(), tt.wantVerified; !reflect.DeepEqual(got, want) {
+ t.Errorf("got verified %v, want %v", got, want)
+ }
+
+ if got, want := vr.Entity(), tt.wantEntity; !reflect.DeepEqual(got, want) {
+ t.Errorf("got entity %v, want %v", got, want)
+ }
})
}
}
-func TestLegacyObjectVerifier_fingerprints(t *testing.T) {
+func TestLegacyObjectVerifier_signatures(t *testing.T) {
oneGroupImage := loadContainer(t, filepath.Join(corpus, "one-group.sif"))
- oneGroupImageSigned := loadContainer(t, filepath.Join(corpus, "one-group-signed-legacy-all.sif"))
+ oneGroupSignedImage := loadContainer(t, filepath.Join(corpus, "one-group-signed-legacy-all.sif"))
- e := getTestEntity(t)
+ sigs, err := oneGroupSignedImage.GetDescriptors(
+ sif.WithDataType(sif.DataSignature),
+ sif.WithLinkedID(1),
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
tests := []struct {
- name string
- f *sif.FileImage
- id uint32
- wantFPs [][]byte
- wantErr error
+ name string
+ f *sif.FileImage
+ id uint32
+ wantSigs []sif.Descriptor
+ wantErr error
}{
{
- name: "Unsigned",
- f: oneGroupImage,
- id: 1,
+ name: "Unsigned",
+ f: oneGroupImage,
+ id: 1,
+ wantErr: &SignatureNotFoundError{},
},
{
- name: "Signed",
- f: oneGroupImageSigned,
- id: 1,
- wantFPs: [][]byte{e.PrimaryKey.Fingerprint},
+ name: "Signed",
+ f: oneGroupSignedImage,
+ id: 1,
+ wantSigs: sigs,
},
}
@@ -438,132 +355,72 @@ func TestLegacyObjectVerifier_fingerprints(t *testing.T) {
od: od,
}
- got, err := v.fingerprints()
+ sigs, err := v.signatures()
- if !errors.Is(err, tt.wantErr) {
- t.Errorf("got error %v, want %v", err, tt.wantErr)
+ if got, want := err, tt.wantErr; !errors.Is(got, want) {
+ t.Fatalf("got error %v, want %v", got, want)
}
- if !reflect.DeepEqual(got, tt.wantFPs) {
- t.Errorf("got fingerprints %v, want %v", got, tt.wantFPs)
+ if got, want := sigs, tt.wantSigs; !reflect.DeepEqual(got, want) {
+ t.Errorf("got signatures %v, want %v", got, want)
}
})
}
}
-func TestLegacyObjectVerifier_verifyWithKeyRing(t *testing.T) {
- oneGroupImage := loadContainer(t, filepath.Join(corpus, "one-group.sif"))
+func TestLegacyObjectVerifier_verify(t *testing.T) {
oneGroupSignedImage := loadContainer(t, filepath.Join(corpus, "one-group-signed-legacy-all.sif"))
+ sig, err := oneGroupSignedImage.GetDescriptor(
+ sif.WithDataType(sif.DataSignature),
+ sif.WithLinkedID(1),
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ verified, err := oneGroupSignedImage.GetDescriptors(sif.WithID(1))
+ if err != nil {
+ t.Fatal(err)
+ }
+
e := getTestEntity(t)
- kr := openpgp.EntityList{e}
tests := []struct {
- name string
- f *sif.FileImage
- testCallback bool
- ignoreError bool
- id uint32
- kr openpgp.KeyRing
- wantCBSignature uint32
- wantCBVerified []uint32
- wantCBEntity *openpgp.Entity
- wantCBErr error
- wantErr error
+ name string
+ f *sif.FileImage
+ id uint32
+ sig sif.Descriptor
+ de decoder
+ wantErr error
+ wantVerified []sif.Descriptor
+ wantEntity *openpgp.Entity
}{
{
- name: "SignatureNotFound",
- f: oneGroupImage,
- id: 1,
- kr: kr,
- wantErr: &SignatureNotFoundError{},
- },
- {
- name: "UnknownIssuer",
- f: oneGroupSignedImage,
- id: 1,
- kr: openpgp.EntityList{},
- wantErr: pgperrors.ErrUnknownIssuer,
- },
- {
- name: "IgnoreError",
- f: oneGroupSignedImage,
- testCallback: true,
- ignoreError: true,
- id: 1,
- kr: openpgp.EntityList{},
- wantCBSignature: 3,
- wantCBErr: pgperrors.ErrUnknownIssuer,
- wantErr: nil,
- },
- {
- name: "OneGroupSigned",
+ name: "UnknownIssuer",
f: oneGroupSignedImage,
id: 1,
- kr: kr,
- },
- {
- name: "OneGroupSignedWithCallback",
- f: oneGroupSignedImage,
- testCallback: true,
- id: 1,
- kr: kr,
- wantCBSignature: 3,
- wantCBVerified: []uint32{1},
- wantCBEntity: e,
- },
- {
- name: "OneGroupSignedSubset",
- f: oneGroupSignedImage,
- id: 1,
- kr: kr,
+ sig: sig,
+ de: newClearsignDecoder(openpgp.EntityList{}),
+ wantErr: &SignatureNotValidError{
+ ID: 3,
+ Err: pgperrors.ErrUnknownIssuer,
+ },
},
{
- name: "OneGroupSignedSubsetWithCallback",
- f: oneGroupSignedImage,
- testCallback: true,
- id: 1,
- kr: kr,
- wantCBSignature: 3,
- wantCBVerified: []uint32{1},
- wantCBEntity: e,
+ name: "OneGroupSigned",
+ f: oneGroupSignedImage,
+ id: 1,
+ sig: sig,
+ de: newClearsignDecoder(openpgp.EntityList{e}),
+ wantVerified: verified,
+ wantEntity: e,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
- // Test callback functionality, if requested.
- var cb VerifyCallback
-
- //nolint:dupl
- if tt.testCallback {
- cb = func(r VerifyResult) bool {
- if got, want := r.Signature().ID(), tt.wantCBSignature; got != want {
- t.Errorf("got signature %v, want %v", got, want)
- }
-
- if got, want := len(r.Verified()), len(tt.wantCBVerified); got != want {
- t.Fatalf("got %v verified objects, want %v", got, want)
- }
- for i, od := range r.Verified() {
- if got, want := od.ID(), tt.wantCBVerified[i]; got != want {
- t.Errorf("got verified ID %v, want %v", got, want)
- }
- }
-
- if got, want := r.Entity(), tt.wantCBEntity; got != want {
- t.Errorf("got entity %v, want %v", got, want)
- }
-
- if got, want := r.Error(), tt.wantCBErr; !errors.Is(got, want) {
- t.Errorf("got error %v, want %v", got, want)
- }
-
- return tt.ignoreError
- }
- }
-
od, err := tt.f.GetDescriptor(sif.WithID(tt.id))
if err != nil {
t.Fatal(err)
@@ -571,13 +428,23 @@ func TestLegacyObjectVerifier_verifyWithKeyRing(t *testing.T) {
v := &legacyObjectVerifier{
f: tt.f,
- cb: cb,
od: od,
}
- if got, want := v.verifyWithKeyRing(tt.kr), tt.wantErr; !errors.Is(got, want) {
+ var vr VerifyResult
+ err = v.verifySignature(tt.sig, tt.de, &vr)
+
+ if got, want := err, tt.wantErr; !errors.Is(got, want) {
t.Errorf("got error %v, want %v", got, want)
}
+
+ if got, want := vr.Verified(), tt.wantVerified; !reflect.DeepEqual(got, want) {
+ t.Errorf("got verified %v, want %v", got, want)
+ }
+
+ if got, want := vr.Entity(), tt.wantEntity; !reflect.DeepEqual(got, want) {
+ t.Errorf("got entity %v, want %v", got, want)
+ }
})
}
}
@@ -811,16 +678,68 @@ func TestNewVerifier(t *testing.T) {
}
type mockVerifier struct {
- fps [][]byte
- err error
+ sigs []sif.Descriptor
+ sigsErr error
+
+ verified []sif.Descriptor
+ e *openpgp.Entity
+ verifyErr error
}
-func (v mockVerifier) fingerprints() ([][]byte, error) {
- return v.fps, v.err
+func (v mockVerifier) signatures() ([]sif.Descriptor, error) {
+ return v.sigs, v.sigsErr
}
-func (v mockVerifier) verifyWithKeyRing(kr openpgp.KeyRing) error {
- return v.err
+func (v mockVerifier) verifySignature(sig sif.Descriptor, de decoder, vr *VerifyResult) error {
+ vr.verified = v.verified
+ vr.e = v.e
+ return v.verifyErr
+}
+
+// getSignedDummy generates a dummy SIF container that contains a data object and one dummy
+// signature per fingerprint.
+func getSignedDummy(t *testing.T, fps ...[]byte) *sif.FileImage {
+ t.Helper()
+
+ di, err := sif.NewDescriptorInput(sif.DataGeneric, strings.NewReader("data"),
+ sif.OptGroupID(1),
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ dis := []sif.DescriptorInput{di}
+
+ for _, fp := range fps {
+ di, err := sif.NewDescriptorInput(sif.DataSignature, strings.NewReader("sig"),
+ sif.OptSignatureMetadata(crypto.SHA256, fp),
+ sif.OptNoGroup(),
+ sif.OptLinkedGroupID(1),
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ dis = append(dis, di)
+ }
+
+ var buf sif.Buffer
+
+ fi, err := sif.CreateContainer(&buf,
+ sif.OptCreateDeterministic(),
+ sif.OptCreateWithDescriptors(dis...),
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ t.Cleanup(func() {
+ if err := fi.UnloadContainer(); err != nil {
+ t.Error(err)
+ }
+ })
+
+ return fi
}
func TestVerifier_AnySignedBy(t *testing.T) {
@@ -834,6 +753,13 @@ func TestVerifier_AnySignedBy(t *testing.T) {
0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
}
+ fi := getSignedDummy(t, fp1, fp2)
+
+ sigs, err := fi.GetDescriptors(sif.WithDataType(sif.DataSignature))
+ if err != nil {
+ t.Fatal(err)
+ }
+
tests := []struct {
name string
tasks []verifyTask
@@ -843,48 +769,48 @@ func TestVerifier_AnySignedBy(t *testing.T) {
{
name: "OneTaskEOF",
tasks: []verifyTask{
- mockVerifier{err: io.EOF},
+ mockVerifier{sigsErr: io.EOF},
},
wantErr: io.EOF,
},
{
name: "TwoTasksEOF",
tasks: []verifyTask{
- mockVerifier{fps: [][]byte{fp1}},
- mockVerifier{err: io.EOF},
+ mockVerifier{sigs: sigs[:1]},
+ mockVerifier{sigsErr: io.EOF},
},
wantErr: io.EOF,
},
{
name: "OneTaskOneFP",
tasks: []verifyTask{
- mockVerifier{fps: [][]byte{fp1}},
+ mockVerifier{sigs: sigs[:1]},
},
wantFingerprints: [][]byte{fp1},
},
{
name: "TwoTasksSameFP",
tasks: []verifyTask{
- mockVerifier{fps: [][]byte{fp1}},
- mockVerifier{fps: [][]byte{fp1}},
+ mockVerifier{sigs: sigs[:1]},
+ mockVerifier{sigs: sigs[:1]},
},
wantFingerprints: [][]byte{fp1},
},
{
name: "TwoTasksTwoFP",
tasks: []verifyTask{
- mockVerifier{fps: [][]byte{fp1}},
- mockVerifier{fps: [][]byte{fp2}},
+ mockVerifier{sigs: sigs[:1]},
+ mockVerifier{sigs: sigs[1:]},
},
wantFingerprints: [][]byte{fp1, fp2},
},
{
name: "KitchenSink",
tasks: []verifyTask{
- mockVerifier{fps: [][]byte{}},
- mockVerifier{fps: [][]byte{fp1}},
- mockVerifier{fps: [][]byte{fp2}},
- mockVerifier{fps: [][]byte{fp1, fp2}},
+ mockVerifier{},
+ mockVerifier{sigs: sigs[:1]},
+ mockVerifier{sigs: sigs[1:]},
+ mockVerifier{sigs: sigs},
},
wantFingerprints: [][]byte{fp1, fp2},
},
@@ -919,6 +845,13 @@ func TestVerifier_AllSignedBy(t *testing.T) {
0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
}
+ fi := getSignedDummy(t, fp1, fp2)
+
+ sigs, err := fi.GetDescriptors(sif.WithDataType(sif.DataSignature))
+ if err != nil {
+ t.Fatal(err)
+ }
+
tests := []struct {
name string
tasks []verifyTask
@@ -928,53 +861,53 @@ func TestVerifier_AllSignedBy(t *testing.T) {
{
name: "OneTaskEOF",
tasks: []verifyTask{
- mockVerifier{err: io.EOF},
+ mockVerifier{sigsErr: io.EOF},
},
wantErr: io.EOF,
},
{
name: "TwoTasksEOF",
tasks: []verifyTask{
- mockVerifier{fps: [][]byte{fp1}},
- mockVerifier{err: io.EOF},
+ mockVerifier{sigs: sigs[:1]},
+ mockVerifier{sigsErr: io.EOF},
},
wantErr: io.EOF,
},
{
name: "OneTaskNoFP",
tasks: []verifyTask{
- mockVerifier{fps: [][]byte{}},
+ mockVerifier{},
},
},
{
name: "OneTaskOneFP",
tasks: []verifyTask{
- mockVerifier{fps: [][]byte{fp1}},
+ mockVerifier{sigs: sigs[:1]},
},
wantFingerprints: [][]byte{fp1},
},
{
name: "TwoTasksSameFP",
tasks: []verifyTask{
- mockVerifier{fps: [][]byte{fp1}},
- mockVerifier{fps: [][]byte{fp1}},
+ mockVerifier{sigs: sigs[:1]},
+ mockVerifier{sigs: sigs[:1]},
},
wantFingerprints: [][]byte{fp1},
},
{
name: "TwoTasksTwoFP",
tasks: []verifyTask{
- mockVerifier{fps: [][]byte{fp1}},
- mockVerifier{fps: [][]byte{fp2}},
+ mockVerifier{sigs: sigs[:1]},
+ mockVerifier{sigs: sigs[1:]},
},
},
{
name: "KitchenSink",
tasks: []verifyTask{
- mockVerifier{fps: [][]byte{}},
- mockVerifier{fps: [][]byte{fp1}},
- mockVerifier{fps: [][]byte{fp2}},
- mockVerifier{fps: [][]byte{fp1, fp2}},
+ mockVerifier{},
+ mockVerifier{sigs: sigs[:1]},
+ mockVerifier{sigs: sigs[1:]},
+ mockVerifier{sigs: sigs},
},
},
}
@@ -998,35 +931,109 @@ func TestVerifier_AllSignedBy(t *testing.T) {
}
func TestVerifier_Verify(t *testing.T) {
+ oneGroupImage := loadContainer(t, filepath.Join(corpus, "one-group.sif"))
oneGroupSignedImage := loadContainer(t, filepath.Join(corpus, "one-group-signed.sif"))
- kr := openpgp.EntityList{getTestEntity(t)}
+ verified, err := oneGroupSignedImage.GetDescriptors(sif.WithGroupID(1))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sig, err := oneGroupSignedImage.GetDescriptor(sif.WithDataType(sif.DataSignature))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ e := getTestEntity(t)
+
+ kr := openpgp.EntityList{e}
tests := []struct {
- name string
- f *sif.FileImage
- kr openpgp.KeyRing
- tasks []verifyTask
- wantErr error
+ name string
+ f *sif.FileImage
+ tasks []verifyTask
+ kr openpgp.KeyRing
+ testCallback bool
+ ignoreError bool
+ wantCBSignature sif.Descriptor
+ wantCBVerified []sif.Descriptor
+ wantCBEntity *openpgp.Entity
+ wantCBErr error
+ wantErr error
}{
{
- name: "ErrNoKeyMaterial",
- f: oneGroupSignedImage,
- tasks: []verifyTask{mockVerifier{}},
+ name: "NoKeyMaterial",
+ f: oneGroupSignedImage,
+ tasks: []verifyTask{
+ mockVerifier{},
+ },
wantErr: ErrNoKeyMaterial,
},
{
- name: "EOF",
- f: oneGroupSignedImage,
+ name: "SignatureNotFound",
+ f: oneGroupImage,
+ tasks: []verifyTask{
+ mockVerifier{
+ sigsErr: &SignatureNotFoundError{ID: 1, IsGroup: true},
+ },
+ },
kr: kr,
- tasks: []verifyTask{mockVerifier{err: io.EOF}},
- wantErr: io.EOF,
+ wantErr: &SignatureNotFoundError{},
+ },
+ {
+ name: "UnknownIssuer",
+ f: oneGroupSignedImage,
+ tasks: []verifyTask{
+ mockVerifier{
+ sigs: []sif.Descriptor{sig},
+ verifyErr: &SignatureNotValidError{ID: 3, Err: pgperrors.ErrUnknownIssuer},
+ },
+ },
+ kr: kr,
+ wantErr: &SignatureNotValidError{},
+ },
+ {
+ name: "UnknownIssuerIgnoreError",
+ f: oneGroupSignedImage,
+ tasks: []verifyTask{
+ mockVerifier{
+ sigs: []sif.Descriptor{sig},
+ verifyErr: &SignatureNotValidError{ID: 3, Err: pgperrors.ErrUnknownIssuer},
+ },
+ },
+ testCallback: true,
+ ignoreError: true,
+ kr: openpgp.EntityList{},
+ wantCBSignature: sig,
+ wantCBErr: &SignatureNotValidError{ID: 3, Err: pgperrors.ErrUnknownIssuer},
+ wantErr: nil,
},
{
- name: "OK",
- f: oneGroupSignedImage,
- kr: kr,
- tasks: []verifyTask{mockVerifier{}},
+ name: "OneGroupSigned",
+ f: oneGroupSignedImage,
+ tasks: []verifyTask{
+ mockVerifier{
+ sigs: []sif.Descriptor{sig},
+ verified: verified,
+ },
+ },
+ kr: kr,
+ },
+ {
+ name: "OneGroupSignedWithCallback",
+ f: oneGroupSignedImage,
+ testCallback: true,
+ tasks: []verifyTask{
+ mockVerifier{
+ sigs: []sif.Descriptor{sig},
+ verified: verified,
+ e: e,
+ },
+ },
+ kr: kr,
+ wantCBSignature: sig,
+ wantCBVerified: verified,
+ wantCBEntity: e,
},
}
@@ -1035,8 +1042,31 @@ func TestVerifier_Verify(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
v := Verifier{
f: tt.f,
- kr: tt.kr,
tasks: tt.tasks,
+ kr: tt.kr,
+ }
+
+ // Test callback functionality, if requested.
+ if tt.testCallback {
+ v.cb = func(r VerifyResult) bool {
+ if got, want := r.Signature(), tt.wantCBSignature; got != want {
+ t.Errorf("got signature %v, want %v", got, want)
+ }
+
+ if got, want := r.Verified(), tt.wantCBVerified; !reflect.DeepEqual(got, want) {
+ t.Errorf("got verified %v, want %v", got, want)
+ }
+
+ if got, want := r.Entity(), tt.wantCBEntity; got != want {
+ t.Errorf("got entity %v, want %v", got, want)
+ }
+
+ if got, want := r.Error(), tt.wantCBErr; !errors.Is(got, want) {
+ t.Errorf("got error %v, want %v", got, want)
+ }
+
+ return tt.ignoreError
+ }
}
if got, want := v.Verify(), tt.wantErr; !errors.Is(got, want) {
diff --git a/pkg/sif/arch.go b/pkg/sif/arch.go
index d7acbb6..d6f6673 100644
--- a/pkg/sif/arch.go
+++ b/pkg/sif/arch.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
@@ -18,6 +18,7 @@ var (
hdrArchMIPS64 archType = [...]byte{'0', '9', '\x00'}
hdrArchMIPS64le archType = [...]byte{'1', '0', '\x00'}
hdrArchS390x archType = [...]byte{'1', '1', '\x00'}
+ hdrArchRISCV64 archType = [...]byte{'1', '2', '\x00'}
)
type archType [3]byte
@@ -36,6 +37,7 @@ func getSIFArch(arch string) archType {
"mips64": hdrArchMIPS64,
"mips64le": hdrArchMIPS64le,
"s390x": hdrArchS390x,
+ "riscv64": hdrArchRISCV64,
}
t, ok := archMap[arch]
@@ -59,6 +61,7 @@ func (t archType) GoArch() string {
hdrArchMIPS64: "mips64",
hdrArchMIPS64le: "mips64le",
hdrArchS390x: "s390x",
+ hdrArchRISCV64: "riscv64",
}
arch, ok := archMap[t]
diff --git a/pkg/sif/buffer.go b/pkg/sif/buffer.go
index d706fb1..1d73a47 100644
--- a/pkg/sif/buffer.go
+++ b/pkg/sif/buffer.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
@@ -25,7 +25,7 @@ func NewBuffer(buf []byte) *Buffer {
var errNegativeOffset = errors.New("negative offset")
// ReadAt implements the io.ReaderAt interface.
-func (b *Buffer) ReadAt(p []byte, off int64) (n int, err error) {
+func (b *Buffer) ReadAt(p []byte, off int64) (int, error) {
if off < 0 {
return 0, errNegativeOffset
}
@@ -34,17 +34,17 @@ func (b *Buffer) ReadAt(p []byte, off int64) (n int, err error) {
return 0, io.EOF
}
- n = copy(p, b.buf[off:])
+ n := copy(p, b.buf[off:])
if n < len(p) {
- err = io.EOF
+ return n, io.EOF
}
- return n, err
+ return n, nil
}
var errNegativePosition = errors.New("negative position")
// Write implements the io.Writer interface.
-func (b *Buffer) Write(p []byte) (n int, err error) {
+func (b *Buffer) Write(p []byte) (int, error) {
if b.pos < 0 {
return 0, errNegativePosition
}
@@ -53,7 +53,7 @@ func (b *Buffer) Write(p []byte) (n int, err error) {
b.buf = append(b.buf, make([]byte, need-have)...)
}
- n = copy(b.buf[b.pos:], p)
+ n := copy(b.buf[b.pos:], p)
b.pos += int64(n)
return n, nil
}
diff --git a/pkg/sif/create.go b/pkg/sif/create.go
index e65bdb7..104e9ea 100644
--- a/pkg/sif/create.go
+++ b/pkg/sif/create.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2018-2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2018-2022, Sylabs Inc. All rights reserved.
// Copyright (c) 2017, SingularityWare, LLC. All rights reserved.
// Copyright (c) 2017, Yannick Cote <yhcote@gmail.com> All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
@@ -104,7 +104,7 @@ func (f *FileImage) writeDescriptors() error {
return binary.Write(f.rw, binary.LittleEndian, f.rds)
}
-// writeHeader writes the the global header in f to backing storage.
+// writeHeader writes the global header in f to backing storage.
func (f *FileImage) writeHeader() error {
if _, err := f.rw.Seek(0, io.SeekStart); err != nil {
return err
diff --git a/pkg/sif/descriptor.go b/pkg/sif/descriptor.go
index da7a6a7..8fa926a 100644
--- a/pkg/sif/descriptor.go
+++ b/pkg/sif/descriptor.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2018-2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2018-2022, Sylabs Inc. All rights reserved.
// Copyright (c) 2017, SingularityWare, LLC. All rights reserved.
// Copyright (c) 2017, Yannick Cote <yhcote@gmail.com> All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
@@ -56,6 +56,11 @@ type cryptoMessage struct {
Messagetype MessageType
}
+// sbom represents the SIF SBOM data object descriptor.
+type sbom struct {
+ Format SBOMFormat
+}
+
var errNameTooLarge = errors.New("name value too large")
// setName encodes name into the name field of d.
@@ -96,7 +101,7 @@ func (d *rawDescriptor) setExtra(v interface{}) error {
}
// getPartitionMetadata gets metadata for a partition data object.
-func (d rawDescriptor) getPartitionMetadata() (fs FSType, pt PartType, arch string, err error) {
+func (d rawDescriptor) getPartitionMetadata() (FSType, PartType, string, error) {
if got, want := d.DataType, DataPartition; got != want {
return 0, 0, "", &unexpectedDataTypeError{got, []DataType{want}}
}
@@ -142,6 +147,8 @@ func (d Descriptor) GroupID() uint32 { return d.raw.GroupID &^ descrGroupMask }
// LinkedID returns the object/group ID d is linked to, or zero if d does not contain a linked
// ID. If isGroup is true, the returned id is an object group ID. Otherwise, the returned id is a
// data object ID.
+//
+//nolint:nonamedreturns // Named returns effective as documentation.
func (d Descriptor) LinkedID() (id uint32, isGroup bool) {
return d.raw.LinkedID &^ descrGroupMask, d.raw.LinkedID&descrGroupMask == descrGroupMask
}
@@ -162,6 +169,8 @@ func (d Descriptor) ModifiedAt() time.Time { return time.Unix(d.raw.ModifiedAt,
func (d Descriptor) Name() string { return strings.TrimRight(string(d.raw.Name[:]), "\000") }
// PartitionMetadata gets metadata for a partition data object.
+//
+//nolint:nonamedreturns // Named returns effective as documentation.
func (d Descriptor) PartitionMetadata() (fs FSType, pt PartType, arch string, err error) {
return d.raw.getPartitionMetadata()
}
@@ -186,6 +195,8 @@ func getHashType(ht hashType) (crypto.Hash, error) {
}
// SignatureMetadata gets metadata for a signature data object.
+//
+//nolint:nonamedreturns // Named returns effective as documentation.
func (d Descriptor) SignatureMetadata() (ht crypto.Hash, fp []byte, err error) {
if got, want := d.raw.DataType, DataSignature; got != want {
return ht, fp, &unexpectedDataTypeError{got, []DataType{want}}
@@ -203,6 +214,11 @@ func (d Descriptor) SignatureMetadata() (ht crypto.Hash, fp []byte, err error) {
}
fp = make([]byte, 20)
+
+ if bytes.Equal(s.Entity[:len(fp)], fp) {
+ return ht, nil, nil // Fingerprint not present.
+ }
+
copy(fp, s.Entity[:])
return ht, fp, nil
@@ -224,6 +240,22 @@ func (d Descriptor) CryptoMessageMetadata() (FormatType, MessageType, error) {
return m.Formattype, m.Messagetype, nil
}
+// SBOMMetadata gets metadata for a SBOM data object.
+func (d Descriptor) SBOMMetadata() (SBOMFormat, error) {
+ if got, want := d.raw.DataType, DataSBOM; got != want {
+ return 0, &unexpectedDataTypeError{got, []DataType{want}}
+ }
+
+ var s sbom
+
+ b := bytes.NewReader(d.raw.Extra[:])
+ if err := binary.Read(b, binary.LittleEndian, &s); err != nil {
+ return 0, fmt.Errorf("%w", err)
+ }
+
+ return s.Format, nil
+}
+
// GetData returns the data object associated with descriptor d.
func (d Descriptor) GetData() ([]byte, error) {
b := make([]byte, d.raw.Size)
diff --git a/pkg/sif/descriptor_input.go b/pkg/sif/descriptor_input.go
index c55cf51..3e81c39 100644
--- a/pkg/sif/descriptor_input.go
+++ b/pkg/sif/descriptor_input.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
@@ -10,7 +10,6 @@ import (
"errors"
"fmt"
"io"
- "os"
"time"
)
@@ -227,6 +226,24 @@ func OptSignatureMetadata(ht crypto.Hash, fp []byte) DescriptorInputOpt {
}
}
+// OptSBOMMetadata sets metadata for a SBOM data object. The SBOM format is set to f.
+//
+// If this option is applied to a data object with an incompatible type, an error is returned.
+func OptSBOMMetadata(f SBOMFormat) DescriptorInputOpt {
+ return func(t DataType, opts *descriptorOpts) error {
+ if got, want := t, DataSBOM; got != want {
+ return &unexpectedDataTypeError{got, []DataType{want}}
+ }
+
+ s := sbom{
+ Format: f,
+ }
+
+ opts.extra = s
+ return nil
+ }
+}
+
// DescriptorInput describes a new data object.
type DescriptorInput struct {
dt DataType
@@ -242,14 +259,15 @@ const DefaultObjectGroup = 1
//
// It is possible (and often necessary) to store additional metadata related to certain types of
// data objects. Consider supplying options such as OptCryptoMessageMetadata, OptPartitionMetadata,
-// and OptSignatureMetadata for this purpose.
+// OptSignatureMetadata, and OptSBOMMetadata for this purpose.
//
// By default, the data object will be placed in the default data object group (1). To override
// this behavior, use OptNoGroup or OptGroupID. To link this data object, use OptLinkedID or
// OptLinkedGroupID.
//
-// By default, the data object will be aligned according to the system's memory page size. To
-// override this behavior, consider using OptObjectAlignment.
+// By default, the data object will not be aligned unless it is of type DataPartition, in which
+// case it will be aligned on a 4096 byte boundary. To override this behavior, consider using
+// OptObjectAlignment.
//
// By default, no name is set for data object. To set a name, use OptObjectName.
//
@@ -258,8 +276,11 @@ const DefaultObjectGroup = 1
// image modification time. To override this behavior, consider using OptObjectTime.
func NewDescriptorInput(t DataType, r io.Reader, opts ...DescriptorInputOpt) (DescriptorInput, error) {
dopts := descriptorOpts{
- groupID: DefaultObjectGroup,
- alignment: os.Getpagesize(),
+ groupID: DefaultObjectGroup,
+ }
+
+ if t == DataPartition {
+ dopts.alignment = 4096
}
for _, opt := range opts {
diff --git a/pkg/sif/descriptor_input_test.go b/pkg/sif/descriptor_input_test.go
index 8431231..5c34099 100644
--- a/pkg/sif/descriptor_input_test.go
+++ b/pkg/sif/descriptor_input_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
@@ -163,6 +163,21 @@ func TestNewDescriptorInput(t *testing.T) {
OptSignatureMetadata(crypto.SHA256, fp),
},
},
+ {
+ name: "OptSBOMMetadataUnexpectedDataType",
+ t: DataGeneric,
+ opts: []DescriptorInputOpt{
+ OptSBOMMetadata(SBOMFormatCycloneDXJSON),
+ },
+ wantErr: &unexpectedDataTypeError{DataGeneric, []DataType{DataSBOM}},
+ },
+ {
+ name: "OptSBOMMetadata",
+ t: DataSBOM,
+ opts: []DescriptorInputOpt{
+ OptSBOMMetadata(SBOMFormatCycloneDXJSON),
+ },
+ },
}
for _, tt := range tests {
tt := tt
diff --git a/pkg/sif/descriptor_test.go b/pkg/sif/descriptor_test.go
index 470d91a..21280d9 100644
--- a/pkg/sif/descriptor_test.go
+++ b/pkg/sif/descriptor_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2018-2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2018-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
@@ -168,48 +168,51 @@ func TestDescriptor_PartitionMetadata(t *testing.T) {
}
func TestDescriptor_SignatureMetadata(t *testing.T) {
- s := signature{
- Hashtype: hashSHA384,
- }
- copy(s.Entity[:], []byte{
- 0x12, 0x04, 0x5c, 0x8c, 0x0b, 0x10, 0x04, 0xd0, 0x58, 0xde,
- 0x4b, 0xed, 0xa2, 0x0c, 0x27, 0xee, 0x7f, 0xf7, 0xba, 0x84,
- })
-
- rd := rawDescriptor{
- DataType: DataSignature,
- }
- if err := rd.setExtra(s); err != nil {
- t.Fatal(err)
- }
-
tests := []struct {
name string
- rd rawDescriptor
- wantHT crypto.Hash
- wantFP []byte
+ dt DataType
+ ht hashType
+ fp []byte
wantErr error
+ wantHT crypto.Hash
}{
{
- name: "UnexpectedDataType",
- rd: rawDescriptor{
- DataType: DataGeneric,
- },
+ name: "UnexpectedDataType",
+ dt: DataGeneric,
wantErr: &unexpectedDataTypeError{DataGeneric, []DataType{DataSignature}},
},
{
- name: "OK",
- rd: rd,
- wantHT: crypto.SHA384,
- wantFP: []byte{
+ name: "Fingerprint",
+ dt: DataSignature,
+ ht: hashSHA384,
+ fp: []byte{
0x12, 0x04, 0x5c, 0x8c, 0x0b, 0x10, 0x04, 0xd0, 0x58, 0xde,
0x4b, 0xed, 0xa2, 0x0c, 0x27, 0xee, 0x7f, 0xf7, 0xba, 0x84,
},
+ wantHT: crypto.SHA384,
+ },
+ {
+ name: "NoFingerprint",
+ dt: DataSignature,
+ ht: hashSHA256,
+ wantHT: crypto.SHA256,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- d := Descriptor{raw: tt.rd}
+ sig := signature{
+ Hashtype: tt.ht,
+ }
+ copy(sig.Entity[:], tt.fp)
+
+ rd := rawDescriptor{
+ DataType: tt.dt,
+ }
+ if err := rd.setExtra(sig); err != nil {
+ t.Fatal(err)
+ }
+
+ d := Descriptor{raw: rd}
ht, fp, err := d.SignatureMetadata()
@@ -222,7 +225,7 @@ func TestDescriptor_SignatureMetadata(t *testing.T) {
t.Fatalf("got hash type %v, want %v", got, want)
}
- if got, want := fp, tt.wantFP; !bytes.Equal(got, want) {
+ if got, want := fp, tt.fp; !bytes.Equal(got, want) {
t.Fatalf("got entity %v, want %v", got, want)
}
}
@@ -287,6 +290,56 @@ func TestDescriptor_CryptoMessageMetadata(t *testing.T) {
}
}
+func TestDescriptor_SBOMMetadata(t *testing.T) {
+ m := sbom{
+ Format: SBOMFormatCycloneDXJSON,
+ }
+
+ rd := rawDescriptor{
+ DataType: DataSBOM,
+ }
+ if err := rd.setExtra(m); err != nil {
+ t.Fatal(err)
+ }
+
+ tests := []struct {
+ name string
+ rd rawDescriptor
+ wantFormat SBOMFormat
+ wantErr error
+ }{
+ {
+ name: "UnexpectedDataType",
+ rd: rawDescriptor{
+ DataType: DataGeneric,
+ },
+ wantErr: &unexpectedDataTypeError{DataGeneric, []DataType{DataSBOM}},
+ },
+ {
+ name: "OK",
+ rd: rd,
+ wantFormat: SBOMFormatCycloneDXJSON,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ d := Descriptor{raw: tt.rd}
+
+ f, err := d.SBOMMetadata()
+
+ if got, want := err, tt.wantErr; !errors.Is(got, want) {
+ t.Fatalf("got error %v, want %v", got, want)
+ }
+
+ if err == nil {
+ if got, want := f, tt.wantFormat; got != want {
+ t.Fatalf("got format %v, want %v", got, want)
+ }
+ }
+ })
+ }
+}
+
func TestDescriptor_GetIntegrityReader(t *testing.T) {
rd := rawDescriptor{
DataType: DataDeffile,
diff --git a/pkg/sif/sif.go b/pkg/sif/sif.go
index 704acee..2d1c209 100644
--- a/pkg/sif/sif.go
+++ b/pkg/sif/sif.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2018-2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2018-2022, Sylabs Inc. All rights reserved.
// Copyright (c) 2017, SingularityWare, LLC. All rights reserved.
// Copyright (c) 2017, Yannick Cote <yhcote@gmail.com> All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
@@ -10,69 +10,68 @@
//
// Layout of a SIF file (example):
//
-// .================================================.
-// | GLOBAL HEADER: Sifheader |
-// | - launch: "#!/usr/bin/env..." |
-// | - magic: "SIF_MAGIC" |
-// | - version: "1" |
-// | - arch: "4" |
-// | - uuid: b2659d4e-bd50-4ea5-bd17-eec5e54f918e |
-// | - ctime: 1504657553 |
-// | - mtime: 1504657653 |
-// | - ndescr: 3 |
-// | - descroff: 120 | --.
-// | - descrlen: 432 | |
-// | - dataoff: 4096 | |
-// | - datalen: 619362 | |
-// |------------------------------------------------| <-'
-// | DESCR[0]: Sifdeffile |
-// | - Sifcommon |
-// | - datatype: DATA_DEFFILE |
-// | - id: 1 |
-// | - groupid: 1 |
-// | - link: NONE |
-// | - fileoff: 4096 | --.
-// | - filelen: 222 | |
-// |------------------------------------------------| <-----.
-// | DESCR[1]: Sifpartition | | |
-// | - Sifcommon | | |
-// | - datatype: DATA_PARTITION | | |
-// | - id: 2 | | |
-// | - groupid: 1 | | |
-// | - link: NONE | | |
-// | - fileoff: 4318 | ----. |
-// | - filelen: 618496 | | | |
-// | - fstype: Squashfs | | | |
-// | - parttype: System | | | |
-// | - content: Linux | | | |
-// |------------------------------------------------| | | |
-// | DESCR[2]: Sifsignature | | | |
-// | - Sifcommon | | | |
-// | - datatype: DATA_SIGNATURE | | | |
-// | - id: 3 | | | |
-// | - groupid: NONE | | | |
-// | - link: 2 | ------'
-// | - fileoff: 622814 | ------.
-// | - filelen: 644 | | | |
-// | - hashtype: SHA384 | | | |
-// | - entity: @ | | | |
-// |------------------------------------------------| <-' | |
-// | Definition file data | | |
-// | . | | |
-// | . | | |
-// | . | | |
-// |------------------------------------------------| <---' |
-// | File system partition image | |
-// | . | |
-// | . | |
-// | . | |
-// |------------------------------------------------| <-----'
-// | Signed verification data |
-// | . |
-// | . |
-// | . |
-// `================================================'
-//
+// .================================================.
+// | GLOBAL HEADER: Sifheader |
+// | - launch: "#!/usr/bin/env..." |
+// | - magic: "SIF_MAGIC" |
+// | - version: "1" |
+// | - arch: "4" |
+// | - uuid: b2659d4e-bd50-4ea5-bd17-eec5e54f918e |
+// | - ctime: 1504657553 |
+// | - mtime: 1504657653 |
+// | - ndescr: 3 |
+// | - descroff: 120 | --.
+// | - descrlen: 432 | |
+// | - dataoff: 4096 | |
+// | - datalen: 619362 | |
+// |------------------------------------------------| <-'
+// | DESCR[0]: Sifdeffile |
+// | - Sifcommon |
+// | - datatype: DATA_DEFFILE |
+// | - id: 1 |
+// | - groupid: 1 |
+// | - link: NONE |
+// | - fileoff: 4096 | --.
+// | - filelen: 222 | |
+// |------------------------------------------------| <-----.
+// | DESCR[1]: Sifpartition | | |
+// | - Sifcommon | | |
+// | - datatype: DATA_PARTITION | | |
+// | - id: 2 | | |
+// | - groupid: 1 | | |
+// | - link: NONE | | |
+// | - fileoff: 4318 | ----. |
+// | - filelen: 618496 | | | |
+// | - fstype: Squashfs | | | |
+// | - parttype: System | | | |
+// | - content: Linux | | | |
+// |------------------------------------------------| | | |
+// | DESCR[2]: Sifsignature | | | |
+// | - Sifcommon | | | |
+// | - datatype: DATA_SIGNATURE | | | |
+// | - id: 3 | | | |
+// | - groupid: NONE | | | |
+// | - link: 2 | ------'
+// | - fileoff: 622814 | ------.
+// | - filelen: 644 | | | |
+// | - hashtype: SHA384 | | | |
+// | - entity: @ | | | |
+// |------------------------------------------------| <-' | |
+// | Definition file data | | |
+// | . | | |
+// | . | | |
+// | . | | |
+// |------------------------------------------------| <---' |
+// | File system partition image | |
+// | . | |
+// | . | |
+// | . | |
+// |------------------------------------------------| <-----'
+// | Signed verification data |
+// | . |
+// | . |
+// | . |
+// `================================================'
package sif
import (
@@ -133,6 +132,7 @@ const (
DataGenericJSON // generic JSON meta-data
DataGeneric // generic / raw data
DataCryptoMessage // cryptographic message data object
+ DataSBOM // software bill of materials
)
// String returns a human-readable representation of t.
@@ -154,6 +154,8 @@ func (t DataType) String() string {
return "Generic/Raw"
case DataCryptoMessage:
return "Cryptographic Message"
+ case DataSBOM:
+ return "SBOM"
}
return "Unknown"
}
@@ -268,6 +270,44 @@ func (t MessageType) String() string {
return "Unknown"
}
+// SBOMFormat represents the format used to store an SBOM object.
+type SBOMFormat int32
+
+// List of supported SBOM formats.
+const (
+ SBOMFormatCycloneDXJSON SBOMFormat = iota + 1 // CycloneDX (JSON)
+ SBOMFormatCycloneDXXML // CycloneDX (XML)
+ SBOMFormatGitHubJSON // GitHub dependency snapshot (JSON)
+ SBOMFormatSPDXJSON // SPDX (JSON)
+ SBOMFormatSPDXRDF // SPDX (RDF/xml)
+ SBOMFormatSPDXTagValue // SPDX (tag/value)
+ SBOMFormatSPDXYAML // SPDX (YAML)
+ SBOMFormatSyftJSON // Syft (JSON)
+)
+
+// String returns a human-readable representation of f.
+func (f SBOMFormat) String() string {
+ switch f {
+ case SBOMFormatCycloneDXJSON:
+ return "cyclonedx-json"
+ case SBOMFormatCycloneDXXML:
+ return "cyclonedx-xml"
+ case SBOMFormatGitHubJSON:
+ return "github-json"
+ case SBOMFormatSPDXJSON:
+ return "spdx-json"
+ case SBOMFormatSPDXRDF:
+ return "spdx-rdf"
+ case SBOMFormatSPDXTagValue:
+ return "spdx-tag-value"
+ case SBOMFormatSPDXYAML:
+ return "spdx-yaml"
+ case SBOMFormatSyftJSON:
+ return "syft-json"
+ }
+ return "unknown"
+}
+
// header describes a loaded SIF file.
type header struct {
LaunchScript [hdrLaunchLen]byte
diff --git a/pkg/sif/testdata/TestAddObject/Empty.golden b/pkg/sif/testdata/TestAddObject/Empty.golden
index c236112..74b6489 100644
--- a/pkg/sif/testdata/TestAddObject/Empty.golden
+++ b/pkg/sif/testdata/TestAddObject/Empty.golden
Binary files differ
diff --git a/pkg/sif/testdata/TestAddObject/NotEmpty.golden b/pkg/sif/testdata/TestAddObject/NotEmpty.golden
index c3dd3d4..df09aee 100644
--- a/pkg/sif/testdata/TestAddObject/NotEmpty.golden
+++ b/pkg/sif/testdata/TestAddObject/NotEmpty.golden
Binary files differ
diff --git a/pkg/sif/testdata/TestAddObject/NotEmptyAligned.golden b/pkg/sif/testdata/TestAddObject/NotEmptyAligned.golden
index 6980717..68feda4 100644
--- a/pkg/sif/testdata/TestAddObject/NotEmptyAligned.golden
+++ b/pkg/sif/testdata/TestAddObject/NotEmptyAligned.golden
Binary files differ
diff --git a/pkg/sif/testdata/TestAddObject/NotEmptyNotAligned.golden b/pkg/sif/testdata/TestAddObject/NotEmptyNotAligned.golden
index f09b553..2855fff 100644
--- a/pkg/sif/testdata/TestAddObject/NotEmptyNotAligned.golden
+++ b/pkg/sif/testdata/TestAddObject/NotEmptyNotAligned.golden
Binary files differ
diff --git a/pkg/sif/testdata/TestAddObject/WithTime.golden b/pkg/sif/testdata/TestAddObject/WithTime.golden
index 269992d..f7d78b1 100644
--- a/pkg/sif/testdata/TestAddObject/WithTime.golden
+++ b/pkg/sif/testdata/TestAddObject/WithTime.golden
Binary files differ
diff --git a/pkg/sif/testdata/TestCreateContainer/OneDescriptor.golden b/pkg/sif/testdata/TestCreateContainer/OneDescriptor.golden
index c236112..74b6489 100644
--- a/pkg/sif/testdata/TestCreateContainer/OneDescriptor.golden
+++ b/pkg/sif/testdata/TestCreateContainer/OneDescriptor.golden
Binary files differ
diff --git a/pkg/sif/testdata/TestCreateContainer/TwoDescriptors.golden b/pkg/sif/testdata/TestCreateContainer/TwoDescriptors.golden
index c3dd3d4..df09aee 100644
--- a/pkg/sif/testdata/TestCreateContainer/TwoDescriptors.golden
+++ b/pkg/sif/testdata/TestCreateContainer/TwoDescriptors.golden
Binary files differ
diff --git a/pkg/sif/testdata/TestCreateContainerAtPath/OneDescriptor.golden b/pkg/sif/testdata/TestCreateContainerAtPath/OneDescriptor.golden
index c236112..74b6489 100644
--- a/pkg/sif/testdata/TestCreateContainerAtPath/OneDescriptor.golden
+++ b/pkg/sif/testdata/TestCreateContainerAtPath/OneDescriptor.golden
Binary files differ
diff --git a/pkg/sif/testdata/TestCreateContainerAtPath/TwoDescriptors.golden b/pkg/sif/testdata/TestCreateContainerAtPath/TwoDescriptors.golden
index c3dd3d4..df09aee 100644
--- a/pkg/sif/testdata/TestCreateContainerAtPath/TwoDescriptors.golden
+++ b/pkg/sif/testdata/TestCreateContainerAtPath/TwoDescriptors.golden
Binary files differ
diff --git a/pkg/sif/testdata/TestDeleteObject/WithTime.golden b/pkg/sif/testdata/TestDeleteObject/WithTime.golden
index c669c00..dd4e608 100644
--- a/pkg/sif/testdata/TestDeleteObject/WithTime.golden
+++ b/pkg/sif/testdata/TestDeleteObject/WithTime.golden
Binary files differ
diff --git a/pkg/sif/testdata/TestDeleteObject/Zero.golden b/pkg/sif/testdata/TestDeleteObject/Zero.golden
index 85b217d..036b369 100644
--- a/pkg/sif/testdata/TestDeleteObject/Zero.golden
+++ b/pkg/sif/testdata/TestDeleteObject/Zero.golden
Binary files differ
diff --git a/pkg/sif/testdata/TestNewDescriptorInput/OptSBOMMetadata.golden b/pkg/sif/testdata/TestNewDescriptorInput/OptSBOMMetadata.golden
new file mode 100644
index 0000000..02516b1
--- /dev/null
+++ b/pkg/sif/testdata/TestNewDescriptorInput/OptSBOMMetadata.golden
Binary files differ
diff --git a/pkg/siftool/add.go b/pkg/siftool/add.go
index 941f9b0..75f11df 100644
--- a/pkg/siftool/add.go
+++ b/pkg/siftool/add.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2019-2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2019-2022, Sylabs Inc. All rights reserved.
// Copyright (c) 2017, SingularityWare, LLC. All rights reserved.
// Copyright (c) 2017, Yannick Cote <yhcote@gmail.com> All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
@@ -27,6 +27,7 @@ var (
partArch *int32
signHash *int32
signEntity *string
+ sbomFormat *string
groupID *uint32
linkID *uint32
alignment *int
@@ -50,9 +51,9 @@ func getAddExamples(rootPath string) string {
func addFlags(fs *pflag.FlagSet) {
dataType = fs.Int("datatype", 0, `the type of data to add
[NEEDED, no default]:
- 1-Deffile, 2-EnvVar, 3-Labels,
- 4-Partition, 5-Signature, 6-GenericJSON,
- 7-Generic, 8-CryptoMessage`)
+ 1-Deffile, 2-EnvVar, 3-Labels,
+ 4-Partition, 5-Signature, 6-GenericJSON,
+ 7-Generic, 8-CryptoMessage, 9-SBOM`)
partType = fs.Int32("parttype", 0, `the type of partition (with -datatype 4-Partition)
[NEEDED, no default]:
1-System, 2-PrimSys, 3-Data,
@@ -66,7 +67,7 @@ func addFlags(fs *pflag.FlagSet) {
1-386, 2-amd64, 3-arm,
4-arm64, 5-ppc64, 6-ppc64le,
7-mips, 8-mipsle, 9-mips64,
- 10-mips64le, 11-s390x`)
+ 10-mips64le, 11-s390x, 12-riscv64`)
signHash = fs.Int32("signhash", 0, `the signature hash used (with -datatype 5-Signature)
[NEEDED, no default]:
1-SHA256, 2-SHA384, 3-SHA512,
@@ -74,9 +75,13 @@ func addFlags(fs *pflag.FlagSet) {
signEntity = fs.String("signentity", "", `the entity that signs (with -datatype 5-Signature)
[NEEDED, no default]:
example: 433FE984155206BD962725E20E8713472A879943`)
+ sbomFormat = fs.String("sbomformat", "", `the SBOM format (with -datatype 9-sbom):
+ cyclonedx-json, cyclonedx-xml, github-json,
+ spdx-json, spdx-rdf, spdx-tag-value,
+ spdx-yaml, syft-json`)
groupID = fs.Uint32("groupid", 0, "set groupid [default: 0]")
linkID = fs.Uint32("link", 0, "set link pointer [default: 0]")
- alignment = fs.Int("alignment", 0, "set alignment constraint [default: aligned on page size]")
+ alignment = fs.Int("alignment", 0, "set alignment [default: 4096 with -datatype 4-Partition, 0 otherwise]")
name = fs.String("filename", "", "set logical filename/handle [default: input filename]")
}
@@ -101,6 +106,8 @@ func getDataType() (sif.DataType, error) {
return sif.DataGeneric, nil
case 8:
return sif.DataCryptoMessage, nil
+ case 9:
+ return sif.DataSBOM, nil
default:
return 0, errDataTypeRequired
}
@@ -130,6 +137,8 @@ func getArch() string {
return "mips64le"
case 11:
return "s390x"
+ case 12:
+ return "riscv64"
default:
return "unknown"
}
@@ -154,9 +163,35 @@ func getHashType() (crypto.Hash, error) {
}
}
+var errInvalidSBOMFormat = errors.New("invalid SBOM format")
+
+func getSBOMFormat() (sif.SBOMFormat, error) {
+ switch *sbomFormat {
+ case "cyclonedx-json":
+ return sif.SBOMFormatCycloneDXJSON, nil
+ case "cyclonedx-xml":
+ return sif.SBOMFormatCycloneDXXML, nil
+ case "github", "github-json":
+ return sif.SBOMFormatGitHubJSON, nil
+ case "spdx-json":
+ return sif.SBOMFormatSPDXJSON, nil
+ case "spdx-rdf":
+ return sif.SBOMFormatSPDXRDF, nil
+ case "spdx-tag-value":
+ return sif.SBOMFormatSPDXTagValue, nil
+ case "spdx-yaml":
+ return sif.SBOMFormatSPDXYAML, nil
+ case "syft-json":
+ return sif.SBOMFormatSyftJSON, nil
+ default:
+ return 0, fmt.Errorf("%w: %v", errInvalidSBOMFormat, *sbomFormat)
+ }
+}
+
var (
errPartitionArgs = errors.New("with partition datatype, -partfs, -parttype and -partarch must be passed")
errInvalidFingerprintLength = errors.New("invalid signing entity fingerprint length")
+ errSBOMArgs = errors.New("with SBOM datatype, -sbomformat must be passed")
)
func getOptions(dt sif.DataType, fs *pflag.FlagSet) ([]sif.DescriptorInputOpt, error) {
@@ -180,7 +215,8 @@ func getOptions(dt sif.DataType, fs *pflag.FlagSet) ([]sif.DescriptorInputOpt, e
opts = append(opts, sif.OptObjectName(*name))
}
- if dt == sif.DataPartition {
+ switch dt {
+ case sif.DataPartition:
if *partType == 0 || *partFS == 0 || *partArch == 0 {
return nil, errPartitionArgs
}
@@ -188,9 +224,8 @@ func getOptions(dt sif.DataType, fs *pflag.FlagSet) ([]sif.DescriptorInputOpt, e
opts = append(opts,
sif.OptPartitionMetadata(sif.FSType(*partFS), sif.PartType(*partType), getArch()),
)
- }
- if dt == sif.DataSignature {
+ case sif.DataSignature:
b, err := hex.DecodeString(*signEntity)
if err != nil {
return nil, fmt.Errorf("failed to decode signing entity fingerprint: %w", err)
@@ -208,6 +243,18 @@ func getOptions(dt sif.DataType, fs *pflag.FlagSet) ([]sif.DescriptorInputOpt, e
copy(fp, b)
opts = append(opts, sif.OptSignatureMetadata(ht, fp))
+
+ case sif.DataSBOM:
+ if *sbomFormat == "" {
+ return nil, errSBOMArgs
+ }
+
+ f, err := getSBOMFormat()
+ if err != nil {
+ return nil, err
+ }
+
+ opts = append(opts, sif.OptSBOMMetadata(f))
}
return opts, nil
diff --git a/pkg/siftool/add_test.go b/pkg/siftool/add_test.go
index 6d51a33..ccbb5b3 100644
--- a/pkg/siftool/add_test.go
+++ b/pkg/siftool/add_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
@@ -46,7 +46,7 @@ func Test_command_getAdd(t *testing.T) {
}
args = append(args, tt.flags...)
- runCommand(t, cmd, args)
+ runCommand(t, cmd, args, nil)
})
}
}
diff --git a/pkg/siftool/del_test.go b/pkg/siftool/del_test.go
index 13fad07..070d609 100644
--- a/pkg/siftool/del_test.go
+++ b/pkg/siftool/del_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
@@ -24,7 +24,7 @@ func Test_command_getDel(t *testing.T) {
cmd := c.getDel()
- runCommand(t, cmd, []string{"1", makeTestSIF(t, true)})
+ runCommand(t, cmd, []string{"1", makeTestSIF(t, true)}, nil)
})
}
}
diff --git a/pkg/siftool/dump_test.go b/pkg/siftool/dump_test.go
index 20362b8..fb13eb4 100644
--- a/pkg/siftool/dump_test.go
+++ b/pkg/siftool/dump_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
@@ -39,7 +39,7 @@ func Test_command_getDump(t *testing.T) {
cmd := c.getDump()
- runCommand(t, cmd, []string{tt.id, tt.path})
+ runCommand(t, cmd, []string{tt.id, tt.path}, nil)
})
}
}
diff --git a/pkg/siftool/header_test.go b/pkg/siftool/header_test.go
index 5c19498..7aeb615 100644
--- a/pkg/siftool/header_test.go
+++ b/pkg/siftool/header_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
@@ -68,7 +68,7 @@ func Test_command_getHeader(t *testing.T) {
cmd := c.getHeader()
- runCommand(t, cmd, []string{tt.path})
+ runCommand(t, cmd, []string{tt.path}, nil)
})
}
}
diff --git a/pkg/siftool/info_test.go b/pkg/siftool/info_test.go
index 87f3a46..963b00a 100644
--- a/pkg/siftool/info_test.go
+++ b/pkg/siftool/info_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
@@ -39,7 +39,7 @@ func Test_command_getInfo(t *testing.T) {
cmd := c.getInfo()
- runCommand(t, cmd, []string{tt.id, tt.path})
+ runCommand(t, cmd, []string{tt.id, tt.path}, nil)
})
}
}
diff --git a/pkg/siftool/list_test.go b/pkg/siftool/list_test.go
index b7c1d32..5a4a47b 100644
--- a/pkg/siftool/list_test.go
+++ b/pkg/siftool/list_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
@@ -68,7 +68,7 @@ func Test_command_getList(t *testing.T) {
cmd := c.getList()
- runCommand(t, cmd, []string{tt.path})
+ runCommand(t, cmd, []string{tt.path}, nil)
})
}
}
diff --git a/pkg/siftool/mount.go b/pkg/siftool/mount.go
new file mode 100644
index 0000000..6b59557
--- /dev/null
+++ b/pkg/siftool/mount.go
@@ -0,0 +1,27 @@
+// Copyright (c) 2022, Sylabs Inc. All rights reserved.
+// This software is licensed under a 3-clause BSD license. Please consult the
+// LICENSE file distributed with the sources of this project regarding your
+// rights to use or distribute this software.
+
+package siftool
+
+import (
+ "github.com/spf13/cobra"
+)
+
+// getMount returns a command that mounts the primary system partition of a SIF image.
+func (c *command) getMount() *cobra.Command {
+ return &cobra.Command{
+ Use: "mount <sif_path> <mount_path>",
+ Short: "Mount primary system partition",
+ Long: "Mount the primary system partition of a SIF image",
+ Example: c.opts.rootPath + " mount image.sif path/",
+ Args: cobra.ExactArgs(2),
+ PreRunE: c.initApp,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return c.app.Mount(cmd.Context(), args[0], args[1])
+ },
+ DisableFlagsInUseLine: true,
+ Hidden: true, // hide while command is experimental
+ }
+}
diff --git a/pkg/siftool/mount_test.go b/pkg/siftool/mount_test.go
new file mode 100644
index 0000000..3b2848e
--- /dev/null
+++ b/pkg/siftool/mount_test.go
@@ -0,0 +1,61 @@
+// Copyright (c) 2022, Sylabs Inc. All rights reserved.
+// This software is licensed under a 3-clause BSD license. Please consult the
+// LICENSE file distributed with the sources of this project regarding your
+// rights to use or distribute this software.
+
+package siftool
+
+import (
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+
+ "github.com/sylabs/sif/v2/pkg/sif"
+)
+
+func Test_command_getMount(t *testing.T) {
+ if _, err := exec.LookPath("squashfuse"); err != nil {
+ t.Skip("squashfuse not found, skipping mount tests")
+ }
+
+ tests := []struct {
+ name string
+ opts commandOpts
+ path string
+ wantErr error
+ }{
+ {
+ name: "Empty",
+ path: filepath.Join(corpus, "empty.sif"),
+ wantErr: sif.ErrNoObjects,
+ },
+ {
+ name: "OneGroup",
+ path: filepath.Join(corpus, "one-group.sif"),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ path, err := os.MkdirTemp("", "siftool-mount-*")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ cmd := exec.Command("fusermount", "-u", path)
+
+ if err := cmd.Run(); err != nil {
+ t.Log(err)
+ }
+
+ os.RemoveAll(path)
+ })
+
+ c := &command{opts: tt.opts}
+
+ cmd := c.getMount()
+
+ runCommand(t, cmd, []string{tt.path, path}, tt.wantErr)
+ })
+ }
+}
diff --git a/pkg/siftool/new_test.go b/pkg/siftool/new_test.go
index af4cd75..81bcd58 100644
--- a/pkg/siftool/new_test.go
+++ b/pkg/siftool/new_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
@@ -32,7 +32,7 @@ func Test_command_getNew(t *testing.T) {
cmd := c.getNew()
- runCommand(t, cmd, []string{tf.Name()})
+ runCommand(t, cmd, []string{tf.Name()}, nil)
})
}
}
diff --git a/pkg/siftool/setprim_test.go b/pkg/siftool/setprim_test.go
index e37b240..13cd0e3 100644
--- a/pkg/siftool/setprim_test.go
+++ b/pkg/siftool/setprim_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
@@ -24,7 +24,7 @@ func Test_command_getSetPrim(t *testing.T) {
cmd := c.getSetPrim()
- runCommand(t, cmd, []string{"1", makeTestSIF(t, true)})
+ runCommand(t, cmd, []string{"1", makeTestSIF(t, true)}, nil)
})
}
}
diff --git a/pkg/siftool/siftool.go b/pkg/siftool/siftool.go
index 4e624f1..aa59f61 100644
--- a/pkg/siftool/siftool.go
+++ b/pkg/siftool/siftool.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2018-2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2018-2022, Sylabs Inc. All rights reserved.
// Copyright (c) 2017, SingularityWare, LLC. All rights reserved.
// Copyright (c) 2017, Yannick Cote <yhcote@gmail.com> All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
@@ -23,6 +23,7 @@ type command struct {
func (c *command) initApp(cmd *cobra.Command, args []string) error {
app, err := siftool.New(
siftool.OptAppOutput(cmd.OutOrStdout()),
+ siftool.OptAppError(cmd.ErrOrStderr()),
)
c.app = app
@@ -31,12 +32,21 @@ func (c *command) initApp(cmd *cobra.Command, args []string) error {
// commandOpts contains configured options.
type commandOpts struct {
- rootPath string
+ rootPath string
+ experimental bool
}
// CommandOpt are used to configure optional command behavior.
type CommandOpt func(*commandOpts) error
+// OptWithExperimental enables/disables experimental commands.
+func OptWithExperimental(b bool) CommandOpt {
+ return func(co *commandOpts) error {
+ co.experimental = b
+ return nil
+ }
+}
+
// AddCommands adds siftool commands to cmd according to opts.
//
// A set of commands are provided to display elements such as the SIF global
@@ -66,5 +76,10 @@ func AddCommands(cmd *cobra.Command, opts ...CommandOpt) error {
c.getSetPrim(),
)
+ if c.opts.experimental {
+ cmd.AddCommand(c.getMount())
+ cmd.AddCommand(c.getUnmount())
+ }
+
return nil
}
diff --git a/pkg/siftool/siftool_test.go b/pkg/siftool/siftool_test.go
index 5236254..606d6eb 100644
--- a/pkg/siftool/siftool_test.go
+++ b/pkg/siftool/siftool_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2021, Sylabs Inc. All rights reserved.
+// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
@@ -6,6 +6,7 @@ package siftool
import (
"bytes"
+ "errors"
"os"
"path/filepath"
"testing"
@@ -47,7 +48,7 @@ func makeTestSIF(t *testing.T, withDataObject bool) string {
return tf.Name()
}
-func runCommand(t *testing.T, cmd *cobra.Command, args []string) {
+func runCommand(t *testing.T, cmd *cobra.Command, args []string, wantErr error) {
t.Helper()
var out, err bytes.Buffer
@@ -56,8 +57,8 @@ func runCommand(t *testing.T, cmd *cobra.Command, args []string) {
cmd.SetArgs(args)
- if err := cmd.Execute(); err != nil {
- t.Fatal(err)
+ if got, want := cmd.Execute(), wantErr; !errors.Is(got, want) {
+ t.Fatalf("got error %v, want %v", got, want)
}
g := goldie.New(t,
@@ -79,6 +80,11 @@ func TestAddCommands(t *testing.T) {
args: []string{"help"},
},
{
+ name: "SifToolExperimental",
+ opts: []CommandOpt{OptWithExperimental(true)},
+ args: []string{"help"},
+ },
+ {
name: "Add",
args: []string{"help", "add"},
},
@@ -110,6 +116,11 @@ func TestAddCommands(t *testing.T) {
name: "SetPrim",
args: []string{"help", "setprim"},
},
+ {
+ name: "Mount",
+ opts: []CommandOpt{OptWithExperimental(true)},
+ args: []string{"help", "mount"},
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -121,7 +132,7 @@ func TestAddCommands(t *testing.T) {
t.Fatal(err)
}
- runCommand(t, cmd, tt.args)
+ runCommand(t, cmd, tt.args, nil)
})
}
}
diff --git a/pkg/siftool/testdata/TestAddCommands/Add/out.golden b/pkg/siftool/testdata/TestAddCommands/Add/out.golden
index 16c47e5..3f39697 100644
--- a/pkg/siftool/testdata/TestAddCommands/Add/out.golden
+++ b/pkg/siftool/testdata/TestAddCommands/Add/out.golden
@@ -9,12 +9,12 @@ siftool add image.sif rootfs.squashfs --datatype 4 --parttype 1 --partfs 1 ----p
siftool add image.sif signature.bin -datatype 5 --signentity 433FE984155206BD962725E20E8713472A879943 --signhash 1
Flags:
- --alignment int set alignment constraint [default: aligned on page size]
+ --alignment int set alignment [default: 4096 with -datatype 4-Partition, 0 otherwise]
--datatype int the type of data to add
[NEEDED, no default]:
- 1-Deffile, 2-EnvVar, 3-Labels,
- 4-Partition, 5-Signature, 6-GenericJSON,
- 7-Generic, 8-CryptoMessage
+ 1-Deffile, 2-EnvVar, 3-Labels,
+ 4-Partition, 5-Signature, 6-GenericJSON,
+ 7-Generic, 8-CryptoMessage, 9-SBOM
--filename string set logical filename/handle [default: input filename]
--groupid uint32 set groupid [default: 0]
-h, --help help for add
@@ -24,7 +24,7 @@ Flags:
1-386, 2-amd64, 3-arm,
4-arm64, 5-ppc64, 6-ppc64le,
7-mips, 8-mipsle, 9-mips64,
- 10-mips64le, 11-s390x
+ 10-mips64le, 11-s390x, 12-riscv64
--partfs int32 the filesystem used (with -datatype 4-Partition)
[NEEDED, no default]:
1-Squash, 2-Ext3, 3-ImmuObj,
@@ -33,6 +33,10 @@ Flags:
[NEEDED, no default]:
1-System, 2-PrimSys, 3-Data,
4-Overlay
+ --sbomformat string the SBOM format (with -datatype 9-sbom):
+ cyclonedx-json, cyclonedx-xml, github-json,
+ spdx-json, spdx-rdf, spdx-tag-value,
+ spdx-yaml, syft-json
--signentity string the entity that signs (with -datatype 5-Signature)
[NEEDED, no default]:
example: 433FE984155206BD962725E20E8713472A879943
diff --git a/pkg/siftool/testdata/TestAddCommands/Mount/err.golden b/pkg/siftool/testdata/TestAddCommands/Mount/err.golden
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pkg/siftool/testdata/TestAddCommands/Mount/err.golden
diff --git a/pkg/siftool/testdata/TestAddCommands/Mount/out.golden b/pkg/siftool/testdata/TestAddCommands/Mount/out.golden
new file mode 100644
index 0000000..3df015d
--- /dev/null
+++ b/pkg/siftool/testdata/TestAddCommands/Mount/out.golden
@@ -0,0 +1,10 @@
+Mount the primary system partition of a SIF image
+
+Usage:
+ siftool mount <sif_path> <mount_path>
+
+Examples:
+siftool mount image.sif path/
+
+Flags:
+ -h, --help help for mount
diff --git a/pkg/siftool/testdata/TestAddCommands/SifToolExperimental/err.golden b/pkg/siftool/testdata/TestAddCommands/SifToolExperimental/err.golden
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pkg/siftool/testdata/TestAddCommands/SifToolExperimental/err.golden
diff --git a/pkg/siftool/testdata/TestAddCommands/SifToolExperimental/out.golden b/pkg/siftool/testdata/TestAddCommands/SifToolExperimental/out.golden
new file mode 100644
index 0000000..2d8532f
--- /dev/null
+++ b/pkg/siftool/testdata/TestAddCommands/SifToolExperimental/out.golden
@@ -0,0 +1,19 @@
+Usage:
+ siftool [command]
+
+Available Commands:
+ add Add data object
+ completion Generate the autocompletion script for the specified shell
+ del Delete data object
+ dump Dump data object
+ header Display global header
+ help Help about any command
+ info Display data object info
+ list List data objects
+ new Create SIF image
+ setprim Set primary system partition
+
+Flags:
+ -h, --help help for siftool
+
+Use "siftool [command] --help" for more information about a command.
diff --git a/pkg/siftool/testdata/Test_command_getDump/Three/out.golden b/pkg/siftool/testdata/Test_command_getDump/Three/out.golden
index 0299536..4e354d9 100644
--- a/pkg/siftool/testdata/Test_command_getDump/Three/out.golden
+++ b/pkg/siftool/testdata/Test_command_getDump/Three/out.golden
@@ -1,15 +1,15 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
-{"version":1,"header":{"digest":"sha256:635fa0a14a8ef0c0351ed3e985799ed1d4f75ce973dea3cc76c99710795cc3f1"},"objects":[{"relativeId":0,"descriptorDigest":"sha256:3634ad01db0dd5482ecf685267b53d6201690438ca27c3d7ea91c971a1f41f92","objectDigest":"sha256:004dfc8da678c309de28b5386a1e9efd57f536b150c40d29b31506aa0fb17ec2"},{"relativeId":1,"descriptorDigest":"sha256:db74cb63348414def73535c9f0f83e8ad7df61229ed2806f4da8b69d6d7464d6","objectDigest":"sha256:5f78c33274e43fa9de5659265c1d917e25c03722dcb0b8d27db8d5feaa813953"}]}
+{"version":1,"header":{"digest":"sha256:635fa0a14a8ef0c0351ed3e985799ed1d4f75ce973dea3cc76c99710795cc3f1"},"objects":[{"relativeId":0,"descriptorDigest":"sha256:3634ad01db0dd5482ecf685267b53d6201690438ca27c3d7ea91c971a1f41f92","objectDigest":"sha256:004dfc8da678c309de28b5386a1e9efd57f536b150c40d29b31506aa0fb17ec2"},{"relativeId":1,"descriptorDigest":"sha256:04b5f87c9692a54f80d10fb6af00c779763aeca29d610348854bd97cd8bf66fd","objectDigest":"sha256:9f9c4e5e131934969b4ac8f495691c70b8c6c8e3f489c2c9ab5f1af82bce0604"}]}
-----BEGIN PGP SIGNATURE-----
-wsBzBAEBCAAnBQJe+oD0CZCiDCfuf/e6hBahBBIEXIwLEATQWN5L7aIMJ+5/97qE
-AABQ4QgAkWcLNLcghZ96VnJ9+67qbsdwp51rfERCKN0dZLBTHKN5Qjn1BWM/XbPj
-Qnl0F6D6YBId7c/KO0sbb3EHUdpmMEQlouQYFOTHWtdyvwO6spRLBx5EQA7Iv0rF
-jREz/jC7GaREK94u+hXRr94+FH5gEnHUL+Vg7pW/+cGiwLY1ddoL8ELgYhxqxd9J
-sET+vU1E4GJ3TyYFhVFsMsNeW7dQauqjQSJxMLTwXNphxTH19ePbJ2uDE2UJ3fn7
-up5ruugRyEe5qgRICGxRSDp8/INGRvoDUi32T9uLORzS+umRX5YW0b6RWD+5R72V
-0ewbMTJIx2lpfQGPMWROwcF7nkLdWQ==
-=WWGX
+wsBzBAEBCAAnBQJe+oD0CZCiDCfuf/e6hBYhBBIEXIwLEATQWN5L7aIMJ+5/97qE
+AAC46gf/VXyzZ649nttrX13JkM5kRVPlAIblBQxfoUxA1xwIXdRoM5ceDY0Em+YD
+8b6Xl1w2sDTqo0R15cJSh8sf0ClFOvYpDQRNCwKx17k1Wd0gHcW4QVu6gJnlbNvN
+o/EJdEN2TkbCM2aFvj34DAIfErRBIEsCeDDvJ/6WUSySWbnydfNU2pCsnK4A7l2H
+KOXFzSaPijG9L/pU3O3vNZ+fXPffqHL9JVhs5Mt/Yo3oeoEnoVaKvJLGx/fyl+Gj
+7qsfWFyHWzRCww9VFg/TCBeUku0CYRfXhxOgo4OuHNr8oo82rKDZU6+l3UZ2Sw8T
++kLe/zUkaILocGOvhvKdi630OGGb/Q==
+=3Jq2
-----END PGP SIGNATURE----- \ No newline at end of file
diff --git a/pkg/siftool/testdata/Test_command_getDump/Two/out.golden b/pkg/siftool/testdata/Test_command_getDump/Two/out.golden
index 7d174b1..cf6539a 100644
--- a/pkg/siftool/testdata/Test_command_getDump/Two/out.golden
+++ b/pkg/siftool/testdata/Test_command_getDump/Two/out.golden
Binary files differ
diff --git a/pkg/siftool/testdata/Test_command_getHeader/OneGroup/out.golden b/pkg/siftool/testdata/Test_command_getHeader/OneGroup/out.golden
index bc631d6..a2941c1 100644
--- a/pkg/siftool/testdata/Test_command_getHeader/OneGroup/out.golden
+++ b/pkg/siftool/testdata/Test_command_getHeader/OneGroup/out.golden
@@ -5,4 +5,4 @@ Descriptors Total: 48
Descriptors Offset: 4096
Descriptors Size: 27 KiB
Data Offset: 32176
-Data Size: 5 KiB
+Data Size: 9 KiB
diff --git a/pkg/siftool/testdata/Test_command_getHeader/TwoGroups/out.golden b/pkg/siftool/testdata/Test_command_getHeader/TwoGroups/out.golden
index 0db18f0..c16ef18 100644
--- a/pkg/siftool/testdata/Test_command_getHeader/TwoGroups/out.golden
+++ b/pkg/siftool/testdata/Test_command_getHeader/TwoGroups/out.golden
@@ -5,4 +5,4 @@ Descriptors Total: 48
Descriptors Offset: 4096
Descriptors Size: 27 KiB
Data Offset: 32176
-Data Size: 9 KiB
+Data Size: 265 KiB
diff --git a/pkg/siftool/testdata/Test_command_getHeader/TwoGroupsSigned/out.golden b/pkg/siftool/testdata/Test_command_getHeader/TwoGroupsSigned/out.golden
index acf8b17..44854c9 100644
--- a/pkg/siftool/testdata/Test_command_getHeader/TwoGroupsSigned/out.golden
+++ b/pkg/siftool/testdata/Test_command_getHeader/TwoGroupsSigned/out.golden
@@ -5,4 +5,4 @@ Descriptors Total: 48
Descriptors Offset: 4096
Descriptors Size: 27 KiB
Data Offset: 32176
-Data Size: 17 KiB
+Data Size: 266 KiB
diff --git a/pkg/siftool/testdata/Test_command_getInfo/Two/out.golden b/pkg/siftool/testdata/Test_command_getInfo/Two/out.golden
index f34651f..f49a554 100644
--- a/pkg/siftool/testdata/Test_command_getInfo/Two/out.golden
+++ b/pkg/siftool/testdata/Test_command_getInfo/Two/out.golden
@@ -3,7 +3,7 @@
Group ID: 1
Linked ID: NONE
Offset: 36864
- Size: 4
+ Size: 4096
Filesystem Type: Squashfs
Partition Type: *System
Architecture: 386
diff --git a/pkg/siftool/testdata/Test_command_getList/OneGroup/out.golden b/pkg/siftool/testdata/Test_command_getList/OneGroup/out.golden
index fe78f62..01400f9 100644
--- a/pkg/siftool/testdata/Test_command_getList/OneGroup/out.golden
+++ b/pkg/siftool/testdata/Test_command_getList/OneGroup/out.golden
@@ -2,4 +2,4 @@
ID |GROUP |LINK |SIF POSITION (start-end) |TYPE
------------------------------------------------------------------------------
1 |1 |NONE |32768-32772 |FS (Raw/System/386)
-2 |1 |NONE |36864-36868 |FS (Squashfs/*System/386)
+2 |1 |NONE |36864-40960 |FS (Squashfs/*System/386)
diff --git a/pkg/siftool/testdata/Test_command_getList/OneGroupSigned/out.golden b/pkg/siftool/testdata/Test_command_getList/OneGroupSigned/out.golden
index 98030e3..5b663d3 100644
--- a/pkg/siftool/testdata/Test_command_getList/OneGroupSigned/out.golden
+++ b/pkg/siftool/testdata/Test_command_getList/OneGroupSigned/out.golden
@@ -2,5 +2,5 @@
ID |GROUP |LINK |SIF POSITION (start-end) |TYPE
------------------------------------------------------------------------------
1 |1 |NONE |32768-32772 |FS (Raw/System/386)
-2 |1 |NONE |36864-36868 |FS (Squashfs/*System/386)
+2 |1 |NONE |36864-40960 |FS (Squashfs/*System/386)
3 |NONE |1 (G) |40960-42014 |Signature (SHA-256)
diff --git a/pkg/siftool/testdata/Test_command_getList/TwoGroups/out.golden b/pkg/siftool/testdata/Test_command_getList/TwoGroups/out.golden
index 648c280..1eca2ab 100644
--- a/pkg/siftool/testdata/Test_command_getList/TwoGroups/out.golden
+++ b/pkg/siftool/testdata/Test_command_getList/TwoGroups/out.golden
@@ -2,5 +2,5 @@
ID |GROUP |LINK |SIF POSITION (start-end) |TYPE
------------------------------------------------------------------------------
1 |1 |NONE |32768-32772 |FS (Raw/System/386)
-2 |1 |NONE |36864-36868 |FS (Squashfs/*System/386)
-3 |2 |NONE |40960-40964 |FS (Ext3/System/amd64)
+2 |1 |NONE |36864-40960 |FS (Squashfs/*System/386)
+3 |2 |NONE |40960-303104 |FS (Ext3/System/amd64)
diff --git a/pkg/siftool/testdata/Test_command_getList/TwoGroupsSigned/out.golden b/pkg/siftool/testdata/Test_command_getList/TwoGroupsSigned/out.golden
index f21bf6d..17240ba 100644
--- a/pkg/siftool/testdata/Test_command_getList/TwoGroupsSigned/out.golden
+++ b/pkg/siftool/testdata/Test_command_getList/TwoGroupsSigned/out.golden
@@ -2,7 +2,7 @@
ID |GROUP |LINK |SIF POSITION (start-end) |TYPE
------------------------------------------------------------------------------
1 |1 |NONE |32768-32772 |FS (Raw/System/386)
-2 |1 |NONE |36864-36868 |FS (Squashfs/*System/386)
-3 |2 |NONE |40960-40964 |FS (Ext3/System/amd64)
-4 |NONE |1 (G) |45056-46110 |Signature (SHA-256)
-5 |NONE |2 (G) |49152-50007 |Signature (SHA-256)
+2 |1 |NONE |36864-40960 |FS (Squashfs/*System/386)
+3 |2 |NONE |40960-303104 |FS (Ext3/System/amd64)
+4 |NONE |1 (G) |303104-304158 |Signature (SHA-256)
+5 |NONE |2 (G) |304158-305013 |Signature (SHA-256)
diff --git a/pkg/siftool/testdata/Test_command_getMount/Empty/err.golden b/pkg/siftool/testdata/Test_command_getMount/Empty/err.golden
new file mode 100644
index 0000000..cd860b8
--- /dev/null
+++ b/pkg/siftool/testdata/Test_command_getMount/Empty/err.golden
@@ -0,0 +1 @@
+Error: failed to get partition descriptor: no objects in image
diff --git a/pkg/siftool/testdata/Test_command_getMount/Empty/out.golden b/pkg/siftool/testdata/Test_command_getMount/Empty/out.golden
new file mode 100644
index 0000000..f22522a
--- /dev/null
+++ b/pkg/siftool/testdata/Test_command_getMount/Empty/out.golden
@@ -0,0 +1,9 @@
+Usage:
+ mount <sif_path> <mount_path>
+
+Examples:
+ mount image.sif path/
+
+Flags:
+ -h, --help help for mount
+
diff --git a/pkg/siftool/testdata/Test_command_getMount/OneGroup/err.golden b/pkg/siftool/testdata/Test_command_getMount/OneGroup/err.golden
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pkg/siftool/testdata/Test_command_getMount/OneGroup/err.golden
diff --git a/pkg/siftool/testdata/Test_command_getMount/OneGroup/out.golden b/pkg/siftool/testdata/Test_command_getMount/OneGroup/out.golden
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pkg/siftool/testdata/Test_command_getMount/OneGroup/out.golden
diff --git a/pkg/siftool/testdata/Test_command_getUnmount/err.golden b/pkg/siftool/testdata/Test_command_getUnmount/err.golden
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pkg/siftool/testdata/Test_command_getUnmount/err.golden
diff --git a/pkg/siftool/testdata/Test_command_getUnmount/out.golden b/pkg/siftool/testdata/Test_command_getUnmount/out.golden
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pkg/siftool/testdata/Test_command_getUnmount/out.golden
diff --git a/pkg/siftool/unmount.go b/pkg/siftool/unmount.go
new file mode 100644
index 0000000..8814182
--- /dev/null
+++ b/pkg/siftool/unmount.go
@@ -0,0 +1,27 @@
+// Copyright (c) 2022, Sylabs Inc. All rights reserved.
+// This software is licensed under a 3-clause BSD license. Please consult the
+// LICENSE file distributed with the sources of this project regarding your
+// rights to use or distribute this software.
+
+package siftool
+
+import (
+ "github.com/spf13/cobra"
+)
+
+// getUnmount returns a command that unmounts the primary system partition of a SIF image.
+func (c *command) getUnmount() *cobra.Command {
+ return &cobra.Command{
+ Use: "unmount <mount_path>",
+ Short: "Unmount primary system partition",
+ Long: "Unmount a primary system partition of a SIF image",
+ Example: c.opts.rootPath + " unmount path/",
+ Args: cobra.ExactArgs(1),
+ PreRunE: c.initApp,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return c.app.Unmount(cmd.Context(), args[0])
+ },
+ DisableFlagsInUseLine: true,
+ Hidden: true, // hide while command is experimental
+ }
+}
diff --git a/pkg/siftool/unmount_test.go b/pkg/siftool/unmount_test.go
new file mode 100644
index 0000000..2cf8160
--- /dev/null
+++ b/pkg/siftool/unmount_test.go
@@ -0,0 +1,42 @@
+// Copyright (c) 2022, Sylabs Inc. All rights reserved.
+// This software is licensed under a 3-clause BSD license. Please consult the
+// LICENSE file distributed with the sources of this project regarding your
+// rights to use or distribute this software.
+
+package siftool
+
+import (
+ "context"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+
+ "github.com/sylabs/sif/v2/pkg/user"
+)
+
+func Test_command_getUnmount(t *testing.T) {
+ if _, err := exec.LookPath("squashfuse"); err != nil {
+ t.Skip(" not found, skipping unmount tests")
+ }
+ if _, err := exec.LookPath("fusermount"); err != nil {
+ t.Skip(" not found, skipping unmount tests")
+ }
+
+ path, err := os.MkdirTemp("", "siftool-unmount-*")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ os.RemoveAll(path)
+ })
+
+ testSIF := filepath.Join(corpus, "one-group.sif")
+ if err := user.Mount(context.Background(), testSIF, path); err != nil {
+ t.Fatal(err)
+ }
+
+ c := &command{}
+ cmd := c.getUnmount()
+ runCommand(t, cmd, []string{path}, nil)
+}
diff --git a/pkg/user/mount.go b/pkg/user/mount.go
new file mode 100644
index 0000000..c96e414
--- /dev/null
+++ b/pkg/user/mount.go
@@ -0,0 +1,122 @@
+// Copyright (c) 2022, Sylabs Inc. All rights reserved.
+// This software is licensed under a 3-clause BSD license. Please consult the
+// LICENSE file distributed with the sources of this project regarding your
+// rights to use or distribute this software.
+
+package user
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+
+ "github.com/sylabs/sif/v2/pkg/sif"
+)
+
+// mountSquashFS mounts the SquashFS filesystem from path at offset into mountPath.
+func mountSquashFS(ctx context.Context, offset int64, path, mountPath string, mo mountOpts) error {
+ args := []string{
+ "-o", fmt.Sprintf("ro,offset=%d", offset),
+ filepath.Clean(path),
+ filepath.Clean(mountPath),
+ }
+ //nolint:gosec // note (gosec exclusion) - we require callers to be able to specify squashfuse not on PATH
+ cmd := exec.CommandContext(ctx, mo.squashfusePath, args...)
+ cmd.Stdout = mo.stdout
+ cmd.Stderr = mo.stderr
+
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("failed to mount: %w", err)
+ }
+
+ return nil
+}
+
+// mountOpts accumulates mount options.
+type mountOpts struct {
+ stdout io.Writer
+ stderr io.Writer
+ squashfusePath string
+}
+
+// MountOpt are used to specify mount options.
+type MountOpt func(*mountOpts) error
+
+// OptMountStdout writes standard output to w.
+func OptMountStdout(w io.Writer) MountOpt {
+ return func(mo *mountOpts) error {
+ mo.stdout = w
+ return nil
+ }
+}
+
+// OptMountStderr writes standard error to w.
+func OptMountStderr(w io.Writer) MountOpt {
+ return func(mo *mountOpts) error {
+ mo.stderr = w
+ return nil
+ }
+}
+
+var errSquashfusePathInvalid = errors.New("squashfuse path must be relative or absolute")
+
+// OptMountSquashfusePath sets an explicit path to the squashfuse binary. The path must be an
+// absolute or relative path.
+func OptMountSquashfusePath(path string) MountOpt {
+ return func(mo *mountOpts) error {
+ if filepath.Base(path) == path {
+ return errSquashfusePathInvalid
+ }
+ mo.squashfusePath = path
+ return nil
+ }
+}
+
+var errUnsupportedFSType = errors.New("unrecognized filesystem type")
+
+// Mount mounts the primary system partition of the SIF file at path into mountPath.
+//
+// Mount may start one or more underlying processes. By default, stdout and stderr of these
+// processes is discarded. To modify this behavior, consider using OptMountStdout and/or
+// OptMountStderr.
+//
+// By default, Mount searches for a squashfuse binary in the directories named by the PATH
+// environment variable. To override this behavior, consider using OptMountSquashfusePath().
+func Mount(ctx context.Context, path, mountPath string, opts ...MountOpt) error {
+ mo := mountOpts{
+ squashfusePath: "squashfuse",
+ }
+
+ for _, opt := range opts {
+ if err := opt(&mo); err != nil {
+ return fmt.Errorf("%w", err)
+ }
+ }
+
+ f, err := sif.LoadContainerFromPath(path, sif.OptLoadWithFlag(os.O_RDONLY))
+ if err != nil {
+ return fmt.Errorf("failed to load image: %w", err)
+ }
+ defer func() { _ = f.UnloadContainer() }()
+
+ d, err := f.GetDescriptor(sif.WithPartitionType(sif.PartPrimSys))
+ if err != nil {
+ return fmt.Errorf("failed to get partition descriptor: %w", err)
+ }
+
+ fs, _, _, err := d.PartitionMetadata()
+ if err != nil {
+ return fmt.Errorf("failed to get partition metadata: %w", err)
+ }
+
+ switch fs {
+ case sif.FsSquash:
+ return mountSquashFS(ctx, d.Offset(), path, mountPath, mo)
+ default:
+ return errUnsupportedFSType
+ }
+}
diff --git a/pkg/user/unmount.go b/pkg/user/unmount.go
new file mode 100644
index 0000000..609abf5
--- /dev/null
+++ b/pkg/user/unmount.go
@@ -0,0 +1,93 @@
+// Copyright (c) 2022, Sylabs Inc. All rights reserved.
+// This software is licensed under a 3-clause BSD license. Please consult the
+// LICENSE file distributed with the sources of this project regarding your
+// rights to use or distribute this software.
+
+package user
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "os/exec"
+ "path/filepath"
+)
+
+// unmountSquashFS unmounts the filesystem at mountPath.
+func unmountSquashFS(ctx context.Context, mountPath string, uo unmountOpts) error {
+ args := []string{
+ "-u",
+ filepath.Clean(mountPath),
+ }
+ cmd := exec.CommandContext(ctx, uo.fusermountPath, args...) //nolint:gosec
+ cmd.Stdout = uo.stdout
+ cmd.Stderr = uo.stderr
+
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("failed to unmount: %w", err)
+ }
+
+ return nil
+}
+
+// unmountOpts accumulates unmount options.
+type unmountOpts struct {
+ stdout io.Writer
+ stderr io.Writer
+ fusermountPath string
+}
+
+// UnmountOpt are used to specify unmount options.
+type UnmountOpt func(*unmountOpts) error
+
+// OptUnmountStdout writes standard output to w.
+func OptUnmountStdout(w io.Writer) UnmountOpt {
+ return func(mo *unmountOpts) error {
+ mo.stdout = w
+ return nil
+ }
+}
+
+// OptUnmountStderr writes standard error to w.
+func OptUnmountStderr(w io.Writer) UnmountOpt {
+ return func(mo *unmountOpts) error {
+ mo.stderr = w
+ return nil
+ }
+}
+
+var errFusermountPathInvalid = errors.New("fusermount path must be relative or absolute")
+
+// OptUnmountFusermountPath sets the path to the fusermount binary.
+func OptUnmountFusermountPath(path string) UnmountOpt {
+ return func(mo *unmountOpts) error {
+ if filepath.Base(path) == path {
+ return errFusermountPathInvalid
+ }
+ mo.fusermountPath = path
+ return nil
+ }
+}
+
+// Unmount unmounts the filesystem at mountPath.
+//
+// Unmount may start one or more underlying processes. By default, stdout and stderr of these
+// processes is discarded. To modify this behavior, consider using OptUnmountStdout and/or
+// OptUnmountStderr.
+//
+// By default, Unmount searches for a fusermount binary in the directories named by the PATH
+// environment variable. To override this behavior, consider using OptUnmountFusermountPath().
+func Unmount(ctx context.Context, mountPath string, opts ...UnmountOpt) error {
+ uo := unmountOpts{
+ fusermountPath: "fusermount",
+ }
+
+ for _, opt := range opts {
+ if err := opt(&uo); err != nil {
+ return fmt.Errorf("%w", err)
+ }
+ }
+
+ return unmountSquashFS(ctx, mountPath, uo)
+}
diff --git a/pkg/user/unmount_test.go b/pkg/user/unmount_test.go
new file mode 100644
index 0000000..e24baf0
--- /dev/null
+++ b/pkg/user/unmount_test.go
@@ -0,0 +1,139 @@
+// Copyright (c) 2022, Sylabs Inc. All rights reserved.
+// This software is licensed under a 3-clause BSD license. Please consult the
+// LICENSE file distributed with the sources of this project regarding your
+// rights to use or distribute this software.
+
+package user
+
+import (
+ "bufio"
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+var corpus = filepath.Join("..", "..", "test", "images")
+
+func Test_Unmount(t *testing.T) {
+ if _, err := exec.LookPath("squashfuse"); err != nil {
+ t.Skip(" not found, skipping mount tests")
+ }
+ fusermountPath, err := exec.LookPath("fusermount")
+ if err != nil {
+ t.Skip(" not found, skipping mount tests")
+ }
+
+ path, err := os.MkdirTemp("", "siftool-mount-*")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ os.RemoveAll(path)
+ })
+
+ tests := []struct {
+ name string
+ mountSIF string
+ mountPath string
+ opts []UnmountOpt
+ wantErr bool
+ wantUnmounted bool
+ }{
+ {
+ name: "Mounted",
+ mountSIF: filepath.Join(corpus, "one-group.sif"),
+ mountPath: path,
+ wantErr: false,
+ wantUnmounted: true,
+ },
+ {
+ name: "NotMounted",
+ mountSIF: "",
+ mountPath: path,
+ wantErr: true,
+ },
+ {
+ name: "NotSquashfuse",
+ mountSIF: "",
+ mountPath: "/dev",
+ wantErr: true,
+ },
+ {
+ name: "FusermountBare",
+ mountSIF: "",
+ mountPath: path,
+ opts: []UnmountOpt{OptUnmountFusermountPath("fusermount")},
+ wantErr: true,
+ },
+ {
+ name: "FusermountValid",
+ mountSIF: filepath.Join(corpus, "one-group.sif"),
+ mountPath: path,
+ opts: []UnmountOpt{OptUnmountFusermountPath(fusermountPath)},
+ wantErr: false,
+ wantUnmounted: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.mountSIF != "" {
+ err := Mount(context.Background(), tt.mountSIF, path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ err := Unmount(context.Background(), tt.mountPath, tt.opts...)
+
+ if err != nil && !tt.wantErr {
+ t.Errorf("Unexpected error: %s", err)
+ }
+ if err == nil && tt.wantErr {
+ t.Error("Unexpected success")
+ }
+
+ mounted, err := isMounted(tt.mountPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tt.wantUnmounted && mounted {
+ t.Errorf("Expected %s to be unmounted, but it is mounted", tt.mountPath)
+ }
+ })
+ }
+}
+
+var errBadMountInfo = errors.New("bad mount info")
+
+func isMounted(mountPath string) (bool, error) {
+ mountPath, err := filepath.Abs(mountPath)
+ if err != nil {
+ return false, err
+ }
+
+ mi, err := os.Open("/proc/self/mountinfo")
+ if err != nil {
+ return false, fmt.Errorf("failed to open /proc/self/mountinfo: %w", err)
+ }
+ defer mi.Close()
+
+ scanner := bufio.NewScanner(mi)
+ for scanner.Scan() {
+ fields := strings.Split(scanner.Text(), " ")
+ if len(fields) < 5 {
+ return false, fmt.Errorf("not enough mountinfo fields: %w", errBadMountInfo)
+ }
+ //nolint:lll
+ // 1348 63 0:77 / /tmp/siftool-mount-956028386 ro,nosuid,nodev,relatime shared:646 - fuse.squashfuse squashfuse ro,user_id=1000,group_id=100
+ mntTarget := fields[4]
+ if mntTarget == mountPath {
+ return true, nil
+ }
+ }
+ return false, nil
+}