From 03f6cb89e6431f5b3779f558483fea13e3f33b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Wed, 23 Mar 2016 18:13:20 +0100 Subject: [PATCH] Add standalone-sign and standalone-verify commands --- Dockerfile | 1 + Makefile | 3 +- cmd/skopeo/main.go | 2 + cmd/skopeo/signing.go | 88 +++++++++++++++++++++++++++ integration/signing_test.go | 117 ++++++++++++++++++++++++++++++++++++ 5 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 cmd/skopeo/signing.go create mode 100644 integration/signing_test.go diff --git a/Dockerfile b/Dockerfile index 4191101c..33077e35 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ FROM fedora RUN dnf -y update && dnf install -y make git golang golang-github-cpuguy83-go-md2man \ # gpgme bindings deps libassuan-devel gpgme-devel \ + gnupg \ # registry v1 deps xz-devel \ python-devel \ diff --git a/Makefile b/Makefile index de927ff8..6785c4c0 100644 --- a/Makefile +++ b/Makefile @@ -53,8 +53,9 @@ shell: build-container check: validate test-unit test-integration +# The tests can run out of entropy and block in containers, so replace /dev/random. test-integration: build-container - $(DOCKER_RUN_DOCKER) hack/make.sh test-integration + $(DOCKER_RUN_DOCKER) bash -c 'rm -f /dev/random; ln -sf /dev/urandom /dev/random; hack/make.sh test-integration' test-unit: build-container # Just call (make test unit-local) here instead of worrying about environment differences, e.g. GO15VENDOREXPERIMENT. diff --git a/cmd/skopeo/main.go b/cmd/skopeo/main.go index 6a7606b1..6c05e218 100644 --- a/cmd/skopeo/main.go +++ b/cmd/skopeo/main.go @@ -54,6 +54,8 @@ func main() { app.Commands = []cli.Command{ inspectCmd, layersCmd, + standaloneSignCmd, + standaloneVerifyCmd, } if err := app.Run(os.Args); err != nil { logrus.Fatal(err) diff --git a/cmd/skopeo/signing.go b/cmd/skopeo/signing.go new file mode 100644 index 00000000..352a5925 --- /dev/null +++ b/cmd/skopeo/signing.go @@ -0,0 +1,88 @@ +package main + +import ( + "fmt" + "io/ioutil" + + "github.com/Sirupsen/logrus" + "github.com/codegangsta/cli" + "github.com/projectatomic/skopeo/signature" +) + +func standaloneSign(context *cli.Context) { + outputFile := context.String("output") + if len(context.Args()) != 3 || outputFile == "" { + logrus.Fatal("Usage: skopeo standalone-sign manifest docker-reference key-fingerprint -o signature") + } + manifestPath := context.Args()[0] + dockerReference := context.Args()[1] + fingerprint := context.Args()[2] + + manifest, err := ioutil.ReadFile(manifestPath) + if err != nil { + logrus.Fatalf("Error reading %s: %s", manifestPath, err.Error()) + } + + mech, err := signature.NewGPGSigningMechanism() + if err != nil { + logrus.Fatalf("Error initializing GPG: %s", err.Error()) + } + signature, err := signature.SignDockerManifest(manifest, dockerReference, mech, fingerprint) + if err != nil { + logrus.Fatalf("Error creating signature: %s", err.Error()) + } + + if err := ioutil.WriteFile(outputFile, signature, 0644); err != nil { + logrus.Fatalf("Error writing signature to %s: %s", outputFile, err.Error()) + } +} + +// FIXME: Document in the man page +var standaloneSignCmd = cli.Command{ + Name: "standalone-sign", + Usage: "Create a signature using local files", + Action: standaloneSign, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "output, o", + Usage: "output signature file name", + }, + }, +} + +func standaloneVerify(context *cli.Context) { + if len(context.Args()) != 4 { + logrus.Fatal("Usage: skopeo standalone-verify manifest docker-reference key-fingerprint signature") + } + manifestPath := context.Args()[0] + expectedDockerReference := context.Args()[1] + expectedFingerprint := context.Args()[2] + signaturePath := context.Args()[3] + + unverifiedManifest, err := ioutil.ReadFile(manifestPath) + if err != nil { + logrus.Fatalf("Error reading manifest from %s: %s", signaturePath, err.Error()) + } + unverifiedSignature, err := ioutil.ReadFile(signaturePath) + if err != nil { + logrus.Fatalf("Error reading signature from %s: %s", signaturePath, err.Error()) + } + + mech, err := signature.NewGPGSigningMechanism() + if err != nil { + logrus.Fatalf("Error initializing GPG: %s", err.Error()) + } + sig, err := signature.VerifyDockerManifestSignature(unverifiedSignature, unverifiedManifest, expectedDockerReference, mech, expectedFingerprint) + if err != nil { + logrus.Fatalf("Error verifying signature: %s", err.Error()) + } + + fmt.Printf("Signature verified, digest %s\n", sig.DockerManifestDigest) +} + +// FIXME: Document in the man page +var standaloneVerifyCmd = cli.Command{ + Name: "standalone-verify", + Usage: "Verify a signature using local files", + Action: standaloneVerify, +} diff --git a/integration/signing_test.go b/integration/signing_test.go new file mode 100644 index 00000000..7489c7a8 --- /dev/null +++ b/integration/signing_test.go @@ -0,0 +1,117 @@ +package main + +import ( + "errors" + "io" + "io/ioutil" + "os" + "os/exec" + "strings" + + "github.com/go-check/check" + "github.com/projectatomic/skopeo/signature/fixtures" +) + +const ( + gpgBinary = "gpg" +) + +func init() { + check.Suite(&SigningSuite{}) +} + +type SigningSuite struct { + gpgHome string + fingerprint string +} + +func findFingerprint(lineBytes []byte) (string, error) { + lines := string(lineBytes) + for _, line := range strings.Split(lines, "\n") { + fields := strings.Split(line, ":") + if len(fields) >= 10 && fields[0] == "fpr" { + return fields[9], nil + } + } + return "", errors.New("No fingerprint found") +} + +// ConsumeAndLogOutput takes (f, err) from an exec.*Pipe(), and causes all output to it to be logged to c. +func ConsumeAndLogOutput(c *check.C, id string, f io.ReadCloser, err error) { + c.Assert(err, check.IsNil) + go func() { + defer func() { + f.Close() + c.Logf("Output %s: Closed", id) + }() + buf := make([]byte, 0, 1024) + for { + c.Logf("Output %s: waiting", id) + n, err := f.Read(buf) + c.Logf("Output %s: got %d,%#v: %#v", id, n, err, buf[:n]) + if n <= 0 { + break + } + } + }() +} + +func (s *SigningSuite) SetUpTest(c *check.C) { + _, err := exec.LookPath(skopeoBinary) + c.Assert(err, check.IsNil) + _, err = exec.LookPath(skopeoBinary) + c.Assert(err, check.IsNil) + + s.gpgHome, err = ioutil.TempDir("", "skopeo-gpg") + c.Assert(err, check.IsNil) + os.Setenv("GNUPGHOME", s.gpgHome) + + cmd := exec.Command(gpgBinary, "--homedir", s.gpgHome, "--batch", "--gen-key") + stdin, err := cmd.StdinPipe() + c.Assert(err, check.IsNil) + stdout, err := cmd.StdoutPipe() + ConsumeAndLogOutput(c, "gen-key stdout", stdout, err) + stderr, err := cmd.StderrPipe() + ConsumeAndLogOutput(c, "gen-key stderr", stderr, err) + err = cmd.Start() + c.Assert(err, check.IsNil) + _, err = stdin.Write([]byte("Key-Type: RSA\nName-Real: Testing user\n%commit\n")) + c.Assert(err, check.IsNil) + err = stdin.Close() + c.Assert(err, check.IsNil) + err = cmd.Wait() + c.Assert(err, check.IsNil) + + lines, err := exec.Command(gpgBinary, "--homedir", s.gpgHome, "--with-colons", "--no-permission-warning", "--fingerprint").Output() + c.Assert(err, check.IsNil) + s.fingerprint, err = findFingerprint(lines) + c.Assert(err, check.IsNil) +} + +func (s *SigningSuite) TearDownTest(c *check.C) { + if s.gpgHome != "" { + err := os.RemoveAll(s.gpgHome) + c.Assert(err, check.IsNil) + } + s.gpgHome = "" + + os.Unsetenv("GNUPGHOME") +} + +func (s *SigningSuite) TestSignVerifySmoke(c *check.C) { + manifestPath := "../signature/fixtures/image.manifest.json" + dockerReference := "testing/smoketest" + + sigOutput, err := ioutil.TempFile("", "sig") + c.Assert(err, check.IsNil) + defer os.Remove(sigOutput.Name()) + out, err := exec.Command(skopeoBinary, "standalone-sign", "-o", sigOutput.Name(), + manifestPath, dockerReference, s.fingerprint).CombinedOutput() + c.Assert(err, check.IsNil) + c.Assert(string(out), check.Equals, "") + + out, err = exec.Command(skopeoBinary, "standalone-verify", manifestPath, + dockerReference, s.fingerprint, sigOutput.Name()).CombinedOutput() + c.Assert(err, check.IsNil) + c.Assert(string(out), check.Equals, "Signature verified, digest "+fixtures.TestImageManifestDigest+"\n") +}