Skip to content

Commit e45d223

Browse files
authored
Merge pull request #5425 from dctrud/3.6.0-integrity-merge
Merge master integrity work for 3.6.0 release
2 parents 6026d03 + 9dc2020 commit e45d223

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+1862
-1078
lines changed

Diff for: CHANGELOG.md

+41-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,38 @@ _With the release of `v3.0.0`, we're introducing a new changelog format in an at
99

1010
_The old changelog can be found in the `release-2.6` branch_
1111

12-
# v3.6.0-rc.5 - [2020-06-25] (pre-release)
12+
# v3.6.0 - [2020-07-14]
13+
14+
## Security related fixes
15+
16+
Singularity 3.6.0 introduces a new signature format for SIF images,
17+
and changes to the signing / verification code to address:
18+
19+
- [CVE-2020-13845](https://cve.mitre.org/cgi-bin/cvename.cgi?name=2020-13845)
20+
In Singularity 3.x versions below 3.6.0, issues allow the ECL to
21+
be bypassed by a malicious user.
22+
- [CVE-2020-13846](https://cve.mitre.org/cgi-bin/cvename.cgi?name=2020-13846)
23+
In Singularity 3.5 the `--all / -a` option to `singularity verify`
24+
returns success even when some objects in a SIF container are not
25+
signed, or cannot be verified.
26+
- [CVE-2020-13847](https://cve.mitre.org/cgi-bin/cvename.cgi?name=2020-13847)
27+
In Singularity 3.x versions below 3.6.0, Singularity's sign and
28+
verify commands do not sign metadata found in the global header or
29+
data object descriptors of a SIF file, allowing an attacker to
30+
cause unexpected behavior. A signed container may verify
31+
successfully, even when it has been modified in ways that could be
32+
exploited to cause malicious behavior.
33+
34+
Please see the published security advisories at
35+
https://github.com/hpcng/singularity/security/advisories for full
36+
detail of these security issues.
37+
38+
Note that the new signature format is necessarily incompatible with
39+
Singularity < 3.6.0 - e.g. Singularity 3.5.3 cannot verify containers
40+
signed by 3.6.0.
41+
42+
We thank Tru Huynh for a report that led to the review of, and changes to,
43+
the signature implementation.
1344

1445
## New features / functionalities
1546
- Singularity now supports the execution of minimal Docker/OCI
@@ -36,8 +67,15 @@ _The old changelog can be found in the `release-2.6` branch_
3667
- A new `--days` flag for `cache clean` allows removal of items older than a
3768
specified number of days. Replaces the `--name` flag which is not generally
3869
useful as the cache entries are stored by hash, not a friendly name.
70+
- A new '--legacy-insecure' flag to `verify` allows verification of SIF signatures
71+
in the old, insecure format.
72+
- A new '-l / --logs' flag for `instance list` that shows the paths
73+
to instance STDERR / STDOUT log files.
74+
- The `--json` output of `instance list` now include paths to STDERR
75+
/ STDOUT log files.
3976

4077
## Changed defaults / behaviours
78+
- New signature format (see security fixes above).
4179
- Environment variables prefixed with `SINGULARITYENV_` always take
4280
precedence over variables without `SINGULARITYENV_` prefix.
4381
- The `%post` build section inherits environment variables from the base image.
@@ -56,6 +94,8 @@ _The old changelog can be found in the `release-2.6` branch_
5694

5795
## Deprecated / removed commands
5896
- Removed `--name` flag for `cache clean`; replaced with `--days`.
97+
- Deprecate `-a / --all` option to `sign/verify` as new signature
98+
behavior makes this the default.
5999

60100
## Bug Fixes
61101
- Don't try to mount `$HOME` when it is `/` (e.g. `nobody` user).

Diff for: cmd/internal/cli/pgp.go

+277
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
// Copyright (c) 2020, Sylabs Inc. All rights reserved.
2+
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
3+
// distributed with the sources of this project regarding your rights to use or distribute this
4+
// software.
5+
6+
package cli
7+
8+
import (
9+
"encoding/hex"
10+
"encoding/json"
11+
"errors"
12+
"fmt"
13+
"io"
14+
"os"
15+
16+
"github.com/fatih/color"
17+
"github.com/sylabs/sif/pkg/integrity"
18+
"github.com/sylabs/sif/pkg/sif"
19+
"github.com/sylabs/singularity/internal/app/singularity"
20+
"github.com/sylabs/singularity/internal/pkg/util/interactive"
21+
"github.com/sylabs/singularity/pkg/sylog"
22+
"github.com/sylabs/singularity/pkg/sypgp"
23+
"golang.org/x/crypto/openpgp"
24+
"golang.org/x/crypto/openpgp/packet"
25+
)
26+
27+
var (
28+
errEmptyKeyring = errors.New("keyring is empty")
29+
errIndexOutOfRange = errors.New("index out of range")
30+
)
31+
32+
// printEntityAtIndex prints entity e, associated with index i, to w.
33+
func printEntityAtIndex(w io.Writer, i int, e *openpgp.Entity) {
34+
for _, v := range e.Identities {
35+
fmt.Fprintf(w, "%d) U: %s (%s) <%s>\n", i, v.UserId.Name, v.UserId.Comment, v.UserId.Email)
36+
}
37+
fmt.Fprintf(w, " C: %s\n", e.PrimaryKey.CreationTime)
38+
fmt.Fprintf(w, " F: %0X\n", e.PrimaryKey.Fingerprint)
39+
bits, _ := e.PrimaryKey.BitLength()
40+
fmt.Fprintf(w, " L: %d\n", bits)
41+
fmt.Fprint(os.Stdout, " --------\n")
42+
}
43+
44+
// selectEntityInteractive returns an EntitySelector that selects an entity from el, prompting the
45+
// user for a selection if there is more than one entity in el.
46+
func selectEntityInteractive() sypgp.EntitySelector {
47+
return func(el openpgp.EntityList) (*openpgp.Entity, error) {
48+
switch len(el) {
49+
case 0:
50+
return nil, errEmptyKeyring
51+
case 1:
52+
return el[0], nil
53+
default:
54+
for i, e := range el {
55+
printEntityAtIndex(os.Stdout, i, e)
56+
}
57+
58+
n, err := interactive.AskNumberInRange(0, len(el)-1, "Enter # of private key to use : ")
59+
if err != nil {
60+
return nil, err
61+
}
62+
return el[n], nil
63+
}
64+
}
65+
}
66+
67+
// selectEntityAtIndex returns an EntitySelector that selects the entity at index i.
68+
func selectEntityAtIndex(i int) sypgp.EntitySelector {
69+
return func(el openpgp.EntityList) (*openpgp.Entity, error) {
70+
if i >= len(el) {
71+
return nil, errIndexOutOfRange
72+
}
73+
return el[i], nil
74+
}
75+
}
76+
77+
// decryptSelectedEntityInteractive wraps f, attempting to decrypt the private key in the selected
78+
// entity with a passpharse provided interactively by the user.
79+
func decryptSelectedEntityInteractive(f sypgp.EntitySelector) sypgp.EntitySelector {
80+
return func(el openpgp.EntityList) (*openpgp.Entity, error) {
81+
e, err := f(el)
82+
if err != nil {
83+
return nil, err
84+
}
85+
86+
if e.PrivateKey.Encrypted {
87+
if err := decryptPrivateKeyInteractive(e); err != nil {
88+
return nil, err
89+
}
90+
}
91+
92+
return e, nil
93+
}
94+
}
95+
96+
// decryptPrivateKeyInteractive decrypts the private key in e, prompting the user for a passphrase.
97+
func decryptPrivateKeyInteractive(e *openpgp.Entity) error {
98+
passphrase, err := interactive.AskQuestionNoEcho("Enter key passphrase : ")
99+
if err != nil {
100+
return err
101+
}
102+
103+
return e.PrivateKey.Decrypt([]byte(passphrase))
104+
}
105+
106+
// primaryIdentity returns the Identity marked as primary, or the first identity if none are so
107+
// marked.
108+
func primaryIdentity(e *openpgp.Entity) *openpgp.Identity {
109+
var first *openpgp.Identity
110+
for _, id := range e.Identities {
111+
if first == nil {
112+
first = id
113+
}
114+
if id.SelfSignature.IsPrimaryId != nil && *id.SelfSignature.IsPrimaryId {
115+
return id
116+
}
117+
}
118+
return first
119+
}
120+
121+
// isLocal returns true if signing entity e is found in the local keyring, and false otherwise.
122+
func isLocal(e *openpgp.Entity) bool {
123+
kr, err := sypgp.PublicKeyRing()
124+
if err != nil {
125+
return false
126+
}
127+
128+
keys := kr.KeysByIdUsage(e.PrimaryKey.KeyId, packet.KeyFlagSign)
129+
return len(keys) > 0
130+
}
131+
132+
// outputVerify outputs a textual representation of r to stdout.
133+
func outputVerify(f *sif.FileImage, r integrity.VerifyResult) bool {
134+
e := r.Entity()
135+
136+
// Print signing entity info.
137+
if e != nil {
138+
prefix := color.New(color.FgYellow).Sprint("[REMOTE]")
139+
if isLocal(e) {
140+
prefix = color.New(color.FgGreen).Sprint("[LOCAL]")
141+
}
142+
143+
// Print identity, if possible.
144+
if id := primaryIdentity(e); id != nil {
145+
fmt.Printf("%-18v Signing entity: %v\n", prefix, id.Name)
146+
} else {
147+
sylog.Warningf("Primary identity unknown")
148+
}
149+
150+
// Always print fingerprint.
151+
fmt.Printf("%-18v Fingerprint: %X\n", prefix, e.PrimaryKey.Fingerprint)
152+
}
153+
154+
// Print table of signed objects.
155+
if len(r.Verified()) > 0 {
156+
fmt.Printf("Objects verified:\n")
157+
fmt.Printf("%-4s|%-8s|%-8s|%s\n", "ID", "GROUP", "LINK", "TYPE")
158+
fmt.Print("------------------------------------------------\n")
159+
}
160+
for _, id := range r.Verified() {
161+
od, _, err := f.GetFromDescrID(id)
162+
if err != nil {
163+
sylog.Errorf("failed to get descriptor: %v", err)
164+
return false
165+
}
166+
167+
group := "NONE"
168+
if gid := od.Groupid; gid != sif.DescrUnusedGroup {
169+
group = fmt.Sprintf("%d", gid&^sif.DescrGroupMask)
170+
}
171+
172+
link := "NONE"
173+
if l := od.Link; l != sif.DescrUnusedLink {
174+
if l&sif.DescrGroupMask == sif.DescrGroupMask {
175+
link = fmt.Sprintf("%d (G)", l&^sif.DescrGroupMask)
176+
} else {
177+
link = fmt.Sprintf("%d", l)
178+
}
179+
}
180+
181+
fmt.Printf("%-4d|%-8s|%-8s|%s\n", id, group, link, od.Datatype)
182+
}
183+
184+
if err := r.Error(); err != nil {
185+
fmt.Printf("\nError encountered during signature verification: %v\n", err)
186+
}
187+
188+
return false
189+
}
190+
191+
type key struct {
192+
Signer keyEntity
193+
}
194+
195+
// keyEntity holds all the key info, used for json output.
196+
type keyEntity struct {
197+
Partition string
198+
Name string
199+
Fingerprint string
200+
KeyLocal bool
201+
KeyCheck bool
202+
DataCheck bool
203+
}
204+
205+
// keyList is a list of one or more keys.
206+
type keyList struct {
207+
Signatures int
208+
SignerKeys []*key
209+
}
210+
211+
// getJSONCallback returns a singularity.VerifyCallback that appends to kl.
212+
func getJSONCallback(kl *keyList) singularity.VerifyCallback {
213+
return func(f *sif.FileImage, r integrity.VerifyResult) bool {
214+
name, fp := "unknown", ""
215+
var keyLocal, keyCheck bool
216+
217+
// Increment signature count.
218+
kl.Signatures++
219+
220+
// If entity is determined, note a few values.
221+
if e := r.Entity(); e != nil {
222+
if id := primaryIdentity(e); id != nil {
223+
name = id.Name
224+
}
225+
fp = hex.EncodeToString(e.PrimaryKey.Fingerprint[:])
226+
keyLocal = isLocal(e)
227+
keyCheck = true
228+
}
229+
230+
// For each verified object, append an entry to the list.
231+
for _, id := range r.Verified() {
232+
od, _, err := f.GetFromDescrID(id)
233+
if err != nil {
234+
sylog.Errorf("failed to get descriptor: %v", err)
235+
continue
236+
}
237+
238+
ke := keyEntity{
239+
Partition: od.Datatype.String(),
240+
Name: name,
241+
Fingerprint: fp,
242+
KeyLocal: keyLocal,
243+
KeyCheck: keyCheck,
244+
DataCheck: true,
245+
}
246+
kl.SignerKeys = append(kl.SignerKeys, &key{ke})
247+
}
248+
249+
var integrityError *integrity.ObjectIntegrityError
250+
if errors.As(r.Error(), &integrityError) {
251+
od, _, err := f.GetFromDescrID(integrityError.ID)
252+
if err != nil {
253+
sylog.Errorf("failed to get descriptor: %v", err)
254+
return false
255+
}
256+
257+
ke := keyEntity{
258+
Partition: od.Datatype.String(),
259+
Name: name,
260+
Fingerprint: fp,
261+
KeyLocal: keyLocal,
262+
KeyCheck: keyCheck,
263+
DataCheck: false,
264+
}
265+
kl.SignerKeys = append(kl.SignerKeys, &key{ke})
266+
}
267+
268+
return false
269+
}
270+
}
271+
272+
// outputJSON outputs a JSON representation of kl to w.
273+
func outputJSON(w io.Writer, kl keyList) error {
274+
e := json.NewEncoder(w)
275+
e.SetIndent("", " ")
276+
return e.Encode(kl)
277+
}

0 commit comments

Comments
 (0)