Skip to content

Commit

Permalink
Merge pull request #5425 from dctrud/3.6.0-integrity-merge
Browse files Browse the repository at this point in the history
Merge master integrity work for 3.6.0 release
  • Loading branch information
dtrudg authored Jul 14, 2020
2 parents 6026d03 + 9dc2020 commit e45d223
Show file tree
Hide file tree
Showing 63 changed files with 1,862 additions and 1,078 deletions.
42 changes: 41 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,38 @@ _With the release of `v3.0.0`, we're introducing a new changelog format in an at

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

# v3.6.0-rc.5 - [2020-06-25] (pre-release)
# v3.6.0 - [2020-07-14]

## Security related fixes

Singularity 3.6.0 introduces a new signature format for SIF images,
and changes to the signing / verification code to address:

- [CVE-2020-13845](https://cve.mitre.org/cgi-bin/cvename.cgi?name=2020-13845)
In Singularity 3.x versions below 3.6.0, issues allow the ECL to
be bypassed by a malicious user.
- [CVE-2020-13846](https://cve.mitre.org/cgi-bin/cvename.cgi?name=2020-13846)
In Singularity 3.5 the `--all / -a` option to `singularity verify`
returns success even when some objects in a SIF container are not
signed, or cannot be verified.
- [CVE-2020-13847](https://cve.mitre.org/cgi-bin/cvename.cgi?name=2020-13847)
In Singularity 3.x versions below 3.6.0, Singularity's sign and
verify commands do not sign metadata found in the global header or
data object descriptors of a SIF file, allowing an attacker to
cause unexpected behavior. A signed container may verify
successfully, even when it has been modified in ways that could be
exploited to cause malicious behavior.

Please see the published security advisories at
https://github.com/hpcng/singularity/security/advisories for full
detail of these security issues.

Note that the new signature format is necessarily incompatible with
Singularity < 3.6.0 - e.g. Singularity 3.5.3 cannot verify containers
signed by 3.6.0.

We thank Tru Huynh for a report that led to the review of, and changes to,
the signature implementation.

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

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

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

## Bug Fixes
- Don't try to mount `$HOME` when it is `/` (e.g. `nobody` user).
Expand Down
277 changes: 277 additions & 0 deletions cmd/internal/cli/pgp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
// Copyright (c) 2020, 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.

package cli

import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"

"github.com/fatih/color"
"github.com/sylabs/sif/pkg/integrity"
"github.com/sylabs/sif/pkg/sif"
"github.com/sylabs/singularity/internal/app/singularity"
"github.com/sylabs/singularity/internal/pkg/util/interactive"
"github.com/sylabs/singularity/pkg/sylog"
"github.com/sylabs/singularity/pkg/sypgp"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/packet"
)

var (
errEmptyKeyring = errors.New("keyring is empty")
errIndexOutOfRange = errors.New("index out of range")
)

// printEntityAtIndex prints entity e, associated with index i, to w.
func printEntityAtIndex(w io.Writer, i int, e *openpgp.Entity) {
for _, v := range e.Identities {
fmt.Fprintf(w, "%d) U: %s (%s) <%s>\n", i, v.UserId.Name, v.UserId.Comment, v.UserId.Email)
}
fmt.Fprintf(w, " C: %s\n", e.PrimaryKey.CreationTime)
fmt.Fprintf(w, " F: %0X\n", e.PrimaryKey.Fingerprint)
bits, _ := e.PrimaryKey.BitLength()
fmt.Fprintf(w, " L: %d\n", bits)
fmt.Fprint(os.Stdout, " --------\n")
}

// selectEntityInteractive returns an EntitySelector that selects an entity from el, prompting the
// user for a selection if there is more than one entity in el.
func selectEntityInteractive() sypgp.EntitySelector {
return func(el openpgp.EntityList) (*openpgp.Entity, error) {
switch len(el) {
case 0:
return nil, errEmptyKeyring
case 1:
return el[0], nil
default:
for i, e := range el {
printEntityAtIndex(os.Stdout, i, e)
}

n, err := interactive.AskNumberInRange(0, len(el)-1, "Enter # of private key to use : ")
if err != nil {
return nil, err
}
return el[n], nil
}
}
}

// selectEntityAtIndex returns an EntitySelector that selects the entity at index i.
func selectEntityAtIndex(i int) sypgp.EntitySelector {
return func(el openpgp.EntityList) (*openpgp.Entity, error) {
if i >= len(el) {
return nil, errIndexOutOfRange
}
return el[i], nil
}
}

// decryptSelectedEntityInteractive wraps f, attempting to decrypt the private key in the selected
// entity with a passpharse provided interactively by the user.
func decryptSelectedEntityInteractive(f sypgp.EntitySelector) sypgp.EntitySelector {
return func(el openpgp.EntityList) (*openpgp.Entity, error) {
e, err := f(el)
if err != nil {
return nil, err
}

if e.PrivateKey.Encrypted {
if err := decryptPrivateKeyInteractive(e); err != nil {
return nil, err
}
}

return e, nil
}
}

// decryptPrivateKeyInteractive decrypts the private key in e, prompting the user for a passphrase.
func decryptPrivateKeyInteractive(e *openpgp.Entity) error {
passphrase, err := interactive.AskQuestionNoEcho("Enter key passphrase : ")
if err != nil {
return err
}

return e.PrivateKey.Decrypt([]byte(passphrase))
}

// primaryIdentity returns the Identity marked as primary, or the first identity if none are so
// marked.
func primaryIdentity(e *openpgp.Entity) *openpgp.Identity {
var first *openpgp.Identity
for _, id := range e.Identities {
if first == nil {
first = id
}
if id.SelfSignature.IsPrimaryId != nil && *id.SelfSignature.IsPrimaryId {
return id
}
}
return first
}

// isLocal returns true if signing entity e is found in the local keyring, and false otherwise.
func isLocal(e *openpgp.Entity) bool {
kr, err := sypgp.PublicKeyRing()
if err != nil {
return false
}

keys := kr.KeysByIdUsage(e.PrimaryKey.KeyId, packet.KeyFlagSign)
return len(keys) > 0
}

// outputVerify outputs a textual representation of r to stdout.
func outputVerify(f *sif.FileImage, r integrity.VerifyResult) bool {
e := r.Entity()

// Print signing entity info.
if e != nil {
prefix := color.New(color.FgYellow).Sprint("[REMOTE]")
if isLocal(e) {
prefix = color.New(color.FgGreen).Sprint("[LOCAL]")
}

// Print identity, if possible.
if id := primaryIdentity(e); id != nil {
fmt.Printf("%-18v Signing entity: %v\n", prefix, id.Name)
} else {
sylog.Warningf("Primary identity unknown")
}

// Always print fingerprint.
fmt.Printf("%-18v Fingerprint: %X\n", prefix, e.PrimaryKey.Fingerprint)
}

// Print table of signed objects.
if len(r.Verified()) > 0 {
fmt.Printf("Objects verified:\n")
fmt.Printf("%-4s|%-8s|%-8s|%s\n", "ID", "GROUP", "LINK", "TYPE")
fmt.Print("------------------------------------------------\n")
}
for _, id := range r.Verified() {
od, _, err := f.GetFromDescrID(id)
if err != nil {
sylog.Errorf("failed to get descriptor: %v", err)
return false
}

group := "NONE"
if gid := od.Groupid; gid != sif.DescrUnusedGroup {
group = fmt.Sprintf("%d", gid&^sif.DescrGroupMask)
}

link := "NONE"
if l := od.Link; l != sif.DescrUnusedLink {
if l&sif.DescrGroupMask == sif.DescrGroupMask {
link = fmt.Sprintf("%d (G)", l&^sif.DescrGroupMask)
} else {
link = fmt.Sprintf("%d", l)
}
}

fmt.Printf("%-4d|%-8s|%-8s|%s\n", id, group, link, od.Datatype)
}

if err := r.Error(); err != nil {
fmt.Printf("\nError encountered during signature verification: %v\n", err)
}

return false
}

type key struct {
Signer keyEntity
}

// keyEntity holds all the key info, used for json output.
type keyEntity struct {
Partition string
Name string
Fingerprint string
KeyLocal bool
KeyCheck bool
DataCheck bool
}

// keyList is a list of one or more keys.
type keyList struct {
Signatures int
SignerKeys []*key
}

// getJSONCallback returns a singularity.VerifyCallback that appends to kl.
func getJSONCallback(kl *keyList) singularity.VerifyCallback {
return func(f *sif.FileImage, r integrity.VerifyResult) bool {
name, fp := "unknown", ""
var keyLocal, keyCheck bool

// Increment signature count.
kl.Signatures++

// If entity is determined, note a few values.
if e := r.Entity(); e != nil {
if id := primaryIdentity(e); id != nil {
name = id.Name
}
fp = hex.EncodeToString(e.PrimaryKey.Fingerprint[:])
keyLocal = isLocal(e)
keyCheck = true
}

// For each verified object, append an entry to the list.
for _, id := range r.Verified() {
od, _, err := f.GetFromDescrID(id)
if err != nil {
sylog.Errorf("failed to get descriptor: %v", err)
continue
}

ke := keyEntity{
Partition: od.Datatype.String(),
Name: name,
Fingerprint: fp,
KeyLocal: keyLocal,
KeyCheck: keyCheck,
DataCheck: true,
}
kl.SignerKeys = append(kl.SignerKeys, &key{ke})
}

var integrityError *integrity.ObjectIntegrityError
if errors.As(r.Error(), &integrityError) {
od, _, err := f.GetFromDescrID(integrityError.ID)
if err != nil {
sylog.Errorf("failed to get descriptor: %v", err)
return false
}

ke := keyEntity{
Partition: od.Datatype.String(),
Name: name,
Fingerprint: fp,
KeyLocal: keyLocal,
KeyCheck: keyCheck,
DataCheck: false,
}
kl.SignerKeys = append(kl.SignerKeys, &key{ke})
}

return false
}
}

// outputJSON outputs a JSON representation of kl to w.
func outputJSON(w io.Writer, kl keyList) error {
e := json.NewEncoder(w)
e.SetIndent("", " ")
return e.Encode(kl)
}
Loading

0 comments on commit e45d223

Please sign in to comment.