Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verify with X.509 Certificate #1139

Merged
merged 4 commits into from
Dec 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,17 @@ jobs:
name: Check Module Tidiness
command: git diff --exit-code -- go.mod go.sum

check-test-corpus:
executor: golang
steps:
- checkout
- run:
name: Generate Certificates
command: pushd test/certs/ && go run ./gen_certs.go && popd
- run:
name: Check Test Corpus Tidiness
command: git diff --exit-code --

check-license-dependencies:
executor: golang
steps:
Expand Down Expand Up @@ -345,6 +356,7 @@ workflows:
jobs:
- lint-markdown
- check-go-mod
- check-test-corpus
- check-license-dependencies
- build-source-alpine
- lint-source
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,17 @@
- `--rocm` to bind ROCm GPU libraries and devices into the container.
- Instance name is available inside an instance via the new
`SINGULARITY_INSTANCE` environment variable.
- The `sign` command now supports signing with non-PGP key material by
specifying the path to a private key via the `--key` flag.
- The `verify` command now supports verification with non-PGP key material by
specifying the path to a public key via the `--key` flag.
- The `verify` command now supports verification with X.509 certificates by
specifying the path to a certificate via the `--certificate` flag. By
default, the system root certificate pool is used as trust anchors unless
overridden via the `--certificate-roots` flag. A pool of intermediate
certificates that are not trust anchors, but can be used to form a
certificate chain can also be specified via the `--certificate-intermediates`
flag.

### Bug Fixes

Expand Down
75 changes: 68 additions & 7 deletions cmd/internal/cli/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ import (
)

var (
sifGroupID uint32 // -g groupid specification
sifDescID uint32 // -i id specification
pubKeyPath string // --key flag
localVerify bool // -l flag
jsonVerify bool // -j flag
verifyAll bool
verifyLegacy bool
sifGroupID uint32 // -g groupid specification
sifDescID uint32 // -i id specification
certificatePath string // --certificate flag
certificateIntermediatesPath string // --certificate-intermediates flag
certificateRootsPath string // --certificate-roots flag
pubKeyPath string // --key flag
localVerify bool // -l flag
jsonVerify bool // -j flag
verifyAll bool
verifyLegacy bool
)

// -u|--url
Expand Down Expand Up @@ -80,6 +83,36 @@ var verifySifDescIDFlag = cmdline.Flag{
Deprecated: "use '--sif-id'",
}

// --certificate
var verifyCertificateFlag = cmdline.Flag{
ID: "certificateFlag",
Value: &certificatePath,
DefaultValue: "",
Name: "certificate",
Usage: "path to the certificate",
EnvKeys: []string{"VERIFY_CERTIFICATE"},
}

// --certificate-intermediates
var verifyCertificateIntermediatesFlag = cmdline.Flag{
ID: "certificateIntermediatesFlag",
Value: &certificateIntermediatesPath,
DefaultValue: "",
Name: "certificate-intermediates",
Usage: "path to pool of intermediate certificates",
EnvKeys: []string{"VERIFY_INTERMEDIATES"},
}

// --certificate-roots
var verifyCertificateRootsFlag = cmdline.Flag{
ID: "certificateRootsFlag",
Value: &certificateRootsPath,
DefaultValue: "",
Name: "certificate-roots",
Usage: "path to pool of root certificates",
EnvKeys: []string{"VERIFY_ROOTS"},
}

// --key
var verifyPublicKeyFlag = cmdline.Flag{
ID: "publicKeyFlag",
Expand Down Expand Up @@ -139,6 +172,9 @@ func init() {
cmdManager.RegisterFlagForCmd(&verifyOldSifGroupIDFlag, VerifyCmd)
cmdManager.RegisterFlagForCmd(&verifySifDescSifIDFlag, VerifyCmd)
cmdManager.RegisterFlagForCmd(&verifySifDescIDFlag, VerifyCmd)
cmdManager.RegisterFlagForCmd(&verifyCertificateFlag, VerifyCmd)
cmdManager.RegisterFlagForCmd(&verifyCertificateIntermediatesFlag, VerifyCmd)
cmdManager.RegisterFlagForCmd(&verifyCertificateRootsFlag, VerifyCmd)
cmdManager.RegisterFlagForCmd(&verifyPublicKeyFlag, VerifyCmd)
cmdManager.RegisterFlagForCmd(&verifyLocalFlag, VerifyCmd)
cmdManager.RegisterFlagForCmd(&verifyJSONFlag, VerifyCmd)
Expand Down Expand Up @@ -167,6 +203,31 @@ func doVerifyCmd(cmd *cobra.Command, cpath string) {
var opts []singularity.VerifyOpt

switch {
case cmd.Flag(verifyCertificateFlag.Name).Changed:
sylog.Infof("Verifying image with key material from certificate '%v'", certificatePath)

c, err := loadCertificate(certificatePath)
if err != nil {
sylog.Fatalf("Failed to load certificate: %v", err)
}
opts = append(opts, singularity.OptVerifyWithCertificate(c))

if cmd.Flag(verifyCertificateIntermediatesFlag.Name).Changed {
p, err := loadCertificatePool(certificateIntermediatesPath)
if err != nil {
sylog.Fatalf("Failed to load intermediate certificates: %v", err)
}
opts = append(opts, singularity.OptVerifyWithIntermediates(p))
}

if cmd.Flag(verifyCertificateRootsFlag.Name).Changed {
p, err := loadCertificatePool(certificateRootsPath)
if err != nil {
sylog.Fatalf("Failed to load root certificates: %v", err)
}
opts = append(opts, singularity.OptVerifyWithRoots(p))
}

case cmd.Flag(verifyPublicKeyFlag.Name).Changed:
sylog.Infof("Verifying image with key material from '%v'", pubKeyPath)

Expand Down
58 changes: 58 additions & 0 deletions cmd/internal/cli/x509.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) 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.

package cli

import (
"bytes"
"crypto/x509"
"encoding/pem"
"errors"
"os"
)

var errFailedToDecodePEM = errors.New("failed to decode PEM")

// loadCertificate returns the certificate read from path.
func loadCertificate(path string) (*x509.Certificate, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}

p, _ := pem.Decode(b)
if p == nil {
return nil, errFailedToDecodePEM
}

return x509.ParseCertificate(p.Bytes)
}

// loadCertificatePool returns the pool of certificates read from path.
func loadCertificatePool(path string) (*x509.CertPool, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}

pool := x509.NewCertPool()

for rest := bytes.TrimSpace(b); len(rest) > 0; {
var p *pem.Block

if p, rest = pem.Decode(rest); p == nil {
return nil, errFailedToDecodePEM
}

c, err := x509.ParseCertificate(p.Bytes)
if err != nil {
return nil, err
}

pool.AddCert(c)
}

return pool, nil
}
29 changes: 29 additions & 0 deletions e2e/verify/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ type ctx struct {

func (c *ctx) verify(t *testing.T) {
keyPath := filepath.Join("..", "test", "keys", "ed25519-public.pem")
certPath := filepath.Join("..", "test", "certs", "leaf.pem")
intPath := filepath.Join("..", "test", "certs", "intermediate.pem")
rootPath := filepath.Join("..", "test", "certs", "root.pem")

tests := []struct {
name string
Expand Down Expand Up @@ -126,6 +129,32 @@ func (c *ctx) verify(t *testing.T) {
e2e.ExpectError(e2e.ContainMatch, "Verified signature(s) from image"),
},
},
{
name: "CertificateFlags",
flags: []string{
"--certificate", certPath,
"--certificate-intermediates", intPath,
"--certificate-roots", rootPath,
},
imagePath: filepath.Join("..", "test", "images", "one-group-signed-dsse.sif"),
expectOps: []e2e.SingularityCmdResultOp{
e2e.ExpectError(e2e.ContainMatch, "Verifying image with key material from certificate '"+certPath+"'"),
e2e.ExpectError(e2e.ContainMatch, "Verified signature(s) from image"),
},
},
{
name: "CertificateEnvVars",
envs: []string{
"SINGULARITY_VERIFY_CERTIFICATE=" + certPath,
"SINGULARITY_VERIFY_INTERMEDIATES=" + intPath,
"SINGULARITY_VERIFY_ROOTS=" + rootPath,
},
imagePath: filepath.Join("..", "test", "images", "one-group-signed-dsse.sif"),
expectOps: []e2e.SingularityCmdResultOp{
e2e.ExpectError(e2e.ContainMatch, "Verifying image with key material from certificate '"+certPath+"'"),
e2e.ExpectError(e2e.ContainMatch, "Verified signature(s) from image"),
},
},
}

for _, tt := range tests {
Expand Down
85 changes: 77 additions & 8 deletions internal/app/singularity/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ package singularity

import (
"context"
"crypto"
"crypto/x509"
"encoding/hex"
"errors"
"os"
Expand All @@ -28,19 +30,48 @@ var errNotSignedByRequired = errors.New("image not signed by required entities")
type VerifyCallback func(*sif.FileImage, integrity.VerifyResult) bool

type verifier struct {
svs []signature.Verifier
pgp bool
pgpOpts []client.Option
groupIDs []uint32
objectIDs []uint32
all bool
legacy bool
cb VerifyCallback
certs []*x509.Certificate
intermediates *x509.CertPool
roots *x509.CertPool
svs []signature.Verifier
pgp bool
pgpOpts []client.Option
groupIDs []uint32
objectIDs []uint32
all bool
legacy bool
cb VerifyCallback
}

// VerifyOpt are used to configure v.
type VerifyOpt func(v *verifier) error

// OptVerifyWithCertificate appends c as a source of key material to verify signatures.
func OptVerifyWithCertificate(c *x509.Certificate) VerifyOpt {
return func(v *verifier) error {
v.certs = append(v.certs, c)
return nil
}
}

// OptVerifyWithIntermediates specifies p as the pool of certificates that can be used to form a
// chain from the leaf certificate to a root certificate.
func OptVerifyWithIntermediates(p *x509.CertPool) VerifyOpt {
return func(v *verifier) error {
v.intermediates = p
return nil
}
}

// OptVerifyWithRoots specifies p as the pool of root certificates to use, instead of the system
// roots or the platform verifier.
func OptVerifyWithRoots(p *x509.CertPool) VerifyOpt {
return func(v *verifier) error {
v.roots = p
return nil
}
}

// OptVerifyWithVerifier appends sv as a source of key material to verify signatures.
func OptVerifyWithVerifier(sv signature.Verifier) VerifyOpt {
return func(v *verifier) error {
Expand Down Expand Up @@ -114,10 +145,40 @@ func newVerifier(opts []VerifyOpt) (verifier, error) {
return v, nil
}

// verifyCertificate attempts to verify c is a valid code signing certificate by building one or
// more chains from c to a certificate in roots, using certificates in intermediates if needed.
// This function does not do any revocation checking.
func verifyCertificate(c *x509.Certificate, intermediates, roots *x509.CertPool) error {
opts := x509.VerifyOptions{
Intermediates: intermediates,
Roots: roots,
KeyUsages: []x509.ExtKeyUsage{
x509.ExtKeyUsageCodeSigning,
},
}

_, err := c.Verify(opts)
return err
}

// getOpts returns integrity.VerifierOpt necessary to validate f.
func (v verifier) getOpts(ctx context.Context, f *sif.FileImage) ([]integrity.VerifierOpt, error) {
var iopts []integrity.VerifierOpt

// Add key material from certificate(s).
for _, c := range v.certs {
if err := verifyCertificate(c, v.intermediates, v.roots); err != nil {
return nil, err
}

sv, err := signature.LoadVerifier(c.PublicKey, crypto.SHA256)
if err != nil {
return nil, err
}

iopts = append(iopts, integrity.OptVerifyWithVerifier(sv))
}

// Add explicitly provided key material source(s).
for _, sv := range v.svs {
iopts = append(iopts, integrity.OptVerifyWithVerifier(sv))
Expand Down Expand Up @@ -192,6 +253,10 @@ func (v verifier) getOpts(ctx context.Context, f *sif.FileImage) ([]integrity.Ve

// Verify verifies digital signature(s) in the SIF image found at path, according to opts.
//
// To use key material from an x.509 certificate, use OptVerifyWithCertificate. The system roots or
// the platform verifier will be used to verify the certificate, unless OptVerifyWithIntermediates
// and/or OptVerifyWithRoots are specified.
//
// To use raw key material, use OptVerifyWithVerifier.
//
// To use PGP key material, use OptVerifyWithPGP.
Expand Down Expand Up @@ -228,6 +293,10 @@ func Verify(ctx context.Context, path string, opts ...VerifyOpt) error {
// VerifyFingerprints verifies an image and checks it was signed by *all* of the provided
// fingerprints.
//
// To use key material from an x.509 certificate, use OptVerifyWithCertificate. The system roots or
// the platform verifier will be used to verify the certificate, unless OptVerifyWithIntermediates
// and/or OptVerifyWithRoots are specified.
//
// To use raw key material, use OptVerifyWithVerifier.
//
// To use PGP key material, use OptVerifyWithPGP.
Expand Down
Loading