From d8bdff0c56364d75bf1197c1ef7eb003ad8aa326 Mon Sep 17 00:00:00 2001 From: Shiwei Zhang Date: Fri, 21 Aug 2020 10:48:20 +0800 Subject: [PATCH 1/2] TUF integration Signed-off-by: Shiwei Zhang --- .gitignore | 19 ++ cmd/nv2/{common.go => common/flags.go} | 21 +- cmd/nv2/common/manifest.go | 74 +++++ cmd/nv2/main.go | 9 +- cmd/nv2/manifest.go | 71 ---- cmd/nv2/{ => signature}/sign.go | 38 +-- cmd/nv2/{ => signature}/verify.go | 29 +- cmd/nv2/tuf/sign.go | 141 ++++++++ cmd/nv2/tuf/tuf.go | 13 + cmd/nv2/tuf/verify.go | 128 ++++++++ docs/nv2/README.md | 38 ++- docs/signature/README.md | 66 ++-- docs/signature/examples/x509_kid.nv2.jwt | 2 +- docs/signature/examples/x509_x5c.nv2.jwt | 2 +- docs/tuf/README.md | 310 ++++++++++++++++++ go.mod | 4 +- go.sum | 108 ++++++ pkg/reference/descriptor.go | 10 + pkg/reference/manifest.go | 10 + pkg/registry/manifest.go | 58 ++-- pkg/signature/encoding.go | 3 +- pkg/signature/model.go | 14 + pkg/signature/scheme.go | 5 +- pkg/signature/x509/signer.go | 9 +- .../x509.go => pkg/signature/x509/utils.go | 2 +- pkg/signature/x509/verifier.go | 2 +- pkg/tuf/defaults.go | 8 + pkg/tuf/local/signer.go | 103 ++++++ pkg/tuf/local/verifier.go | 155 +++++++++ pkg/tuf/signature.go | 54 +++ pkg/tuf/signing.go | 45 +++ pkg/tuf/targets.go | 159 +++++++++ 32 files changed, 1509 insertions(+), 201 deletions(-) create mode 100644 .gitignore rename cmd/nv2/{common.go => common/flags.go} (52%) create mode 100644 cmd/nv2/common/manifest.go delete mode 100644 cmd/nv2/manifest.go rename cmd/nv2/{ => signature}/sign.go (75%) rename cmd/nv2/{ => signature}/verify.go (74%) create mode 100644 cmd/nv2/tuf/sign.go create mode 100644 cmd/nv2/tuf/tuf.go create mode 100644 cmd/nv2/tuf/verify.go create mode 100644 docs/tuf/README.md create mode 100644 pkg/reference/descriptor.go create mode 100644 pkg/reference/manifest.go rename internal/crypto/x509.go => pkg/signature/x509/utils.go (94%) create mode 100644 pkg/tuf/defaults.go create mode 100644 pkg/tuf/local/signer.go create mode 100644 pkg/tuf/local/verifier.go create mode 100644 pkg/tuf/signature.go create mode 100644 pkg/tuf/signing.go create mode 100644 pkg/tuf/targets.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..8672a1939 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# VS Code +.vscode +debug + +# Jetbrains +.idea diff --git a/cmd/nv2/common.go b/cmd/nv2/common/flags.go similarity index 52% rename from cmd/nv2/common.go rename to cmd/nv2/common/flags.go index 981b36788..aadd84ffb 100644 --- a/cmd/nv2/common.go +++ b/cmd/nv2/common/flags.go @@ -1,25 +1,36 @@ -package main +package common import "github.com/urfave/cli/v2" +// Common flags var ( - usernameFlag = &cli.StringFlag{ + UsernameFlag = &cli.StringFlag{ Name: "username", Aliases: []string{"u"}, Usage: "username for generic remote access", } - passwordFlag = &cli.StringFlag{ + PasswordFlag = &cli.StringFlag{ Name: "password", Aliases: []string{"p"}, Usage: "password for generic remote access", } - insecureFlag = &cli.BoolFlag{ + InsecureFlag = &cli.BoolFlag{ Name: "insecure", Usage: "enable insecure remote access", } - mediaTypeFlag = &cli.StringFlag{ + MediaTypeFlag = &cli.StringFlag{ Name: "media-type", Usage: "specify the media type of the manifest read from file or stdin", Value: "application/vnd.docker.distribution.manifest.v2+json", } + ExpiryFlag = &cli.DurationFlag{ + Name: "expiry", + Aliases: []string{"e"}, + Usage: "expire duration", + } + OutputFlag = &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "write signature to a specific path", + } ) diff --git a/cmd/nv2/common/manifest.go b/cmd/nv2/common/manifest.go new file mode 100644 index 000000000..58e1e9adf --- /dev/null +++ b/cmd/nv2/common/manifest.go @@ -0,0 +1,74 @@ +package common + +import ( + "fmt" + "io" + "math" + "net/url" + "os" + "strings" + "time" + + "github.com/notaryproject/nv2/pkg/reference" + "github.com/notaryproject/nv2/pkg/registry" + "github.com/opencontainers/go-digest" + "github.com/urfave/cli/v2" +) + +// GetManifestFromContext reterives the manifest according to CLI context +func GetManifestFromContext(ctx *cli.Context) (*reference.Manifest, error) { + if uri := ctx.Args().First(); uri != "" { + return getManfestsFromURI(ctx, uri) + } + return getManifestFromReader(os.Stdin, ctx.String(MediaTypeFlag.Name)) +} + +func getManifestFromReader(r io.Reader, mediaType string) (*reference.Manifest, error) { + lr := &io.LimitedReader{ + R: r, + N: math.MaxInt64, + } + manifestDigest, err := digest.SHA256.FromReader(lr) + if err != nil { + return nil, err + } + return &reference.Manifest{ + Descriptor: reference.Descriptor{ + MediaType: mediaType, + Digests: []digest.Digest{manifestDigest}, + Size: math.MaxInt64 - lr.N, + }, + AccessedAt: time.Now().UTC(), + }, nil +} + +func getManfestsFromURI(ctx *cli.Context, uri string) (*reference.Manifest, error) { + parsed, err := url.Parse(uri) + if err != nil { + return nil, err + } + var r io.Reader + switch strings.ToLower(parsed.Scheme) { + case "file": + path := parsed.Path + if parsed.Opaque != "" { + path = parsed.Opaque + } + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + r = file + case "docker", "oci": + remote := registry.NewClient(nil, ®istry.ClientOptions{ + Username: ctx.String(UsernameFlag.Name), + Password: ctx.String(PasswordFlag.Name), + Insecure: ctx.Bool(InsecureFlag.Name), + }) + return remote.GetManifestMetadata(parsed) + default: + return nil, fmt.Errorf("unsupported URI scheme: %s", parsed.Scheme) + } + return getManifestFromReader(r, ctx.String(MediaTypeFlag.Name)) +} diff --git a/cmd/nv2/main.go b/cmd/nv2/main.go index 93c82436d..09a317d74 100644 --- a/cmd/nv2/main.go +++ b/cmd/nv2/main.go @@ -4,6 +4,8 @@ import ( "log" "os" + "github.com/notaryproject/nv2/cmd/nv2/signature" + "github.com/notaryproject/nv2/cmd/nv2/tuf" "github.com/urfave/cli/v2" ) @@ -11,7 +13,7 @@ func main() { app := &cli.App{ Name: "nv2", Usage: "Notary V2 - Prototype", - Version: "0.2.0", + Version: "0.3.1", Authors: []*cli.Author{ { Name: "Shiwei Zhang", @@ -19,8 +21,9 @@ func main() { }, }, Commands: []*cli.Command{ - signCommand, - verifyCommand, + signature.SignCommand, + signature.VerifyCommand, + tuf.TUFCommand, }, } if err := app.Run(os.Args); err != nil { diff --git a/cmd/nv2/manifest.go b/cmd/nv2/manifest.go deleted file mode 100644 index 588e696a2..000000000 --- a/cmd/nv2/manifest.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "fmt" - "io" - "math" - "net/url" - "os" - "strings" - - "github.com/notaryproject/nv2/pkg/registry" - "github.com/notaryproject/nv2/pkg/signature" - "github.com/opencontainers/go-digest" - "github.com/urfave/cli/v2" -) - -func getManifestFromContext(ctx *cli.Context) (signature.Manifest, error) { - if uri := ctx.Args().First(); uri != "" { - return getManfestsFromURI(ctx, uri) - } - return getManifestFromReader(os.Stdin, ctx.String(mediaTypeFlag.Name)) -} - -func getManifestFromReader(r io.Reader, mediaType string) (signature.Manifest, error) { - lr := &io.LimitedReader{ - R: r, - N: math.MaxInt64, - } - digest, err := digest.SHA256.FromReader(lr) - if err != nil { - return signature.Manifest{}, err - } - return signature.Manifest{ - Descriptor: signature.Descriptor{ - MediaType: mediaType, - Digest: digest.String(), - Size: math.MaxInt64 - lr.N, - }, - }, nil -} - -func getManfestsFromURI(ctx *cli.Context, uri string) (signature.Manifest, error) { - parsed, err := url.Parse(uri) - if err != nil { - return signature.Manifest{}, err - } - var r io.Reader - switch strings.ToLower(parsed.Scheme) { - case "file": - path := parsed.Path - if parsed.Opaque != "" { - path = parsed.Opaque - } - file, err := os.Open(path) - if err != nil { - return signature.Manifest{}, err - } - defer file.Close() - r = file - case "docker", "oci": - remote := registry.NewClient(nil, ®istry.ClientOptions{ - Username: ctx.String(usernameFlag.Name), - Password: ctx.String(passwordFlag.Name), - Insecure: ctx.Bool(insecureFlag.Name), - }) - return remote.GetManifestMetadata(parsed) - default: - return signature.Manifest{}, fmt.Errorf("unsupported URI scheme: %s", parsed.Scheme) - } - return getManifestFromReader(r, ctx.String(mediaTypeFlag.Name)) -} diff --git a/cmd/nv2/sign.go b/cmd/nv2/signature/sign.go similarity index 75% rename from cmd/nv2/sign.go rename to cmd/nv2/signature/sign.go index df79d2af4..219b41f40 100644 --- a/cmd/nv2/sign.go +++ b/cmd/nv2/signature/sign.go @@ -1,4 +1,4 @@ -package main +package signature import ( "fmt" @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/notaryproject/nv2/cmd/nv2/common" "github.com/notaryproject/nv2/pkg/signature" "github.com/notaryproject/nv2/pkg/signature/x509" "github.com/urfave/cli/v2" @@ -13,7 +14,8 @@ import ( const signerID = "nv2" -var signCommand = &cli.Command{ +// SignCommand defines sign command +var SignCommand = &cli.Command{ Name: "sign", Usage: "signs OCI Artifacts", ArgsUsage: "[]", @@ -36,25 +38,17 @@ var signCommand = &cli.Command{ Usage: "signing cert [x509]", TakesFile: true, }, - &cli.DurationFlag{ - Name: "expiry", - Aliases: []string{"e"}, - Usage: "expire duration", - }, &cli.StringSliceFlag{ Name: "reference", Aliases: []string{"r"}, Usage: "original references", }, - &cli.StringFlag{ - Name: "output", - Aliases: []string{"o"}, - Usage: "write signature to a specific path", - }, - usernameFlag, - passwordFlag, - insecureFlag, - mediaTypeFlag, + common.ExpiryFlag, + common.OutputFlag, + common.MediaTypeFlag, + common.UsernameFlag, + common.PasswordFlag, + common.InsecureFlag, }, Action: runSign, } @@ -77,7 +71,7 @@ func runSign(ctx *cli.Context) error { } // write out - path := ctx.String("output") + path := ctx.String(common.OutputFlag.Name) if path == "" { path = strings.Split(claims.Manifest.Digest, ":")[1] + ".nv2" } @@ -90,18 +84,20 @@ func runSign(ctx *cli.Context) error { } func prepareClaimsForSigning(ctx *cli.Context) (signature.Claims, error) { - manifest, err := getManifestFromContext(ctx) + manifest, err := common.GetManifestFromContext(ctx) if err != nil { return signature.Claims{}, err } - manifest.References = ctx.StringSlice("reference") now := time.Now() nowUnix := now.Unix() claims := signature.Claims{ - Manifest: manifest, + Manifest: signature.Manifest{ + Descriptor: signature.DescriptorFromReference(manifest.Descriptor), + References: ctx.StringSlice("reference"), + }, IssuedAt: nowUnix, } - if expiry := ctx.Duration("expiry"); expiry != 0 { + if expiry := ctx.Duration(common.ExpiryFlag.Name); expiry != 0 { claims.NotBefore = nowUnix claims.Expiration = now.Add(expiry).Unix() } diff --git a/cmd/nv2/verify.go b/cmd/nv2/signature/verify.go similarity index 74% rename from cmd/nv2/verify.go rename to cmd/nv2/signature/verify.go index 2db1652a7..1c1e31114 100644 --- a/cmd/nv2/verify.go +++ b/cmd/nv2/signature/verify.go @@ -1,17 +1,18 @@ -package main +package signature import ( "crypto/x509" "fmt" "io/ioutil" - "github.com/notaryproject/nv2/internal/crypto" + "github.com/notaryproject/nv2/cmd/nv2/common" "github.com/notaryproject/nv2/pkg/signature" x509nv2 "github.com/notaryproject/nv2/pkg/signature/x509" "github.com/urfave/cli/v2" ) -var verifyCommand = &cli.Command{ +// VerifyCommand defines verify command +var VerifyCommand = &cli.Command{ Name: "verify", Usage: "verifies OCI Artifacts", ArgsUsage: "[]", @@ -34,10 +35,10 @@ var verifyCommand = &cli.Command{ Usage: "CA certs for verification [x509]", TakesFile: true, }, - usernameFlag, - passwordFlag, - insecureFlag, - mediaTypeFlag, + common.MediaTypeFlag, + common.UsernameFlag, + common.PasswordFlag, + common.InsecureFlag, }, Action: runVerify, } @@ -58,16 +59,18 @@ func runVerify(ctx *cli.Context) error { if err != nil { return fmt.Errorf("verification failure: %v", err) } - manifest, err := getManifestFromContext(ctx) + manifest, err := common.GetManifestFromContext(ctx) if err != nil { return err } - if manifest.Descriptor != claims.Manifest.Descriptor { - return fmt.Errorf("verification failure: %s: ", manifest.Digest) + descriptor := signature.DescriptorFromReference(manifest.Descriptor) + + if descriptor != claims.Manifest.Descriptor { + return fmt.Errorf("verification failure: %s: ", descriptor.Digest) } // write out - fmt.Println(manifest.Digest) + fmt.Println(descriptor.Digest) return nil } @@ -97,7 +100,7 @@ func getX509Verifier(ctx *cli.Context) (signature.Verifier, error) { var certs []*x509.Certificate for _, path := range ctx.StringSlice("cert") { - bundledCerts, err := crypto.ReadCertificateFile(path) + bundledCerts, err := x509nv2.ReadCertificateFile(path) if err != nil { return nil, err } @@ -107,7 +110,7 @@ func getX509Verifier(ctx *cli.Context) (signature.Verifier, error) { } } for _, path := range ctx.StringSlice("ca-cert") { - bundledCerts, err := crypto.ReadCertificateFile(path) + bundledCerts, err := x509nv2.ReadCertificateFile(path) if err != nil { return nil, err } diff --git a/cmd/nv2/tuf/sign.go b/cmd/nv2/tuf/sign.go new file mode 100644 index 000000000..6374d1d0b --- /dev/null +++ b/cmd/nv2/tuf/sign.go @@ -0,0 +1,141 @@ +package tuf + +import ( + "errors" + "fmt" + "io/ioutil" + "strings" + "time" + + "github.com/docker/go/canonical/json" + "github.com/notaryproject/nv2/cmd/nv2/common" + "github.com/notaryproject/nv2/pkg/tuf" + "github.com/notaryproject/nv2/pkg/tuf/local" + "github.com/theupdateframework/notary/tuf/data" + "github.com/urfave/cli/v2" +) + +// SignCommand defines sign command +var SignCommand = &cli.Command{ + Name: "sign", + Usage: "signs OCI Artifacts", + ArgsUsage: "[]", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "key", + Aliases: []string{"k"}, + Usage: "signing key file", + TakesFile: true, + Required: true, + }, + &cli.StringFlag{ + Name: "cert", + Aliases: []string{"c"}, + Usage: "signing cert", + TakesFile: true, + }, + &cli.StringFlag{ + Name: "signature", + Aliases: []string{"s", "f"}, + Usage: "base signature file", + TakesFile: true, + }, + &cli.StringFlag{ + Name: "reference", + Aliases: []string{"r"}, + Usage: "original references", + }, + common.ExpiryFlag, + common.OutputFlag, + common.MediaTypeFlag, + common.UsernameFlag, + common.PasswordFlag, + common.InsecureFlag, + }, + Action: runSign, +} + +func runSign(ctx *cli.Context) error { + // initialize + signer, err := local.NewSignerFromFiles(ctx.String("key"), ctx.String("cert")) + if err != nil { + return err + } + + // core process + targets, manifestDigest, err := prepareTargetsForSigning(ctx) + if err != nil { + return err + } + signed, err := tuf.SignTargets(ctx.Context, signer, targets) + if err != nil { + return err + } + // non-canonical JSON marshal to match Docker Notary 0.6.0 implementation + sig, err := json.Marshal(signed) + if err != nil { + return err + } + + // write out + path := ctx.String(common.OutputFlag.Name) + if path == "" { + path = strings.Split(manifestDigest, ":")[1] + ".nv2" + } + if err := ioutil.WriteFile(path, []byte(sig), 0666); err != nil { + return err + } + + fmt.Println(manifestDigest) + return nil +} + +func prepareTargetsForSigning(ctx *cli.Context) (*data.Targets, string, error) { + manifest, err := common.GetManifestFromContext(ctx) + if err != nil { + return nil, "", err + } + if reference := ctx.String("reference"); reference != "" { + manifest.Name = reference + } + if manifest.Name == "" { + return nil, "", errors.New("manifest is not referenced") + } + target, err := tuf.NewTarget(manifest) + if err != nil { + return nil, "", err + } + + var targets *data.Targets + if path := ctx.String("signature"); path != "" { + targets, err = readTargetsFromFile(path) + if err != nil { + return nil, "", err + } + } + targets = tuf.AddTargets(targets, target) + + if expiry := ctx.Duration(common.ExpiryFlag.Name); expiry != 0 { + targets.Expires = time.Now().UTC().Add(expiry) + } + + return targets, manifest.Digests[0].String(), nil +} + +func readTargetsFromFile(path string) (*data.Targets, error) { + raw, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + signed := new(data.Signed) + if err := json.Unmarshal(raw, signed); err != nil { + return nil, err + } + + signedTargets, err := data.TargetsFromSigned(signed, data.CanonicalTargetsRole) + if err != nil { + return nil, err + } + return &signedTargets.Signed, nil +} diff --git a/cmd/nv2/tuf/tuf.go b/cmd/nv2/tuf/tuf.go new file mode 100644 index 000000000..8bb86cacd --- /dev/null +++ b/cmd/nv2/tuf/tuf.go @@ -0,0 +1,13 @@ +package tuf + +import "github.com/urfave/cli/v2" + +// TUFCommand contains the TUF related commands +var TUFCommand = &cli.Command{ + Name: "tuf", + Usage: "TUF related commands", + Subcommands: []*cli.Command{ + SignCommand, + VerifyCommand, + }, +} diff --git a/cmd/nv2/tuf/verify.go b/cmd/nv2/tuf/verify.go new file mode 100644 index 000000000..43b9da821 --- /dev/null +++ b/cmd/nv2/tuf/verify.go @@ -0,0 +1,128 @@ +package tuf + +import ( + "crypto/x509" + "fmt" + "io/ioutil" + + "github.com/docker/go/canonical/json" + "github.com/notaryproject/nv2/cmd/nv2/common" + "github.com/notaryproject/nv2/pkg/tuf" + "github.com/notaryproject/nv2/pkg/tuf/local" + "github.com/theupdateframework/notary/tuf/utils" + "github.com/urfave/cli/v2" +) + +// VerifyCommand defines verify command +var VerifyCommand = &cli.Command{ + Name: "verify", + Usage: "verifies OCI Artifacts", + ArgsUsage: "[]", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "signature", + Aliases: []string{"s", "f"}, + Usage: "signature file", + Required: true, + TakesFile: true, + }, + &cli.StringSliceFlag{ + Name: "cert", + Aliases: []string{"c"}, + Usage: "certs for verification", + TakesFile: true, + }, + &cli.StringSliceFlag{ + Name: "ca-cert", + Usage: "CA certs for verification", + TakesFile: true, + }, + &cli.IntFlag{ + Name: "min-version", + Aliases: []string{"m"}, + Usage: "min version of the signature", + }, + &cli.StringFlag{ + Name: "reference", + Aliases: []string{"r"}, + Usage: "original reference", + }, + common.MediaTypeFlag, + common.UsernameFlag, + common.PasswordFlag, + common.InsecureFlag, + }, + Action: runVerify, +} + +func runVerify(ctx *cli.Context) error { + // initialize + verifier, err := getVerifier(ctx) + if err != nil { + return err + } + sig, err := readSignatrueFile(ctx.String("signature")) + if err != nil { + return err + } + + // core process + minVer := ctx.Int("min-version") + targets, err := tuf.VerifyTargets(ctx.Context, verifier, sig, minVer) + if err != nil { + return fmt.Errorf("verification failure: %v", err) + } + manifest, err := common.GetManifestFromContext(ctx) + if err != nil { + return err + } + if reference := ctx.String("reference"); reference != "" { + manifest.Name = reference + } + if !tuf.IsManifestInTargets(manifest, targets) { + return fmt.Errorf("verification failure: %s: not found", manifest.Digests[0]) + } + + // write out + fmt.Println(manifest.Digests[0]) + return nil +} + +func readSignatrueFile(path string) (*tuf.Signed, error) { + bytes, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + signed := new(tuf.Signed) + if err := json.Unmarshal(bytes, signed); err != nil { + return nil, err + } + return signed, nil +} + +func getVerifier(ctx *cli.Context) (tuf.Verifier, error) { + roots := x509.NewCertPool() + + var certs []*x509.Certificate + for _, path := range ctx.StringSlice("cert") { + bundledCerts, err := utils.LoadCertBundleFromFile(path) + if err != nil { + return nil, err + } + certs = append(certs, bundledCerts...) + for _, cert := range bundledCerts { + roots.AddCert(cert) + } + } + for _, path := range ctx.StringSlice("ca-cert") { + bundledCerts, err := utils.LoadCertBundleFromFile(path) + if err != nil { + return nil, err + } + for _, cert := range bundledCerts { + roots.AddCert(cert) + } + } + + return local.NewVerifier(certs, roots) +} diff --git a/docs/nv2/README.md b/docs/nv2/README.md index 319cfbfcd..4bb95776d 100644 --- a/docs/nv2/README.md +++ b/docs/nv2/README.md @@ -12,7 +12,7 @@ ### Build and Install -This plugin requires [golang](https://golang.org/dl/) with version `>= 1.14`. +This binary requires [golang](https://golang.org/dl/) with version `>= 1.15`. To build and install, run @@ -43,11 +43,13 @@ openssl req \ -newkey rsa:2048 \ -days 365 \ -subj "/CN=registry.example.com/O=example inc/C=US/ST=Washington/L=Seattle" \ + -addext "subjectAltName=DNS:registry.example.com" \ -keyout example.key \ -out example.crt ``` -When generating the certificate, make sure that the Common Name (`CN`) is set properly in the `Subject` field. The Common Name will be verified against the registry name within the signature. +When generating the certificate, make sure that the Common Name (`CN`) in the `Subject` field and the Subject Alternative Name (`subjectAltName`) are set properly. +The Common Name (`go < 1.15`) or the Subject Alternative Name (`go >= 1.15`) will be verified against the registry name within the signature. ## Offline Signing @@ -66,13 +68,13 @@ OPTIONS: --method value, -m value signing method --key value, -k value signing key file [x509] --cert value, -c value signing cert [x509] - --expiry value, -e value expire duration (default: 0s) --reference value, -r value original references + --expiry value, -e value expire duration (default: 0s) --output value, -o value write signature to a specific path + --media-type value specify the media type of the manifest read from file or stdin (default: "application/vnd.docker.distribution.manifest.v2+json") --username value, -u value username for generic remote access --password value, -p value password for generic remote access --insecure enable insecure remote access (default: false) - --media-type value specify the media type of the manifest read from file or stdin (default: "application/vnd.docker.distribution.manifest.v2+json") --help, -h show help (default: false) ``` @@ -114,19 +116,19 @@ The formatted x509 signature: `hello-world.signature.config.jwt` is: ``` json { - "typ": "x509", "alg": "RS256", + "typ": "x509", "x5c": [ - "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" ] }.{ - "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "digest": "sha256:24a74900a4e749ef31295e5aabde7093e3244b119582bd6e64b1a88c71c410d0", - "size": 3056, + "iat": 1597893535, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "references": [ "registry.acme-rockets.io/hello-world:v1" ], - "iat": 1597053936 + "size": 3056 }.[Signature] ``` @@ -145,17 +147,17 @@ The formatted x509, without the `x5c` chain signature: `hello-world.signature.co ```json { - "typ": "x509", "alg": "RS256", - "kid": "RQGT:OPJI:IABT:DFXB:52VS:FNOJ:4XBS:H4KY:WHGM:HQMC:WSMN:LKXM" + "kid": "JF3F:UG7I:NCNR:3TCC:XNW3:3ZIW:S77O:O2PT:QXC3:IQ5X:GMMS:CUYB", + "typ": "x509" }.{ - "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "digest": "sha256:24a74900a4e749ef31295e5aabde7093e3244b119582bd6e64b1a88c71c410d0", - "size": 3056, + "iat": 1597893598, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "references": [ "registry.acme-rockets.io/hello-world:v1" ], - "iat": 1597053992 + "size": 3056 }.[Signature] ``` @@ -176,16 +178,14 @@ OPTIONS: --signature value, -s value, -f value signature file --cert value, -c value certs for verification [x509] --ca-cert value CA certs for verification [x509] + --media-type value specify the media type of the manifest read from file or stdin (default: "application/vnd.docker.distribution.manifest.v2+json") --username value, -u value username for generic remote access --password value, -p value password for generic remote access --insecure enable insecure remote access (default: false) - --media-type value specify the media type of the manifest read from file or stdin (default: "application/vnd.docker.distribution.manifest.v2+json") --help, -h show help (default: false) ``` -To verify a manifest `example.json` with a signature file `example.nv2`, run - -Since the manifest was signed by a self-signed certificate, that certificate `cert.pem` is required to be provided to `nv2`. +To verify a manifest `hello-world_v1-manifest.json` with a signature file `hello-world.signature.config.jwt`, run ```shell nv2 verify \ @@ -194,6 +194,8 @@ nv2 verify \ file:hello-world_v1-manifest.json ``` +Since the manifest was signed by a self-signed certificate, that certificate `cert.crt` is required to be provided to `nv2`. + If the cert isn't self-signed, you can omit the `-c` parameter. ``` shell diff --git a/docs/signature/README.md b/docs/signature/README.md index 76bca766a..0bcc5bbed 100644 --- a/docs/signature/README.md +++ b/docs/signature/README.md @@ -42,6 +42,7 @@ openssl req \ -newkey rsa:2048 \ -days 365 \ -subj "/CN=registry.example.com/O=example inc/C=US/ST=Washington/L=Seattle" \ + -addext "subjectAltName=DNS:registry.example.com" \ -keyout example.key \ -out example.crt ``` @@ -51,17 +52,17 @@ An nv2 client would generate the following header and claims to be signed. The header would be a base64 URL encoded string without paddings: ``` -eyJ0eXAiOiJ4NTA5IiwiYWxnIjoiUlMyNTYiLCJ4NWMiOlsiTUlJRHN6Q0NBcHVnQXdJQkFnSVVMMWFuRVUveUp5NjdWSlRiSGtOWDBiQk5BbkV3RFFZSktvWklodmNOQVFFTEJRQXdhVEVkTUJzR0ExVUVBd3dVY21WbmFYTjBjbmt1WlhoaGJYQnNaUzVqYjIweEZEQVNCZ05WQkFvTUMyVjRZVzF3YkdVZ2FXNWpNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1YyRnphR2x1WjNSdmJqRVFNQTRHQTFVRUJ3d0hVMlZoZEhSc1pUQWVGdzB5TURBM01qY3hORFF6TkRaYUZ3MHlNVEEzTWpjeE5EUXpORFphTUdreEhUQWJCZ05WQkFNTUZISmxaMmx6ZEhKNUxtVjRZVzF3YkdVdVkyOXRNUlF3RWdZRFZRUUtEQXRsZUdGdGNHeGxJR2x1WXpFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01DbGRoYzJocGJtZDBiMjR4RURBT0JnTlZCQWNNQjFObFlYUjBiR1V3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRGtLd0FjVjQ0cHNqTjhubm8xZVozenYxWktVaEpBb3h3Qk9JR2ZJeEllK2lIdHBYTHZGRlZ3azVKYnh1K1BraWcyTjRCM0lscmovVnJ5aTBoeHA0bWFnMDJNNzMzYlhMUkVOU09GT05Sa3NscE84ekhVTjVwWWRuaFRTd1lUTGFwMSsxYmdjRlN1VVhMV2llcVpCNnFjN2tpdjNiajNTUGFmNDIrczQ4VjQ5dC9PcFh4THRnaVdMOVhrdURUWmN0cEpKQTR2SEhrNk91MGJjZzdpR20rTDF4d0lmYjhNbDRvV3ZUMFNGMzVmZ1cwOGJiTFhaMnYxWENMUnNyV1VnYnE0VStLeHRFcEczWElZY1loS3gxcklyVWhmRUprdUh6Z1BnbE0xMWdHNVcrQ3lmZyt3Zk9KaWc1cTZheElLV3pJZjZDOG04bG15NmJNK041RXNEOVN2QWdNQkFBR2pVekJSTUIwR0ExVWREZ1FXQkJUZjFoTTYvaWJHRit1L1NWQUs4OEZVTWp6Um9UQWZCZ05WSFNNRUdEQVdnQlRmMWhNNi9pYkdGK3UvU1ZBSzg4RlVNanpSb1RBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFCZ3ZWYXU1KzJ3QXVDc21PeXlHMjhoMXp5QzRJUG1NbXBSWlRET3AvcExkd1hlSGpKcjhrRUMzbDkycUpFdmMrV0Fib0oxUm91Y0h5Y1VlN1JXaDJDNlpGL1dQQ0JMeVdHd25seXFHeVJNOS9qODZVSjFPZ2l1Wmw3a2w5enh3V29heFBCQ21IYTBSSG93ZFFCN0FWbHBxZzFjN0ZoS2poVUNCbUdUNFZlOHRWMGhkWnRyWm9RVis2eEhQYlVkMzdLVjFCMUJtZm8zbzRla29KS2hVdTk5RW8wM09wRTNKTHRNMTNBMUh4QUJFdVFHSFRJMHR5Y0RCQmRSbjNiMDNIb0loVTBWbnFqdnBWMUtQdnNyZ1lpLzBWU3RMTmV6WlBnR2UwZkczWGd5OHlla2RCOU5NVW4relpMQVRJNCt6OGo0UUg1V2o1WlBhVWt5b0FEMm9VSk8iXX0 +eyJhbGciOiJSUzI1NiIsInR5cCI6Ing1MDkiLCJ4NWMiOlsiTUlJRDFEQ0NBcnlnQXdJQkFnSVVNRVdDTW1vSHU3VGFZb2pBVXJDdGhIKy9nUFl3RFFZSktvWklodmNOQVFFTEJRQXdhVEVkTUJzR0ExVUVBd3dVY21WbmFYTjBjbmt1WlhoaGJYQnNaUzVqYjIweEZEQVNCZ05WQkFvTUMyVjRZVzF3YkdVZ2FXNWpNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1YyRnphR2x1WjNSdmJqRVFNQTRHQTFVRUJ3d0hVMlZoZEhSc1pUQWVGdzB5TURBNE1qQXdNalF4TUROYUZ3MHlNVEE0TWpBd01qUXhNRE5hTUdreEhUQWJCZ05WQkFNTUZISmxaMmx6ZEhKNUxtVjRZVzF3YkdVdVkyOXRNUlF3RWdZRFZRUUtEQXRsZUdGdGNHeGxJR2x1WXpFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01DbGRoYzJocGJtZDBiMjR4RURBT0JnTlZCQWNNQjFObFlYUjBiR1V3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQ3pvbHN0YXFMUGt5cmR1dmRCNktoK2o4Vi9XckZRM1dtYnc3VW1pbWlFOVdLRFJHK3d6TndqL3hSN0YyVzZoNUduekdkSSs1eUlzbmx3UzUrZm9NYndOdWJwZlAxK0c0Qk05UjFtTUd0dWxDYUFvbHZRWHBqTkwwbytvbC84RDBLdGFvMlFCbEIyamFZVjlSMlhGWDFvOFo1OWkwbXA1Q1ZnUmdGM2lsQm9ycjFHUXN3aWFXb2RPUEJXZWJXSGhVcGVPWWRRZWY4bXNOTTdKQjJkM3VMU0sxc2FrUGtYVThHZCthQXZXSUtmY3did00yTHZxRmtKY2NQYlNoT3ErdE1PSnJ2MGdhTW4zbVNCSGlXQXRuNGJ0ZEJVZjFUTGtMQmpTTDk4dncwaVVNUUJVbk1aWWtvT2pCSmlpSkNESE1mOW9Ba3hVcVdtRjVqeEdBZlJudk14QWdNQkFBR2pkREJ5TUIwR0ExVWREZ1FXQkJSVG5pb1hJMUpIYkp5blJoRzhyMHF2c2craUJqQWZCZ05WSFNNRUdEQVdnQlJUbmlvWEkxSkhiSnluUmhHOHIwcXZzZytpQmpBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUI4R0ExVWRFUVFZTUJhQ0ZISmxaMmx6ZEhKNUxtVjRZVzF3YkdVdVkyOXRNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUNPNy94YmJXTzRXUzMxQ1V0bGx2N2xEN05VbFpHM0t6VGZSWWhIZTFtN0xiakRpY3o0MVgzb2Q3Q0J3REJiYjhFNkxSdGVlZUlyK3k1Y2FRQm9FUHlWamFab2xwd0xpdy9LMytQYm5rSjBVTm5wdnRJMlpaNy9OS1daSTdqK3pTTkh0N24wTWpOcXZENFFlY04wWWQxOFFTVTBUd3FuVnphLzhUK0lRbnRMMDRZNU1CaFA2ZldNR1p5ZURob2lEYlhJRm1HRzd4N3pmN3FYcFphU0FLbHFHTDkvdzJETUY1OTh0V2YxT3NTN1BDdE9xUGZPZ2JnR3FKMSs4S2FoWTlxN0pBQUZ6bDY4Y3Jmdkg2MzNGcXdtNVErT2lyZUtuMXA2d20yZzhBK0ZsOWtHWEdQVVdqd05wdUZCL0JSSXZ0TEhYRjJ6R3Flam9ET1dzekJ1TmhmOCJdfQ ``` The parsed and formatted header would be: ```json { - "typ": "x509", "alg": "RS256", + "typ": "x509", "x5c": [ - "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" ] } ``` @@ -69,36 +70,36 @@ The parsed and formatted header would be: The claims would be a base64 URL encoded string without paddings: ``` -eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OmM0NTE2YjhhMzExZTg1ZjFmMmE2MDU3M2FiZjRjNmI3NDBjYTNhZGU0MTI3ZTI5YjA1NjE2ODQ4ZGU0ODdkMzQiLCJzaXplIjo1MjgsInJlZmVyZW5jZXMiOlsicmVnaXN0cnkuZXhhbXBsZS5jb20vZXhhbXBsZTpsYXRlc3QiLCJyZWdpc3RyeS5leGFtcGxlLmNvbS9leGFtcGxlOnYxLjAiXSwiZXhwIjoxNjI4NTg3MTE5LCJpYXQiOjE1OTcwNTExMTksIm5iZiI6MTU5NzA1MTExOX0 +eyJkaWdlc3QiOiJzaGEyNTY6YzQ1MTZiOGEzMTFlODVmMWYyYTYwNTczYWJmNGM2Yjc0MGNhM2FkZTQxMjdlMjliMDU2MTY4NDhkZTQ4N2QzNCIsImV4cCI6MTYyOTEwNTk5NCwiaWF0IjoxNTk3ODkzOTk0LCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwibmJmIjoxNTk3ODkzOTk0LCJyZWZlcmVuY2VzIjpbInJlZ2lzdHJ5LmV4YW1wbGUuY29tL2V4YW1wbGU6bGF0ZXN0IiwicmVnaXN0cnkuZXhhbXBsZS5jb20vZXhhbXBsZTp2MS4wIl0sInNpemUiOjUyOH0 ``` The parsed and formatted claims would be: ``` JSON { - "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "digest": "sha256:c4516b8a311e85f1f2a60573abf4c6b740ca3ade4127e29b05616848de487d34", - "size": 528, + "exp": 1629105994, + "iat": 1597893994, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "nbf": 1597893994, "references": [ "registry.example.com/example:latest", "registry.example.com/example:v1.0" ], - "exp": 1628587119, - "iat": 1597051119, - "nbf": 1597051119 + "size": 528 } ``` The signature of the above would be represented as a base64 URL encoded string without paddings: ``` -MtQBOL2FERM2fMSikruHOMQdHuEXAE1wf6J6TfDY2W_7PfQQllBKbJJE0HqJ5ENAbuqNYHNZeIeKUCYFrNx2XgtrKuTe7WCa1ZZKDtp5bmANp484ekdl6lW23YB8r_SRtseJuibqjI3HuiMyELj9uYV1CdRYaD2BIZ_qxraYH1fMpjDWjehU4RYLI37hsSuDQ90o09BwaNfzbQXYPsGmkSUSmej7rOFPDnuwhNy4WcUed3kRKYEW8eIjO9OUBGQq3PWvhDjxZi3QF4QFDoiKBOXL70AjaiVIveQRkJI9-xHZSYwje9OFEMioeNWB5ceZR-r4L7VzDcU-Fxqjxn79Fw +Zxlv4mVVFKUFwGmyBC85pEiRD-qzwu4myUT89frvfsZ8PRNDV1_B4eoHQVJFpAn5HsTJ_MtHSP1dHd1OeoLRYlOpeDFy_rQlio--zXsyJDMvxIK4SIwnwhohf41hOoF-N_tAUv93fUunEdW5tt55pWrb3jOewYAWbpTRkBoEVHgHmiJriT4eeJqhi8C_gXn9B6KejH4RGUq3GQtclAKTNpZju0g0l1gp_zfHMhm4kn-5NlXlg96MA3JVkib9TglliCbMvPqaMdAZi74JQwd6KyH-2QMspw3tFZKyy9gQnqC9LFaMN2gZgAX1XX0bMB2XV5hA2-_S4uCgVTG7lpgehA ``` Putting everything together: ``` -eyJ0eXAiOiJ4NTA5IiwiYWxnIjoiUlMyNTYiLCJ4NWMiOlsiTUlJRHN6Q0NBcHVnQXdJQkFnSVVMMWFuRVUveUp5NjdWSlRiSGtOWDBiQk5BbkV3RFFZSktvWklodmNOQVFFTEJRQXdhVEVkTUJzR0ExVUVBd3dVY21WbmFYTjBjbmt1WlhoaGJYQnNaUzVqYjIweEZEQVNCZ05WQkFvTUMyVjRZVzF3YkdVZ2FXNWpNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1YyRnphR2x1WjNSdmJqRVFNQTRHQTFVRUJ3d0hVMlZoZEhSc1pUQWVGdzB5TURBM01qY3hORFF6TkRaYUZ3MHlNVEEzTWpjeE5EUXpORFphTUdreEhUQWJCZ05WQkFNTUZISmxaMmx6ZEhKNUxtVjRZVzF3YkdVdVkyOXRNUlF3RWdZRFZRUUtEQXRsZUdGdGNHeGxJR2x1WXpFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01DbGRoYzJocGJtZDBiMjR4RURBT0JnTlZCQWNNQjFObFlYUjBiR1V3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRGtLd0FjVjQ0cHNqTjhubm8xZVozenYxWktVaEpBb3h3Qk9JR2ZJeEllK2lIdHBYTHZGRlZ3azVKYnh1K1BraWcyTjRCM0lscmovVnJ5aTBoeHA0bWFnMDJNNzMzYlhMUkVOU09GT05Sa3NscE84ekhVTjVwWWRuaFRTd1lUTGFwMSsxYmdjRlN1VVhMV2llcVpCNnFjN2tpdjNiajNTUGFmNDIrczQ4VjQ5dC9PcFh4THRnaVdMOVhrdURUWmN0cEpKQTR2SEhrNk91MGJjZzdpR20rTDF4d0lmYjhNbDRvV3ZUMFNGMzVmZ1cwOGJiTFhaMnYxWENMUnNyV1VnYnE0VStLeHRFcEczWElZY1loS3gxcklyVWhmRUprdUh6Z1BnbE0xMWdHNVcrQ3lmZyt3Zk9KaWc1cTZheElLV3pJZjZDOG04bG15NmJNK041RXNEOVN2QWdNQkFBR2pVekJSTUIwR0ExVWREZ1FXQkJUZjFoTTYvaWJHRit1L1NWQUs4OEZVTWp6Um9UQWZCZ05WSFNNRUdEQVdnQlRmMWhNNi9pYkdGK3UvU1ZBSzg4RlVNanpSb1RBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFCZ3ZWYXU1KzJ3QXVDc21PeXlHMjhoMXp5QzRJUG1NbXBSWlRET3AvcExkd1hlSGpKcjhrRUMzbDkycUpFdmMrV0Fib0oxUm91Y0h5Y1VlN1JXaDJDNlpGL1dQQ0JMeVdHd25seXFHeVJNOS9qODZVSjFPZ2l1Wmw3a2w5enh3V29heFBCQ21IYTBSSG93ZFFCN0FWbHBxZzFjN0ZoS2poVUNCbUdUNFZlOHRWMGhkWnRyWm9RVis2eEhQYlVkMzdLVjFCMUJtZm8zbzRla29KS2hVdTk5RW8wM09wRTNKTHRNMTNBMUh4QUJFdVFHSFRJMHR5Y0RCQmRSbjNiMDNIb0loVTBWbnFqdnBWMUtQdnNyZ1lpLzBWU3RMTmV6WlBnR2UwZkczWGd5OHlla2RCOU5NVW4relpMQVRJNCt6OGo0UUg1V2o1WlBhVWt5b0FEMm9VSk8iXX0.eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OmM0NTE2YjhhMzExZTg1ZjFmMmE2MDU3M2FiZjRjNmI3NDBjYTNhZGU0MTI3ZTI5YjA1NjE2ODQ4ZGU0ODdkMzQiLCJzaXplIjo1MjgsInJlZmVyZW5jZXMiOlsicmVnaXN0cnkuZXhhbXBsZS5jb20vZXhhbXBsZTpsYXRlc3QiLCJyZWdpc3RyeS5leGFtcGxlLmNvbS9leGFtcGxlOnYxLjAiXSwiZXhwIjoxNjI4NTg3MTE5LCJpYXQiOjE1OTcwNTExMTksIm5iZiI6MTU5NzA1MTExOX0.MtQBOL2FERM2fMSikruHOMQdHuEXAE1wf6J6TfDY2W_7PfQQllBKbJJE0HqJ5ENAbuqNYHNZeIeKUCYFrNx2XgtrKuTe7WCa1ZZKDtp5bmANp484ekdl6lW23YB8r_SRtseJuibqjI3HuiMyELj9uYV1CdRYaD2BIZ_qxraYH1fMpjDWjehU4RYLI37hsSuDQ90o09BwaNfzbQXYPsGmkSUSmej7rOFPDnuwhNy4WcUed3kRKYEW8eIjO9OUBGQq3PWvhDjxZi3QF4QFDoiKBOXL70AjaiVIveQRkJI9-xHZSYwje9OFEMioeNWB5ceZR-r4L7VzDcU-Fxqjxn79Fw +eyJhbGciOiJSUzI1NiIsInR5cCI6Ing1MDkiLCJ4NWMiOlsiTUlJRDFEQ0NBcnlnQXdJQkFnSVVNRVdDTW1vSHU3VGFZb2pBVXJDdGhIKy9nUFl3RFFZSktvWklodmNOQVFFTEJRQXdhVEVkTUJzR0ExVUVBd3dVY21WbmFYTjBjbmt1WlhoaGJYQnNaUzVqYjIweEZEQVNCZ05WQkFvTUMyVjRZVzF3YkdVZ2FXNWpNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1YyRnphR2x1WjNSdmJqRVFNQTRHQTFVRUJ3d0hVMlZoZEhSc1pUQWVGdzB5TURBNE1qQXdNalF4TUROYUZ3MHlNVEE0TWpBd01qUXhNRE5hTUdreEhUQWJCZ05WQkFNTUZISmxaMmx6ZEhKNUxtVjRZVzF3YkdVdVkyOXRNUlF3RWdZRFZRUUtEQXRsZUdGdGNHeGxJR2x1WXpFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01DbGRoYzJocGJtZDBiMjR4RURBT0JnTlZCQWNNQjFObFlYUjBiR1V3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQ3pvbHN0YXFMUGt5cmR1dmRCNktoK2o4Vi9XckZRM1dtYnc3VW1pbWlFOVdLRFJHK3d6TndqL3hSN0YyVzZoNUduekdkSSs1eUlzbmx3UzUrZm9NYndOdWJwZlAxK0c0Qk05UjFtTUd0dWxDYUFvbHZRWHBqTkwwbytvbC84RDBLdGFvMlFCbEIyamFZVjlSMlhGWDFvOFo1OWkwbXA1Q1ZnUmdGM2lsQm9ycjFHUXN3aWFXb2RPUEJXZWJXSGhVcGVPWWRRZWY4bXNOTTdKQjJkM3VMU0sxc2FrUGtYVThHZCthQXZXSUtmY3did00yTHZxRmtKY2NQYlNoT3ErdE1PSnJ2MGdhTW4zbVNCSGlXQXRuNGJ0ZEJVZjFUTGtMQmpTTDk4dncwaVVNUUJVbk1aWWtvT2pCSmlpSkNESE1mOW9Ba3hVcVdtRjVqeEdBZlJudk14QWdNQkFBR2pkREJ5TUIwR0ExVWREZ1FXQkJSVG5pb1hJMUpIYkp5blJoRzhyMHF2c2craUJqQWZCZ05WSFNNRUdEQVdnQlJUbmlvWEkxSkhiSnluUmhHOHIwcXZzZytpQmpBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUI4R0ExVWRFUVFZTUJhQ0ZISmxaMmx6ZEhKNUxtVjRZVzF3YkdVdVkyOXRNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUNPNy94YmJXTzRXUzMxQ1V0bGx2N2xEN05VbFpHM0t6VGZSWWhIZTFtN0xiakRpY3o0MVgzb2Q3Q0J3REJiYjhFNkxSdGVlZUlyK3k1Y2FRQm9FUHlWamFab2xwd0xpdy9LMytQYm5rSjBVTm5wdnRJMlpaNy9OS1daSTdqK3pTTkh0N24wTWpOcXZENFFlY04wWWQxOFFTVTBUd3FuVnphLzhUK0lRbnRMMDRZNU1CaFA2ZldNR1p5ZURob2lEYlhJRm1HRzd4N3pmN3FYcFphU0FLbHFHTDkvdzJETUY1OTh0V2YxT3NTN1BDdE9xUGZPZ2JnR3FKMSs4S2FoWTlxN0pBQUZ6bDY4Y3Jmdkg2MzNGcXdtNVErT2lyZUtuMXA2d20yZzhBK0ZsOWtHWEdQVVdqd05wdUZCL0JSSXZ0TEhYRjJ6R3Flam9ET1dzekJ1TmhmOCJdfQ.eyJkaWdlc3QiOiJzaGEyNTY6YzQ1MTZiOGEzMTFlODVmMWYyYTYwNTczYWJmNGM2Yjc0MGNhM2FkZTQxMjdlMjliMDU2MTY4NDhkZTQ4N2QzNCIsImV4cCI6MTYyOTEwNTk5NCwiaWF0IjoxNTk3ODkzOTk0LCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwibmJmIjoxNTk3ODkzOTk0LCJyZWZlcmVuY2VzIjpbInJlZ2lzdHJ5LmV4YW1wbGUuY29tL2V4YW1wbGU6bGF0ZXN0IiwicmVnaXN0cnkuZXhhbXBsZS5jb20vZXhhbXBsZTp2MS4wIl0sInNpemUiOjUyOH0.Zxlv4mVVFKUFwGmyBC85pEiRD-qzwu4myUT89frvfsZ8PRNDV1_B4eoHQVJFpAn5HsTJ_MtHSP1dHd1OeoLRYlOpeDFy_rQlio--zXsyJDMvxIK4SIwnwhohf41hOoF-N_tAUv93fUunEdW5tt55pWrb3jOewYAWbpTRkBoEVHgHmiJriT4eeJqhi8C_gXn9B6KejH4RGUq3GQtclAKTNpZju0g0l1gp_zfHMhm4kn-5NlXlg96MA3JVkib9TglliCbMvPqaMdAZi74JQwd6KyH-2QMspw3tFZKyy9gQnqC9LFaMN2gZgAX1XX0bMB2XV5hA2-_S4uCgVTG7lpgehA ``` ### Signature Persisted within an OCI Artifact Enabled Registry @@ -116,13 +117,13 @@ Would push the following manifest: ``` JSON { - "schemaVersion": 2, - "config": { - "mediaType": "application/vnd.cncf.notary.config.v2+jwt", - "size": 1906, - "digest": "sha256:c7848182f2c817415f0de63206f9e4220012cbb0bdb750c2ecf8020350239814" - }, - "layers": [] + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.cncf.notary.config.v2+jwt", + "size": 1906, + "digest": "sha256:c7848182f2c817415f0de63206f9e4220012cbb0bdb750c2ecf8020350239814" + }, + "layers": [] } ``` @@ -192,22 +193,22 @@ Example showing a formatted `x509` signature file [examples/x509_x5c.nv2.jwt](ex ```json { - "typ": "x509", "alg": "RS256", + "typ": "x509", "x5c": [ - "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" ] }.{ - "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "digest": "sha256:c4516b8a311e85f1f2a60573abf4c6b740ca3ade4127e29b05616848de487d34", - "size": 528, + "exp": 1629105994, + "iat": 1597893994, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "nbf": 1597893994, "references": [ "registry.example.com/example:latest", "registry.example.com/example:v1.0" ], - "exp": 1628587119, - "iat": 1597051119, - "nbf": 1597051119 + "size": 528 }.[Signature] ``` @@ -215,20 +216,20 @@ Example showing a formatted `x509` signature file [examples/x509_kid.nv2.jwt](ex ```json { - "typ": "x509", "alg": "RS256", - "kid": "XP5O:Y7W2:PRB6:O355:56CC:P3A6:CBDV:EDMN:QZCK:W5PO:QMV3:T2LX" + "kid": "GLCY:N6YH:YD7T:7TKW:B3L3:MXER:AS63:EAYF:PJL7:DS4R:ESJN:4MZQ", + "typ": "x509" }.{ - "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "digest": "sha256:c4516b8a311e85f1f2a60573abf4c6b740ca3ade4127e29b05616848de487d34", - "size": 528, + "exp": 1629106005, + "iat": 1597894005, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "nbf": 1597894005, "references": [ "registry.example.com/example:latest", "registry.example.com/example:v1.0" ], - "exp": 1628587341, - "iat": 1597051341, - "nbf": 1597051341 + "size": 528 }.[Signature] ``` @@ -236,4 +237,3 @@ Example showing a formatted `x509` signature file [examples/x509_kid.nv2.jwt](ex [oci-artifacts]: https://github.com/opencontainers/artifacts [oci-manifest]: https://github.com/opencontainers/image-spec/blob/master/manifest.md [oci-manifest-list]: https://github.com/opencontainers/image-spec/blob/master/image-index.md - diff --git a/docs/signature/examples/x509_kid.nv2.jwt b/docs/signature/examples/x509_kid.nv2.jwt index 444cd2790..dab2f0268 100644 --- a/docs/signature/examples/x509_kid.nv2.jwt +++ b/docs/signature/examples/x509_kid.nv2.jwt @@ -1 +1 @@ -eyJ0eXAiOiJ4NTA5IiwiYWxnIjoiUlMyNTYiLCJraWQiOiJYUDVPOlk3VzI6UFJCNjpPMzU1OjU2Q0M6UDNBNjpDQkRWOkVETU46UVpDSzpXNVBPOlFNVjM6VDJMWCJ9.eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OmM0NTE2YjhhMzExZTg1ZjFmMmE2MDU3M2FiZjRjNmI3NDBjYTNhZGU0MTI3ZTI5YjA1NjE2ODQ4ZGU0ODdkMzQiLCJzaXplIjo1MjgsInJlZmVyZW5jZXMiOlsicmVnaXN0cnkuZXhhbXBsZS5jb20vZXhhbXBsZTpsYXRlc3QiLCJyZWdpc3RyeS5leGFtcGxlLmNvbS9leGFtcGxlOnYxLjAiXSwiZXhwIjoxNjI4NTg3MzQxLCJpYXQiOjE1OTcwNTEzNDEsIm5iZiI6MTU5NzA1MTM0MX0.cr9C_Py-IJcgIUXtHAQ9dFmZO4JBEOedPdg67Fm-Av8vMQBHrs7kHZOqZhF33OYR7tuG94v760RlrCrBl1OhUpk5umLjeCOk1-RBqSWUhM7GxwfeIWEIC10gzmolHVI55nb27QQxq0pTqhAC9Nof6QljFG8kyqYqjn0cr3X1zt23ppyJ1CYkcdXdDL0QD8-1EnngHAYcssun8A9dKveld-O-dMq94wk2FkSuKz6WSOM1I5E-thbq6NltB7dzLuZAkU4LXAqODCJ7fTQgUvtapzyEMvV6cQwAG1sUV1yEST0A6t6U_0Tt-X32_kciptVuzbtRLYuOW8Wzv7E41ryU6w \ No newline at end of file +eyJhbGciOiJSUzI1NiIsImtpZCI6IkdMQ1k6TjZZSDpZRDdUOjdUS1c6QjNMMzpNWEVSOkFTNjM6RUFZRjpQSkw3OkRTNFI6RVNKTjo0TVpRIiwidHlwIjoieDUwOSJ9.eyJkaWdlc3QiOiJzaGEyNTY6YzQ1MTZiOGEzMTFlODVmMWYyYTYwNTczYWJmNGM2Yjc0MGNhM2FkZTQxMjdlMjliMDU2MTY4NDhkZTQ4N2QzNCIsImV4cCI6MTYyOTEwNjAwNSwiaWF0IjoxNTk3ODk0MDA1LCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwibmJmIjoxNTk3ODk0MDA1LCJyZWZlcmVuY2VzIjpbInJlZ2lzdHJ5LmV4YW1wbGUuY29tL2V4YW1wbGU6bGF0ZXN0IiwicmVnaXN0cnkuZXhhbXBsZS5jb20vZXhhbXBsZTp2MS4wIl0sInNpemUiOjUyOH0.FCS3kEcoWHF6NSRrAFnzUcP0BoPFPmn7CO70Dk2Dr1A0I7cZCoWoqq9XHws3Sj_BqxncWIp-M5akm6tBdqVomuDuPKjMnrdGVWGEBOOItg8K_TvCyPGoeFMMzLZ4lao-8Wo9Yig3ggdUlu_EkuzNgFRHHUV7q6DkvFmogy5P8IRqpJPJBHYCeQSdXphEa6FIjRb_oGNXIif6xT6Bu7x0MI_bacLeshC5TLFIVx7H1Dh6mbei_ewWptYqiT15gOG2a-ERRR9XnHxDC3gnoTly7aT277Sf8nrTLlM8n3qRclAxkR4CwHta4n6oljX_lAoUzQeECnFRGuQL-DmMxnXD6Q \ No newline at end of file diff --git a/docs/signature/examples/x509_x5c.nv2.jwt b/docs/signature/examples/x509_x5c.nv2.jwt index 78ff07e12..d344f3b88 100644 --- a/docs/signature/examples/x509_x5c.nv2.jwt +++ b/docs/signature/examples/x509_x5c.nv2.jwt @@ -1 +1 @@ -eyJ0eXAiOiJ4NTA5IiwiYWxnIjoiUlMyNTYiLCJ4NWMiOlsiTUlJRHN6Q0NBcHVnQXdJQkFnSVVMMWFuRVUveUp5NjdWSlRiSGtOWDBiQk5BbkV3RFFZSktvWklodmNOQVFFTEJRQXdhVEVkTUJzR0ExVUVBd3dVY21WbmFYTjBjbmt1WlhoaGJYQnNaUzVqYjIweEZEQVNCZ05WQkFvTUMyVjRZVzF3YkdVZ2FXNWpNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1YyRnphR2x1WjNSdmJqRVFNQTRHQTFVRUJ3d0hVMlZoZEhSc1pUQWVGdzB5TURBM01qY3hORFF6TkRaYUZ3MHlNVEEzTWpjeE5EUXpORFphTUdreEhUQWJCZ05WQkFNTUZISmxaMmx6ZEhKNUxtVjRZVzF3YkdVdVkyOXRNUlF3RWdZRFZRUUtEQXRsZUdGdGNHeGxJR2x1WXpFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01DbGRoYzJocGJtZDBiMjR4RURBT0JnTlZCQWNNQjFObFlYUjBiR1V3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRGtLd0FjVjQ0cHNqTjhubm8xZVozenYxWktVaEpBb3h3Qk9JR2ZJeEllK2lIdHBYTHZGRlZ3azVKYnh1K1BraWcyTjRCM0lscmovVnJ5aTBoeHA0bWFnMDJNNzMzYlhMUkVOU09GT05Sa3NscE84ekhVTjVwWWRuaFRTd1lUTGFwMSsxYmdjRlN1VVhMV2llcVpCNnFjN2tpdjNiajNTUGFmNDIrczQ4VjQ5dC9PcFh4THRnaVdMOVhrdURUWmN0cEpKQTR2SEhrNk91MGJjZzdpR20rTDF4d0lmYjhNbDRvV3ZUMFNGMzVmZ1cwOGJiTFhaMnYxWENMUnNyV1VnYnE0VStLeHRFcEczWElZY1loS3gxcklyVWhmRUprdUh6Z1BnbE0xMWdHNVcrQ3lmZyt3Zk9KaWc1cTZheElLV3pJZjZDOG04bG15NmJNK041RXNEOVN2QWdNQkFBR2pVekJSTUIwR0ExVWREZ1FXQkJUZjFoTTYvaWJHRit1L1NWQUs4OEZVTWp6Um9UQWZCZ05WSFNNRUdEQVdnQlRmMWhNNi9pYkdGK3UvU1ZBSzg4RlVNanpSb1RBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFCZ3ZWYXU1KzJ3QXVDc21PeXlHMjhoMXp5QzRJUG1NbXBSWlRET3AvcExkd1hlSGpKcjhrRUMzbDkycUpFdmMrV0Fib0oxUm91Y0h5Y1VlN1JXaDJDNlpGL1dQQ0JMeVdHd25seXFHeVJNOS9qODZVSjFPZ2l1Wmw3a2w5enh3V29heFBCQ21IYTBSSG93ZFFCN0FWbHBxZzFjN0ZoS2poVUNCbUdUNFZlOHRWMGhkWnRyWm9RVis2eEhQYlVkMzdLVjFCMUJtZm8zbzRla29KS2hVdTk5RW8wM09wRTNKTHRNMTNBMUh4QUJFdVFHSFRJMHR5Y0RCQmRSbjNiMDNIb0loVTBWbnFqdnBWMUtQdnNyZ1lpLzBWU3RMTmV6WlBnR2UwZkczWGd5OHlla2RCOU5NVW4relpMQVRJNCt6OGo0UUg1V2o1WlBhVWt5b0FEMm9VSk8iXX0.eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OmM0NTE2YjhhMzExZTg1ZjFmMmE2MDU3M2FiZjRjNmI3NDBjYTNhZGU0MTI3ZTI5YjA1NjE2ODQ4ZGU0ODdkMzQiLCJzaXplIjo1MjgsInJlZmVyZW5jZXMiOlsicmVnaXN0cnkuZXhhbXBsZS5jb20vZXhhbXBsZTpsYXRlc3QiLCJyZWdpc3RyeS5leGFtcGxlLmNvbS9leGFtcGxlOnYxLjAiXSwiZXhwIjoxNjI4NTg3MTE5LCJpYXQiOjE1OTcwNTExMTksIm5iZiI6MTU5NzA1MTExOX0.MtQBOL2FERM2fMSikruHOMQdHuEXAE1wf6J6TfDY2W_7PfQQllBKbJJE0HqJ5ENAbuqNYHNZeIeKUCYFrNx2XgtrKuTe7WCa1ZZKDtp5bmANp484ekdl6lW23YB8r_SRtseJuibqjI3HuiMyELj9uYV1CdRYaD2BIZ_qxraYH1fMpjDWjehU4RYLI37hsSuDQ90o09BwaNfzbQXYPsGmkSUSmej7rOFPDnuwhNy4WcUed3kRKYEW8eIjO9OUBGQq3PWvhDjxZi3QF4QFDoiKBOXL70AjaiVIveQRkJI9-xHZSYwje9OFEMioeNWB5ceZR-r4L7VzDcU-Fxqjxn79Fw \ No newline at end of file +eyJhbGciOiJSUzI1NiIsInR5cCI6Ing1MDkiLCJ4NWMiOlsiTUlJRDFEQ0NBcnlnQXdJQkFnSVVNRVdDTW1vSHU3VGFZb2pBVXJDdGhIKy9nUFl3RFFZSktvWklodmNOQVFFTEJRQXdhVEVkTUJzR0ExVUVBd3dVY21WbmFYTjBjbmt1WlhoaGJYQnNaUzVqYjIweEZEQVNCZ05WQkFvTUMyVjRZVzF3YkdVZ2FXNWpNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1YyRnphR2x1WjNSdmJqRVFNQTRHQTFVRUJ3d0hVMlZoZEhSc1pUQWVGdzB5TURBNE1qQXdNalF4TUROYUZ3MHlNVEE0TWpBd01qUXhNRE5hTUdreEhUQWJCZ05WQkFNTUZISmxaMmx6ZEhKNUxtVjRZVzF3YkdVdVkyOXRNUlF3RWdZRFZRUUtEQXRsZUdGdGNHeGxJR2x1WXpFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01DbGRoYzJocGJtZDBiMjR4RURBT0JnTlZCQWNNQjFObFlYUjBiR1V3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQ3pvbHN0YXFMUGt5cmR1dmRCNktoK2o4Vi9XckZRM1dtYnc3VW1pbWlFOVdLRFJHK3d6TndqL3hSN0YyVzZoNUduekdkSSs1eUlzbmx3UzUrZm9NYndOdWJwZlAxK0c0Qk05UjFtTUd0dWxDYUFvbHZRWHBqTkwwbytvbC84RDBLdGFvMlFCbEIyamFZVjlSMlhGWDFvOFo1OWkwbXA1Q1ZnUmdGM2lsQm9ycjFHUXN3aWFXb2RPUEJXZWJXSGhVcGVPWWRRZWY4bXNOTTdKQjJkM3VMU0sxc2FrUGtYVThHZCthQXZXSUtmY3did00yTHZxRmtKY2NQYlNoT3ErdE1PSnJ2MGdhTW4zbVNCSGlXQXRuNGJ0ZEJVZjFUTGtMQmpTTDk4dncwaVVNUUJVbk1aWWtvT2pCSmlpSkNESE1mOW9Ba3hVcVdtRjVqeEdBZlJudk14QWdNQkFBR2pkREJ5TUIwR0ExVWREZ1FXQkJSVG5pb1hJMUpIYkp5blJoRzhyMHF2c2craUJqQWZCZ05WSFNNRUdEQVdnQlJUbmlvWEkxSkhiSnluUmhHOHIwcXZzZytpQmpBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUI4R0ExVWRFUVFZTUJhQ0ZISmxaMmx6ZEhKNUxtVjRZVzF3YkdVdVkyOXRNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUNPNy94YmJXTzRXUzMxQ1V0bGx2N2xEN05VbFpHM0t6VGZSWWhIZTFtN0xiakRpY3o0MVgzb2Q3Q0J3REJiYjhFNkxSdGVlZUlyK3k1Y2FRQm9FUHlWamFab2xwd0xpdy9LMytQYm5rSjBVTm5wdnRJMlpaNy9OS1daSTdqK3pTTkh0N24wTWpOcXZENFFlY04wWWQxOFFTVTBUd3FuVnphLzhUK0lRbnRMMDRZNU1CaFA2ZldNR1p5ZURob2lEYlhJRm1HRzd4N3pmN3FYcFphU0FLbHFHTDkvdzJETUY1OTh0V2YxT3NTN1BDdE9xUGZPZ2JnR3FKMSs4S2FoWTlxN0pBQUZ6bDY4Y3Jmdkg2MzNGcXdtNVErT2lyZUtuMXA2d20yZzhBK0ZsOWtHWEdQVVdqd05wdUZCL0JSSXZ0TEhYRjJ6R3Flam9ET1dzekJ1TmhmOCJdfQ.eyJkaWdlc3QiOiJzaGEyNTY6YzQ1MTZiOGEzMTFlODVmMWYyYTYwNTczYWJmNGM2Yjc0MGNhM2FkZTQxMjdlMjliMDU2MTY4NDhkZTQ4N2QzNCIsImV4cCI6MTYyOTEwNTk5NCwiaWF0IjoxNTk3ODkzOTk0LCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwibmJmIjoxNTk3ODkzOTk0LCJyZWZlcmVuY2VzIjpbInJlZ2lzdHJ5LmV4YW1wbGUuY29tL2V4YW1wbGU6bGF0ZXN0IiwicmVnaXN0cnkuZXhhbXBsZS5jb20vZXhhbXBsZTp2MS4wIl0sInNpemUiOjUyOH0.Zxlv4mVVFKUFwGmyBC85pEiRD-qzwu4myUT89frvfsZ8PRNDV1_B4eoHQVJFpAn5HsTJ_MtHSP1dHd1OeoLRYlOpeDFy_rQlio--zXsyJDMvxIK4SIwnwhohf41hOoF-N_tAUv93fUunEdW5tt55pWrb3jOewYAWbpTRkBoEVHgHmiJriT4eeJqhi8C_gXn9B6KejH4RGUq3GQtclAKTNpZju0g0l1gp_zfHMhm4kn-5NlXlg96MA3JVkib9TglliCbMvPqaMdAZi74JQwd6KyH-2QMspw3tFZKyy9gQnqC9LFaMN2gZgAX1XX0bMB2XV5hA2-_S4uCgVTG7lpgehA \ No newline at end of file diff --git a/docs/tuf/README.md b/docs/tuf/README.md new file mode 100644 index 000000000..aa6044ebe --- /dev/null +++ b/docs/tuf/README.md @@ -0,0 +1,310 @@ +# TUF Integration Proposal + +## Introduction + +While developer-signed signatures guarantee the unforgeability of software packages, they are not good enough to defend attacks in the software update scenarios like rollback attacks, fast-forward attacks, indefinite freeze attacks, and many others. To resolve the security challenges in software update systems, Samuel et al. proposed the update framework (TUF) [[1][1]] and it was later enhanced by Kuppusamy et al. [[2][2], [3][3]]. + +Many organizations [[4][4], [5][5], [6][6], [7][7], [8][8], [9][9]] adopt TUF for their software repositories. PyPI [[9][9]] accepted to integrate TUF in the minimum security model [[10][10]] for security protection in continuous delivery of distributions. However, the adversaries are able to forge packages if PyPI is compromised. Therefore, the maximum security model [[11][11]] was proposed for end-to-end signing. + +## Proposal + +We extract the ideas in the Python Enhancement Proposals (PEPs) [[10][10], [11][11]] that + +- TUF operated by organizations are used to defend *update related attacks*. +- Regular signatures signed by developed are used to defend *forgery*. + +It is worth noting that the verification keys / public keys are managed by trusted PKIs, and not by TUF since each TUF trust collection includes an unique PKI. + +Both techniques are standalone and orthogonal. Organizations and end users have options to choose either of them or both, depending on their security requirements or security model. If organizations or end users do not care about update scenarios, regular signatures are sufficient. If they care about software update, TUF is a better choice. If they care about the origin of the content and software update, they can choose both where trust can split and come from different parties. + +In the case of double security, the developer-signed signatures must be in the format of a JSON file for a TUF delegation role so that those signatures can seamlessly integrated to TUF as a real delegation role. For instance, Alice publishes a software package and signs it in the TUF delegation role format. Later, Alice publishes the TUF-formatted signature to a TUF trust collection operated by Bob. If Charlie downloads the package and wants to verify, Charlie can verify the TUF trust collection by Bob for update protection, and then verify the TUF-formatted signature with contemporary PKI for potential forgery. + +Readers may find that the separation of trust is useful in signature movement and even in air-gapped network as a better solution than [[12][12]]. Continuing with the above example instance, Charlie can copy Alice's package and signature to his own possibly air-gapped registry, and register the signature as a delegation role in the TUF trusted collection operated by David in the same network. As a result, Charlie can verify the package without Bob. In this scenario, Charlie still trusts Alice for forgery protection. However, he moves the trust for update protection from Bob for David. + +## Prototype + +The `nv2` prototype with the `tuf` sub commands illustrates what a TUF-formatted signature, which is a JSON file for a delegation role, looks like. + +```shell +NAME: + nv2 tuf - TUF related commands + +USAGE: + nv2 tuf command [command options] [arguments...] + +COMMANDS: + sign signs OCI Artifacts + verify verifies OCI Artifacts + help, h Shows a list of commands or help for one command + +OPTIONS: + --help, -h show help (default: false) + --version, -v print the version (default: false) +``` + +Unlike Docker Content Trust [[6][6]], each organization owns a trust collection instead of per docker repository basis. We also use fully qualified image reference for the target name (e.g. `registry.acme-rockets.io/hello-world:v1`) instead of tag names (e.g. `v1`). + +### Offline Signing + +Offline signing is accomplished with the `nv2 tuf sign` command. + +```shell +NAME: + nv2 tuf sign - signs OCI Artifacts + +USAGE: + nv2 tuf sign [command options] [] + +OPTIONS: + --key value, -k value signing key file + --cert value, -c value signing cert + --signature value, -s value, -f value base signature file + --reference value, -r value original references + --expiry value, -e value expire duration (default: 0s) + --output value, -o value write signature to a specific path + --media-type value specify the media type of the manifest read from file or stdin (default: "application/vnd.docker.distribution.manifest.v2+json") + --username value, -u value username for generic remote access + --password value, -p value password for generic remote access + --insecure enable insecure remote access (default: false) + --help, -h show help (default: false) +``` + +To sign the manifest `hello-world_v1-manifest.json` using the key `key.key` from the `x509` certificate `cert.crt` with the Common Name `registry.acme-rockets.io`, run + +```shell +nv2 tuf sign \ + -k key.key \ + -c cert.crt \ + -r registry.acme-rockets.io/hello-world:v1 \ + -e 8670h \ + -o hello-world.signature.config.json \ + file:hello-world_v1-manifest.json +``` + +The formatted x509 signature: `hello-world.signature.config.json` is: + +``` json +{ + "signed": { + "_type": "Targets", + "delegations": { + "keys": {}, + "roles": [] + }, + "expires": "2021-08-16T15:57:23.4190247Z", + "targets": { + "registry.acme-rockets.io/hello-world:v1": { + "custom": { + "accessedAt": "2020-08-20T09:57:23.4189973Z", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json" + }, + "hashes": { + "sha256": "JKdJAKTnSe8xKV5aq95wk+MkSxGVgr1uZLGojHHEENA=" + }, + "length": 3056 + } + }, + "version": 1 + }, + "signatures": [ + { + "keyid": "cd0e842b294d61bb73a03260b4228069a5c3d6af86842360cc53c35e58d90868", + "method": "rsapss", + "sig": "aJ9iR72Mgej55Ds2t5nFAvcHnSB//tefp7eXvmFWTTQYklmwZ4oPrgii5DhA+rNNDPKAA7z1y861+b5MhbET8Fd4tmQSsoay5lBHVrt2MtpEMYgXD/sqlrRWvQmgvLn+ibWfhfW1MkqOPQ14Y4iz8JIC4UK+c1xc5KWbykVGgyTHM0/JEe5rq/iVwq6rhurpn1rGjV4mbCoFLYMhpKVWuguu2Pj73ertJ+VxCCacNgbsC2DOJIg277lbZB9e/YmWufy5ZTKbc9ECwF4uLRsx2qVHXei3eop3nFpGzTt1cWRTLkKllpdW9mEnpp1GxFHAE1UYXP3E7xdVAL8VZQWIMA==", + "x5c": [ + "MIID4DCCAsigAwIBAgIUJVfYMmyHvQ9u0TbluizZ3BtgATEwDQYJKoZIhvcNAQELBQAwbTEhMB8GA1UEAwwYcmVnaXN0cnkuYWNtZS1yb2NrZXRzLmlvMRQwEgYDVQQKDAtleGFtcGxlIGluYzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUwHhcNMjAwODIwMDI0MDU2WhcNMjEwODIwMDI0MDU2WjBtMSEwHwYDVQQDDBhyZWdpc3RyeS5hY21lLXJvY2tldHMuaW8xFDASBgNVBAoMC2V4YW1wbGUgaW5jMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN4qEOEICopQ9t1BSrh4Iu3f4LRMWI0p+P9Js92nhCQusn3wcEIddtb3J2eSy0d1MwTykXEMvSp3OU3+yJIm7BW3ZrCD+UjU+Je8JcDCXKZpKesLb8i42UAk7J35zWE/nRs9uzGQWelZTCJHBM1NVSnP4QqcGF2VkNwtsti7NbL4f+AunBMZvrK4p3kUwh92FoDgXen6+vrnqHb3MA8uJBa5pVsPOvcga13TWaYRfMm/nxM6xGvIwly2QpWfdJrC58aqEiRQAfUtfuD/MTYEQ0PL/fOpHJw/FcHLt0vkja1ggMexATinhvOPMhcJbL2JPliS1ExFtCjx2D+mtHjt9rECAwEAAaN4MHYwHQYDVR0OBBYEFME2lTAI0lfw6/2/SNNSydlMqWNzMB8GA1UdIwQYMBaAFME2lTAI0lfw6/2/SNNSydlMqWNzMA8GA1UdEwEB/wQFMAMBAf8wIwYDVR0RBBwwGoIYcmVnaXN0cnkuYWNtZS1yb2NrZXRzLmlvMA0GCSqGSIb3DQEBCwUAA4IBAQB6HIusgEwUaQxcO9jm+iX4rgnGeDYT9qNdxtSL/tz7zzPTx2gPDSEzZinhFO+0AnH0kleiaMfJXFedvpX8xofP0zWNDgXAabZa1JR9HV42OmxMg/gBm3lpSQtnNoraqy6N88ot9xpRA0FQ/gAnGdRakrK7oeljDNpz3ay6ZgBqz3MpYIKkHL6dpvmQ4BbGEHfjLX1j/bC397XzapOcFqqhekc3Nk7vH51TheqQRIW1nI4BRo/guf6zjfxGcskTM4winCd/fk0F7XMlOddWleeg1vI+i/1TKV0p03aN23JZNUAt7MeZlz4nieaIGGFNinwOEsIRIXAZ65IMZwaLsCgY" + ] + } + ] +} +``` + +If the embedded cert chain `x5c` is not desired, which is strongly not recommended, it can be removed by omitting the `-c` option. In this case, it is exactly a TUF metadata for a delegation role. Since the certificate is not provided, the key ID is purely calculated by the public key information extracted from the provided private key. + +```shell +nv2 tuf sign \ + -k key.key \ + -r registry.acme-rockets.io/hello-world:v1 \ + -e 8670h \ + -o hello-world.signature.config.json \ + file:hello-world_v1-manifest.json +``` + +The formatted x509, without the `x5c` chain signature: `hello-world.signature.config.json` is: + + +```json +{ + "signed": { + "_type": "Targets", + "delegations": { + "keys": {}, + "roles": [] + }, + "expires": "2021-08-16T16:00:10.3222917Z", + "targets": { + "registry.acme-rockets.io/hello-world:v1": { + "custom": { + "accessedAt": "2020-08-20T10:00:10.3222657Z", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json" + }, + "hashes": { + "sha256": "JKdJAKTnSe8xKV5aq95wk+MkSxGVgr1uZLGojHHEENA=" + }, + "length": 3056 + } + }, + "version": 1 + }, + "signatures": [ + { + "keyid": "b18f65d69cfce50b03fe92f159726aa48f1c75b62e23f53b4ac4fda9d0739c10", + "method": "rsapss", + "sig": "tjrMGfrz0TaZWWsvTwbwKGQRHCdUpuS+CfP7TJPxh7oRrMgh3I74MGSy3w8S2DsAoMGipWBQ1vcWxCGPO49k8+ND3T10GvU66J7fkkSbesamiG3WCfZBR+6mNUr5fzu6gNAHop9twaSstnOPtXgTW5gLcf7geD4XHTCkzYgY7ejoFUiFUD+oBQCdIS7gj7rEgT2Wi0mYWjrt7tbsoDf26nDODnOmUhxrgBJ1iYqn8h8ZWG01iXmrRl7txUC4DINrxPQZQh8o7AP7R8M6hcDW12B+acWxt2Smb16FxhhUTNmZ3nSRL6Uyz9NqgtUftmdd4KjHYGOiKIez7ktSoITocQ==" + } + ] +} +``` + +It is also possible to add targets, refresh the expiry time, and auto increment the version number by providing the previous state. + +```shell +nv2 tuf sign \ + -k key.key \ + -c cert.crt \ + -r registry.acme-rockets.io/hello-world:latest \ + -e 8670h \ + -f hello-world.signature.config.json \ + -o hello-world.signature.config.json \ + file:hello-world_v1-manifest.json +``` + +The formatted x509 signature: `hello-world.signature.config.json` is: + +``` json +{ + "signed": { + "_type": "Targets", + "delegations": { + "keys": {}, + "roles": [] + }, + "expires": "2021-08-16T16:06:04.8466197Z", + "targets": { + "registry.acme-rockets.io/hello-world:latest": { + "custom": { + "accessedAt": "2020-08-20T10:06:04.8460942Z", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json" + }, + "hashes": { + "sha256": "JKdJAKTnSe8xKV5aq95wk+MkSxGVgr1uZLGojHHEENA=" + }, + "length": 3056 + }, + "registry.acme-rockets.io/hello-world:v1": { + "custom": { + "accessedAt": "2020-08-20T10:06:02.3743791Z", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json" + }, + "hashes": { + "sha256": "JKdJAKTnSe8xKV5aq95wk+MkSxGVgr1uZLGojHHEENA=" + }, + "length": 3056 + } + }, + "version": 2 + }, + "signatures": [ + { + "keyid": "cd0e842b294d61bb73a03260b4228069a5c3d6af86842360cc53c35e58d90868", + "method": "rsapss", + "sig": "juUQtXgu9fFdEXk20mOk7XtfC4bniPt6EmoEFe5w1shBFmZLtsxeXRR9nW9+n2jmF1zTT/wk50Y9q14+nc3DA0EKlAnvW+ulUOfWSqEN6t7K6wHesJlkK3uTKa/TMW3pGTwtdMFUU823H7eJKxFR8n4lINuAGmakUk/Cr766dRdvOv85FepFwvM0ZwuwZ3aaDNGOUtwWjHLTgCNwljDym1sQ4sc96N8igQ+lG7wb0WNGMhZzTGPBDypr4lDO03A8s4AcD13j/uCUHWG0cVN3TBgPEZmqrmJLJ7iCsCO4ieJQLiGZKVee3QOBi1F0JgbNQ1LEeHtNegikDkPa2GOxgw==", + "x5c": [ + "MIID4DCCAsigAwIBAgIUJVfYMmyHvQ9u0TbluizZ3BtgATEwDQYJKoZIhvcNAQELBQAwbTEhMB8GA1UEAwwYcmVnaXN0cnkuYWNtZS1yb2NrZXRzLmlvMRQwEgYDVQQKDAtleGFtcGxlIGluYzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUwHhcNMjAwODIwMDI0MDU2WhcNMjEwODIwMDI0MDU2WjBtMSEwHwYDVQQDDBhyZWdpc3RyeS5hY21lLXJvY2tldHMuaW8xFDASBgNVBAoMC2V4YW1wbGUgaW5jMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN4qEOEICopQ9t1BSrh4Iu3f4LRMWI0p+P9Js92nhCQusn3wcEIddtb3J2eSy0d1MwTykXEMvSp3OU3+yJIm7BW3ZrCD+UjU+Je8JcDCXKZpKesLb8i42UAk7J35zWE/nRs9uzGQWelZTCJHBM1NVSnP4QqcGF2VkNwtsti7NbL4f+AunBMZvrK4p3kUwh92FoDgXen6+vrnqHb3MA8uJBa5pVsPOvcga13TWaYRfMm/nxM6xGvIwly2QpWfdJrC58aqEiRQAfUtfuD/MTYEQ0PL/fOpHJw/FcHLt0vkja1ggMexATinhvOPMhcJbL2JPliS1ExFtCjx2D+mtHjt9rECAwEAAaN4MHYwHQYDVR0OBBYEFME2lTAI0lfw6/2/SNNSydlMqWNzMB8GA1UdIwQYMBaAFME2lTAI0lfw6/2/SNNSydlMqWNzMA8GA1UdEwEB/wQFMAMBAf8wIwYDVR0RBBwwGoIYcmVnaXN0cnkuYWNtZS1yb2NrZXRzLmlvMA0GCSqGSIb3DQEBCwUAA4IBAQB6HIusgEwUaQxcO9jm+iX4rgnGeDYT9qNdxtSL/tz7zzPTx2gPDSEzZinhFO+0AnH0kleiaMfJXFedvpX8xofP0zWNDgXAabZa1JR9HV42OmxMg/gBm3lpSQtnNoraqy6N88ot9xpRA0FQ/gAnGdRakrK7oeljDNpz3ay6ZgBqz3MpYIKkHL6dpvmQ4BbGEHfjLX1j/bC397XzapOcFqqhekc3Nk7vH51TheqQRIW1nI4BRo/guf6zjfxGcskTM4winCd/fk0F7XMlOddWleeg1vI+i/1TKV0p03aN23JZNUAt7MeZlz4nieaIGGFNinwOEsIRIXAZ65IMZwaLsCgY" + ] + } + ] +} +``` + +### Offline Verification + +Offline verification without TUF for update protection can be accomplished with the `nv2 tuf verify` command. + +```shell +NAME: + nv2 tuf verify - verifies OCI Artifacts + +USAGE: + nv2 tuf verify [command options] [] + +OPTIONS: + --signature value, -s value, -f value signature file + --cert value, -c value certs for verification + --ca-cert value CA certs for verification + --min-version value, -m value min version of the signature (default: 0) + --reference value, -r value original reference + --media-type value specify the media type of the manifest read from file or stdin (default: "application/vnd.docker.distribution.manifest.v2+json") + --username value, -u value username for generic remote access + --password value, -p value password for generic remote access + --insecure enable insecure remote access (default: false) + --help, -h show help (default: false) +``` + +To verify a manifest `hello-world_v1-manifest.json` with a signature file `hello-world.signature.config.json`, run + +```shell +nv2 tuf verify \ + -f hello-world.signature.config.json \ + -c cert.crt \ + file:hello-world_v1-manifest.json +``` + +Since the manifest was signed by a self-signed certificate, that certificate `cert.crt` is required to be provided to `nv2`. + +If the cert isn't self-signed, you can omit the `-c` parameter. + +``` shell +nv2 tuf verify \ + -f hello-world.signature.config.json \ + file:hello-world_v1-manifest.json + +sha256:3351c53952446db17d21b86cfe5829ae70f823aff5d410fbf09dff820a39ab55 +``` + +On successful verification, the `sha256` digest of the manifest is printed. Otherwise, `nv2` prints error messages and returns non-zero values. + +### Working with TUF + +**NOTE** This section is for information only and might not be implemented. + +To make the TUF-formatted signature working in a trust collection, a delegation role should be created with the certificate / public key associated with the signing key. This operation can be done using the Docker Notary CLI: + +```shell +notary delegation add $gun targets/example example.crt --paths "" --publish +``` + +Once the delegation role is registered, the TUF-formatted signature can be uploaded to the remote notary server with server-managed snapshot role: + +```shell +curl -H "Authorization: bearer $token" \ + -F 'upload=@"";filename=targets/example.json' \ + "https://$notaryserver/v2/$gun/_trust/tuf/" +``` + +Later, consumers should be able to verify the TUF trust collection as normal. After the TUF verification against the TUF imbedded PKI is done, the consumers should verify the signature again with `nv2 tuf verify` against the contemporary PKI. + + + +[1]: https://doi.org/10.1145/1866307.1866315 "Survivable key compromise in software update systems" +[2]: https://dl.acm.org/doi/10.5555/2930611.2930648 "Diplomat: using delegations to protect community repositories" +[3]: https://dl.acm.org/doi/10.5555/3154690.3154754 "Mercury: bandwidth-effective prevention of rollback attacks against community repositories" +[4]: https://theupdateframework.io/ "The Update Framework" +[5]: https://github.com/theupdateframework/notary "The Notary Project" +[6]: https://docs.docker.com/engine/security/trust/content_trust/ "Docker Content Trust" +[7]: https://docs.microsoft.com/en-us/azure/container-registry/container-registry-content-trust "Content trust in Azure Container Registry" +[8]: https://github.com/cnabio/cnab-spec/blob/cnab-security-1.0.0/300-CNAB-security.md "Cloud Native Application Bundles Security (CNAB-Sec) 1.0.0 GA" +[9]: https://pypi.org/ "PyPI" +[10]: https://www.python.org/dev/peps/pep-0458/ "PEP 458 -- Secure PyPI downloads with signed repository metadata" +[11]: https://www.python.org/dev/peps/pep-0480/ "PEP 480 -- Surviving a Compromise of PyPI: The Maximum Security Model" +[12]: https://github.com/cnabio/cnab-spec/blob/cnab-security-1.0.0/805-airgap.md#cnab-security "CNAB Security in Disconnected Scenarios" diff --git a/go.mod b/go.mod index 745083dc8..288f3dcf4 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,12 @@ module github.com/notaryproject/nv2 -go 1.14 +go 1.15 require ( + github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.0.1 + github.com/theupdateframework/notary v0.6.2-0.20200804143915-84287fd8df4f github.com/urfave/cli/v2 v2.2.0 ) diff --git a/go.sum b/go.sum index 83b110164..8d92e37ce 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,127 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= +github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= +github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= +github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= +github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/theupdateframework/notary v0.6.2-0.20200804143915-84287fd8df4f h1:myKguilK7Xy8V5sjfJ8CYn2cD/aRlDFO4hzkqZ9HhgQ= +github.com/theupdateframework/notary v0.6.2-0.20200804143915-84287fd8df4f/go.mod h1:VmySTua0RaZOe78Zx4/i3bCl9eNs0UvBOPV+1ps9t6U= github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/reference/descriptor.go b/pkg/reference/descriptor.go new file mode 100644 index 000000000..d4f0ae8fd --- /dev/null +++ b/pkg/reference/descriptor.go @@ -0,0 +1,10 @@ +package reference + +import "github.com/opencontainers/go-digest" + +// Descriptor describes the basic information of the target content +type Descriptor struct { + MediaType string `json:"mediaType,omitempty"` + Digests []digest.Digest `json:"digests"` + Size int64 `json:"size"` +} diff --git a/pkg/reference/manifest.go b/pkg/reference/manifest.go new file mode 100644 index 000000000..5d58385e4 --- /dev/null +++ b/pkg/reference/manifest.go @@ -0,0 +1,10 @@ +package reference + +import "time" + +// Manifest to be signed +type Manifest struct { + Descriptor + Name string `json:"name,omitempty"` + AccessedAt time.Time `json:"accessedAt,omitempty"` +} diff --git a/pkg/registry/manifest.go b/pkg/registry/manifest.go index 694d73f1f..0feee497d 100644 --- a/pkg/registry/manifest.go +++ b/pkg/registry/manifest.go @@ -6,26 +6,28 @@ import ( "net/url" "strconv" "strings" + "time" - "github.com/notaryproject/nv2/pkg/signature" + "github.com/notaryproject/nv2/pkg/reference" + "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" ) // GetManifestMetadata returns signature manifest information by URI scheme -func (c *Client) GetManifestMetadata(uri *url.URL) (signature.Manifest, error) { +func (c *Client) GetManifestMetadata(uri *url.URL) (*reference.Manifest, error) { switch scheme := strings.ToLower(uri.Scheme); scheme { case "docker": return c.GetDockerManifestMetadata(uri) case "oci": return c.GetOCIManifestMetadata(uri) default: - return signature.Manifest{}, fmt.Errorf("unsupported scheme: %s", scheme) + return nil, fmt.Errorf("unsupported scheme: %s", scheme) } } // GetDockerManifestMetadata returns signature manifest information // from a remote Docker manifest -func (c *Client) GetDockerManifestMetadata(uri *url.URL) (signature.Manifest, error) { +func (c *Client) GetDockerManifestMetadata(uri *url.URL) (*reference.Manifest, error) { return c.getManifestMetadata(uri, MediaTypeManifestList, MediaTypeManifest, @@ -34,7 +36,7 @@ func (c *Client) GetDockerManifestMetadata(uri *url.URL) (signature.Manifest, er // GetOCIManifestMetadata returns signature manifest information // from a remote OCI manifest -func (c *Client) GetOCIManifestMetadata(uri *url.URL) (signature.Manifest, error) { +func (c *Client) GetOCIManifestMetadata(uri *url.URL) (*reference.Manifest, error) { return c.getManifestMetadata(uri, v1.MediaTypeImageIndex, v1.MediaTypeImageManifest, @@ -42,23 +44,25 @@ func (c *Client) GetOCIManifestMetadata(uri *url.URL) (signature.Manifest, error } // GetManifestMetadata returns signature manifest information -func (c *Client) getManifestMetadata(uri *url.URL, mediaTypes ...string) (signature.Manifest, error) { +func (c *Client) getManifestMetadata(uri *url.URL, mediaTypes ...string) (*reference.Manifest, error) { + name := uri.Host + uri.Path host := uri.Host if host == "docker.io" { host = "registry-1.docker.io" } var repository string - var reference string + var manifestReference string path := strings.TrimPrefix(uri.Path, "/") if index := strings.Index(path, "@"); index != -1 { repository = path[:index] - reference = path[index+1:] + manifestReference = path[index+1:] } else if index := strings.Index(path, ":"); index != -1 { repository = path[:index] - reference = path[index+1:] + manifestReference = path[index+1:] } else { repository = path - reference = "latest" + manifestReference = "latest" + name += ":latest" } scheme := "https" if c.insecure { @@ -68,11 +72,11 @@ func (c *Client) getManifestMetadata(uri *url.URL, mediaTypes ...string) (signat scheme, host, repository, - reference, + manifestReference, ) req, err := http.NewRequest(http.MethodHead, url, nil) if err != nil { - return signature.Manifest{}, fmt.Errorf("invalid uri: %v", uri) + return nil, fmt.Errorf("invalid uri: %v", uri) } req.Header.Set("Connection", "close") for _, mediaType := range mediaTypes { @@ -81,40 +85,46 @@ func (c *Client) getManifestMetadata(uri *url.URL, mediaTypes ...string) (signat resp, err := c.base.RoundTrip(req) if err != nil { - return signature.Manifest{}, fmt.Errorf("%v: %v", url, err) + return nil, fmt.Errorf("%v: %v", url, err) } resp.Body.Close() switch resp.StatusCode { case http.StatusOK: // no op case http.StatusUnauthorized, http.StatusNotFound: - return signature.Manifest{}, fmt.Errorf("%v: %s", uri, resp.Status) + return nil, fmt.Errorf("%v: %s", uri, resp.Status) default: - return signature.Manifest{}, fmt.Errorf("%v: %s", url, resp.Status) + return nil, fmt.Errorf("%v: %s", url, resp.Status) } header := resp.Header mediaType := header.Get("Content-Type") if mediaType == "" { - return signature.Manifest{}, fmt.Errorf("%v: missing Content-Type", url) + return nil, fmt.Errorf("%v: missing Content-Type", url) } - digest := header.Get("Docker-Content-Digest") - if digest == "" { - return signature.Manifest{}, fmt.Errorf("%v: missing Docker-Content-Digest", url) + contentDigest := header.Get("Docker-Content-Digest") + if contentDigest == "" { + return nil, fmt.Errorf("%v: missing Docker-Content-Digest", url) + } + parsedDigest, err := digest.Parse(contentDigest) + if err != nil { + return nil, fmt.Errorf("%v: invalid Docker-Content-Digest: %s", url, contentDigest) } length := header.Get("Content-Length") if length == "" { - return signature.Manifest{}, fmt.Errorf("%v: missing Content-Length", url) + return nil, fmt.Errorf("%v: missing Content-Length", url) } size, err := strconv.ParseInt(length, 10, 64) if err != nil { - return signature.Manifest{}, fmt.Errorf("%v: invalid Content-Length", url) + return nil, fmt.Errorf("%v: invalid Content-Length", url) } - return signature.Manifest{ - Descriptor: signature.Descriptor{ + return &reference.Manifest{ + Descriptor: reference.Descriptor{ MediaType: mediaType, - Digest: digest, + Digests: []digest.Digest{parsedDigest}, Size: size, }, + Name: name, + AccessedAt: time.Now(), }, nil } diff --git a/pkg/signature/encoding.go b/pkg/signature/encoding.go index 325e8ad55..59aa6eb52 100644 --- a/pkg/signature/encoding.go +++ b/pkg/signature/encoding.go @@ -2,8 +2,9 @@ package signature import ( "encoding/base64" - "encoding/json" "fmt" + + "github.com/docker/go/canonical/json" ) // EncodeSegment JWT specific base64url encoding with padding stripped diff --git a/pkg/signature/model.go b/pkg/signature/model.go index 282c48ea7..80fee598e 100644 --- a/pkg/signature/model.go +++ b/pkg/signature/model.go @@ -1,5 +1,7 @@ package signature +import "github.com/notaryproject/nv2/pkg/reference" + // Header defines the signature header type Header struct { Raw []byte `json:"-"` @@ -26,3 +28,15 @@ type Descriptor struct { Digest string `json:"digest"` Size int64 `json:"size"` } + +// DescriptorFromReference converts descriptor from generic reference +func DescriptorFromReference(d reference.Descriptor) Descriptor { + result := Descriptor{ + MediaType: d.MediaType, + Size: d.Size, + } + if len(d.Digests) > 0 { + result.Digest = d.Digests[0].String() + } + return result +} diff --git a/pkg/signature/scheme.go b/pkg/signature/scheme.go index d531be54f..d4140bf50 100644 --- a/pkg/signature/scheme.go +++ b/pkg/signature/scheme.go @@ -1,10 +1,11 @@ package signature import ( - "encoding/json" "fmt" "strings" "time" + + "github.com/docker/go/canonical/json" ) // Scheme is a signature scheme @@ -33,7 +34,7 @@ func (s *Scheme) RegisterVerifier(verifier Verifier) { // Sign signs claims by a signer func (s *Scheme) Sign(signerID string, claims Claims) (string, error) { - bytes, err := json.Marshal(claims) + bytes, err := json.MarshalCanonical(claims) if err != nil { return "", err } diff --git a/pkg/signature/x509/signer.go b/pkg/signature/x509/signer.go index eb39882d0..25fe787be 100644 --- a/pkg/signature/x509/signer.go +++ b/pkg/signature/x509/signer.go @@ -3,13 +3,12 @@ package x509 import ( "crypto" "crypto/x509" - "encoding/json" "errors" "io" "strings" + "github.com/docker/go/canonical/json" "github.com/docker/libtrust" - cryptoutil "github.com/notaryproject/nv2/internal/crypto" "github.com/notaryproject/nv2/pkg/signature" ) @@ -23,7 +22,7 @@ type signer struct { // NewSignerFromFiles creates a signer from files func NewSignerFromFiles(keyPath, certPath string) (signature.Signer, error) { - key, err := cryptoutil.ReadPrivateKeyFile(keyPath) + key, err := ReadPrivateKeyFile(keyPath) if err != nil { return nil, err } @@ -31,7 +30,7 @@ func NewSignerFromFiles(keyPath, certPath string) (signature.Signer, error) { return NewSigner(key, nil) } - certs, err := cryptoutil.ReadCertificateFile(certPath) + certs, err := ReadCertificateFile(certPath) if err != nil { return nil, err } @@ -94,7 +93,7 @@ func (s *signer) Sign(claims string) (string, []byte, error) { } else { header.KeyID = s.keyID } - headerJSON, err := json.Marshal(header) + headerJSON, err := json.MarshalCanonical(header) if err != nil { return "", nil, err } diff --git a/internal/crypto/x509.go b/pkg/signature/x509/utils.go similarity index 94% rename from internal/crypto/x509.go rename to pkg/signature/x509/utils.go index e53173d06..bd03ac46e 100644 --- a/internal/crypto/x509.go +++ b/pkg/signature/x509/utils.go @@ -1,4 +1,4 @@ -package crypto +package x509 import ( "crypto/x509" diff --git a/pkg/signature/x509/verifier.go b/pkg/signature/x509/verifier.go index 97eb17c99..574a2059f 100644 --- a/pkg/signature/x509/verifier.go +++ b/pkg/signature/x509/verifier.go @@ -3,10 +3,10 @@ package x509 import ( "crypto" "crypto/x509" - "encoding/json" "errors" "strings" + "github.com/docker/go/canonical/json" "github.com/docker/libtrust" "github.com/notaryproject/nv2/pkg/signature" ) diff --git a/pkg/tuf/defaults.go b/pkg/tuf/defaults.go new file mode 100644 index 000000000..3a5c7d0cb --- /dev/null +++ b/pkg/tuf/defaults.go @@ -0,0 +1,8 @@ +package tuf + +import "time" + +// Default expiry +const ( + DefaultTargetExpiry = time.Hour * 24 * 365 // 1 year +) diff --git a/pkg/tuf/local/signer.go b/pkg/tuf/local/signer.go new file mode 100644 index 000000000..c33bc6480 --- /dev/null +++ b/pkg/tuf/local/signer.go @@ -0,0 +1,103 @@ +package local + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/x509" + "errors" + "io/ioutil" + + "github.com/notaryproject/nv2/pkg/tuf" + "github.com/theupdateframework/notary/tuf/data" + "github.com/theupdateframework/notary/tuf/utils" +) + +type signer struct { + key data.PrivateKey + keyID string + cert *x509.Certificate + rawCerts [][]byte + hash crypto.Hash +} + +// NewSignerFromFiles creates a signer from files +func NewSignerFromFiles(keyPath, certPath string) (tuf.Signer, error) { + keyPEM, err := ioutil.ReadFile(keyPath) + if err != nil { + return nil, err + } + key, err := utils.ParsePEMPrivateKey(keyPEM, "") + if err != nil { + return nil, err + } + if certPath == "" { + return NewSigner(key, nil) + } + + certs, err := utils.LoadCertBundleFromFile(certPath) + if err != nil { + return nil, err + } + return NewSigner(key, certs) +} + +// NewSigner creates a signer +func NewSigner(key data.PrivateKey, certs []*x509.Certificate) (tuf.Signer, error) { + s := &signer{ + key: key, + hash: crypto.SHA256, + } + if len(certs) == 0 { + s.keyID = key.ID() + return s, nil + } + + cert := certs[0] + publicKey := utils.CertToKey(cert) + if publicKey == nil { + return nil, errors.New("unknown certificate key type") + } + keyID, err := utils.CanonicalKeyID(publicKey) + if err != nil { + return nil, err + } + if keyID != s.key.ID() { + return nil, errors.New("key and certificate mismatch") + } + // Docker Notary 0.6.0 implementation uses non-canonical key ID for delegation roles, + // which should be canonical. + s.keyID = publicKey.ID() + s.cert = cert + + rawCerts := make([][]byte, 0, len(certs)) + for _, cert := range certs { + rawCerts = append(rawCerts, cert.Raw) + } + s.rawCerts = rawCerts + + return s, nil +} + +func (s *signer) Sign(_ context.Context, raw []byte) (tuf.Signature, error) { + if s.cert != nil { + if err := verifyReferences(raw, s.cert); err != nil { + return tuf.Signature{}, err + } + } + + sig, err := s.key.Sign(rand.Reader, raw, nil) + if err != nil { + return tuf.Signature{}, err + } + sigma := tuf.Signature{ + KeyID: s.keyID, + Method: s.key.SignatureAlgorithm(), + Signature: sig, + } + + if s.cert != nil { + sigma.X5c = s.rawCerts + } + return sigma, nil +} diff --git a/pkg/tuf/local/verifier.go b/pkg/tuf/local/verifier.go new file mode 100644 index 000000000..e6f2f64c2 --- /dev/null +++ b/pkg/tuf/local/verifier.go @@ -0,0 +1,155 @@ +package local + +import ( + "context" + "crypto/x509" + "errors" + "fmt" + "strings" + + "github.com/docker/go/canonical/json" + "github.com/notaryproject/nv2/pkg/tuf" + "github.com/theupdateframework/notary/tuf/data" + "github.com/theupdateframework/notary/tuf/signed" + "github.com/theupdateframework/notary/tuf/utils" +) + +type verifier struct { + keys map[string]data.PublicKey + certs map[string]*x509.Certificate + roots *x509.CertPool +} + +// NewVerifier creates a verifier +func NewVerifier(certs []*x509.Certificate, roots *x509.CertPool) (tuf.Verifier, error) { + if roots == nil { + if certs == nil { + pool, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + roots = pool + } else { + roots = x509.NewCertPool() + } + for _, cert := range certs { + roots.AddCert(cert) + } + } + + keys := make(map[string]data.PublicKey, len(certs)) + keyedCerts := make(map[string]*x509.Certificate, len(certs)) + for _, cert := range certs { + key := utils.CertToKey(cert) + if key == nil { + return nil, errors.New("unknown certificate key type") + } + keyID, err := utils.CanonicalKeyID(key) + if err != nil { + return nil, err + } + keys[keyID] = key + keyedCerts[keyID] = cert + } + + return &verifier{ + keys: keys, + certs: keyedCerts, + roots: roots, + }, nil +} + +func (v *verifier) Verify(ctx context.Context, content []byte, sig tuf.Signature) error { + key, cert, err := v.getVerificationKeyPair(sig) + if err != nil { + return err + } + alg, ok := signed.Verifiers[sig.Method] + if !ok { + return fmt.Errorf("signing method is not supported: %s", sig.Method) + } + if err := alg.Verify(key, sig.Signature, content); err != nil { + return err + } + return verifyReferences(content, cert) +} + +func (v *verifier) getVerificationKeyPair(sig tuf.Signature) (data.PublicKey, *x509.Certificate, error) { + if len(sig.X5c) > 0 { + return v.getVerificationKeyPairFromX5c(sig.KeyID, sig.X5c) + } + return v.getVerificationKeyPairFromKeyID(sig.KeyID) +} + +func (v *verifier) getVerificationKeyPairFromKeyID(keyID string) (data.PublicKey, *x509.Certificate, error) { + key, found := v.keys[keyID] + if !found { + return nil, nil, errors.New("key not found: " + keyID) + } + cert, found := v.certs[keyID] + if !found { + return nil, nil, errors.New("cert not found: " + keyID) + } + return key, cert, nil +} + +func (v *verifier) getVerificationKeyPairFromX5c(claimedKeyID string, x5c [][]byte) (data.PublicKey, *x509.Certificate, error) { + certs := make([]*x509.Certificate, 0, len(x5c)) + for _, certBytes := range x5c { + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + return nil, nil, err + } + certs = append(certs, cert) + } + + intermediates := x509.NewCertPool() + for _, cert := range certs[1:] { + intermediates.AddCert(cert) + } + + cert := certs[0] + if _, err := cert.Verify(x509.VerifyOptions{ + Intermediates: intermediates, + Roots: v.roots, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + }); err != nil { + return nil, nil, err + } + + key := utils.CertToKey(cert) + if key == nil { + return nil, nil, errors.New("unknown certificate key type") + } + // Docker Notary 0.6.0 implementation uses non-canonical key ID for delegation roles, + // which should be canonical. + keyID := key.ID() + if keyID != claimedKeyID { + return nil, nil, errors.New("certificate key ID mismatch") + } + + return key, cert, nil +} + +func verifyReferences(raw []byte, cert *x509.Certificate) error { + // Skip unrecognizable contents + var targets data.Targets + if err := json.Unmarshal(raw, &targets); err != nil { + return nil + } + if targets.Type != data.TUFTypes[data.CanonicalTargetsRole] { + return nil + } + + roots := x509.NewCertPool() + roots.AddCert(cert) + for reference := range targets.Targets { + if _, err := cert.Verify(x509.VerifyOptions{ + DNSName: strings.SplitN(reference, "/", 2)[0], + Roots: roots, + }); err != nil { + return err + } + } + return nil +} diff --git a/pkg/tuf/signature.go b/pkg/tuf/signature.go new file mode 100644 index 000000000..15f54267e --- /dev/null +++ b/pkg/tuf/signature.go @@ -0,0 +1,54 @@ +package tuf + +import ( + "github.com/docker/go/canonical/json" + "github.com/theupdateframework/notary/tuf/data" +) + +// Signature is a signature on a piece of metadata +type Signature struct { + KeyID string `json:"keyid"` + Method data.SigAlgorithm `json:"method"` + Signature []byte `json:"sig"` + X5c [][]byte `json:"x5c,omitempty"` +} + +// Signed is the high level, partially deserialized metadata object +// used to verify signatures before fully unpacking, or to add signatures +// before fully packing +type Signed struct { + Signed *json.RawMessage `json:"signed"` + Signatures []Signature `json:"signatures"` +} + +// ToTUF converts signed to TUF +func (s Signed) ToTUF() *data.Signed { + signatures := make([]data.Signature, 0, len(s.Signatures)) + for _, s := range s.Signatures { + signatures = append(signatures, data.Signature{ + KeyID: s.KeyID, + Method: s.Method, + Signature: s.Signature, + }) + } + return &data.Signed{ + Signed: s.Signed, + Signatures: signatures, + } +} + +// SignedFromTUF converts signed from TUF +func SignedFromTUF(signed *data.Signed) *Signed { + signatures := make([]Signature, 0, len(signed.Signatures)) + for _, s := range signed.Signatures { + signatures = append(signatures, Signature{ + KeyID: s.KeyID, + Method: s.Method, + Signature: s.Signature, + }) + } + return &Signed{ + Signed: signed.Signed, + Signatures: signatures, + } +} diff --git a/pkg/tuf/signing.go b/pkg/tuf/signing.go new file mode 100644 index 000000000..a5082326f --- /dev/null +++ b/pkg/tuf/signing.go @@ -0,0 +1,45 @@ +package tuf + +import ( + "context" + "errors" +) + +// Signer (possibly remote) signs content +type Signer interface { + Sign(ctx context.Context, content []byte) (Signature, error) +} + +// Verifier (possibly remote) verifies content +type Verifier interface { + Verify(ctx context.Context, content []byte, signature Signature) error +} + +// Sign signs TUF metadata and appends the signature +func Sign(ctx context.Context, signer Signer, signed *Signed) error { + sig, err := signer.Sign(ctx, *signed.Signed) + if err != nil { + return err + } + signed.Signatures = append(signed.Signatures, sig) + return nil +} + +// Verify verifies TUF metadata. Returns the number of valid signatures +func Verify(ctx context.Context, verifier Verifier, signed *Signed) (int, error) { + var err error + valid := 0 + for _, s := range signed.Signatures { + err = verifier.Verify(ctx, *signed.Signed, s) + if err == nil { + valid++ + } + } + if valid == 0 { + if err != nil { + return 0, err + } + return 0, errors.New("no valid signature") + } + return valid, nil +} diff --git a/pkg/tuf/targets.go b/pkg/tuf/targets.go new file mode 100644 index 000000000..eac6c21c5 --- /dev/null +++ b/pkg/tuf/targets.go @@ -0,0 +1,159 @@ +package tuf + +import ( + "context" + "encoding/hex" + "time" + + "github.com/docker/go/canonical/json" + "github.com/notaryproject/nv2/pkg/reference" + "github.com/theupdateframework/notary/tuf/data" + "github.com/theupdateframework/notary/tuf/signed" +) + +// Target represents a TUF target with name +type Target struct { + data.FileMeta + Name string `json:"name"` +} + +// TargetMetadata describes the target content with extra information +type TargetMetadata struct { + AccessedAt time.Time `json:"accessedAt,omitempty"` + MediaType string `json:"mediaType,omitempty"` +} + +// NewTarget is a helper method that returns a Target +func NewTarget(manifest *reference.Manifest) (*Target, error) { + metadata := TargetMetadata{ + AccessedAt: manifest.AccessedAt, + MediaType: manifest.MediaType, + } + metadataJSON, err := json.MarshalCanonical(metadata) + if err != nil { + return nil, err + } + tartgetCustom := new(json.RawMessage) + if err := tartgetCustom.UnmarshalJSON(metadataJSON); err != nil { + return nil, err + } + + hashes := make(data.Hashes) + for _, digest := range manifest.Digests { + alg := digest.Algorithm().String() + hash, err := hex.DecodeString(digest.Encoded()) + if err != nil { + return nil, err + } + hashes[alg] = hash + } + + return &Target{ + FileMeta: data.FileMeta{ + Hashes: hashes, + Length: manifest.Size, + Custom: tartgetCustom, + }, + Name: manifest.Name, + }, nil +} + +// AddTargets adds targets to the existing targets. +func AddTargets(base *data.Targets, targets ...*Target) *data.Targets { + if base == nil { + base = &data.NewTargets().Signed + } + + for _, target := range targets { + base.Targets[target.Name] = target.FileMeta + } + base.Expires = time.Now().UTC().Add(DefaultTargetExpiry) + base.Version++ + return base +} + +// SignTargets signs the targets +func SignTargets(ctx context.Context, signer Signer, targets *data.Targets) (*Signed, error) { + signedTargets := data.SignedTargets{ + Signed: *targets, + } + tufSigned, err := signedTargets.ToSigned() + if err != nil { + return nil, err + } + signed := SignedFromTUF(tufSigned) + err = Sign(ctx, signer, signed) + if err != nil { + return nil, err + } + return signed, nil +} + +// VerifyTargets verifies the targets +func VerifyTargets(ctx context.Context, verifier Verifier, signedContent *Signed, minVersion int) (*data.Targets, error) { + _, err := Verify(ctx, verifier, signedContent) + if err != nil { + return nil, err + } + + signedTargets, err := data.TargetsFromSigned(signedContent.ToTUF(), data.CanonicalTargetsRole) + if err != nil { + return nil, err + } + targets := signedTargets.Signed + + if err := signed.VerifyExpiry(&targets.SignedCommon, data.CanonicalTargetsRole); err != nil { + return nil, err + } + if err := signed.VerifyVersion(&targets.SignedCommon, minVersion); err != nil { + return nil, err + } + return &targets, nil +} + +// IsManifestInTargets checks if a manifest is referenced by the targets +func IsManifestInTargets(manifest *reference.Manifest, targets *data.Targets) bool { + if name := manifest.Name; name != "" { + target, ok := targets.Targets[name] + return ok && ManifestMatchesTarget(manifest, &target) + } + + for _, target := range targets.Targets { + if found := ManifestMatchesTarget(manifest, &target); found { + return true + } + } + return false +} + +// ManifestMatchesTarget checks if a manifest is referenced by the specified target +func ManifestMatchesTarget(manifest *reference.Manifest, target *data.FileMeta) bool { + if manifest.Size != target.Length { + return false + } + + found := false + for _, digest := range manifest.Digests { + alg := digest.Algorithm().String() + hash, ok := target.Hashes[alg] + if !ok { + continue + } + if hex.EncodeToString(hash) == digest.Encoded() { + found = true + break + } + } + if !found { + return false + } + + if target.Custom == nil { + return false + } + var metadata TargetMetadata + if err := json.Unmarshal(*target.Custom, &metadata); err != nil { + return false + } + return metadata.MediaType == manifest.MediaType +} From 3dc762312e9fd4f0cc223c3ae4160239ef0cb5af Mon Sep 17 00:00:00 2001 From: Shiwei Zhang Date: Tue, 1 Sep 2020 20:21:08 +0800 Subject: [PATCH 2/2] Fix bug for registry names with ports Signed-off-by: Shiwei Zhang --- cmd/nv2/main.go | 2 +- pkg/signature/scheme.go | 2 +- pkg/signature/x509/verifier.go | 4 +++- pkg/tuf/local/verifier.go | 4 +++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cmd/nv2/main.go b/cmd/nv2/main.go index 09a317d74..ecff19177 100644 --- a/cmd/nv2/main.go +++ b/cmd/nv2/main.go @@ -13,7 +13,7 @@ func main() { app := &cli.App{ Name: "nv2", Usage: "Notary V2 - Prototype", - Version: "0.3.1", + Version: "0.3.2", Authors: []*cli.Author{ { Name: "Shiwei Zhang", diff --git a/pkg/signature/scheme.go b/pkg/signature/scheme.go index d4140bf50..055150f0c 100644 --- a/pkg/signature/scheme.go +++ b/pkg/signature/scheme.go @@ -50,7 +50,7 @@ func (s *Scheme) SignRaw(signerID string, content []byte) (string, error) { signed, sig, err := signer.Sign(EncodeSegment(content)) if err != nil { - return "", nil + return "", err } return strings.Join([]string{ diff --git a/pkg/signature/x509/verifier.go b/pkg/signature/x509/verifier.go index 574a2059f..1d49d1966 100644 --- a/pkg/signature/x509/verifier.go +++ b/pkg/signature/x509/verifier.go @@ -144,8 +144,10 @@ func verifyReferences(seg string, cert *x509.Certificate) error { roots := x509.NewCertPool() roots.AddCert(cert) for _, reference := range claims.Manifest.References { + domain := strings.SplitN(reference, "/", 2)[0] + domain = strings.SplitN(domain, ":", 2)[0] if _, err := cert.Verify(x509.VerifyOptions{ - DNSName: strings.SplitN(reference, "/", 2)[0], + DNSName: domain, Roots: roots, }); err != nil { return err diff --git a/pkg/tuf/local/verifier.go b/pkg/tuf/local/verifier.go index e6f2f64c2..a2cac441c 100644 --- a/pkg/tuf/local/verifier.go +++ b/pkg/tuf/local/verifier.go @@ -144,8 +144,10 @@ func verifyReferences(raw []byte, cert *x509.Certificate) error { roots := x509.NewCertPool() roots.AddCert(cert) for reference := range targets.Targets { + domain := strings.SplitN(reference, "/", 2)[0] + domain = strings.SplitN(domain, ":", 2)[0] if _, err := cert.Verify(x509.VerifyOptions{ - DNSName: strings.SplitN(reference, "/", 2)[0], + DNSName: domain, Roots: roots, }); err != nil { return err