Merge pull request #1950 from Jamstah/easy-verify-options

Verify signatures from a list of public keys
This commit is contained in:
Miloslav Trmač 2023-04-05 17:40:52 +02:00 committed by GitHub
commit 841eab319f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 75 additions and 21 deletions

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"strings"
"github.com/containers/image/v5/pkg/cli" "github.com/containers/image/v5/pkg/cli"
"github.com/containers/image/v5/signature" "github.com/containers/image/v5/signature"
@ -41,12 +42,12 @@ func (opts *standaloneSignOptions) run(args []string, stdout io.Writer) error {
manifest, err := os.ReadFile(manifestPath) manifest, err := os.ReadFile(manifestPath)
if err != nil { if err != nil {
return fmt.Errorf("Error reading %s: %v", manifestPath, err) return fmt.Errorf("Error reading %s: %w", manifestPath, err)
} }
mech, err := signature.NewGPGSigningMechanism() mech, err := signature.NewGPGSigningMechanism()
if err != nil { if err != nil {
return fmt.Errorf("Error initializing GPG: %v", err) return fmt.Errorf("Error initializing GPG: %w", err)
} }
defer mech.Close() defer mech.Close()
@ -57,25 +58,31 @@ func (opts *standaloneSignOptions) run(args []string, stdout io.Writer) error {
signature, err := signature.SignDockerManifestWithOptions(manifest, dockerReference, mech, fingerprint, &signature.SignOptions{Passphrase: passphrase}) signature, err := signature.SignDockerManifestWithOptions(manifest, dockerReference, mech, fingerprint, &signature.SignOptions{Passphrase: passphrase})
if err != nil { if err != nil {
return fmt.Errorf("Error creating signature: %v", err) return fmt.Errorf("Error creating signature: %w", err)
} }
if err := os.WriteFile(opts.output, signature, 0644); err != nil { if err := os.WriteFile(opts.output, signature, 0644); err != nil {
return fmt.Errorf("Error writing signature to %s: %v", opts.output, err) return fmt.Errorf("Error writing signature to %s: %w", opts.output, err)
} }
return nil return nil
} }
type standaloneVerifyOptions struct { type standaloneVerifyOptions struct {
publicKeyFile string
} }
func standaloneVerifyCmd() *cobra.Command { func standaloneVerifyCmd() *cobra.Command {
opts := standaloneVerifyOptions{} opts := standaloneVerifyOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "standalone-verify MANIFEST DOCKER-REFERENCE KEY-FINGERPRINT SIGNATURE", Use: "standalone-verify MANIFEST DOCKER-REFERENCE KEY-FINGERPRINTS SIGNATURE",
Short: "Verify a signature using local files", Short: "Verify a signature using local files",
Long: `Verify a signature using local files
KEY-FINGERPRINTS can be a comma separated list of fingerprints, or "any" if you trust all the keys in the public key file.`,
RunE: commandAction(opts.run), RunE: commandAction(opts.run),
} }
flags := cmd.Flags()
flags.StringVar(&opts.publicKeyFile, "public-key-file", "", `File containing public keys. If not specified, will use local GPG keys.`)
adjustUsage(cmd) adjustUsage(cmd)
return cmd return cmd
} }
@ -86,29 +93,47 @@ func (opts *standaloneVerifyOptions) run(args []string, stdout io.Writer) error
} }
manifestPath := args[0] manifestPath := args[0]
expectedDockerReference := args[1] expectedDockerReference := args[1]
expectedFingerprint := args[2] expectedFingerprints := strings.Split(args[2], ",")
signaturePath := args[3] signaturePath := args[3]
if opts.publicKeyFile == "" && len(expectedFingerprints) == 1 && expectedFingerprints[0] == "any" {
return fmt.Errorf("Cannot use any fingerprint without a public key file")
}
unverifiedManifest, err := os.ReadFile(manifestPath) unverifiedManifest, err := os.ReadFile(manifestPath)
if err != nil { if err != nil {
return fmt.Errorf("Error reading manifest from %s: %v", manifestPath, err) return fmt.Errorf("Error reading manifest from %s: %w", manifestPath, err)
} }
unverifiedSignature, err := os.ReadFile(signaturePath) unverifiedSignature, err := os.ReadFile(signaturePath)
if err != nil { if err != nil {
return fmt.Errorf("Error reading signature from %s: %v", signaturePath, err) return fmt.Errorf("Error reading signature from %s: %w", signaturePath, err)
} }
mech, err := signature.NewGPGSigningMechanism() var mech signature.SigningMechanism
var publicKeyfingerprints []string
if opts.publicKeyFile != "" {
publicKeys, err := os.ReadFile(opts.publicKeyFile)
if err != nil { if err != nil {
return fmt.Errorf("Error initializing GPG: %v", err) return fmt.Errorf("Error reading public keys from %s: %w", opts.publicKeyFile, err)
}
mech, publicKeyfingerprints, err = signature.NewEphemeralGPGSigningMechanism(publicKeys)
} else {
mech, err = signature.NewGPGSigningMechanism()
}
if err != nil {
return fmt.Errorf("Error initializing GPG: %w", err)
} }
defer mech.Close() defer mech.Close()
sig, err := signature.VerifyDockerManifestSignature(unverifiedSignature, unverifiedManifest, expectedDockerReference, mech, expectedFingerprint)
if err != nil { if len(expectedFingerprints) == 1 && expectedFingerprints[0] == "any" {
return fmt.Errorf("Error verifying signature: %v", err) expectedFingerprints = publicKeyfingerprints
} }
fmt.Fprintf(stdout, "Signature verified, digest %s\n", sig.DockerManifestDigest) sig, verificationFingerprint, err := signature.VerifyImageManifestSignatureUsingKeyIdentityList(unverifiedSignature, unverifiedManifest, expectedDockerReference, mech, expectedFingerprints)
if err != nil {
return fmt.Errorf("Error verifying signature: %w", err)
}
fmt.Fprintf(stdout, "Signature verified using fingerprint %s, digest %s\n", verificationFingerprint, sig.DockerManifestDigest)
return nil return nil
} }
@ -141,7 +166,7 @@ func (opts *untrustedSignatureDumpOptions) run(args []string, stdout io.Writer)
untrustedSignature, err := os.ReadFile(untrustedSignaturePath) untrustedSignature, err := os.ReadFile(untrustedSignaturePath)
if err != nil { if err != nil {
return fmt.Errorf("Error reading untrusted signature from %s: %v", untrustedSignaturePath, err) return fmt.Errorf("Error reading untrusted signature from %s: %w", untrustedSignaturePath, err)
} }
untrustedInfo, err := signature.GetUntrustedSignatureInformationWithoutVerifying(untrustedSignature) untrustedInfo, err := signature.GetUntrustedSignatureInformationWithoutVerifying(untrustedSignature)

View File

@ -127,11 +127,36 @@ func TestStandaloneVerify(t *testing.T) {
dockerReference, fixturesTestKeyFingerprint, "fixtures/corrupt.signature") dockerReference, fixturesTestKeyFingerprint, "fixtures/corrupt.signature")
assertTestFailed(t, out, err, "Error verifying signature") assertTestFailed(t, out, err, "Error verifying signature")
// Error using any without a public key file
out, err = runSkopeo("standalone-verify", manifestPath,
dockerReference, "any", signaturePath)
assertTestFailed(t, out, err, "Cannot use any fingerprint without a public key file")
// Success // Success
out, err = runSkopeo("standalone-verify", manifestPath, out, err = runSkopeo("standalone-verify", manifestPath,
dockerReference, fixturesTestKeyFingerprint, signaturePath) dockerReference, fixturesTestKeyFingerprint, signaturePath)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "Signature verified, digest "+fixturesTestImageManifestDigest.String()+"\n", out) assert.Equal(t, "Signature verified using fingerprint "+fixturesTestKeyFingerprint+", digest "+fixturesTestImageManifestDigest.String()+"\n", out)
// Using multiple fingerprints
out, err = runSkopeo("standalone-verify", manifestPath,
dockerReference, "0123456789ABCDEF0123456789ABCDEF01234567,"+fixturesTestKeyFingerprint+",DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF", signaturePath)
assert.NoError(t, err)
assert.Equal(t, "Signature verified using fingerprint "+fixturesTestKeyFingerprint+", digest "+fixturesTestImageManifestDigest.String()+"\n", out)
// Using a public key file
t.Setenv("GNUPGHOME", "")
out, err = runSkopeo("standalone-verify", "--public-key-file", "fixtures/pubring.gpg", manifestPath,
dockerReference, fixturesTestKeyFingerprint, signaturePath)
assert.NoError(t, err)
assert.Equal(t, "Signature verified using fingerprint "+fixturesTestKeyFingerprint+", digest "+fixturesTestImageManifestDigest.String()+"\n", out)
// Using a public key file matching any public key
t.Setenv("GNUPGHOME", "")
out, err = runSkopeo("standalone-verify", "--public-key-file", "fixtures/pubring.gpg", manifestPath,
dockerReference, "any", signaturePath)
assert.NoError(t, err)
assert.Equal(t, "Signature verified using fingerprint "+fixturesTestKeyFingerprint+", digest "+fixturesTestImageManifestDigest.String()+"\n", out)
} }
func TestUntrustedSignatureDump(t *testing.T) { func TestUntrustedSignatureDump(t *testing.T) {

View File

@ -4,7 +4,7 @@
skopeo\-standalone\-verify - Verify an image signature. skopeo\-standalone\-verify - Verify an image signature.
## SYNOPSIS ## SYNOPSIS
**skopeo standalone-verify** _manifest_ _docker-reference_ _key-fingerprint_ _signature_ **skopeo standalone-verify** _manifest_ _docker-reference_ _key-fingerprints_ _signature_
## DESCRIPTION ## DESCRIPTION
@ -16,7 +16,7 @@ as per containers-policy.json(5).
_docker-reference_ A docker reference expected to identify the image in the signature _docker-reference_ A docker reference expected to identify the image in the signature
_key-fingerprint_ Expected identity of the signing key _key-fingerprints_ Identities of trusted signing keys (comma separated), or "any" to trust any known key when using a public key file
_signature_ Path to signature file _signature_ Path to signature file
@ -28,6 +28,10 @@ as per containers-policy.json(5).
Print usage statement Print usage statement
**--public-key-file** _public key file_
File containing the public keys to use when verifying signatures. If this is not specified, keys from the GPG homedir are used.
## EXAMPLES ## EXAMPLES
```console ```console

View File

@ -73,7 +73,7 @@ func (s *signingSuite) TestSignVerifySmoke() {
assertSkopeoSucceeds(t, "^$", "standalone-sign", "-o", sigOutput.Name(), assertSkopeoSucceeds(t, "^$", "standalone-sign", "-o", sigOutput.Name(),
manifestPath, dockerReference, s.fingerprint) manifestPath, dockerReference, s.fingerprint)
expected := fmt.Sprintf("^Signature verified, digest %s\n$", TestImageManifestDigest) expected := fmt.Sprintf("^Signature verified using fingerprint %s, digest %s\n$", s.fingerprint, TestImageManifestDigest)
assertSkopeoSucceeds(t, expected, "standalone-verify", manifestPath, assertSkopeoSucceeds(t, expected, "standalone-verify", manifestPath,
dockerReference, s.fingerprint, sigOutput.Name()) dockerReference, s.fingerprint, sigOutput.Name())
} }

View File

@ -242,7 +242,7 @@ END_TESTS
$fingerprint \ $fingerprint \
$TESTDIR/busybox.signature $TESTDIR/busybox.signature
# manifest digest # manifest digest
digest=$(echo "$output" | awk '{print $4;}') digest=$(echo "$output" | awk '{print $NF;}')
run_skopeo manifest-digest $TESTDIR/busybox/manifest.json run_skopeo manifest-digest $TESTDIR/busybox/manifest.json
expect_output $digest expect_output $digest
} }