From efe151ddf6a7fd3848fea340cab7553d0a7d295b Mon Sep 17 00:00:00 2001 From: Shiwei Zhang Date: Fri, 31 Jul 2020 13:36:04 +0800 Subject: [PATCH 1/2] Initial prototype This is a joint commit of - Shiwei Zhang - Steve Lasker - Aviral Takkar Signed-off-by: Shiwei Zhang --- README.md | 110 +++++++- cmd/nv2/common.go | 20 ++ cmd/nv2/main.go | 29 +++ cmd/nv2/manifest.go | 68 +++++ cmd/nv2/sign.go | 137 ++++++++++ cmd/nv2/verify.go | 122 +++++++++ docs/artifact/README.md | 22 ++ docs/artifact/examples/manifest.json | 9 + docs/nv2/README.md | 293 ++++++++++++++++++++++ docs/signature/README.md | 225 +++++++++++++++++ docs/signature/examples/x509_kid.nv2.json | 1 + docs/signature/examples/x509_x5c.nv2.json | 1 + docs/signature/schema.json | 78 ++++++ go.mod | 11 + go.sum | 26 ++ internal/crypto/x509.go | 51 ++++ media/acme-rockets-cert.png | Bin 0 -> 17157 bytes media/example-cert.png | Bin 0 -> 18030 bytes media/notary-e2e-scenarios.png | Bin 0 -> 54561 bytes media/nv2-client-components.png | Bin 0 -> 26127 bytes pkg/registry/client.go | 32 +++ pkg/registry/docker.go | 7 + pkg/registry/manifest.go | 113 +++++++++ pkg/registry/transport.go | 110 ++++++++ pkg/signature/errors.go | 10 + pkg/signature/interface.go | 12 + pkg/signature/scheme.go | 90 +++++++ pkg/signature/signature.go | 35 +++ pkg/signature/util.go | 17 ++ pkg/signature/x509/signer.go | 92 +++++++ pkg/signature/x509/type.go | 4 + pkg/signature/x509/verifier.go | 146 +++++++++++ 32 files changed, 1870 insertions(+), 1 deletion(-) create mode 100644 cmd/nv2/common.go create mode 100644 cmd/nv2/main.go create mode 100644 cmd/nv2/manifest.go create mode 100644 cmd/nv2/sign.go create mode 100644 cmd/nv2/verify.go create mode 100644 docs/artifact/README.md create mode 100644 docs/artifact/examples/manifest.json create mode 100644 docs/nv2/README.md create mode 100644 docs/signature/README.md create mode 100644 docs/signature/examples/x509_kid.nv2.json create mode 100644 docs/signature/examples/x509_x5c.nv2.json create mode 100644 docs/signature/schema.json create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/crypto/x509.go create mode 100644 media/acme-rockets-cert.png create mode 100644 media/example-cert.png create mode 100644 media/notary-e2e-scenarios.png create mode 100644 media/nv2-client-components.png create mode 100644 pkg/registry/client.go create mode 100644 pkg/registry/docker.go create mode 100644 pkg/registry/manifest.go create mode 100644 pkg/registry/transport.go create mode 100644 pkg/signature/errors.go create mode 100644 pkg/signature/interface.go create mode 100644 pkg/signature/scheme.go create mode 100644 pkg/signature/signature.go create mode 100644 pkg/signature/util.go create mode 100644 pkg/signature/x509/signer.go create mode 100644 pkg/signature/x509/type.go create mode 100644 pkg/signature/x509/verifier.go diff --git a/README.md b/README.md index 996158e49..1eb1b9ddc 100644 --- a/README.md +++ b/README.md @@ -1 +1,109 @@ -# nv2 \ No newline at end of file +# Notary V2 (nv2) - Prototype + +nv2 is an incubation and prototype for the [Notary v2][notary-v2] efforts, securing artifacts stored in [distribution-spec][distribution-spec] based registries. +The `nv2` prototype covers the scenarios outlined in [notaryproject/requirements](https://github.com/notaryproject/requirements/blob/master/scenarios.md#scenarios). It also follows the [prototyping approach described here](https://github.com/stevelasker/nv2#prototyping-approach). + +![nv2-components](media/notary-e2e-scenarios.png) + +To enable the above workflow: + +- The nv2 client (1) will sign any OCI artifact type (2) (including a Docker Image, Helm Chart, OPA, SBoM or any OCI Artifact type), generating a Notary v2 signature (3) +- The [ORAS][oras] client (4) can then push the artifact (2) and the Notary v2 signature (3) to an OCI Artifacts supported registry (5) +- In a subsequent prototype, signatures may be retrieved from the OCI Artifacts supported registry (5) + +![nv2-components](media/nv2-client-components.png) + +## Table of Contents + +1. [Scenarios](#scenarios) +1. [nv2 signature spec](./docs/signature/README.md) +1. [nv2 signing and verification docs](docs/nv2/README.md) +1. [OCI Artifact schema for storing signatures](docs/artifact/README.md) +1. [nv2 prototype scope](#prototype-scope) + +## Scenarios + +The current implementation focuses on x509 cert based signatures. Using this approach, the digest and references block are signed, with the cert Common Name required to match the registry references. This enables both the public registry and private registry scenarios. + +### Public Registry + +Public registries generally have two cateogires of content: + +1. Public, certified content. This content is scanned, certified and signed by the registry that wishes to claim the content is "certified". It may be additionaly signed by the originating vendor. +2. Public, community driven content. Community content is a choice for the consumer to trust (downloading their key), or accept as un-trusted. + +#### End to End Experience + +The user works for ACME Rockets. They build `FROM` and use certified content from docker hub. +Their environemt is configured to only trust content from `docker.io` and `acme-rockets.io` + +#### Public Certified Content + +1. The user discovers some certified content they wish to acquire +1. The user copies the URI for the content, passing it to the docker cli + - `docker run docker.io/hello-world:latest` +1. The user already has the `docker.io` certificate, enabling all certified content from docker hub +1. The image runs, as verification passes + +#### Public non-certified content + +1. The user discovers some community content they wish to acquire, such as a new network-monitor project +1. The user copies the URI for the content, passing it to the docker cli + - `docker run docker.io/wabbit-networks/net-monitor:latest` +1. The image fails to run as the user has `trust-required` enabled, and doesn't have the wabbit-networks key.The docker cli produces an error with a url for acquiring the wabbit-networks key. + - The user can disable `trust-requried`, or acquire the required key. +1. The user acquires the wabbit-networks key, saves it in their local store +1. The user again runs: + - `docker run docker.io/wabbit-networks/net-monitor:latest` + and the image is sucessfully run + +### Key acquisition + +*TBD by the key-management working group* + +### Private Registry + +Private registries serve the follwing scenarios: + +- Host public content, ceritifed for use within an orgnization +- Host privately built content, containing the intellectual property of the orgnization. + + +![acme-rockets cert](./media/acme-rockets-cert.png) + +```json +{ + "signed": { + "exp": 1626938793, + "nbf": 1595402793, + "iat": 1595402793, + "digest": "sha256:3351c53952446db17d21b86cfe5829ae70f823aff5d410fbf09dff820a39ab55", + "size": 528, + "references": [ + "registry.acme-rockets.io/hello-world:latest", + "registry.acme-rockets.io/hello-world:v1.0" + ] + }, + "signature": { + "typ": "x509", + ... +``` + +## Prototype Scope + +- Client + - CLI experience + - Signing + - Verification + - Binaries plug-in + - Actual pull / push should be done by external binaries +- Server + - Access control + - HTTP API changes + - Registry storage changes + +Key management is offloaded to the underlying signing tools. + +[distribution-spec]: https://github.com/opencontainers/distribution-spec +[notary-v2]: http://github.com/notaryproject/ +[oras]: https://github.com/deislabs/oras diff --git a/cmd/nv2/common.go b/cmd/nv2/common.go new file mode 100644 index 000000000..f6752a827 --- /dev/null +++ b/cmd/nv2/common.go @@ -0,0 +1,20 @@ +package main + +import "github.com/urfave/cli/v2" + +var ( + usernameFlag = &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Usage: "username for generic remote access", + } + passwordFlag = &cli.StringFlag{ + Name: "password", + Aliases: []string{"p"}, + Usage: "password for generic remote access", + } + insecureFlag = &cli.BoolFlag{ + Name: "insecure", + Usage: "enable insecure remote access", + } +) diff --git a/cmd/nv2/main.go b/cmd/nv2/main.go new file mode 100644 index 000000000..828faca52 --- /dev/null +++ b/cmd/nv2/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Name: "nv2", + Usage: "Notary V2 - Prototype", + Version: "0.1.2", + Authors: []*cli.Author{ + { + Name: "Shiwei Zhang", + Email: "shizh@microsoft.com", + }, + }, + Commands: []*cli.Command{ + signCommand, + verifyCommand, + }, + } + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/nv2/manifest.go b/cmd/nv2/manifest.go new file mode 100644 index 000000000..9bd837c2f --- /dev/null +++ b/cmd/nv2/manifest.go @@ -0,0 +1,68 @@ +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) +} + +func getManifestFromReader(r io.Reader) (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{ + 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("username"), + Password: ctx.String("password"), + Insecure: ctx.Bool("insecure"), + }) + return remote.GetManifestMetadata(parsed) + default: + return signature.Manifest{}, fmt.Errorf("unsupported URI scheme: %s", parsed.Scheme) + } + return getManifestFromReader(r) +} diff --git a/cmd/nv2/sign.go b/cmd/nv2/sign.go new file mode 100644 index 000000000..9a04ffde2 --- /dev/null +++ b/cmd/nv2/sign.go @@ -0,0 +1,137 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "strings" + "time" + + "github.com/notaryproject/nv2/pkg/signature" + "github.com/notaryproject/nv2/pkg/signature/x509" + "github.com/urfave/cli/v2" +) + +const signerID = "nv2" + +var signCommand = &cli.Command{ + Name: "sign", + Usage: "signs artifacts or images", + ArgsUsage: "[]", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "method", + Aliases: []string{"m"}, + Usage: "signing method", + Required: true, + }, + &cli.StringFlag{ + Name: "key", + Aliases: []string{"k"}, + Usage: "signing key file [x509]", + TakesFile: true, + }, + &cli.StringFlag{ + Name: "cert", + Aliases: []string{"c"}, + 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, + }, + Action: runSign, +} + +func runSign(ctx *cli.Context) error { + // initialize + scheme, err := getSchemeForSigning(ctx) + if err != nil { + return err + } + + // core process + content, err := prepareContentForSigning(ctx) + if err != nil { + return err + } + sig, err := scheme.Sign(signerID, content) + if err != nil { + return err + } + sigma, err := signature.Pack(content, sig) + if err != nil { + return err + } + + // write out + sigmaJSON, err := json.Marshal(sigma) + if err != nil { + return err + } + path := ctx.String("output") + if path == "" { + path = strings.Split(content.Manifest.Digest, ":")[1] + ".nv2" + } + if err := ioutil.WriteFile(path, sigmaJSON, 0666); err != nil { + return err + } + + fmt.Println(content.Manifest.Digest) + return nil +} + +func prepareContentForSigning(ctx *cli.Context) (signature.Content, error) { + manifest, err := getManifestFromContext(ctx) + if err != nil { + return signature.Content{}, err + } + manifest.References = ctx.StringSlice("reference") + now := time.Now() + nowUnix := now.Unix() + content := signature.Content{ + Manifest: manifest, + IssuedAt: nowUnix, + } + if expiry := ctx.Duration("expiry"); expiry != 0 { + content.NotBefore = nowUnix + content.Expiration = now.Add(expiry).Unix() + } + + return content, nil +} + +func getSchemeForSigning(ctx *cli.Context) (*signature.Scheme, error) { + var ( + signer signature.Signer + err error + ) + switch method := ctx.String("method"); method { + case "x509": + signer, err = x509.NewSignerFromFiles(ctx.String("key"), ctx.String("cert")) + default: + return nil, fmt.Errorf("unsupported signing method: %s", method) + } + scheme := signature.NewScheme() + if err != nil { + return nil, err + } + scheme.RegisterSigner(signerID, signer) + return scheme, nil +} diff --git a/cmd/nv2/verify.go b/cmd/nv2/verify.go new file mode 100644 index 000000000..d2bcf4eca --- /dev/null +++ b/cmd/nv2/verify.go @@ -0,0 +1,122 @@ +package main + +import ( + "crypto/x509" + "encoding/json" + "fmt" + "os" + + "github.com/notaryproject/nv2/internal/crypto" + "github.com/notaryproject/nv2/pkg/signature" + x509nv2 "github.com/notaryproject/nv2/pkg/signature/x509" + "github.com/urfave/cli/v2" +) + +var verifyCommand = &cli.Command{ + Name: "verify", + Usage: "verifies artifacts or images", + 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 [x509]", + TakesFile: true, + }, + &cli.StringSliceFlag{ + Name: "ca-cert", + Usage: "CA certs for verification [x509]", + TakesFile: true, + }, + usernameFlag, + passwordFlag, + insecureFlag, + }, + Action: runVerify, +} + +func runVerify(ctx *cli.Context) error { + // initialize + scheme, err := getSchemeForVerification(ctx) + if err != nil { + return err + } + sigma, err := readSignatrueFile(ctx.String("signature")) + if err != nil { + return err + } + + // core process + content, _, err := scheme.Verify(sigma) + if err != nil { + return fmt.Errorf("verification failure: %v", err) + } + manifest, err := getManifestFromContext(ctx) + if err != nil { + return err + } + if content.Manifest.Digest != manifest.Digest || content.Manifest.Size != manifest.Size { + return fmt.Errorf("verification failure: manifest is not signed: %s", manifest.Digest) + } + + // write out + fmt.Println(manifest.Digest) + return nil +} + +func readSignatrueFile(path string) (sig signature.Signed, err error) { + file, err := os.Open(path) + if err != nil { + return sig, err + } + defer file.Close() + err = json.NewDecoder(file).Decode(&sig) + return sig, err +} + +func getSchemeForVerification(ctx *cli.Context) (*signature.Scheme, error) { + scheme := signature.NewScheme() + + // add x509 verifier + verifier, err := getX509Verifier(ctx) + if err != nil { + return nil, err + } + scheme.RegisterVerifier(verifier) + + return scheme, nil +} + +func getX509Verifier(ctx *cli.Context) (signature.Verifier, error) { + roots := x509.NewCertPool() + + var certs []*x509.Certificate + for _, path := range ctx.StringSlice("cert") { + bundledCerts, err := crypto.ReadCertificateFile(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 := crypto.ReadCertificateFile(path) + if err != nil { + return nil, err + } + for _, cert := range bundledCerts { + roots.AddCert(cert) + } + } + + return x509nv2.NewVerifier(certs, roots) +} diff --git a/docs/artifact/README.md b/docs/artifact/README.md new file mode 100644 index 000000000..0e5df5c05 --- /dev/null +++ b/docs/artifact/README.md @@ -0,0 +1,22 @@ +# Notary V2 Artifact +[Notary v2 signatures](../signature/README.md) can be stored as [OCI artifacts](https://github.com/opencontainers/artifacts). Precisely, it is a [OCI manifest](https://github.com/opencontainers/image-spec/blob/master/manifest.md) with a config of type + +- `application/vnd.cncf.notary.config.v2+json` + +and no layers. + +## Example Artifact + +Example showing the manifest ([examples/manifest.json](examples/manifest.json)) of an artifact. + +```json +{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.cncf.notary.config.v2+json", + "size": 1906, + "digest": "sha256:c7848182f2c817415f0de63206f9e4220012cbb0bdb750c2ecf8020350239814" + }, + "layers": [] +} +``` diff --git a/docs/artifact/examples/manifest.json b/docs/artifact/examples/manifest.json new file mode 100644 index 000000000..5e57feda6 --- /dev/null +++ b/docs/artifact/examples/manifest.json @@ -0,0 +1,9 @@ +{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.cncf.notary.config.v2+json", + "size": 1906, + "digest": "sha256:c7848182f2c817415f0de63206f9e4220012cbb0bdb750c2ecf8020350239814" + }, + "layers": [] +} \ No newline at end of file diff --git a/docs/nv2/README.md b/docs/nv2/README.md new file mode 100644 index 000000000..6ee30200e --- /dev/null +++ b/docs/nv2/README.md @@ -0,0 +1,293 @@ +# Notary V2 (nv2) - Prototype + +`nv2` is a command line tool for signing and verifying [OCI Artifacts]. This implementation supports `x509` signing mechanisms. + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [CLI Overview](#cli-overview) +- [Offline signing & verification](#offline-signing-and-verification) + +## Prerequisites + +### Build and Install + +This plugin requires [golang](https://golang.org/dl/) with version `>= 1.14`. + +To build and install, run + +```shell +go install github.com/notaryproject/nv2/cmd/nv2 +``` + +To build and install to an optional path, run + +```shell +go build -o nv2 ./cmd/nv2 +``` + +Next, install optional components: + +- Install [docker-generate](https://github.com/shizhMSFT/docker-generate) for local Docker manifest generation and local signing. +- Install [OpenSSL](https://www.openssl.org/) for key generation. + +### Self-signed certificate key generation + +To generate a `x509` self-signed certificate key pair `example.key` and `example.crt`, run + +``` shell +openssl req \ + -x509 \ + -sha256 \ + -nodes \ + -newkey rsa:2048 \ + -days 365 \ + -subj "/CN=registry.example.com/O=example inc/C=US/ST=Washington/L=Seattle" \ + -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. + +## Offline Signing + +Offline signing is accomplished with the `nv2 sign` command. + +### nv2 sign options + +```shell +NAME: + nv2 sign - signs OCI Artifacts + +USAGE: + nv2 sign [command options] [] + +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 + --output value, -o value write signature to a specific path + --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) +``` + +Signing and verification are based on [OCI manifests](https://github.com/opencontainers/image-spec/blob/master/manifest.md), [docker-generate](https://github.com/shizhMSFT/docker-generate) is used to generate the manifest, which is exactly the same manifest as the `docker push` produces. + +### Generating a manifest + +Notary v2 signing is accomplished by signing the OCI manifest representing the artifact. When building docker images, the manifest is not generated until the image is pushed to a registry. To accomplish offline/local signing, the manifest must first exist. + +- Build the hello-world image + + ``` shell + docker build \ + -f Dockerfile.build \ + -t registry.acme-rockets.io/hello-world:v1 \ + https://github.com/docker-library/hello-world.git + ``` + +- Generate a manifest, saving it as `hello-world_v1-manifest.json` + + ``` shell + docker generate manifest registry.acme-rockets.io/hello-world:v1 > hello-world_v1-manifest.json + ``` + +### Signing using `x509` + +To sign the manifest `hello-world_v1-manifest.json` using the key `key.pem` from the `x509` certificate `cert.pem` with the Common Name `registry.acme-rockets.io`, run + +```shell +nv2 sign --method x509 \ + -k key.key \ + -c cert.crt \ + -r registry.acme-rockets.io/hello-world:v1 \ + -o hello-world.signature.config.json \ + file:hello-world_v1-manifest.json +``` + +The formatted x509 signature: `hello-world.signature.config.json` is: + +``` json +{ + "signed": { + "digest": "sha256:5de47f48e0be1a9d41176a980728449a696fd4fcc37e9d99b8d26618c0f5bf51", + "size": 3056, + "references": [ + "registry.acme-rockets.io/hello-world:v1" + ], + "iat": 1596020554 + }, + "signature": { + "typ": "x509", + "sig": "vUNmuwrdHmcMyvG//eZQLjmIz2gnOUFNaL5Y5Jc3x1oaYu3nFnJxBEkB8232l0zBmV30sVUX2vjao0IDgLMv0Q7VWT2hiTutocgf+oRq88Jz/xKGvByGUWmVyYx9sMW6R+JHK/LlzthCLgDoYTjFD9qDTHf+AWnmRNPLv5nSYNQrVSxNH22jiO3CV/bNEQD8xoR7kZOdov6QzNw3rAP+XvlKxdf/D7vcYdR0D5T9G5xGa72aQSZmzXL/Zd2V7JQnxyJmw6PL3moU1i/8t8RK7LbsU6slvTScLUokFLZxzqCz8TcjujtaThyyxPF47ekx/HVsKW0mYXidpgCOfl+nqw==", + "alg": "RS256", + "x5c": [ + "MIIDJzCCAg+gAwIBAgIUMwVg7bpx8QmWaFzRcgpRFBN6JoQwDQYJKoZIhvcNAQELBQAwIzEhMB8GA1UEAwwYcmVnaXN0cnkuYWNtZS1yb2NrZXRzLmlvMB4XDTIwMDcyOTExMDIzMloXDTIxMDcyOTExMDIzMlowIzEhMB8GA1UEAwwYcmVnaXN0cnkuYWNtZS1yb2NrZXRzLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx2mXqcXqkllwxj7S12WhVDsIu6y4ebZ/CwVwwime44yDcd0bcpdJExqIH/Qy6axQd/1zmLCHPeOXGFq48Ul0oS4Bawj1GEeLvB7VFvqB0KaBeAdxrZAvdKXCXIDH5qyFSGnOmvkja1BuR8XrH7tts5u56i+U3KEDBZg5tfx4cQuKKt0DfXZAL+4RZkNh1LoO77X0ThaBThFoRsg6aZA/cEpttoWmvnO6uUkK73oZEVgZNKGGIZZKzhUjnydRSTphp9GmZzbqUHlOiMvbzdtsQYC0qeQeNqua38HN93Ur3p+oH7oSrBWxX1Xlx933oVb+4G6h5oz0aZvMQ0G6gCLzjwIDAQABo1MwUTAdBgNVHQ4EFgQU8l2F7avSjFZ9TvnpHackunxSFcswHwYDVR0jBBgwFoAU8l2F7avSjFZ9TvnpHackunxSFcswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAwECYhttcbCbqyi7DvOTHw5bixmxplbgD0AmMvE6Ci4P/MrooBququlkri/Jcp58GBaMjxItE4qVsaWwFCEvZEfP2xN4DAbr+rdrIFy9VYuwEIBs5l0ZLRH2H2N3HlqBzhYOjVzNlYfIqnqHUDip2VsUKqhcVFkCmb3cpJ1VNAgjQU2N60JUW28L0XrGyBctBIiicLvdP4NMhHP/hhN2vr2VGIyyo5XtP+QHFi/Uwa48BJ+c9bbVpXeghOMOPMeSJmJ2b/qlp95e/YHlSCfxDXyxZ70N2vBGecrc8ly4tD9KGLb9y3Q7RBgsagOFe7cGQ2db/t60AwTIxP0a9bIyJMg==" + ] + } +} +``` + +If the embedded cert chain `x5c` is not desired, it can be replaced by a key ID `kid` by omitting the `-c` option. + +```shell +nv2 sign -m x509 \ + -k key.key \ + -r registry.acme-rockets.io/hello-world:v1 \ + -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": { + "digest": "sha256:5de47f48e0be1a9d41176a980728449a696fd4fcc37e9d99b8d26618c0f5bf51", + "size": 3056, + "references": [ + "registry.acme-rockets.io/hello-world:v1" + ], + "iat": 1596020616 + }, + "signature": { + "typ": "x509", + "sig": "OyRPlwwsO5mYDxKkiNeTQlSl4WV8SOiQMCJv4i1+sx7uv6Pe8dHDaPt1SE5s64HzFvo6s26PrfiPYp4RphQOd/KvW2Hh03nS8ZByE4NWFOE6VLQcfNpScba6Q9vAzc3TnZrg1c9t992MGuec1oZB9pR77Ms7Jv/+gZd1qr6VPpA0A6+UucEbN6+pKRTiPRx5WkFXTkN0a4jmlJnev6MyBY3VI0EzjLI4nbCu9P05e4SK1dO0hXtD7aQCf2CCVKdYNHAMX4pNPTLxS3a5p4CFjV3oCbZO6cYT/5ZxgQrVV7vaGEI1MGCOEXS2KSI14zO6KlU1awtOQq3g04e03O+SVQ==", + "alg": "RS256", + "kid": "RQGT:OPJI:IABT:DFXB:52VS:FNOJ:4XBS:H4KY:WHGM:HQMC:WSMN:LKXM" + } +} +``` + +The detailed signature specification is [available](../signature/README.md). + +### Offline Verification + +Notary v2 verification can be accomplished with the `nv2 verify` command. + +```shell +NAME: + nv2 verify - verifies OCI Artifacts + +USAGE: + nv2 verify [command options] [] + +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] + --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 `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`. + +```shell +nv2 verify \ + -f hello-world.signature.config.json \ + -c cert.crt \ + file:hello-world_v1-manifest.json +``` + +If the cert isn't self-signed, you can omit the `-c` parameter. + +``` shell +nv2 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. + +The command `nv2 verify` takes care of all signing methods. + +## Remote Manifests + +With `nv2`, it is also possible to sign and verify a manifest or a manifest list in a remote registry where the registry can be a docker registry or an OCI registry. + +### Docker Registry + +Here is an example to sign and verify the image `hello-world` in DockerHub, i.e. `docker.io/library/hello-world:latest`, using `x509`. + +``` shell +nv2 sign -m x509 \ + -k key.key \ + -o hello-world_latest.signature.config.json \ + docker://docker.io/library/hello-world:latest + +sha256:49a1c8800c94df04e9658809b006fd8a686cab8028d33cfba2cc049724254202 + +nv2 verify \ + -c cert.crt \ + -f hello-world_latest.signature.config.json \ + docker://docker.io/library/hello-world:latest +sha256:49a1c8800c94df04e9658809b006fd8a686cab8028d33cfba2cc049724254202 +``` + +It is possible to use `digest` in the reference. For instance: + +``` shell +docker.io/library/hello-world@sha256:49a1c8800c94df04e9658809b006fd8a686cab8028d33cfba2cc049724254202 +``` + +If neither `tag` nor `digest` is specified, the default tag `latest` is used. + +### OCI Registry + +OCI registry works the same as Docker but with the scheme `oci`. + + +``` shell +nv2 sign -m x509 \ + -k key.key \ + -o hello-world_latest.signature.config.json \ + oci://docker.io/library/hello-world:latest + +sha256:0ebe6f409b373c8baf39879fccee6cae5e718003ec3167ded7d54cb2b5da2946 + +nv2 verify \ + -c cert.crt \ + -f hello-world_latest.signature.config.json \ + oci://docker.io/library/hello-world:latest + +sha256:0ebe6f409b373c8baf39879fccee6cae5e718003ec3167ded7d54cb2b5da2946 +``` + +**Note** The digest of the OCI manifest is different from the Docker manifest for the same image since their format is different. Therefore, the signer should be careful with the manifest type when signing. + +### Insecure Registries + +To sign and verify images from insecure registries accessed via `HTTP`, such as `localhost`, the option `--insecure` is required. + +``` shell +docker tag example localhost:5000/example +docker push localhost:5000/example +The push refers to repository [localhost:5000/example] +50644c29ef5a: Pushed +latest: digest: sha256:3351c53952446db17d21b86cfe5829ae70f823aff5d410fbf09dff820a39ab55 size: 528 +nv2 verify -f example.nv2 --insecure docker://localhost:5000/example + +sha256:3351c53952446db17d21b86cfe5829ae70f823aff5d410fbf09dff820a39ab55 +``` + +### Secure Image Pulling + +Since the tag might be changed during the verification process, it is required to pull by digest after verification. + +```shell +digest=$(nv2 verify -f hello-world_latest.signature.config.json -c cert.crt docker://docker.io/library/hello-world:latest) +if [ $? -eq 0 ]; then + docker pull docker.io/library/hello-world@$digest +fi +``` diff --git a/docs/signature/README.md b/docs/signature/README.md new file mode 100644 index 000000000..d3b49d7fd --- /dev/null +++ b/docs/signature/README.md @@ -0,0 +1,225 @@ +# Notary V2 Signature Specification + +This section defines the signature file, which is in JSON format with no whitespaces. Its JSON schema is available at [schema.json](schema.json). + +## Signature Goals + +- Offline signature creation +- Persistance within an [OCI Artifact][oci-artifacts] enabled, [distribution-spec][distribution-spec] based registry +- Artifact and signature copying within and across [OCI Artifact][oci-artifacts] enabled, [distribution-spec][distribution-spec] based registries +- Support public registry acquisition of content - where the public registry may host certified content as well as public, non-certified content +- Support private registries, where public content may be copied to, and new content originated within +- Air-gapped environments, where the originating registry of content is not accessable +- Multiple signatures per artifact, enabling the originating vendor signature, public registry certification and user/environment signatures +- Maintain the original artifact digest and collection of associated tags, supporting dev/ops deployment definitions + +## Signature + +A Notary v2 signature is clear-signed signature of manifest metadata, including but not limited to + +- [OCI Image Index](https://github.com/opencontainers/image-spec/blob/master/image-index.md) +- [OCI Image Manifest](https://github.com/opencontainers/image-spec/blob/master/manifest.md) +- [Docker Image Manifest List](https://docs.docker.com/registry/spec/manifest-v2-2/#manifest-list) +- [Docker Image Manifest](https://docs.docker.com/registry/spec/manifest-v2-2/#image-manifest) + +### Signing an Artifact Manifest + +For any [OCI Artifact][oci-artifacts] submitted to a registry, an [OCI Manifest][oci-manifest] and an optional [OCI Manifest List/Index][oci-manifest-list] is required. + +The nv2 prototype signs a combination of: + +- Key properties +- The target artifact manifest digest +- *optional:* list of associated tags + +#### Generating a self-signed x509 cert + +``` shell +openssl req \ + -x509 \ + -sha256 \ + -nodes \ + -newkey rsa:2048 \ + -days 365 \ + -subj "/CN=registry.example.com/O=example inc/C=US/ST=Washington/L=Seattle" \ + -keyout example.key \ + -out example.crt +``` + +An nv2 client would generate the following content to be signed: + +``` JSON +{ + "signed": { + "digest": "sha256:c4516b8a311e85f1f2a60573abf4c6b740ca3ade4127e29b05616848de487d34", + "size": 528, + "references": [ + "registry.example.com/example:latest", + "registry.example.com/example:v1.0" + ], + "exp": 1627555319, + "nbf": 1596019319, + "iat": 1596019319 + } +} +``` + +The signature of the above would be represented as: + +``` JSON +{ + "signature": { + "typ": "x509", + "sig": "UFqN24K2fLj7/h2slM68PLTfF9CDhrEVGuMQ8m3kkQJ4SKusj9fNxYV78tTiedqB+E8SqVH66mZbdlTrVQFJAd7aL2c3NZFfo92pE9SaHnqEDqnnGWXGRVjtBRM13YyRDm2wD8aRyuL5jEDUkTw7jBLY0+LfKHMDuYCsOOzvedof7aiaFc3qA+qKiW53jn2uEGCFfAs0LmsNafGfAtVmdGSO4zX4fdnQFAGT8sbUmL71uXl9W1B6tGeLfx5nBoQUvtplQipHly/yMQvWw7qMXsaAsf/BbGDmivN06CRahSb7VOwNq6K7Py4zYeiW40hEFVz9L7/5xT5XI1unKPZDuw==", + "alg": "RS256", + "x5c": [ + "MIIDszCCApugAwIBAgIUL1anEU/yJy67VJTbHkNX0bBNAnEwDQYJKoZIhvcNAQELBQAwaTEdMBsGA1UEAwwUcmVnaXN0cnkuZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUgaW5jMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTAeFw0yMDA3MjcxNDQzNDZaFw0yMTA3MjcxNDQzNDZaMGkxHTAbBgNVBAMMFHJlZ2lzdHJ5LmV4YW1wbGUuY29tMRQwEgYDVQQKDAtleGFtcGxlIGluYzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkKwAcV44psjN8nno1eZ3zv1ZKUhJAoxwBOIGfIxIe+iHtpXLvFFVwk5Jbxu+Pkig2N4B3Ilrj/Vryi0hxp4mag02M733bXLRENSOFONRkslpO8zHUN5pYdnhTSwYTLap1+1bgcFSuUXLWieqZB6qc7kiv3bj3SPaf42+s48V49t/OpXxLtgiWL9XkuDTZctpJJA4vHHk6Ou0bcg7iGm+L1xwIfb8Ml4oWvT0SF35fgW08bbLXZ2v1XCLRsrWUgbq4U+KxtEpG3XIYcYhKx1rIrUhfEJkuHzgPglM11gG5W+Cyfg+wfOJig5q6axIKWzIf6C8m8lmy6bM+N5EsD9SvAgMBAAGjUzBRMB0GA1UdDgQWBBTf1hM6/ibGF+u/SVAK88FUMjzRoTAfBgNVHSMEGDAWgBTf1hM6/ibGF+u/SVAK88FUMjzRoTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBgvVau5+2wAuCsmOyyG28h1zyC4IPmMmpRZTDOp/pLdwXeHjJr8kEC3l92qJEvc+WAboJ1RoucHycUe7RWh2C6ZF/WPCBLyWGwnlyqGyRM9/j86UJ1OgiuZl7kl9zxwWoaxPBCmHa0RHowdQB7AVlpqg1c7FhKjhUCBmGT4Ve8tV0hdZtrZoQV+6xHPbUd37KV1B1Bmfo3o4ekoJKhUu99Eo03OpE3JLtM13A1HxABEuQGHTI0tycDBBdRn3b03HoIhU0VnqjvpV1KPvsrgYi/0VStLNezZPgGe0fG3Xgy8yekdB9NMUn+zZLATI4+z8j4QH5Wj5ZPaUkyoAD2oUJO" + ] + } +} +``` + +### Signature Persisted within an OCI Artifact Enabled Registry + +Both values are persisted in a `signature.json` file. The file would be submitted to a registry as an Artifact with null layers. +The `signature.json` would be persisted within the `manifest.config` object + +``` SHELL +oras push \ + registry.example.com/hello-world:v1.0 \ + --manifest-config signature.json:application/vnd.cncf.notary.config.v2+json +``` + +Would push the following manifest: + +``` JSON +{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.cncf.notary.config.v2+json", + "size": 1906, + "digest": "sha256:c7848182f2c817415f0de63206f9e4220012cbb0bdb750c2ecf8020350239814" + }, + "layers": [] +} +``` + +## *Signature* Property Descriptions + +- **`signed`** *object* + + This REQUIRED property provides the signed content. + + - **`iat`** *integer* + + This OPTIONAL property identities the time at which the manifests were presented to the notary. This field is based on [RFC 7519 Section 4.1.6](https://tools.ietf.org/html/rfc7519#section-4.1.6). When used, it does not imply the issue time of any signature in the `signatures` property. + + - **`nbf`** *integer* + + This OPTIONAL property identifies the time before which the signed content MUST NOT be accepted for processing. This field is based on [RFC 7519 Section 4.1.5](https://tools.ietf.org/html/rfc7519#section-4.1.5). + + - **`exp`** *integer* + + This OPTIONAL property identifies the expiration time on or after which the signed content MUST NOT be accepted for processing. This field is based on [RFC 7519 Section 4.1.4](https://tools.ietf.org/html/rfc7519#section-4.1.4). + + - **`digest`** *string* + + This REQUIRED property is the *digest* of the target manifest, conforming to the requirements outlined in [Digests](https://github.com/opencontainers/image-spec/blob/master/descriptor.md#digests). If the actual content is fetched according to the *digest*, implementations MUST verify the content against the *digest*. + + - **`size`** *integer* + + This REQUIRED property is the *size* of the target manifest. If the actual content is fetched according the *digest*, implementations MUST verify the content against the *size*. + + - **`references`** *array of strings* + + This OPTIONAL property claims the manifest references of its origin. The format of the value MUST matches the [*reference* grammar](https://github.com/docker/distribution/blob/master/reference/reference.go). With used, the `x509` signatures are valid only if the domain names of all references match the Common Name (`CN`) in the `Subject` field of the certificate. + +- **`signature`** *string* + + This REQUIRED property provides the signature of the signed content. The entire signature file is valid if any signature is valid. The `signature` object is influenced by JSON Web Signature (JWS) at [RFC 7515](https://tools.ietf.org/html/rfc7515). + + - **`typ`** *string* + + This REQUIRED property identifies the signature type. Implementations MUST support at least the following types + + - `x509`: X.509 public key certificates. Implementations MUST verify that the certificate of the signing key has the `digitalSignature` `Key Usage` extension ([RFC 5280 Section 4.2.1.3](https://tools.ietf.org/html/rfc5280#section-4.2.1.3)). + + Implementations MAY support the following types + + - `tuf`: [The update framework](https://theupdateframework.io/). + + - **`sig`** *string* + + This REQUIRED property provides the base64-encoded signature binary of the specified signature type. + + - **`alg`** *string* + + This REQUIRED property for the `x509` type identifies the cryptographic algorithm used to sign the content. This field is based on [RFC 7515 Section 4.1.1](https://tools.ietf.org/html/rfc7515#section-4.1.1). + + - **`x5c`** *array of strings* + + This OPTIONAL property for the `x509` type contains the X.509 public key certificate or certificate chain corresponding to the key used to digitally sign the content. The certificates are encoded in base64. This field is based on [RFC 7515 Section 4.1.6](https://tools.ietf.org/html/rfc7515#section-4.1.6). + + - **`kid`** *string* + + This OPTIONAL property for the `x509` type is a hint (key ID) indicating which key was used to sign the content. This field is based on [RFC 7515 Section 4.1.4](https://tools.ietf.org/html/rfc7515#section-4.1.4). + +## Example Signatures + +### x509 Signature + +Example showing a formatted `x509` signature file [examples/x509_x5c.nv2.json](examples/x509_x5c.nv2.json) with certificates provided by `x5c`: + +```json +{ + "signed": { + "digest": "sha256:c4516b8a311e85f1f2a60573abf4c6b740ca3ade4127e29b05616848de487d34", + "size": 528, + "references": [ + "registry.example.com/example:latest", + "registry.example.com/example:v1.0" + ], + "exp": 1627555319, + "nbf": 1596019319, + "iat": 1596019319 + }, + "signature": { + "typ": "x509", + "sig": "UFqN24K2fLj7/h2slM68PLTfF9CDhrEVGuMQ8m3kkQJ4SKusj9fNxYV78tTiedqB+E8SqVH66mZbdlTrVQFJAd7aL2c3NZFfo92pE9SaHnqEDqnnGWXGRVjtBRM13YyRDm2wD8aRyuL5jEDUkTw7jBLY0+LfKHMDuYCsOOzvedof7aiaFc3qA+qKiW53jn2uEGCFfAs0LmsNafGfAtVmdGSO4zX4fdnQFAGT8sbUmL71uXl9W1B6tGeLfx5nBoQUvtplQipHly/yMQvWw7qMXsaAsf/BbGDmivN06CRahSb7VOwNq6K7Py4zYeiW40hEFVz9L7/5xT5XI1unKPZDuw==", + "alg": "RS256", + "x5c": [ + "MIIDszCCApugAwIBAgIUL1anEU/yJy67VJTbHkNX0bBNAnEwDQYJKoZIhvcNAQELBQAwaTEdMBsGA1UEAwwUcmVnaXN0cnkuZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUgaW5jMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTAeFw0yMDA3MjcxNDQzNDZaFw0yMTA3MjcxNDQzNDZaMGkxHTAbBgNVBAMMFHJlZ2lzdHJ5LmV4YW1wbGUuY29tMRQwEgYDVQQKDAtleGFtcGxlIGluYzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkKwAcV44psjN8nno1eZ3zv1ZKUhJAoxwBOIGfIxIe+iHtpXLvFFVwk5Jbxu+Pkig2N4B3Ilrj/Vryi0hxp4mag02M733bXLRENSOFONRkslpO8zHUN5pYdnhTSwYTLap1+1bgcFSuUXLWieqZB6qc7kiv3bj3SPaf42+s48V49t/OpXxLtgiWL9XkuDTZctpJJA4vHHk6Ou0bcg7iGm+L1xwIfb8Ml4oWvT0SF35fgW08bbLXZ2v1XCLRsrWUgbq4U+KxtEpG3XIYcYhKx1rIrUhfEJkuHzgPglM11gG5W+Cyfg+wfOJig5q6axIKWzIf6C8m8lmy6bM+N5EsD9SvAgMBAAGjUzBRMB0GA1UdDgQWBBTf1hM6/ibGF+u/SVAK88FUMjzRoTAfBgNVHSMEGDAWgBTf1hM6/ibGF+u/SVAK88FUMjzRoTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBgvVau5+2wAuCsmOyyG28h1zyC4IPmMmpRZTDOp/pLdwXeHjJr8kEC3l92qJEvc+WAboJ1RoucHycUe7RWh2C6ZF/WPCBLyWGwnlyqGyRM9/j86UJ1OgiuZl7kl9zxwWoaxPBCmHa0RHowdQB7AVlpqg1c7FhKjhUCBmGT4Ve8tV0hdZtrZoQV+6xHPbUd37KV1B1Bmfo3o4ekoJKhUu99Eo03OpE3JLtM13A1HxABEuQGHTI0tycDBBdRn3b03HoIhU0VnqjvpV1KPvsrgYi/0VStLNezZPgGe0fG3Xgy8yekdB9NMUn+zZLATI4+z8j4QH5Wj5ZPaUkyoAD2oUJO" + ] + } +} +``` + +Example showing a formatted `x509` signature file [examples/x509_kid.nv2.json](examples/x509_kid.nv2.json) with certificates referenced by `kid`: + +```json +{ + "signed": { + "digest": "sha256:c4516b8a311e85f1f2a60573abf4c6b740ca3ade4127e29b05616848de487d34", + "size": 528, + "references": [ + "registry.example.com/example:latest", + "registry.example.com/example:v1.0" + ], + "exp": 1627554920, + "nbf": 1596018920, + "iat": 1596018920 + }, + "signature": { + "typ": "x509", + "sig": "emzP9ygJD3y2ZWMYGO/wyqOhaSxrhd4ZdmjC9Zd+Ba7gGmGzBylsY1CskyZw389Hz2Z0xA6AQLhaNBbbqyxuAxVXtataMRsqCl/cgyNbyYU1URB2aTUZY/3V4iJzH1O/QfwSkpQa3aN1OCL8uMBNCtM6Rde9+SX8Q8XNMByDbuXtyPDvnKunZxpofEn2ibLe2Cm3o+MTK4pgxacEWeld85gTb06NicARf7mcVj7bflLyUIgel4qvmdqT6896Gtd2ES1KawvyjoEyskdlVlneSTdEKGRYxfchwIUK4E7p3EtTnmj+FuD9MpCtP0M4CQiOr19j0NtQe2bHuTo4bwtjuw==", + "alg": "RS256", + "kid": "XP5O:Y7W2:PRB6:O355:56CC:P3A6:CBDV:EDMN:QZCK:W5PO:QMV3:T2LX" + } +} +``` + +[distribution-spec]: https://github.com/opencontainers/distribution-spec +[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.json b/docs/signature/examples/x509_kid.nv2.json new file mode 100644 index 000000000..dc30f0a6e --- /dev/null +++ b/docs/signature/examples/x509_kid.nv2.json @@ -0,0 +1 @@ +{"signed":{"digest":"sha256:c4516b8a311e85f1f2a60573abf4c6b740ca3ade4127e29b05616848de487d34","size":528,"references":["registry.example.com/example:latest","registry.example.com/example:v1.0"],"exp":1627554920,"nbf":1596018920,"iat":1596018920},"signature":{"typ":"x509","sig":"emzP9ygJD3y2ZWMYGO/wyqOhaSxrhd4ZdmjC9Zd+Ba7gGmGzBylsY1CskyZw389Hz2Z0xA6AQLhaNBbbqyxuAxVXtataMRsqCl/cgyNbyYU1URB2aTUZY/3V4iJzH1O/QfwSkpQa3aN1OCL8uMBNCtM6Rde9+SX8Q8XNMByDbuXtyPDvnKunZxpofEn2ibLe2Cm3o+MTK4pgxacEWeld85gTb06NicARf7mcVj7bflLyUIgel4qvmdqT6896Gtd2ES1KawvyjoEyskdlVlneSTdEKGRYxfchwIUK4E7p3EtTnmj+FuD9MpCtP0M4CQiOr19j0NtQe2bHuTo4bwtjuw==","alg":"RS256","kid":"XP5O:Y7W2:PRB6:O355:56CC:P3A6:CBDV:EDMN:QZCK:W5PO:QMV3:T2LX"}} \ No newline at end of file diff --git a/docs/signature/examples/x509_x5c.nv2.json b/docs/signature/examples/x509_x5c.nv2.json new file mode 100644 index 000000000..2c621e223 --- /dev/null +++ b/docs/signature/examples/x509_x5c.nv2.json @@ -0,0 +1 @@ +{"signed":{"digest":"sha256:c4516b8a311e85f1f2a60573abf4c6b740ca3ade4127e29b05616848de487d34","size":528,"references":["registry.example.com/example:latest","registry.example.com/example:v1.0"],"exp":1627555319,"nbf":1596019319,"iat":1596019319},"signature":{"typ":"x509","sig":"UFqN24K2fLj7/h2slM68PLTfF9CDhrEVGuMQ8m3kkQJ4SKusj9fNxYV78tTiedqB+E8SqVH66mZbdlTrVQFJAd7aL2c3NZFfo92pE9SaHnqEDqnnGWXGRVjtBRM13YyRDm2wD8aRyuL5jEDUkTw7jBLY0+LfKHMDuYCsOOzvedof7aiaFc3qA+qKiW53jn2uEGCFfAs0LmsNafGfAtVmdGSO4zX4fdnQFAGT8sbUmL71uXl9W1B6tGeLfx5nBoQUvtplQipHly/yMQvWw7qMXsaAsf/BbGDmivN06CRahSb7VOwNq6K7Py4zYeiW40hEFVz9L7/5xT5XI1unKPZDuw==","alg":"RS256","x5c":["MIIDszCCApugAwIBAgIUL1anEU/yJy67VJTbHkNX0bBNAnEwDQYJKoZIhvcNAQELBQAwaTEdMBsGA1UEAwwUcmVnaXN0cnkuZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUgaW5jMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTAeFw0yMDA3MjcxNDQzNDZaFw0yMTA3MjcxNDQzNDZaMGkxHTAbBgNVBAMMFHJlZ2lzdHJ5LmV4YW1wbGUuY29tMRQwEgYDVQQKDAtleGFtcGxlIGluYzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkKwAcV44psjN8nno1eZ3zv1ZKUhJAoxwBOIGfIxIe+iHtpXLvFFVwk5Jbxu+Pkig2N4B3Ilrj/Vryi0hxp4mag02M733bXLRENSOFONRkslpO8zHUN5pYdnhTSwYTLap1+1bgcFSuUXLWieqZB6qc7kiv3bj3SPaf42+s48V49t/OpXxLtgiWL9XkuDTZctpJJA4vHHk6Ou0bcg7iGm+L1xwIfb8Ml4oWvT0SF35fgW08bbLXZ2v1XCLRsrWUgbq4U+KxtEpG3XIYcYhKx1rIrUhfEJkuHzgPglM11gG5W+Cyfg+wfOJig5q6axIKWzIf6C8m8lmy6bM+N5EsD9SvAgMBAAGjUzBRMB0GA1UdDgQWBBTf1hM6/ibGF+u/SVAK88FUMjzRoTAfBgNVHSMEGDAWgBTf1hM6/ibGF+u/SVAK88FUMjzRoTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBgvVau5+2wAuCsmOyyG28h1zyC4IPmMmpRZTDOp/pLdwXeHjJr8kEC3l92qJEvc+WAboJ1RoucHycUe7RWh2C6ZF/WPCBLyWGwnlyqGyRM9/j86UJ1OgiuZl7kl9zxwWoaxPBCmHa0RHowdQB7AVlpqg1c7FhKjhUCBmGT4Ve8tV0hdZtrZoQV+6xHPbUd37KV1B1Bmfo3o4ekoJKhUu99Eo03OpE3JLtM13A1HxABEuQGHTI0tycDBBdRn3b03HoIhU0VnqjvpV1KPvsrgYi/0VStLNezZPgGe0fG3Xgy8yekdB9NMUn+zZLATI4+z8j4QH5Wj5ZPaUkyoAD2oUJO"]}} \ No newline at end of file diff --git a/docs/signature/schema.json b/docs/signature/schema.json new file mode 100644 index 000000000..a66e843c7 --- /dev/null +++ b/docs/signature/schema.json @@ -0,0 +1,78 @@ +{ + "description": "Notary V2 Signature Config Specification", + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://localhost:5000/schema/signature/config", + "type": "object", + "properties": { + "signed": { + "type": "object", + "properties": { + "exp": { + "type": "integer", + "description": "Expiration time. Ref RFC7519." + }, + "nbf": { + "type": "integer", + "description": "Not before time. Ref RFC7519." + }, + "iat": { + "type": "integer", + "description": "Issued at time. Ref RFC7519." + }, + "digest": { + "description": "The cryptographic checksum digest of the object, in the pattern ':'", + "$ref": "defs-descriptor.json#/definitions/digest" + }, + "size": { + "description": "The size in bytes of the referenced object.", + "$ref": "defs.json#/definitions/int64" + }, + "references": { + "type": "array", + "description": "Each element in this array represents a fully qualified tag reference to the object.", + "minItems": 1, + "items": { + "type": "string", + "description": "Example: localhost:5000/hello-world:latest" + } + } + } + }, + "signature": { + "type": "object", + "properties": { + "typ": { + "type": "string", + "description": "Media type. Ref RFC7519.", + "enum": [ + "x509" + ] + }, + "sig": { + "type": "string", + "description": "The signature blob." + }, + "alg": { + "type": "string", + "description": "Signing algorithm. Ref RFC7515." + }, + "x5c": { + "type": "string", + "description": "X509 public key certificate or certificate chain. Ref RFC7515." + }, + "kid": { + "type": "string", + "description": "Signing key hint. Ref RFC7515." + } + }, + "required": [ + "typ", + "sig" + ] + } + }, + "required": [ + "signed", + "signatures" + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..9bad1ee32 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/notaryproject/nv2 + +go 1.14 + +require ( + 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/urfave/cli/v2 v2.2.0 + golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..64123900c --- /dev/null +++ b/go.sum @@ -0,0 +1,26 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +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/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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/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-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg= +golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/crypto/x509.go b/internal/crypto/x509.go new file mode 100644 index 000000000..e53173d06 --- /dev/null +++ b/internal/crypto/x509.go @@ -0,0 +1,51 @@ +package crypto + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "io/ioutil" + + "github.com/docker/libtrust" +) + +// ReadPrivateKeyFile reads a key PEM file as a libtrust key +func ReadPrivateKeyFile(path string) (libtrust.PrivateKey, error) { + raw, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + block, _ := pem.Decode(raw) + if block == nil { + return nil, errors.New("no PEM data found") + } + switch block.Type { + case "PRIVATE KEY": + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + return libtrust.FromCryptoPrivateKey(key) + default: + return libtrust.UnmarshalPrivateKeyPEM(raw) + } +} + +// ReadCertificateFile reads a certificate PEM file +func ReadCertificateFile(path string) ([]*x509.Certificate, error) { + raw, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + var certs []*x509.Certificate + block, rest := pem.Decode(raw) + for block != nil { + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + certs = append(certs, cert) + block, rest = pem.Decode(rest) + } + return certs, nil +} diff --git a/media/acme-rockets-cert.png b/media/acme-rockets-cert.png new file mode 100644 index 0000000000000000000000000000000000000000..0c3e8cd3df8179b5e6bf25a2a92fe7411ed443c4 GIT binary patch literal 17157 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 literal 0 HcmV?d00001 diff --git a/media/example-cert.png b/media/example-cert.png new file mode 100644 index 0000000000000000000000000000000000000000..3f84aa20dbea5dbf1c6d4cc2e4850417e3d00175 GIT binary patch literal 18030 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 S1^HPO6nPmH>2k@BU;Zy!4&}7~ literal 0 HcmV?d00001 diff --git a/media/notary-e2e-scenarios.png b/media/notary-e2e-scenarios.png new file mode 100644 index 0000000000000000000000000000000000000000..2669f3fd447a8bf92873dddc415f09484256f03d GIT binary patch literal 54561 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%0DNEEB07lzZwGE$5@hY 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 literal 0 HcmV?d00001 diff --git a/pkg/registry/client.go b/pkg/registry/client.go new file mode 100644 index 000000000..8bfd056ae --- /dev/null +++ b/pkg/registry/client.go @@ -0,0 +1,32 @@ +package registry + +import ( + "net/http" +) + +// Client is a customized registry client +type Client struct { + base http.RoundTripper + insecure bool +} + +// ClientOptions configures the client +type ClientOptions struct { + Username string + Password string + Insecure bool +} + +// NewClient creates a new registry client +func NewClient(base http.RoundTripper, opts *ClientOptions) *Client { + if base == nil { + base = http.DefaultTransport + } + if opts == nil { + opts = &ClientOptions{} + } + return &Client{ + base: newV2transport(base, opts.Username, opts.Password), + insecure: opts.Insecure, + } +} diff --git a/pkg/registry/docker.go b/pkg/registry/docker.go new file mode 100644 index 000000000..d6351dcfd --- /dev/null +++ b/pkg/registry/docker.go @@ -0,0 +1,7 @@ +package registry + +// docker media types +const ( + MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json" + MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json" +) diff --git a/pkg/registry/manifest.go b/pkg/registry/manifest.go new file mode 100644 index 000000000..2d5b38ff9 --- /dev/null +++ b/pkg/registry/manifest.go @@ -0,0 +1,113 @@ +package registry + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/notaryproject/nv2/pkg/signature" + 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) { + 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) + } +} + +// GetDockerManifestMetadata returns signature manifest information +// from a remote Docker manifest +func (c *Client) GetDockerManifestMetadata(uri *url.URL) (signature.Manifest, error) { + return c.getManifestMetadata(uri, + MediaTypeManifestList, + MediaTypeManifest, + ) +} + +// GetOCIManifestMetadata returns signature manifest information +// from a remote OCI manifest +func (c *Client) GetOCIManifestMetadata(uri *url.URL) (signature.Manifest, error) { + return c.getManifestMetadata(uri, + v1.MediaTypeImageIndex, + v1.MediaTypeImageManifest, + ) +} + +// GetManifestMetadata returns signature manifest information +func (c *Client) getManifestMetadata(uri *url.URL, mediaTypes ...string) (signature.Manifest, error) { + host := uri.Host + if host == "docker.io" { + host = "registry-1.docker.io" + } + var repository string + var reference string + path := strings.TrimPrefix(uri.Path, "/") + if index := strings.Index(path, "@"); index != -1 { + repository = path[:index] + reference = path[index+1:] + } else if index := strings.Index(path, ":"); index != -1 { + repository = path[:index] + reference = path[index+1:] + } else { + repository = path + reference = "latest" + } + scheme := "https" + if c.insecure { + scheme = "http" + } + url := fmt.Sprintf("%s://%s/v2/%s/manifests/%s", + scheme, + host, + repository, + reference, + ) + req, err := http.NewRequest(http.MethodHead, url, nil) + if err != nil { + return signature.Manifest{}, fmt.Errorf("invalid uri: %v", uri) + } + req.Header.Set("Connection", "close") + for _, mediaType := range mediaTypes { + req.Header.Add("Accept", mediaType) + } + + resp, err := c.base.RoundTrip(req) + if err != nil { + return signature.Manifest{}, 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) + default: + return signature.Manifest{}, fmt.Errorf("%v: %s", url, resp.Status) + } + + header := resp.Header + digest := header.Get("Docker-Content-Digest") + if digest == "" { + return signature.Manifest{}, fmt.Errorf("%v: missing Docker-Content-Digest", url) + } + length := header.Get("Content-Length") + if length == "" { + return signature.Manifest{}, 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 signature.Manifest{ + Digest: digest, + Size: size, + }, nil +} diff --git a/pkg/registry/transport.go b/pkg/registry/transport.go new file mode 100644 index 000000000..a4ff6eb21 --- /dev/null +++ b/pkg/registry/transport.go @@ -0,0 +1,110 @@ +package registry + +import ( + "encoding/json" + "net/http" + "net/url" + "regexp" + "strings" +) + +var authHeaderRegex = regexp.MustCompile(`(realm|service|scope)="([^"]*)`) + +type v2Transport struct { + base http.RoundTripper + username string + password string +} + +func newV2transport(base http.RoundTripper, username, password string) http.RoundTripper { + return &v2Transport{ + base: base, + username: username, + password: password, + } +} + +func (t *v2Transport) RoundTrip(originalReq *http.Request) (*http.Response, error) { + req := originalReq.Clone(originalReq.Context()) + if t.username != "" { + req.SetBasicAuth(t.username, t.password) + } + + resp, err := t.base.RoundTrip(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusUnauthorized { + return resp, nil + } + + scheme, params := parseAuthHeader(resp.Header.Get("Www-Authenticate")) + if scheme != "bearer" { + return resp, nil + } + resp.Body.Close() + + token, resp, err := t.fetchToken(params) + if err != nil { + if resp != nil { + return resp, nil + } + return nil, err + } + + req = originalReq.Clone(originalReq.Context()) + req.Header.Set("Authorization", "Bearer "+token) + return t.base.RoundTrip(req) +} + +func (t *v2Transport) fetchToken(params map[string]string) (string, *http.Response, error) { + req, err := http.NewRequest(http.MethodGet, params["realm"], nil) + if err != nil { + return "", nil, err + } + if t.username != "" { + req.SetBasicAuth(t.username, t.password) + } + + query := url.Values{} + if service, ok := params["service"]; ok { + query.Set("service", service) + } + if scope, ok := params["scope"]; ok { + query.Set("scope", scope) + } + req.URL.RawQuery = query.Encode() + + resp, err := t.base.RoundTrip(req) + if err != nil { + return "", nil, err + } + if resp.StatusCode != http.StatusOK { + return "", resp, nil + } + defer resp.Body.Close() + + var result struct { + AccessToken string `json:"access_token"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", nil, err + } + return result.AccessToken, nil, nil +} + +func parseAuthHeader(header string) (string, map[string]string) { + parts := strings.SplitN(header, " ", 2) + scheme := strings.ToLower(parts[0]) + if len(parts) < 2 { + return scheme, nil + } + + params := make(map[string]string) + result := authHeaderRegex.FindAllStringSubmatch(parts[1], -1) + for _, match := range result { + params[strings.ToLower(match[1])] = match[2] + } + + return scheme, params +} diff --git a/pkg/signature/errors.go b/pkg/signature/errors.go new file mode 100644 index 000000000..6c939d883 --- /dev/null +++ b/pkg/signature/errors.go @@ -0,0 +1,10 @@ +package signature + +import "errors" + +// common errors +var ( + ErrInvalidSignatureType = errors.New("invalid signature type") + ErrUnknownSignatureType = errors.New("unknown signature type") + ErrUnknownSigner = errors.New("unknown signer") +) diff --git a/pkg/signature/interface.go b/pkg/signature/interface.go new file mode 100644 index 000000000..e1127e10f --- /dev/null +++ b/pkg/signature/interface.go @@ -0,0 +1,12 @@ +package signature + +// Signer signs content +type Signer interface { + Sign(content []byte) (Signature, error) +} + +// Verifier verifies content +type Verifier interface { + Type() string + Verify(content []byte, signature Signature) error +} diff --git a/pkg/signature/scheme.go b/pkg/signature/scheme.go new file mode 100644 index 000000000..30b9337b8 --- /dev/null +++ b/pkg/signature/scheme.go @@ -0,0 +1,90 @@ +package signature + +import ( + "encoding/json" + "fmt" + "time" +) + +// Scheme is a signature scheme +type Scheme struct { + signers map[string]Signer + verifiers map[string]Verifier +} + +// NewScheme creates a new scheme +func NewScheme() *Scheme { + return &Scheme{ + signers: make(map[string]Signer), + verifiers: make(map[string]Verifier), + } +} + +// RegisterSigner registers signer with a name +func (s *Scheme) RegisterSigner(signerID string, signer Signer) { + s.signers[signerID] = signer +} + +// RegisterVerifier registers verifier +func (s *Scheme) RegisterVerifier(verifier Verifier) { + s.verifiers[verifier.Type()] = verifier +} + +// Sign signs content by a signer +func (s *Scheme) Sign(signerID string, content Content) (Signature, error) { + bytes, err := json.Marshal(content) + if err != nil { + return Signature{}, err + } + return s.SignRaw(signerID, bytes) +} + +// SignRaw signs raw content by a signer +func (s *Scheme) SignRaw(signerID string, content []byte) (Signature, error) { + signer, found := s.signers[signerID] + if !found { + return Signature{}, ErrUnknownSigner + } + return signer.Sign(content) +} + +// Verify verifies signed data +func (s *Scheme) Verify(signed Signed) (Content, Signature, error) { + sig, err := s.verifySignature(signed) + if err != nil { + return Content{}, sig, err + } + + var content Content + if err := json.Unmarshal(signed.Signed, &content); err != nil { + return Content{}, sig, err + } + + return content, sig, s.verifyContent(content) +} + +func (s *Scheme) verifySignature(signed Signed) (Signature, error) { + sig := signed.Signature + verifier, found := s.verifiers[sig.Type] + if !found { + return Signature{}, ErrUnknownSignatureType + } + + content := []byte(signed.Signed) + if err := verifier.Verify(content, sig); err != nil { + return Signature{}, err + } + + return sig, nil +} + +func (s *Scheme) verifyContent(content Content) error { + now := time.Now().Unix() + if content.Expiration != 0 && now > content.Expiration { + return fmt.Errorf("content expired: %d: current: %d", content.Expiration, now) + } + if content.NotBefore != 0 && now < content.NotBefore { + return fmt.Errorf("content is not available yet: %d: current: %d", content.NotBefore, now) + } + return nil +} diff --git a/pkg/signature/signature.go b/pkg/signature/signature.go new file mode 100644 index 000000000..f116d9450 --- /dev/null +++ b/pkg/signature/signature.go @@ -0,0 +1,35 @@ +package signature + +import ( + "encoding/json" +) + +// Signed is the high level, partially deserialized metadata object +type Signed struct { + Signed json.RawMessage `json:"signed"` + Signature Signature `json:"signature"` +} + +// Content contains the contents to be signed +type Content struct { + Manifest + Expiration int64 `json:"exp,omitempty"` + NotBefore int64 `json:"nbf,omitempty"` + IssuedAt int64 `json:"iat,omitempty"` +} + +// Manifest to be signed +type Manifest struct { + Digest string `json:"digest"` + Size int64 `json:"size"` + References []string `json:"references,omitempty"` +} + +// Signature to verify the content +type Signature struct { + Type string `json:"typ"` + Signature []byte `json:"sig"` + Algorithm string `json:"alg,omitempty"` + KeyID string `json:"kid,omitempty"` + X5c [][]byte `json:"x5c,omitempty"` +} diff --git a/pkg/signature/util.go b/pkg/signature/util.go new file mode 100644 index 000000000..0a62bfd06 --- /dev/null +++ b/pkg/signature/util.go @@ -0,0 +1,17 @@ +package signature + +import ( + "encoding/json" +) + +// Pack packs content with its signature +func Pack(content Content, signature Signature) (Signed, error) { + signed, err := json.Marshal(content) + if err != nil { + return Signed{}, err + } + return Signed{ + Signed: signed, + Signature: signature, + }, nil +} diff --git a/pkg/signature/x509/signer.go b/pkg/signature/x509/signer.go new file mode 100644 index 000000000..f5e8de755 --- /dev/null +++ b/pkg/signature/x509/signer.go @@ -0,0 +1,92 @@ +package x509 + +import ( + "bytes" + "crypto" + "crypto/x509" + "errors" + + "github.com/docker/libtrust" + cryptoutil "github.com/notaryproject/nv2/internal/crypto" + "github.com/notaryproject/nv2/pkg/signature" +) + +type signer struct { + key libtrust.PrivateKey + keyID string + cert *x509.Certificate + rawCerts [][]byte + hash crypto.Hash +} + +// NewSignerFromFiles creates a signer from files +func NewSignerFromFiles(keyPath, certPath string) (signature.Signer, error) { + key, err := cryptoutil.ReadPrivateKeyFile(keyPath) + if err != nil { + return nil, err + } + if certPath == "" { + return NewSigner(key, nil) + } + + certs, err := cryptoutil.ReadCertificateFile(certPath) + if err != nil { + return nil, err + } + return NewSigner(key, certs) +} + +// NewSigner creates a signer +func NewSigner(key libtrust.PrivateKey, certs []*x509.Certificate) (signature.Signer, error) { + s := &signer{ + key: key, + keyID: key.KeyID(), + hash: crypto.SHA256, + } + if len(certs) == 0 { + return s, nil + } + + cert := certs[0] + publicKey, err := libtrust.FromCryptoPublicKey(crypto.PublicKey(cert.PublicKey)) + if err != nil { + return nil, err + } + if s.keyID != publicKey.KeyID() { + return nil, errors.New("key and certificate mismatch") + } + 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(raw []byte) (signature.Signature, error) { + if s.cert != nil { + if err := verifyReferences(raw, s.cert); err != nil { + return signature.Signature{}, err + } + } + + sig, alg, err := s.key.Sign(bytes.NewReader(raw), s.hash) + if err != nil { + return signature.Signature{}, err + } + sigma := signature.Signature{ + Type: Type, + Algorithm: alg, + Signature: sig, + } + + if s.cert != nil { + sigma.X5c = s.rawCerts + } else { + sigma.KeyID = s.keyID + } + return sigma, nil +} diff --git a/pkg/signature/x509/type.go b/pkg/signature/x509/type.go new file mode 100644 index 000000000..82dd54370 --- /dev/null +++ b/pkg/signature/x509/type.go @@ -0,0 +1,4 @@ +package x509 + +// Type indicates the signature type +const Type = "x509" diff --git a/pkg/signature/x509/verifier.go b/pkg/signature/x509/verifier.go new file mode 100644 index 000000000..e906e896c --- /dev/null +++ b/pkg/signature/x509/verifier.go @@ -0,0 +1,146 @@ +package x509 + +import ( + "bytes" + "crypto" + "crypto/x509" + "encoding/json" + "errors" + "strings" + + "github.com/docker/libtrust" + "github.com/notaryproject/nv2/pkg/signature" +) + +type verifier struct { + keys map[string]libtrust.PublicKey + certs map[string]*x509.Certificate + roots *x509.CertPool +} + +// NewVerifier creates a verifier +func NewVerifier(certs []*x509.Certificate, roots *x509.CertPool) (signature.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]libtrust.PublicKey, len(certs)) + keyedCerts := make(map[string]*x509.Certificate, len(certs)) + for _, cert := range certs { + key, err := libtrust.FromCryptoPublicKey(crypto.PublicKey(cert.PublicKey)) + if err != nil { + return nil, err + } + keyID := key.KeyID() + keys[keyID] = key + keyedCerts[keyID] = cert + } + + return &verifier{ + keys: keys, + certs: keyedCerts, + roots: roots, + }, nil +} + +func (v *verifier) Type() string { + return Type +} + +func (v *verifier) Verify(content []byte, sig signature.Signature) error { + if sig.Type != Type { + return signature.ErrInvalidSignatureType + } + + key, cert, err := v.getVerificationKeyPair(sig) + if err != nil { + return err + } + if err := key.Verify(bytes.NewReader(content), sig.Algorithm, sig.Signature); err != nil { + return err + } + return verifyReferences(content, cert) +} + +func (v *verifier) getVerificationKeyPair(sig signature.Signature) (libtrust.PublicKey, *x509.Certificate, error) { + switch { + case len(sig.X5c) > 0: + return v.getVerificationKeyPairFromX5c(sig.X5c) + case sig.KeyID != "": + return v.getVerificationKeyPairFromKeyID(sig.KeyID) + default: + return nil, nil, errors.New("missing verification key") + } +} + +func (v *verifier) getVerificationKeyPairFromKeyID(keyID string) (libtrust.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(x5c [][]byte) (libtrust.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, err := libtrust.FromCryptoPublicKey(crypto.PublicKey(cert.PublicKey)) + if err != nil { + return nil, nil, err + } + return key, cert, nil +} + +func verifyReferences(raw []byte, cert *x509.Certificate) error { + var content signature.Content + if err := json.Unmarshal(raw, &content); err != nil { + return err + } + roots := x509.NewCertPool() + roots.AddCert(cert) + for _, reference := range content.Manifest.References { + if _, err := cert.Verify(x509.VerifyOptions{ + DNSName: strings.SplitN(reference, "/", 2)[0], + Roots: roots, + }); err != nil { + return err + } + } + return nil +} From a42acd8ade7dbb63f718f955aac488cea557e032 Mon Sep 17 00:00:00 2001 From: Shiwei Zhang Date: Wed, 19 Aug 2020 11:25:13 +0800 Subject: [PATCH 2/2] JWT based system (#16) Signed-off-by: Shiwei Zhang --- cmd/nv2/common.go | 5 + cmd/nv2/main.go | 2 +- cmd/nv2/manifest.go | 19 +- cmd/nv2/sign.go | 34 ++-- cmd/nv2/verify.go | 24 ++- docs/artifact/README.md | 22 --- docs/artifact/examples/manifest.json | 9 - docs/nv2/README.md | 85 +++++---- docs/signature/README.md | 212 ++++++++++++---------- docs/signature/examples/x509_kid.nv2.json | 1 - docs/signature/examples/x509_kid.nv2.jwt | 1 + docs/signature/examples/x509_x5c.nv2.json | 1 - docs/signature/examples/x509_x5c.nv2.jwt | 1 + docs/signature/schema.json | 78 -------- go.mod | 1 - go.sum | 7 - pkg/registry/manifest.go | 11 +- pkg/signature/encoding.go | 30 +++ pkg/signature/errors.go | 4 + pkg/signature/interface.go | 4 +- pkg/signature/model.go | 28 +++ pkg/signature/scheme.go | 85 ++++++--- pkg/signature/signature.go | 35 ---- pkg/signature/util.go | 17 -- pkg/signature/x509/header.go | 18 ++ pkg/signature/x509/signer.go | 49 +++-- pkg/signature/x509/verifier.go | 39 ++-- 27 files changed, 403 insertions(+), 419 deletions(-) delete mode 100644 docs/artifact/README.md delete mode 100644 docs/artifact/examples/manifest.json delete mode 100644 docs/signature/examples/x509_kid.nv2.json create mode 100644 docs/signature/examples/x509_kid.nv2.jwt delete mode 100644 docs/signature/examples/x509_x5c.nv2.json create mode 100644 docs/signature/examples/x509_x5c.nv2.jwt delete mode 100644 docs/signature/schema.json create mode 100644 pkg/signature/encoding.go create mode 100644 pkg/signature/model.go delete mode 100644 pkg/signature/signature.go delete mode 100644 pkg/signature/util.go create mode 100644 pkg/signature/x509/header.go diff --git a/cmd/nv2/common.go b/cmd/nv2/common.go index f6752a827..981b36788 100644 --- a/cmd/nv2/common.go +++ b/cmd/nv2/common.go @@ -17,4 +17,9 @@ var ( Name: "insecure", Usage: "enable insecure remote access", } + 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", + } ) diff --git a/cmd/nv2/main.go b/cmd/nv2/main.go index 828faca52..93c82436d 100644 --- a/cmd/nv2/main.go +++ b/cmd/nv2/main.go @@ -11,7 +11,7 @@ func main() { app := &cli.App{ Name: "nv2", Usage: "Notary V2 - Prototype", - Version: "0.1.2", + Version: "0.2.0", Authors: []*cli.Author{ { Name: "Shiwei Zhang", diff --git a/cmd/nv2/manifest.go b/cmd/nv2/manifest.go index 9bd837c2f..588e696a2 100644 --- a/cmd/nv2/manifest.go +++ b/cmd/nv2/manifest.go @@ -18,10 +18,10 @@ func getManifestFromContext(ctx *cli.Context) (signature.Manifest, error) { if uri := ctx.Args().First(); uri != "" { return getManfestsFromURI(ctx, uri) } - return getManifestFromReader(os.Stdin) + return getManifestFromReader(os.Stdin, ctx.String(mediaTypeFlag.Name)) } -func getManifestFromReader(r io.Reader) (signature.Manifest, error) { +func getManifestFromReader(r io.Reader, mediaType string) (signature.Manifest, error) { lr := &io.LimitedReader{ R: r, N: math.MaxInt64, @@ -31,8 +31,11 @@ func getManifestFromReader(r io.Reader) (signature.Manifest, error) { return signature.Manifest{}, err } return signature.Manifest{ - Digest: digest.String(), - Size: math.MaxInt64 - lr.N, + Descriptor: signature.Descriptor{ + MediaType: mediaType, + Digest: digest.String(), + Size: math.MaxInt64 - lr.N, + }, }, nil } @@ -56,13 +59,13 @@ func getManfestsFromURI(ctx *cli.Context, uri string) (signature.Manifest, error r = file case "docker", "oci": remote := registry.NewClient(nil, ®istry.ClientOptions{ - Username: ctx.String("username"), - Password: ctx.String("password"), - Insecure: ctx.Bool("insecure"), + 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) + return getManifestFromReader(r, ctx.String(mediaTypeFlag.Name)) } diff --git a/cmd/nv2/sign.go b/cmd/nv2/sign.go index 9a04ffde2..df79d2af4 100644 --- a/cmd/nv2/sign.go +++ b/cmd/nv2/sign.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "fmt" "io/ioutil" "strings" @@ -16,7 +15,7 @@ const signerID = "nv2" var signCommand = &cli.Command{ Name: "sign", - Usage: "signs artifacts or images", + Usage: "signs OCI Artifacts", ArgsUsage: "[]", Flags: []cli.Flag{ &cli.StringFlag{ @@ -55,6 +54,7 @@ var signCommand = &cli.Command{ usernameFlag, passwordFlag, insecureFlag, + mediaTypeFlag, }, Action: runSign, } @@ -67,54 +67,46 @@ func runSign(ctx *cli.Context) error { } // core process - content, err := prepareContentForSigning(ctx) + claims, err := prepareClaimsForSigning(ctx) if err != nil { return err } - sig, err := scheme.Sign(signerID, content) - if err != nil { - return err - } - sigma, err := signature.Pack(content, sig) + sig, err := scheme.Sign(signerID, claims) if err != nil { return err } // write out - sigmaJSON, err := json.Marshal(sigma) - if err != nil { - return err - } path := ctx.String("output") if path == "" { - path = strings.Split(content.Manifest.Digest, ":")[1] + ".nv2" + path = strings.Split(claims.Manifest.Digest, ":")[1] + ".nv2" } - if err := ioutil.WriteFile(path, sigmaJSON, 0666); err != nil { + if err := ioutil.WriteFile(path, []byte(sig), 0666); err != nil { return err } - fmt.Println(content.Manifest.Digest) + fmt.Println(claims.Manifest.Digest) return nil } -func prepareContentForSigning(ctx *cli.Context) (signature.Content, error) { +func prepareClaimsForSigning(ctx *cli.Context) (signature.Claims, error) { manifest, err := getManifestFromContext(ctx) if err != nil { - return signature.Content{}, err + return signature.Claims{}, err } manifest.References = ctx.StringSlice("reference") now := time.Now() nowUnix := now.Unix() - content := signature.Content{ + claims := signature.Claims{ Manifest: manifest, IssuedAt: nowUnix, } if expiry := ctx.Duration("expiry"); expiry != 0 { - content.NotBefore = nowUnix - content.Expiration = now.Add(expiry).Unix() + claims.NotBefore = nowUnix + claims.Expiration = now.Add(expiry).Unix() } - return content, nil + return claims, nil } func getSchemeForSigning(ctx *cli.Context) (*signature.Scheme, error) { diff --git a/cmd/nv2/verify.go b/cmd/nv2/verify.go index d2bcf4eca..2db1652a7 100644 --- a/cmd/nv2/verify.go +++ b/cmd/nv2/verify.go @@ -2,9 +2,8 @@ package main import ( "crypto/x509" - "encoding/json" "fmt" - "os" + "io/ioutil" "github.com/notaryproject/nv2/internal/crypto" "github.com/notaryproject/nv2/pkg/signature" @@ -14,7 +13,7 @@ import ( var verifyCommand = &cli.Command{ Name: "verify", - Usage: "verifies artifacts or images", + Usage: "verifies OCI Artifacts", ArgsUsage: "[]", Flags: []cli.Flag{ &cli.StringFlag{ @@ -38,6 +37,7 @@ var verifyCommand = &cli.Command{ usernameFlag, passwordFlag, insecureFlag, + mediaTypeFlag, }, Action: runVerify, } @@ -48,13 +48,13 @@ func runVerify(ctx *cli.Context) error { if err != nil { return err } - sigma, err := readSignatrueFile(ctx.String("signature")) + sig, err := readSignatrueFile(ctx.String("signature")) if err != nil { return err } // core process - content, _, err := scheme.Verify(sigma) + claims, err := scheme.Verify(sig) if err != nil { return fmt.Errorf("verification failure: %v", err) } @@ -62,8 +62,8 @@ func runVerify(ctx *cli.Context) error { if err != nil { return err } - if content.Manifest.Digest != manifest.Digest || content.Manifest.Size != manifest.Size { - return fmt.Errorf("verification failure: manifest is not signed: %s", manifest.Digest) + if manifest.Descriptor != claims.Manifest.Descriptor { + return fmt.Errorf("verification failure: %s: ", manifest.Digest) } // write out @@ -71,14 +71,12 @@ func runVerify(ctx *cli.Context) error { return nil } -func readSignatrueFile(path string) (sig signature.Signed, err error) { - file, err := os.Open(path) +func readSignatrueFile(path string) (string, error) { + bytes, err := ioutil.ReadFile(path) if err != nil { - return sig, err + return "", err } - defer file.Close() - err = json.NewDecoder(file).Decode(&sig) - return sig, err + return string(bytes), nil } func getSchemeForVerification(ctx *cli.Context) (*signature.Scheme, error) { diff --git a/docs/artifact/README.md b/docs/artifact/README.md deleted file mode 100644 index 0e5df5c05..000000000 --- a/docs/artifact/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Notary V2 Artifact -[Notary v2 signatures](../signature/README.md) can be stored as [OCI artifacts](https://github.com/opencontainers/artifacts). Precisely, it is a [OCI manifest](https://github.com/opencontainers/image-spec/blob/master/manifest.md) with a config of type - -- `application/vnd.cncf.notary.config.v2+json` - -and no layers. - -## Example Artifact - -Example showing the manifest ([examples/manifest.json](examples/manifest.json)) of an artifact. - -```json -{ - "schemaVersion": 2, - "config": { - "mediaType": "application/vnd.cncf.notary.config.v2+json", - "size": 1906, - "digest": "sha256:c7848182f2c817415f0de63206f9e4220012cbb0bdb750c2ecf8020350239814" - }, - "layers": [] -} -``` diff --git a/docs/artifact/examples/manifest.json b/docs/artifact/examples/manifest.json deleted file mode 100644 index 5e57feda6..000000000 --- a/docs/artifact/examples/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "schemaVersion": 2, - "config": { - "mediaType": "application/vnd.cncf.notary.config.v2+json", - "size": 1906, - "digest": "sha256:c7848182f2c817415f0de63206f9e4220012cbb0bdb750c2ecf8020350239814" - }, - "layers": [] -} \ No newline at end of file diff --git a/docs/nv2/README.md b/docs/nv2/README.md index 6ee30200e..319cfbfcd 100644 --- a/docs/nv2/README.md +++ b/docs/nv2/README.md @@ -72,6 +72,7 @@ OPTIONS: --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) ``` @@ -98,38 +99,35 @@ Notary v2 signing is accomplished by signing the OCI manifest representing the a ### Signing using `x509` -To sign the manifest `hello-world_v1-manifest.json` using the key `key.pem` from the `x509` certificate `cert.pem` with the Common Name `registry.acme-rockets.io`, run +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 sign --method x509 \ -k key.key \ -c cert.crt \ -r registry.acme-rockets.io/hello-world:v1 \ - -o hello-world.signature.config.json \ + -o hello-world.signature.config.jwt \ file:hello-world_v1-manifest.json ``` -The formatted x509 signature: `hello-world.signature.config.json` is: +The formatted x509 signature: `hello-world.signature.config.jwt` is: ``` json { - "signed": { - "digest": "sha256:5de47f48e0be1a9d41176a980728449a696fd4fcc37e9d99b8d26618c0f5bf51", - "size": 3056, - "references": [ - "registry.acme-rockets.io/hello-world:v1" - ], - "iat": 1596020554 - }, - "signature": { - "typ": "x509", - "sig": "vUNmuwrdHmcMyvG//eZQLjmIz2gnOUFNaL5Y5Jc3x1oaYu3nFnJxBEkB8232l0zBmV30sVUX2vjao0IDgLMv0Q7VWT2hiTutocgf+oRq88Jz/xKGvByGUWmVyYx9sMW6R+JHK/LlzthCLgDoYTjFD9qDTHf+AWnmRNPLv5nSYNQrVSxNH22jiO3CV/bNEQD8xoR7kZOdov6QzNw3rAP+XvlKxdf/D7vcYdR0D5T9G5xGa72aQSZmzXL/Zd2V7JQnxyJmw6PL3moU1i/8t8RK7LbsU6slvTScLUokFLZxzqCz8TcjujtaThyyxPF47ekx/HVsKW0mYXidpgCOfl+nqw==", - "alg": "RS256", - "x5c": [ - "MIIDJzCCAg+gAwIBAgIUMwVg7bpx8QmWaFzRcgpRFBN6JoQwDQYJKoZIhvcNAQELBQAwIzEhMB8GA1UEAwwYcmVnaXN0cnkuYWNtZS1yb2NrZXRzLmlvMB4XDTIwMDcyOTExMDIzMloXDTIxMDcyOTExMDIzMlowIzEhMB8GA1UEAwwYcmVnaXN0cnkuYWNtZS1yb2NrZXRzLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx2mXqcXqkllwxj7S12WhVDsIu6y4ebZ/CwVwwime44yDcd0bcpdJExqIH/Qy6axQd/1zmLCHPeOXGFq48Ul0oS4Bawj1GEeLvB7VFvqB0KaBeAdxrZAvdKXCXIDH5qyFSGnOmvkja1BuR8XrH7tts5u56i+U3KEDBZg5tfx4cQuKKt0DfXZAL+4RZkNh1LoO77X0ThaBThFoRsg6aZA/cEpttoWmvnO6uUkK73oZEVgZNKGGIZZKzhUjnydRSTphp9GmZzbqUHlOiMvbzdtsQYC0qeQeNqua38HN93Ur3p+oH7oSrBWxX1Xlx933oVb+4G6h5oz0aZvMQ0G6gCLzjwIDAQABo1MwUTAdBgNVHQ4EFgQU8l2F7avSjFZ9TvnpHackunxSFcswHwYDVR0jBBgwFoAU8l2F7avSjFZ9TvnpHackunxSFcswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAwECYhttcbCbqyi7DvOTHw5bixmxplbgD0AmMvE6Ci4P/MrooBququlkri/Jcp58GBaMjxItE4qVsaWwFCEvZEfP2xN4DAbr+rdrIFy9VYuwEIBs5l0ZLRH2H2N3HlqBzhYOjVzNlYfIqnqHUDip2VsUKqhcVFkCmb3cpJ1VNAgjQU2N60JUW28L0XrGyBctBIiicLvdP4NMhHP/hhN2vr2VGIyyo5XtP+QHFi/Uwa48BJ+c9bbVpXeghOMOPMeSJmJ2b/qlp95e/YHlSCfxDXyxZ70N2vBGecrc8ly4tD9KGLb9y3Q7RBgsagOFe7cGQ2db/t60AwTIxP0a9bIyJMg==" - ] - } -} + "typ": "x509", + "alg": "RS256", + "x5c": [ + "MIIDJzCCAg+gAwIBAgIUMwVg7bpx8QmWaFzRcgpRFBN6JoQwDQYJKoZIhvcNAQELBQAwIzEhMB8GA1UEAwwYcmVnaXN0cnkuYWNtZS1yb2NrZXRzLmlvMB4XDTIwMDcyOTExMDIzMloXDTIxMDcyOTExMDIzMlowIzEhMB8GA1UEAwwYcmVnaXN0cnkuYWNtZS1yb2NrZXRzLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx2mXqcXqkllwxj7S12WhVDsIu6y4ebZ/CwVwwime44yDcd0bcpdJExqIH/Qy6axQd/1zmLCHPeOXGFq48Ul0oS4Bawj1GEeLvB7VFvqB0KaBeAdxrZAvdKXCXIDH5qyFSGnOmvkja1BuR8XrH7tts5u56i+U3KEDBZg5tfx4cQuKKt0DfXZAL+4RZkNh1LoO77X0ThaBThFoRsg6aZA/cEpttoWmvnO6uUkK73oZEVgZNKGGIZZKzhUjnydRSTphp9GmZzbqUHlOiMvbzdtsQYC0qeQeNqua38HN93Ur3p+oH7oSrBWxX1Xlx933oVb+4G6h5oz0aZvMQ0G6gCLzjwIDAQABo1MwUTAdBgNVHQ4EFgQU8l2F7avSjFZ9TvnpHackunxSFcswHwYDVR0jBBgwFoAU8l2F7avSjFZ9TvnpHackunxSFcswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAwECYhttcbCbqyi7DvOTHw5bixmxplbgD0AmMvE6Ci4P/MrooBququlkri/Jcp58GBaMjxItE4qVsaWwFCEvZEfP2xN4DAbr+rdrIFy9VYuwEIBs5l0ZLRH2H2N3HlqBzhYOjVzNlYfIqnqHUDip2VsUKqhcVFkCmb3cpJ1VNAgjQU2N60JUW28L0XrGyBctBIiicLvdP4NMhHP/hhN2vr2VGIyyo5XtP+QHFi/Uwa48BJ+c9bbVpXeghOMOPMeSJmJ2b/qlp95e/YHlSCfxDXyxZ70N2vBGecrc8ly4tD9KGLb9y3Q7RBgsagOFe7cGQ2db/t60AwTIxP0a9bIyJMg==" + ] +}.{ + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "digest": "sha256:24a74900a4e749ef31295e5aabde7093e3244b119582bd6e64b1a88c71c410d0", + "size": 3056, + "references": [ + "registry.acme-rockets.io/hello-world:v1" + ], + "iat": 1597053936 +}.[Signature] ``` If the embedded cert chain `x5c` is not desired, it can be replaced by a key ID `kid` by omitting the `-c` option. @@ -138,30 +136,27 @@ If the embedded cert chain `x5c` is not desired, it can be replaced by a key ID nv2 sign -m x509 \ -k key.key \ -r registry.acme-rockets.io/hello-world:v1 \ - -o hello-world.signature.config.json \ + -o hello-world.signature.config.jwt \ file:hello-world_v1-manifest.json ``` -The formatted x509, without the `x5c` chain signature: `hello-world.signature.config.json` is: +The formatted x509, without the `x5c` chain signature: `hello-world.signature.config.jwt` is: ```json { - "signed": { - "digest": "sha256:5de47f48e0be1a9d41176a980728449a696fd4fcc37e9d99b8d26618c0f5bf51", - "size": 3056, - "references": [ - "registry.acme-rockets.io/hello-world:v1" - ], - "iat": 1596020616 - }, - "signature": { - "typ": "x509", - "sig": "OyRPlwwsO5mYDxKkiNeTQlSl4WV8SOiQMCJv4i1+sx7uv6Pe8dHDaPt1SE5s64HzFvo6s26PrfiPYp4RphQOd/KvW2Hh03nS8ZByE4NWFOE6VLQcfNpScba6Q9vAzc3TnZrg1c9t992MGuec1oZB9pR77Ms7Jv/+gZd1qr6VPpA0A6+UucEbN6+pKRTiPRx5WkFXTkN0a4jmlJnev6MyBY3VI0EzjLI4nbCu9P05e4SK1dO0hXtD7aQCf2CCVKdYNHAMX4pNPTLxS3a5p4CFjV3oCbZO6cYT/5ZxgQrVV7vaGEI1MGCOEXS2KSI14zO6KlU1awtOQq3g04e03O+SVQ==", - "alg": "RS256", - "kid": "RQGT:OPJI:IABT:DFXB:52VS:FNOJ:4XBS:H4KY:WHGM:HQMC:WSMN:LKXM" - } -} + "typ": "x509", + "alg": "RS256", + "kid": "RQGT:OPJI:IABT:DFXB:52VS:FNOJ:4XBS:H4KY:WHGM:HQMC:WSMN:LKXM" +}.{ + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "digest": "sha256:24a74900a4e749ef31295e5aabde7093e3244b119582bd6e64b1a88c71c410d0", + "size": 3056, + "references": [ + "registry.acme-rockets.io/hello-world:v1" + ], + "iat": 1597053992 +}.[Signature] ``` The detailed signature specification is [available](../signature/README.md). @@ -184,6 +179,7 @@ OPTIONS: --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) ``` @@ -193,7 +189,7 @@ Since the manifest was signed by a self-signed certificate, that certificate `ce ```shell nv2 verify \ - -f hello-world.signature.config.json \ + -f hello-world.signature.config.jwt \ -c cert.crt \ file:hello-world_v1-manifest.json ``` @@ -202,7 +198,7 @@ If the cert isn't self-signed, you can omit the `-c` parameter. ``` shell nv2 verify \ - -f hello-world.signature.config.json \ + -f hello-world.signature.config.jwt \ file:hello-world_v1-manifest.json sha256:3351c53952446db17d21b86cfe5829ae70f823aff5d410fbf09dff820a39ab55 @@ -223,15 +219,16 @@ Here is an example to sign and verify the image `hello-world` in DockerHub, i.e. ``` shell nv2 sign -m x509 \ -k key.key \ - -o hello-world_latest.signature.config.json \ + -o hello-world_latest.signature.config.jwt \ docker://docker.io/library/hello-world:latest sha256:49a1c8800c94df04e9658809b006fd8a686cab8028d33cfba2cc049724254202 nv2 verify \ -c cert.crt \ - -f hello-world_latest.signature.config.json \ + -f hello-world_latest.signature.config.jwt \ docker://docker.io/library/hello-world:latest + sha256:49a1c8800c94df04e9658809b006fd8a686cab8028d33cfba2cc049724254202 ``` @@ -251,14 +248,14 @@ OCI registry works the same as Docker but with the scheme `oci`. ``` shell nv2 sign -m x509 \ -k key.key \ - -o hello-world_latest.signature.config.json \ + -o hello-world_latest.signature.config.jwt \ oci://docker.io/library/hello-world:latest sha256:0ebe6f409b373c8baf39879fccee6cae5e718003ec3167ded7d54cb2b5da2946 nv2 verify \ -c cert.crt \ - -f hello-world_latest.signature.config.json \ + -f hello-world_latest.signature.config.jwt \ oci://docker.io/library/hello-world:latest sha256:0ebe6f409b373c8baf39879fccee6cae5e718003ec3167ded7d54cb2b5da2946 @@ -286,7 +283,7 @@ sha256:3351c53952446db17d21b86cfe5829ae70f823aff5d410fbf09dff820a39ab55 Since the tag might be changed during the verification process, it is required to pull by digest after verification. ```shell -digest=$(nv2 verify -f hello-world_latest.signature.config.json -c cert.crt docker://docker.io/library/hello-world:latest) +digest=$(nv2 verify -f hello-world_latest.signature.config.jwt -c cert.crt docker://docker.io/library/hello-world:latest) if [ $? -eq 0 ]; then docker pull docker.io/library/hello-world@$digest fi diff --git a/docs/signature/README.md b/docs/signature/README.md index d3b49d7fd..76bca766a 100644 --- a/docs/signature/README.md +++ b/docs/signature/README.md @@ -1,15 +1,15 @@ # Notary V2 Signature Specification -This section defines the signature file, which is in JSON format with no whitespaces. Its JSON schema is available at [schema.json](schema.json). +This section defines the signature file, which is a [JWT](https://tools.ietf.org/html/rfc7519) variant. ## Signature Goals - Offline signature creation -- Persistance within an [OCI Artifact][oci-artifacts] enabled, [distribution-spec][distribution-spec] based registry +- Persistence within an [OCI Artifact][oci-artifacts] enabled, [distribution-spec][distribution-spec] based registry - Artifact and signature copying within and across [OCI Artifact][oci-artifacts] enabled, [distribution-spec][distribution-spec] based registries - Support public registry acquisition of content - where the public registry may host certified content as well as public, non-certified content - Support private registries, where public content may be copied to, and new content originated within -- Air-gapped environments, where the originating registry of content is not accessable +- Air-gapped environments, where the originating registry of content is not accessible - Multiple signatures per artifact, enabling the originating vendor signature, public registry certification and user/environment signatures - Maintain the original artifact digest and collection of associated tags, supporting dev/ops deployment definitions @@ -46,48 +46,70 @@ openssl req \ -out example.crt ``` -An nv2 client would generate the following content to be signed: +An nv2 client would generate the following header and claims to be signed. -``` JSON +The header would be a base64 URL encoded string without paddings: + +``` +eyJ0eXAiOiJ4NTA5IiwiYWxnIjoiUlMyNTYiLCJ4NWMiOlsiTUlJRHN6Q0NBcHVnQXdJQkFnSVVMMWFuRVUveUp5NjdWSlRiSGtOWDBiQk5BbkV3RFFZSktvWklodmNOQVFFTEJRQXdhVEVkTUJzR0ExVUVBd3dVY21WbmFYTjBjbmt1WlhoaGJYQnNaUzVqYjIweEZEQVNCZ05WQkFvTUMyVjRZVzF3YkdVZ2FXNWpNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1YyRnphR2x1WjNSdmJqRVFNQTRHQTFVRUJ3d0hVMlZoZEhSc1pUQWVGdzB5TURBM01qY3hORFF6TkRaYUZ3MHlNVEEzTWpjeE5EUXpORFphTUdreEhUQWJCZ05WQkFNTUZISmxaMmx6ZEhKNUxtVjRZVzF3YkdVdVkyOXRNUlF3RWdZRFZRUUtEQXRsZUdGdGNHeGxJR2x1WXpFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01DbGRoYzJocGJtZDBiMjR4RURBT0JnTlZCQWNNQjFObFlYUjBiR1V3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRGtLd0FjVjQ0cHNqTjhubm8xZVozenYxWktVaEpBb3h3Qk9JR2ZJeEllK2lIdHBYTHZGRlZ3azVKYnh1K1BraWcyTjRCM0lscmovVnJ5aTBoeHA0bWFnMDJNNzMzYlhMUkVOU09GT05Sa3NscE84ekhVTjVwWWRuaFRTd1lUTGFwMSsxYmdjRlN1VVhMV2llcVpCNnFjN2tpdjNiajNTUGFmNDIrczQ4VjQ5dC9PcFh4THRnaVdMOVhrdURUWmN0cEpKQTR2SEhrNk91MGJjZzdpR20rTDF4d0lmYjhNbDRvV3ZUMFNGMzVmZ1cwOGJiTFhaMnYxWENMUnNyV1VnYnE0VStLeHRFcEczWElZY1loS3gxcklyVWhmRUprdUh6Z1BnbE0xMWdHNVcrQ3lmZyt3Zk9KaWc1cTZheElLV3pJZjZDOG04bG15NmJNK041RXNEOVN2QWdNQkFBR2pVekJSTUIwR0ExVWREZ1FXQkJUZjFoTTYvaWJHRit1L1NWQUs4OEZVTWp6Um9UQWZCZ05WSFNNRUdEQVdnQlRmMWhNNi9pYkdGK3UvU1ZBSzg4RlVNanpSb1RBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFCZ3ZWYXU1KzJ3QXVDc21PeXlHMjhoMXp5QzRJUG1NbXBSWlRET3AvcExkd1hlSGpKcjhrRUMzbDkycUpFdmMrV0Fib0oxUm91Y0h5Y1VlN1JXaDJDNlpGL1dQQ0JMeVdHd25seXFHeVJNOS9qODZVSjFPZ2l1Wmw3a2w5enh3V29heFBCQ21IYTBSSG93ZFFCN0FWbHBxZzFjN0ZoS2poVUNCbUdUNFZlOHRWMGhkWnRyWm9RVis2eEhQYlVkMzdLVjFCMUJtZm8zbzRla29KS2hVdTk5RW8wM09wRTNKTHRNMTNBMUh4QUJFdVFHSFRJMHR5Y0RCQmRSbjNiMDNIb0loVTBWbnFqdnBWMUtQdnNyZ1lpLzBWU3RMTmV6WlBnR2UwZkczWGd5OHlla2RCOU5NVW4relpMQVRJNCt6OGo0UUg1V2o1WlBhVWt5b0FEMm9VSk8iXX0 +``` + +The parsed and formatted header would be: + +```json { - "signed": { - "digest": "sha256:c4516b8a311e85f1f2a60573abf4c6b740ca3ade4127e29b05616848de487d34", - "size": 528, - "references": [ - "registry.example.com/example:latest", - "registry.example.com/example:v1.0" - ], - "exp": 1627555319, - "nbf": 1596019319, - "iat": 1596019319 - } + "typ": "x509", + "alg": "RS256", + "x5c": [ + "MIIDszCCApugAwIBAgIUL1anEU/yJy67VJTbHkNX0bBNAnEwDQYJKoZIhvcNAQELBQAwaTEdMBsGA1UEAwwUcmVnaXN0cnkuZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUgaW5jMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTAeFw0yMDA3MjcxNDQzNDZaFw0yMTA3MjcxNDQzNDZaMGkxHTAbBgNVBAMMFHJlZ2lzdHJ5LmV4YW1wbGUuY29tMRQwEgYDVQQKDAtleGFtcGxlIGluYzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkKwAcV44psjN8nno1eZ3zv1ZKUhJAoxwBOIGfIxIe+iHtpXLvFFVwk5Jbxu+Pkig2N4B3Ilrj/Vryi0hxp4mag02M733bXLRENSOFONRkslpO8zHUN5pYdnhTSwYTLap1+1bgcFSuUXLWieqZB6qc7kiv3bj3SPaf42+s48V49t/OpXxLtgiWL9XkuDTZctpJJA4vHHk6Ou0bcg7iGm+L1xwIfb8Ml4oWvT0SF35fgW08bbLXZ2v1XCLRsrWUgbq4U+KxtEpG3XIYcYhKx1rIrUhfEJkuHzgPglM11gG5W+Cyfg+wfOJig5q6axIKWzIf6C8m8lmy6bM+N5EsD9SvAgMBAAGjUzBRMB0GA1UdDgQWBBTf1hM6/ibGF+u/SVAK88FUMjzRoTAfBgNVHSMEGDAWgBTf1hM6/ibGF+u/SVAK88FUMjzRoTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBgvVau5+2wAuCsmOyyG28h1zyC4IPmMmpRZTDOp/pLdwXeHjJr8kEC3l92qJEvc+WAboJ1RoucHycUe7RWh2C6ZF/WPCBLyWGwnlyqGyRM9/j86UJ1OgiuZl7kl9zxwWoaxPBCmHa0RHowdQB7AVlpqg1c7FhKjhUCBmGT4Ve8tV0hdZtrZoQV+6xHPbUd37KV1B1Bmfo3o4ekoJKhUu99Eo03OpE3JLtM13A1HxABEuQGHTI0tycDBBdRn3b03HoIhU0VnqjvpV1KPvsrgYi/0VStLNezZPgGe0fG3Xgy8yekdB9NMUn+zZLATI4+z8j4QH5Wj5ZPaUkyoAD2oUJO" + ] } ``` -The signature of the above would be represented as: +The claims would be a base64 URL encoded string without paddings: + +``` +eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OmM0NTE2YjhhMzExZTg1ZjFmMmE2MDU3M2FiZjRjNmI3NDBjYTNhZGU0MTI3ZTI5YjA1NjE2ODQ4ZGU0ODdkMzQiLCJzaXplIjo1MjgsInJlZmVyZW5jZXMiOlsicmVnaXN0cnkuZXhhbXBsZS5jb20vZXhhbXBsZTpsYXRlc3QiLCJyZWdpc3RyeS5leGFtcGxlLmNvbS9leGFtcGxlOnYxLjAiXSwiZXhwIjoxNjI4NTg3MTE5LCJpYXQiOjE1OTcwNTExMTksIm5iZiI6MTU5NzA1MTExOX0 +``` + +The parsed and formatted claims would be: ``` JSON { - "signature": { - "typ": "x509", - "sig": "UFqN24K2fLj7/h2slM68PLTfF9CDhrEVGuMQ8m3kkQJ4SKusj9fNxYV78tTiedqB+E8SqVH66mZbdlTrVQFJAd7aL2c3NZFfo92pE9SaHnqEDqnnGWXGRVjtBRM13YyRDm2wD8aRyuL5jEDUkTw7jBLY0+LfKHMDuYCsOOzvedof7aiaFc3qA+qKiW53jn2uEGCFfAs0LmsNafGfAtVmdGSO4zX4fdnQFAGT8sbUmL71uXl9W1B6tGeLfx5nBoQUvtplQipHly/yMQvWw7qMXsaAsf/BbGDmivN06CRahSb7VOwNq6K7Py4zYeiW40hEFVz9L7/5xT5XI1unKPZDuw==", - "alg": "RS256", - "x5c": [ - "MIIDszCCApugAwIBAgIUL1anEU/yJy67VJTbHkNX0bBNAnEwDQYJKoZIhvcNAQELBQAwaTEdMBsGA1UEAwwUcmVnaXN0cnkuZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUgaW5jMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTAeFw0yMDA3MjcxNDQzNDZaFw0yMTA3MjcxNDQzNDZaMGkxHTAbBgNVBAMMFHJlZ2lzdHJ5LmV4YW1wbGUuY29tMRQwEgYDVQQKDAtleGFtcGxlIGluYzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkKwAcV44psjN8nno1eZ3zv1ZKUhJAoxwBOIGfIxIe+iHtpXLvFFVwk5Jbxu+Pkig2N4B3Ilrj/Vryi0hxp4mag02M733bXLRENSOFONRkslpO8zHUN5pYdnhTSwYTLap1+1bgcFSuUXLWieqZB6qc7kiv3bj3SPaf42+s48V49t/OpXxLtgiWL9XkuDTZctpJJA4vHHk6Ou0bcg7iGm+L1xwIfb8Ml4oWvT0SF35fgW08bbLXZ2v1XCLRsrWUgbq4U+KxtEpG3XIYcYhKx1rIrUhfEJkuHzgPglM11gG5W+Cyfg+wfOJig5q6axIKWzIf6C8m8lmy6bM+N5EsD9SvAgMBAAGjUzBRMB0GA1UdDgQWBBTf1hM6/ibGF+u/SVAK88FUMjzRoTAfBgNVHSMEGDAWgBTf1hM6/ibGF+u/SVAK88FUMjzRoTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBgvVau5+2wAuCsmOyyG28h1zyC4IPmMmpRZTDOp/pLdwXeHjJr8kEC3l92qJEvc+WAboJ1RoucHycUe7RWh2C6ZF/WPCBLyWGwnlyqGyRM9/j86UJ1OgiuZl7kl9zxwWoaxPBCmHa0RHowdQB7AVlpqg1c7FhKjhUCBmGT4Ve8tV0hdZtrZoQV+6xHPbUd37KV1B1Bmfo3o4ekoJKhUu99Eo03OpE3JLtM13A1HxABEuQGHTI0tycDBBdRn3b03HoIhU0VnqjvpV1KPvsrgYi/0VStLNezZPgGe0fG3Xgy8yekdB9NMUn+zZLATI4+z8j4QH5Wj5ZPaUkyoAD2oUJO" - ] - } + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "digest": "sha256:c4516b8a311e85f1f2a60573abf4c6b740ca3ade4127e29b05616848de487d34", + "size": 528, + "references": [ + "registry.example.com/example:latest", + "registry.example.com/example:v1.0" + ], + "exp": 1628587119, + "iat": 1597051119, + "nbf": 1597051119 } ``` +The signature of the above would be represented as a base64 URL encoded string without paddings: + +``` +MtQBOL2FERM2fMSikruHOMQdHuEXAE1wf6J6TfDY2W_7PfQQllBKbJJE0HqJ5ENAbuqNYHNZeIeKUCYFrNx2XgtrKuTe7WCa1ZZKDtp5bmANp484ekdl6lW23YB8r_SRtseJuibqjI3HuiMyELj9uYV1CdRYaD2BIZ_qxraYH1fMpjDWjehU4RYLI37hsSuDQ90o09BwaNfzbQXYPsGmkSUSmej7rOFPDnuwhNy4WcUed3kRKYEW8eIjO9OUBGQq3PWvhDjxZi3QF4QFDoiKBOXL70AjaiVIveQRkJI9-xHZSYwje9OFEMioeNWB5ceZR-r4L7VzDcU-Fxqjxn79Fw +``` + +Putting everything together: + +``` +eyJ0eXAiOiJ4NTA5IiwiYWxnIjoiUlMyNTYiLCJ4NWMiOlsiTUlJRHN6Q0NBcHVnQXdJQkFnSVVMMWFuRVUveUp5NjdWSlRiSGtOWDBiQk5BbkV3RFFZSktvWklodmNOQVFFTEJRQXdhVEVkTUJzR0ExVUVBd3dVY21WbmFYTjBjbmt1WlhoaGJYQnNaUzVqYjIweEZEQVNCZ05WQkFvTUMyVjRZVzF3YkdVZ2FXNWpNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1YyRnphR2x1WjNSdmJqRVFNQTRHQTFVRUJ3d0hVMlZoZEhSc1pUQWVGdzB5TURBM01qY3hORFF6TkRaYUZ3MHlNVEEzTWpjeE5EUXpORFphTUdreEhUQWJCZ05WQkFNTUZISmxaMmx6ZEhKNUxtVjRZVzF3YkdVdVkyOXRNUlF3RWdZRFZRUUtEQXRsZUdGdGNHeGxJR2x1WXpFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01DbGRoYzJocGJtZDBiMjR4RURBT0JnTlZCQWNNQjFObFlYUjBiR1V3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRGtLd0FjVjQ0cHNqTjhubm8xZVozenYxWktVaEpBb3h3Qk9JR2ZJeEllK2lIdHBYTHZGRlZ3azVKYnh1K1BraWcyTjRCM0lscmovVnJ5aTBoeHA0bWFnMDJNNzMzYlhMUkVOU09GT05Sa3NscE84ekhVTjVwWWRuaFRTd1lUTGFwMSsxYmdjRlN1VVhMV2llcVpCNnFjN2tpdjNiajNTUGFmNDIrczQ4VjQ5dC9PcFh4THRnaVdMOVhrdURUWmN0cEpKQTR2SEhrNk91MGJjZzdpR20rTDF4d0lmYjhNbDRvV3ZUMFNGMzVmZ1cwOGJiTFhaMnYxWENMUnNyV1VnYnE0VStLeHRFcEczWElZY1loS3gxcklyVWhmRUprdUh6Z1BnbE0xMWdHNVcrQ3lmZyt3Zk9KaWc1cTZheElLV3pJZjZDOG04bG15NmJNK041RXNEOVN2QWdNQkFBR2pVekJSTUIwR0ExVWREZ1FXQkJUZjFoTTYvaWJHRit1L1NWQUs4OEZVTWp6Um9UQWZCZ05WSFNNRUdEQVdnQlRmMWhNNi9pYkdGK3UvU1ZBSzg4RlVNanpSb1RBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFCZ3ZWYXU1KzJ3QXVDc21PeXlHMjhoMXp5QzRJUG1NbXBSWlRET3AvcExkd1hlSGpKcjhrRUMzbDkycUpFdmMrV0Fib0oxUm91Y0h5Y1VlN1JXaDJDNlpGL1dQQ0JMeVdHd25seXFHeVJNOS9qODZVSjFPZ2l1Wmw3a2w5enh3V29heFBCQ21IYTBSSG93ZFFCN0FWbHBxZzFjN0ZoS2poVUNCbUdUNFZlOHRWMGhkWnRyWm9RVis2eEhQYlVkMzdLVjFCMUJtZm8zbzRla29KS2hVdTk5RW8wM09wRTNKTHRNMTNBMUh4QUJFdVFHSFRJMHR5Y0RCQmRSbjNiMDNIb0loVTBWbnFqdnBWMUtQdnNyZ1lpLzBWU3RMTmV6WlBnR2UwZkczWGd5OHlla2RCOU5NVW4relpMQVRJNCt6OGo0UUg1V2o1WlBhVWt5b0FEMm9VSk8iXX0.eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OmM0NTE2YjhhMzExZTg1ZjFmMmE2MDU3M2FiZjRjNmI3NDBjYTNhZGU0MTI3ZTI5YjA1NjE2ODQ4ZGU0ODdkMzQiLCJzaXplIjo1MjgsInJlZmVyZW5jZXMiOlsicmVnaXN0cnkuZXhhbXBsZS5jb20vZXhhbXBsZTpsYXRlc3QiLCJyZWdpc3RyeS5leGFtcGxlLmNvbS9leGFtcGxlOnYxLjAiXSwiZXhwIjoxNjI4NTg3MTE5LCJpYXQiOjE1OTcwNTExMTksIm5iZiI6MTU5NzA1MTExOX0.MtQBOL2FERM2fMSikruHOMQdHuEXAE1wf6J6TfDY2W_7PfQQllBKbJJE0HqJ5ENAbuqNYHNZeIeKUCYFrNx2XgtrKuTe7WCa1ZZKDtp5bmANp484ekdl6lW23YB8r_SRtseJuibqjI3HuiMyELj9uYV1CdRYaD2BIZ_qxraYH1fMpjDWjehU4RYLI37hsSuDQ90o09BwaNfzbQXYPsGmkSUSmej7rOFPDnuwhNy4WcUed3kRKYEW8eIjO9OUBGQq3PWvhDjxZi3QF4QFDoiKBOXL70AjaiVIveQRkJI9-xHZSYwje9OFEMioeNWB5ceZR-r4L7VzDcU-Fxqjxn79Fw +``` + ### Signature Persisted within an OCI Artifact Enabled Registry -Both values are persisted in a `signature.json` file. The file would be submitted to a registry as an Artifact with null layers. -The `signature.json` would be persisted within the `manifest.config` object +All values are persisted in a `signature.jwt` file. The file would be submitted to a registry as an Artifact with null layers. +The `signature.jwt` would be persisted within the `manifest.config` object ``` SHELL oras push \ registry.example.com/hello-world:v1.0 \ - --manifest-config signature.json:application/vnd.cncf.notary.config.v2+json + --manifest-config signature.json:application/vnd.cncf.notary.config.v2+jwt ``` Would push the following manifest: @@ -96,7 +118,7 @@ Would push the following manifest: { "schemaVersion": 2, "config": { - "mediaType": "application/vnd.cncf.notary.config.v2+json", + "mediaType": "application/vnd.cncf.notary.config.v2+jwt", "size": 1906, "digest": "sha256:c7848182f2c817415f0de63206f9e4220012cbb0bdb750c2ecf8020350239814" }, @@ -106,116 +128,108 @@ Would push the following manifest: ## *Signature* Property Descriptions -- **`signed`** *object* - - This REQUIRED property provides the signed content. +### Header - - **`iat`** *integer* +- **`typ`** *string* - This OPTIONAL property identities the time at which the manifests were presented to the notary. This field is based on [RFC 7519 Section 4.1.6](https://tools.ietf.org/html/rfc7519#section-4.1.6). When used, it does not imply the issue time of any signature in the `signatures` property. + This REQUIRED property identifies the signature type. Implementations MUST support at least the following types - - **`nbf`** *integer* + - `x509`: X.509 public key certificates. Implementations MUST verify that the certificate of the signing key has the `digitalSignature` `Key Usage` extension ([RFC 5280 Section 4.2.1.3](https://tools.ietf.org/html/rfc5280#section-4.2.1.3)). - This OPTIONAL property identifies the time before which the signed content MUST NOT be accepted for processing. This field is based on [RFC 7519 Section 4.1.5](https://tools.ietf.org/html/rfc7519#section-4.1.5). + Implementations MAY support the following types - - **`exp`** *integer* + - `tuf`: [The update framework](https://theupdateframework.io/). - This OPTIONAL property identifies the expiration time on or after which the signed content MUST NOT be accepted for processing. This field is based on [RFC 7519 Section 4.1.4](https://tools.ietf.org/html/rfc7519#section-4.1.4). + Although the signature file is a JWT, type `JWT` is not used as it is not an authentication or authorization token. - - **`digest`** *string* +- **`alg`** *string* - This REQUIRED property is the *digest* of the target manifest, conforming to the requirements outlined in [Digests](https://github.com/opencontainers/image-spec/blob/master/descriptor.md#digests). If the actual content is fetched according to the *digest*, implementations MUST verify the content against the *digest*. + This REQUIRED property for the `x509` type identifies the cryptographic algorithm used to sign the content. This field is based on [RFC 7515 Section 4.1.1](https://tools.ietf.org/html/rfc7515#section-4.1.1). - - **`size`** *integer* +- **`x5c`** *array of strings* - This REQUIRED property is the *size* of the target manifest. If the actual content is fetched according the *digest*, implementations MUST verify the content against the *size*. + This OPTIONAL property for the `x509` type contains the X.509 public key certificate or certificate chain corresponding to the key used to digitally sign the content. The certificates are encoded in base64. This field is based on [RFC 7515 Section 4.1.6](https://tools.ietf.org/html/rfc7515#section-4.1.6). - - **`references`** *array of strings* +- **`kid`** *string* - This OPTIONAL property claims the manifest references of its origin. The format of the value MUST matches the [*reference* grammar](https://github.com/docker/distribution/blob/master/reference/reference.go). With used, the `x509` signatures are valid only if the domain names of all references match the Common Name (`CN`) in the `Subject` field of the certificate. + This OPTIONAL property for the `x509` type is a hint (key ID) indicating which key was used to sign the content. This field is based on [RFC 7515 Section 4.1.4](https://tools.ietf.org/html/rfc7515#section-4.1.4). -- **`signature`** *string* +### Claims - This REQUIRED property provides the signature of the signed content. The entire signature file is valid if any signature is valid. The `signature` object is influenced by JSON Web Signature (JWS) at [RFC 7515](https://tools.ietf.org/html/rfc7515). +- **`iat`** *integer* - - **`typ`** *string* + This OPTIONAL property identities the time at which the manifests were presented to the notary. This field is based on [RFC 7519 Section 4.1.6](https://tools.ietf.org/html/rfc7519#section-4.1.6). When used, it does not imply the issue time of any signature in the `signatures` property. - This REQUIRED property identifies the signature type. Implementations MUST support at least the following types +- **`nbf`** *integer* - - `x509`: X.509 public key certificates. Implementations MUST verify that the certificate of the signing key has the `digitalSignature` `Key Usage` extension ([RFC 5280 Section 4.2.1.3](https://tools.ietf.org/html/rfc5280#section-4.2.1.3)). + This OPTIONAL property identifies the time before which the signed content MUST NOT be accepted for processing. This field is based on [RFC 7519 Section 4.1.5](https://tools.ietf.org/html/rfc7519#section-4.1.5). - Implementations MAY support the following types +- **`exp`** *integer* - - `tuf`: [The update framework](https://theupdateframework.io/). + This OPTIONAL property identifies the expiration time on or after which the signed content MUST NOT be accepted for processing. This field is based on [RFC 7519 Section 4.1.4](https://tools.ietf.org/html/rfc7519#section-4.1.4). - - **`sig`** *string* +- **`mediaType`** *string* - This REQUIRED property provides the base64-encoded signature binary of the specified signature type. + This REQUIRED property contains the media type of the referenced content. Values MUST comply with [RFC 6838][rfc6838], including the [naming requirements in its section 4.2][rfc6838-s4.2]. - - **`alg`** *string* +- **`digest`** *string* - This REQUIRED property for the `x509` type identifies the cryptographic algorithm used to sign the content. This field is based on [RFC 7515 Section 4.1.1](https://tools.ietf.org/html/rfc7515#section-4.1.1). + This REQUIRED property is the *digest* of the target manifest, conforming to the requirements outlined in [Digests](https://github.com/opencontainers/image-spec/blob/master/descriptor.md#digests). If the actual content is fetched according to the *digest*, implementations MUST verify the content against the *digest*. - - **`x5c`** *array of strings* +- **`size`** *integer* - This OPTIONAL property for the `x509` type contains the X.509 public key certificate or certificate chain corresponding to the key used to digitally sign the content. The certificates are encoded in base64. This field is based on [RFC 7515 Section 4.1.6](https://tools.ietf.org/html/rfc7515#section-4.1.6). + This REQUIRED property is the *size* of the target manifest. If the actual content is fetched according the *digest*, implementations MUST verify the content against the *size*. - - **`kid`** *string* +- **`references`** *array of strings* - This OPTIONAL property for the `x509` type is a hint (key ID) indicating which key was used to sign the content. This field is based on [RFC 7515 Section 4.1.4](https://tools.ietf.org/html/rfc7515#section-4.1.4). + This OPTIONAL property claims the manifest references of its origin. The format of the value MUST matches the [*reference* grammar](https://github.com/docker/distribution/blob/master/reference/reference.go). With used, the `x509` signatures are valid only if the domain names of all references match the Common Name (`CN`) in the `Subject` field of the certificate. ## Example Signatures ### x509 Signature -Example showing a formatted `x509` signature file [examples/x509_x5c.nv2.json](examples/x509_x5c.nv2.json) with certificates provided by `x5c`: +Example showing a formatted `x509` signature file [examples/x509_x5c.nv2.jwt](examples/x509_x5c.nv2.jwt) with certificates provided by `x5c`: ```json { - "signed": { - "digest": "sha256:c4516b8a311e85f1f2a60573abf4c6b740ca3ade4127e29b05616848de487d34", - "size": 528, - "references": [ - "registry.example.com/example:latest", - "registry.example.com/example:v1.0" - ], - "exp": 1627555319, - "nbf": 1596019319, - "iat": 1596019319 - }, - "signature": { - "typ": "x509", - "sig": "UFqN24K2fLj7/h2slM68PLTfF9CDhrEVGuMQ8m3kkQJ4SKusj9fNxYV78tTiedqB+E8SqVH66mZbdlTrVQFJAd7aL2c3NZFfo92pE9SaHnqEDqnnGWXGRVjtBRM13YyRDm2wD8aRyuL5jEDUkTw7jBLY0+LfKHMDuYCsOOzvedof7aiaFc3qA+qKiW53jn2uEGCFfAs0LmsNafGfAtVmdGSO4zX4fdnQFAGT8sbUmL71uXl9W1B6tGeLfx5nBoQUvtplQipHly/yMQvWw7qMXsaAsf/BbGDmivN06CRahSb7VOwNq6K7Py4zYeiW40hEFVz9L7/5xT5XI1unKPZDuw==", - "alg": "RS256", - "x5c": [ - "MIIDszCCApugAwIBAgIUL1anEU/yJy67VJTbHkNX0bBNAnEwDQYJKoZIhvcNAQELBQAwaTEdMBsGA1UEAwwUcmVnaXN0cnkuZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUgaW5jMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTAeFw0yMDA3MjcxNDQzNDZaFw0yMTA3MjcxNDQzNDZaMGkxHTAbBgNVBAMMFHJlZ2lzdHJ5LmV4YW1wbGUuY29tMRQwEgYDVQQKDAtleGFtcGxlIGluYzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkKwAcV44psjN8nno1eZ3zv1ZKUhJAoxwBOIGfIxIe+iHtpXLvFFVwk5Jbxu+Pkig2N4B3Ilrj/Vryi0hxp4mag02M733bXLRENSOFONRkslpO8zHUN5pYdnhTSwYTLap1+1bgcFSuUXLWieqZB6qc7kiv3bj3SPaf42+s48V49t/OpXxLtgiWL9XkuDTZctpJJA4vHHk6Ou0bcg7iGm+L1xwIfb8Ml4oWvT0SF35fgW08bbLXZ2v1XCLRsrWUgbq4U+KxtEpG3XIYcYhKx1rIrUhfEJkuHzgPglM11gG5W+Cyfg+wfOJig5q6axIKWzIf6C8m8lmy6bM+N5EsD9SvAgMBAAGjUzBRMB0GA1UdDgQWBBTf1hM6/ibGF+u/SVAK88FUMjzRoTAfBgNVHSMEGDAWgBTf1hM6/ibGF+u/SVAK88FUMjzRoTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBgvVau5+2wAuCsmOyyG28h1zyC4IPmMmpRZTDOp/pLdwXeHjJr8kEC3l92qJEvc+WAboJ1RoucHycUe7RWh2C6ZF/WPCBLyWGwnlyqGyRM9/j86UJ1OgiuZl7kl9zxwWoaxPBCmHa0RHowdQB7AVlpqg1c7FhKjhUCBmGT4Ve8tV0hdZtrZoQV+6xHPbUd37KV1B1Bmfo3o4ekoJKhUu99Eo03OpE3JLtM13A1HxABEuQGHTI0tycDBBdRn3b03HoIhU0VnqjvpV1KPvsrgYi/0VStLNezZPgGe0fG3Xgy8yekdB9NMUn+zZLATI4+z8j4QH5Wj5ZPaUkyoAD2oUJO" - ] - } -} + "typ": "x509", + "alg": "RS256", + "x5c": [ + "MIIDszCCApugAwIBAgIUL1anEU/yJy67VJTbHkNX0bBNAnEwDQYJKoZIhvcNAQELBQAwaTEdMBsGA1UEAwwUcmVnaXN0cnkuZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUgaW5jMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTAeFw0yMDA3MjcxNDQzNDZaFw0yMTA3MjcxNDQzNDZaMGkxHTAbBgNVBAMMFHJlZ2lzdHJ5LmV4YW1wbGUuY29tMRQwEgYDVQQKDAtleGFtcGxlIGluYzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkKwAcV44psjN8nno1eZ3zv1ZKUhJAoxwBOIGfIxIe+iHtpXLvFFVwk5Jbxu+Pkig2N4B3Ilrj/Vryi0hxp4mag02M733bXLRENSOFONRkslpO8zHUN5pYdnhTSwYTLap1+1bgcFSuUXLWieqZB6qc7kiv3bj3SPaf42+s48V49t/OpXxLtgiWL9XkuDTZctpJJA4vHHk6Ou0bcg7iGm+L1xwIfb8Ml4oWvT0SF35fgW08bbLXZ2v1XCLRsrWUgbq4U+KxtEpG3XIYcYhKx1rIrUhfEJkuHzgPglM11gG5W+Cyfg+wfOJig5q6axIKWzIf6C8m8lmy6bM+N5EsD9SvAgMBAAGjUzBRMB0GA1UdDgQWBBTf1hM6/ibGF+u/SVAK88FUMjzRoTAfBgNVHSMEGDAWgBTf1hM6/ibGF+u/SVAK88FUMjzRoTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBgvVau5+2wAuCsmOyyG28h1zyC4IPmMmpRZTDOp/pLdwXeHjJr8kEC3l92qJEvc+WAboJ1RoucHycUe7RWh2C6ZF/WPCBLyWGwnlyqGyRM9/j86UJ1OgiuZl7kl9zxwWoaxPBCmHa0RHowdQB7AVlpqg1c7FhKjhUCBmGT4Ve8tV0hdZtrZoQV+6xHPbUd37KV1B1Bmfo3o4ekoJKhUu99Eo03OpE3JLtM13A1HxABEuQGHTI0tycDBBdRn3b03HoIhU0VnqjvpV1KPvsrgYi/0VStLNezZPgGe0fG3Xgy8yekdB9NMUn+zZLATI4+z8j4QH5Wj5ZPaUkyoAD2oUJO" + ] +}.{ + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "digest": "sha256:c4516b8a311e85f1f2a60573abf4c6b740ca3ade4127e29b05616848de487d34", + "size": 528, + "references": [ + "registry.example.com/example:latest", + "registry.example.com/example:v1.0" + ], + "exp": 1628587119, + "iat": 1597051119, + "nbf": 1597051119 +}.[Signature] ``` -Example showing a formatted `x509` signature file [examples/x509_kid.nv2.json](examples/x509_kid.nv2.json) with certificates referenced by `kid`: +Example showing a formatted `x509` signature file [examples/x509_kid.nv2.jwt](examples/x509_kid.nv2.jwt) with certificates referenced by `kid`: ```json { - "signed": { - "digest": "sha256:c4516b8a311e85f1f2a60573abf4c6b740ca3ade4127e29b05616848de487d34", - "size": 528, - "references": [ - "registry.example.com/example:latest", - "registry.example.com/example:v1.0" - ], - "exp": 1627554920, - "nbf": 1596018920, - "iat": 1596018920 - }, - "signature": { - "typ": "x509", - "sig": "emzP9ygJD3y2ZWMYGO/wyqOhaSxrhd4ZdmjC9Zd+Ba7gGmGzBylsY1CskyZw389Hz2Z0xA6AQLhaNBbbqyxuAxVXtataMRsqCl/cgyNbyYU1URB2aTUZY/3V4iJzH1O/QfwSkpQa3aN1OCL8uMBNCtM6Rde9+SX8Q8XNMByDbuXtyPDvnKunZxpofEn2ibLe2Cm3o+MTK4pgxacEWeld85gTb06NicARf7mcVj7bflLyUIgel4qvmdqT6896Gtd2ES1KawvyjoEyskdlVlneSTdEKGRYxfchwIUK4E7p3EtTnmj+FuD9MpCtP0M4CQiOr19j0NtQe2bHuTo4bwtjuw==", - "alg": "RS256", - "kid": "XP5O:Y7W2:PRB6:O355:56CC:P3A6:CBDV:EDMN:QZCK:W5PO:QMV3:T2LX" - } -} + "typ": "x509", + "alg": "RS256", + "kid": "XP5O:Y7W2:PRB6:O355:56CC:P3A6:CBDV:EDMN:QZCK:W5PO:QMV3:T2LX" +}.{ + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "digest": "sha256:c4516b8a311e85f1f2a60573abf4c6b740ca3ade4127e29b05616848de487d34", + "size": 528, + "references": [ + "registry.example.com/example:latest", + "registry.example.com/example:v1.0" + ], + "exp": 1628587341, + "iat": 1597051341, + "nbf": 1597051341 +}.[Signature] ``` [distribution-spec]: https://github.com/opencontainers/distribution-spec diff --git a/docs/signature/examples/x509_kid.nv2.json b/docs/signature/examples/x509_kid.nv2.json deleted file mode 100644 index dc30f0a6e..000000000 --- a/docs/signature/examples/x509_kid.nv2.json +++ /dev/null @@ -1 +0,0 @@ -{"signed":{"digest":"sha256:c4516b8a311e85f1f2a60573abf4c6b740ca3ade4127e29b05616848de487d34","size":528,"references":["registry.example.com/example:latest","registry.example.com/example:v1.0"],"exp":1627554920,"nbf":1596018920,"iat":1596018920},"signature":{"typ":"x509","sig":"emzP9ygJD3y2ZWMYGO/wyqOhaSxrhd4ZdmjC9Zd+Ba7gGmGzBylsY1CskyZw389Hz2Z0xA6AQLhaNBbbqyxuAxVXtataMRsqCl/cgyNbyYU1URB2aTUZY/3V4iJzH1O/QfwSkpQa3aN1OCL8uMBNCtM6Rde9+SX8Q8XNMByDbuXtyPDvnKunZxpofEn2ibLe2Cm3o+MTK4pgxacEWeld85gTb06NicARf7mcVj7bflLyUIgel4qvmdqT6896Gtd2ES1KawvyjoEyskdlVlneSTdEKGRYxfchwIUK4E7p3EtTnmj+FuD9MpCtP0M4CQiOr19j0NtQe2bHuTo4bwtjuw==","alg":"RS256","kid":"XP5O:Y7W2:PRB6:O355:56CC:P3A6:CBDV:EDMN:QZCK:W5PO:QMV3:T2LX"}} \ No newline at end of file diff --git a/docs/signature/examples/x509_kid.nv2.jwt b/docs/signature/examples/x509_kid.nv2.jwt new file mode 100644 index 000000000..444cd2790 --- /dev/null +++ b/docs/signature/examples/x509_kid.nv2.jwt @@ -0,0 +1 @@ +eyJ0eXAiOiJ4NTA5IiwiYWxnIjoiUlMyNTYiLCJraWQiOiJYUDVPOlk3VzI6UFJCNjpPMzU1OjU2Q0M6UDNBNjpDQkRWOkVETU46UVpDSzpXNVBPOlFNVjM6VDJMWCJ9.eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OmM0NTE2YjhhMzExZTg1ZjFmMmE2MDU3M2FiZjRjNmI3NDBjYTNhZGU0MTI3ZTI5YjA1NjE2ODQ4ZGU0ODdkMzQiLCJzaXplIjo1MjgsInJlZmVyZW5jZXMiOlsicmVnaXN0cnkuZXhhbXBsZS5jb20vZXhhbXBsZTpsYXRlc3QiLCJyZWdpc3RyeS5leGFtcGxlLmNvbS9leGFtcGxlOnYxLjAiXSwiZXhwIjoxNjI4NTg3MzQxLCJpYXQiOjE1OTcwNTEzNDEsIm5iZiI6MTU5NzA1MTM0MX0.cr9C_Py-IJcgIUXtHAQ9dFmZO4JBEOedPdg67Fm-Av8vMQBHrs7kHZOqZhF33OYR7tuG94v760RlrCrBl1OhUpk5umLjeCOk1-RBqSWUhM7GxwfeIWEIC10gzmolHVI55nb27QQxq0pTqhAC9Nof6QljFG8kyqYqjn0cr3X1zt23ppyJ1CYkcdXdDL0QD8-1EnngHAYcssun8A9dKveld-O-dMq94wk2FkSuKz6WSOM1I5E-thbq6NltB7dzLuZAkU4LXAqODCJ7fTQgUvtapzyEMvV6cQwAG1sUV1yEST0A6t6U_0Tt-X32_kciptVuzbtRLYuOW8Wzv7E41ryU6w \ No newline at end of file diff --git a/docs/signature/examples/x509_x5c.nv2.json b/docs/signature/examples/x509_x5c.nv2.json deleted file mode 100644 index 2c621e223..000000000 --- a/docs/signature/examples/x509_x5c.nv2.json +++ /dev/null @@ -1 +0,0 @@ -{"signed":{"digest":"sha256:c4516b8a311e85f1f2a60573abf4c6b740ca3ade4127e29b05616848de487d34","size":528,"references":["registry.example.com/example:latest","registry.example.com/example:v1.0"],"exp":1627555319,"nbf":1596019319,"iat":1596019319},"signature":{"typ":"x509","sig":"UFqN24K2fLj7/h2slM68PLTfF9CDhrEVGuMQ8m3kkQJ4SKusj9fNxYV78tTiedqB+E8SqVH66mZbdlTrVQFJAd7aL2c3NZFfo92pE9SaHnqEDqnnGWXGRVjtBRM13YyRDm2wD8aRyuL5jEDUkTw7jBLY0+LfKHMDuYCsOOzvedof7aiaFc3qA+qKiW53jn2uEGCFfAs0LmsNafGfAtVmdGSO4zX4fdnQFAGT8sbUmL71uXl9W1B6tGeLfx5nBoQUvtplQipHly/yMQvWw7qMXsaAsf/BbGDmivN06CRahSb7VOwNq6K7Py4zYeiW40hEFVz9L7/5xT5XI1unKPZDuw==","alg":"RS256","x5c":["MIIDszCCApugAwIBAgIUL1anEU/yJy67VJTbHkNX0bBNAnEwDQYJKoZIhvcNAQELBQAwaTEdMBsGA1UEAwwUcmVnaXN0cnkuZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUgaW5jMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTAeFw0yMDA3MjcxNDQzNDZaFw0yMTA3MjcxNDQzNDZaMGkxHTAbBgNVBAMMFHJlZ2lzdHJ5LmV4YW1wbGUuY29tMRQwEgYDVQQKDAtleGFtcGxlIGluYzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkKwAcV44psjN8nno1eZ3zv1ZKUhJAoxwBOIGfIxIe+iHtpXLvFFVwk5Jbxu+Pkig2N4B3Ilrj/Vryi0hxp4mag02M733bXLRENSOFONRkslpO8zHUN5pYdnhTSwYTLap1+1bgcFSuUXLWieqZB6qc7kiv3bj3SPaf42+s48V49t/OpXxLtgiWL9XkuDTZctpJJA4vHHk6Ou0bcg7iGm+L1xwIfb8Ml4oWvT0SF35fgW08bbLXZ2v1XCLRsrWUgbq4U+KxtEpG3XIYcYhKx1rIrUhfEJkuHzgPglM11gG5W+Cyfg+wfOJig5q6axIKWzIf6C8m8lmy6bM+N5EsD9SvAgMBAAGjUzBRMB0GA1UdDgQWBBTf1hM6/ibGF+u/SVAK88FUMjzRoTAfBgNVHSMEGDAWgBTf1hM6/ibGF+u/SVAK88FUMjzRoTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBgvVau5+2wAuCsmOyyG28h1zyC4IPmMmpRZTDOp/pLdwXeHjJr8kEC3l92qJEvc+WAboJ1RoucHycUe7RWh2C6ZF/WPCBLyWGwnlyqGyRM9/j86UJ1OgiuZl7kl9zxwWoaxPBCmHa0RHowdQB7AVlpqg1c7FhKjhUCBmGT4Ve8tV0hdZtrZoQV+6xHPbUd37KV1B1Bmfo3o4ekoJKhUu99Eo03OpE3JLtM13A1HxABEuQGHTI0tycDBBdRn3b03HoIhU0VnqjvpV1KPvsrgYi/0VStLNezZPgGe0fG3Xgy8yekdB9NMUn+zZLATI4+z8j4QH5Wj5ZPaUkyoAD2oUJO"]}} \ No newline at end of file diff --git a/docs/signature/examples/x509_x5c.nv2.jwt b/docs/signature/examples/x509_x5c.nv2.jwt new file mode 100644 index 000000000..78ff07e12 --- /dev/null +++ b/docs/signature/examples/x509_x5c.nv2.jwt @@ -0,0 +1 @@ +eyJ0eXAiOiJ4NTA5IiwiYWxnIjoiUlMyNTYiLCJ4NWMiOlsiTUlJRHN6Q0NBcHVnQXdJQkFnSVVMMWFuRVUveUp5NjdWSlRiSGtOWDBiQk5BbkV3RFFZSktvWklodmNOQVFFTEJRQXdhVEVkTUJzR0ExVUVBd3dVY21WbmFYTjBjbmt1WlhoaGJYQnNaUzVqYjIweEZEQVNCZ05WQkFvTUMyVjRZVzF3YkdVZ2FXNWpNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1YyRnphR2x1WjNSdmJqRVFNQTRHQTFVRUJ3d0hVMlZoZEhSc1pUQWVGdzB5TURBM01qY3hORFF6TkRaYUZ3MHlNVEEzTWpjeE5EUXpORFphTUdreEhUQWJCZ05WQkFNTUZISmxaMmx6ZEhKNUxtVjRZVzF3YkdVdVkyOXRNUlF3RWdZRFZRUUtEQXRsZUdGdGNHeGxJR2x1WXpFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01DbGRoYzJocGJtZDBiMjR4RURBT0JnTlZCQWNNQjFObFlYUjBiR1V3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRGtLd0FjVjQ0cHNqTjhubm8xZVozenYxWktVaEpBb3h3Qk9JR2ZJeEllK2lIdHBYTHZGRlZ3azVKYnh1K1BraWcyTjRCM0lscmovVnJ5aTBoeHA0bWFnMDJNNzMzYlhMUkVOU09GT05Sa3NscE84ekhVTjVwWWRuaFRTd1lUTGFwMSsxYmdjRlN1VVhMV2llcVpCNnFjN2tpdjNiajNTUGFmNDIrczQ4VjQ5dC9PcFh4THRnaVdMOVhrdURUWmN0cEpKQTR2SEhrNk91MGJjZzdpR20rTDF4d0lmYjhNbDRvV3ZUMFNGMzVmZ1cwOGJiTFhaMnYxWENMUnNyV1VnYnE0VStLeHRFcEczWElZY1loS3gxcklyVWhmRUprdUh6Z1BnbE0xMWdHNVcrQ3lmZyt3Zk9KaWc1cTZheElLV3pJZjZDOG04bG15NmJNK041RXNEOVN2QWdNQkFBR2pVekJSTUIwR0ExVWREZ1FXQkJUZjFoTTYvaWJHRit1L1NWQUs4OEZVTWp6Um9UQWZCZ05WSFNNRUdEQVdnQlRmMWhNNi9pYkdGK3UvU1ZBSzg4RlVNanpSb1RBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFCZ3ZWYXU1KzJ3QXVDc21PeXlHMjhoMXp5QzRJUG1NbXBSWlRET3AvcExkd1hlSGpKcjhrRUMzbDkycUpFdmMrV0Fib0oxUm91Y0h5Y1VlN1JXaDJDNlpGL1dQQ0JMeVdHd25seXFHeVJNOS9qODZVSjFPZ2l1Wmw3a2w5enh3V29heFBCQ21IYTBSSG93ZFFCN0FWbHBxZzFjN0ZoS2poVUNCbUdUNFZlOHRWMGhkWnRyWm9RVis2eEhQYlVkMzdLVjFCMUJtZm8zbzRla29KS2hVdTk5RW8wM09wRTNKTHRNMTNBMUh4QUJFdVFHSFRJMHR5Y0RCQmRSbjNiMDNIb0loVTBWbnFqdnBWMUtQdnNyZ1lpLzBWU3RMTmV6WlBnR2UwZkczWGd5OHlla2RCOU5NVW4relpMQVRJNCt6OGo0UUg1V2o1WlBhVWt5b0FEMm9VSk8iXX0.eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OmM0NTE2YjhhMzExZTg1ZjFmMmE2MDU3M2FiZjRjNmI3NDBjYTNhZGU0MTI3ZTI5YjA1NjE2ODQ4ZGU0ODdkMzQiLCJzaXplIjo1MjgsInJlZmVyZW5jZXMiOlsicmVnaXN0cnkuZXhhbXBsZS5jb20vZXhhbXBsZTpsYXRlc3QiLCJyZWdpc3RyeS5leGFtcGxlLmNvbS9leGFtcGxlOnYxLjAiXSwiZXhwIjoxNjI4NTg3MTE5LCJpYXQiOjE1OTcwNTExMTksIm5iZiI6MTU5NzA1MTExOX0.MtQBOL2FERM2fMSikruHOMQdHuEXAE1wf6J6TfDY2W_7PfQQllBKbJJE0HqJ5ENAbuqNYHNZeIeKUCYFrNx2XgtrKuTe7WCa1ZZKDtp5bmANp484ekdl6lW23YB8r_SRtseJuibqjI3HuiMyELj9uYV1CdRYaD2BIZ_qxraYH1fMpjDWjehU4RYLI37hsSuDQ90o09BwaNfzbQXYPsGmkSUSmej7rOFPDnuwhNy4WcUed3kRKYEW8eIjO9OUBGQq3PWvhDjxZi3QF4QFDoiKBOXL70AjaiVIveQRkJI9-xHZSYwje9OFEMioeNWB5ceZR-r4L7VzDcU-Fxqjxn79Fw \ No newline at end of file diff --git a/docs/signature/schema.json b/docs/signature/schema.json deleted file mode 100644 index a66e843c7..000000000 --- a/docs/signature/schema.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "description": "Notary V2 Signature Config Specification", - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "https://localhost:5000/schema/signature/config", - "type": "object", - "properties": { - "signed": { - "type": "object", - "properties": { - "exp": { - "type": "integer", - "description": "Expiration time. Ref RFC7519." - }, - "nbf": { - "type": "integer", - "description": "Not before time. Ref RFC7519." - }, - "iat": { - "type": "integer", - "description": "Issued at time. Ref RFC7519." - }, - "digest": { - "description": "The cryptographic checksum digest of the object, in the pattern ':'", - "$ref": "defs-descriptor.json#/definitions/digest" - }, - "size": { - "description": "The size in bytes of the referenced object.", - "$ref": "defs.json#/definitions/int64" - }, - "references": { - "type": "array", - "description": "Each element in this array represents a fully qualified tag reference to the object.", - "minItems": 1, - "items": { - "type": "string", - "description": "Example: localhost:5000/hello-world:latest" - } - } - } - }, - "signature": { - "type": "object", - "properties": { - "typ": { - "type": "string", - "description": "Media type. Ref RFC7519.", - "enum": [ - "x509" - ] - }, - "sig": { - "type": "string", - "description": "The signature blob." - }, - "alg": { - "type": "string", - "description": "Signing algorithm. Ref RFC7515." - }, - "x5c": { - "type": "string", - "description": "X509 public key certificate or certificate chain. Ref RFC7515." - }, - "kid": { - "type": "string", - "description": "Signing key hint. Ref RFC7515." - } - }, - "required": [ - "typ", - "sig" - ] - } - }, - "required": [ - "signed", - "signatures" - ] -} \ No newline at end of file diff --git a/go.mod b/go.mod index 9bad1ee32..745083dc8 100644 --- a/go.mod +++ b/go.mod @@ -7,5 +7,4 @@ require ( github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.0.1 github.com/urfave/cli/v2 v2.2.0 - golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 ) diff --git a/go.sum b/go.sum index 64123900c..83b110164 100644 --- a/go.sum +++ b/go.sum @@ -15,12 +15,5 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 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-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg= -golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/registry/manifest.go b/pkg/registry/manifest.go index 2d5b38ff9..694d73f1f 100644 --- a/pkg/registry/manifest.go +++ b/pkg/registry/manifest.go @@ -94,6 +94,10 @@ func (c *Client) getManifestMetadata(uri *url.URL, mediaTypes ...string) (signat } header := resp.Header + mediaType := header.Get("Content-Type") + if mediaType == "" { + return signature.Manifest{}, 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) @@ -107,7 +111,10 @@ func (c *Client) getManifestMetadata(uri *url.URL, mediaTypes ...string) (signat return signature.Manifest{}, fmt.Errorf("%v: invalid Content-Length", url) } return signature.Manifest{ - Digest: digest, - Size: size, + Descriptor: signature.Descriptor{ + MediaType: mediaType, + Digest: digest, + Size: size, + }, }, nil } diff --git a/pkg/signature/encoding.go b/pkg/signature/encoding.go new file mode 100644 index 000000000..325e8ad55 --- /dev/null +++ b/pkg/signature/encoding.go @@ -0,0 +1,30 @@ +package signature + +import ( + "encoding/base64" + "encoding/json" + "fmt" +) + +// EncodeSegment JWT specific base64url encoding with padding stripped +func EncodeSegment(seg []byte) string { + return base64.RawURLEncoding.EncodeToString(seg) +} + +// DecodeSegment JWT specific base64url encoding with padding stripped +func DecodeSegment(seg string) ([]byte, error) { + return base64.RawURLEncoding.DecodeString(seg) +} + +// DecodeClaims JWT specific base64url encoding with padding stripped as Claims +func DecodeClaims(seg string) (Claims, error) { + bytes, err := DecodeSegment(seg) + if err != nil { + return Claims{}, fmt.Errorf("invalid base64 encoded claims: %v", err) + } + var claims Claims + if err := json.Unmarshal(bytes, &claims); err != nil { + return Claims{}, fmt.Errorf("invalid JSON encoded claims: %v", err) + } + return claims, nil +} diff --git a/pkg/signature/errors.go b/pkg/signature/errors.go index 6c939d883..9387af88a 100644 --- a/pkg/signature/errors.go +++ b/pkg/signature/errors.go @@ -4,7 +4,11 @@ import "errors" // common errors var ( + ErrInvalidToken = errors.New("invalid token") ErrInvalidSignatureType = errors.New("invalid signature type") ErrUnknownSignatureType = errors.New("unknown signature type") ErrUnknownSigner = errors.New("unknown signer") + ErrDigestMismatch = errors.New("digest mismatch") + ErrSizeMismatch = errors.New("size mismatch") + ErrMediaTypeMismatch = errors.New("media type mismatch") ) diff --git a/pkg/signature/interface.go b/pkg/signature/interface.go index e1127e10f..0cc0d14d4 100644 --- a/pkg/signature/interface.go +++ b/pkg/signature/interface.go @@ -2,11 +2,11 @@ package signature // Signer signs content type Signer interface { - Sign(content []byte) (Signature, error) + Sign(claims string) (string, []byte, error) } // Verifier verifies content type Verifier interface { Type() string - Verify(content []byte, signature Signature) error + Verify(header Header, signed string, sig []byte) error } diff --git a/pkg/signature/model.go b/pkg/signature/model.go new file mode 100644 index 000000000..282c48ea7 --- /dev/null +++ b/pkg/signature/model.go @@ -0,0 +1,28 @@ +package signature + +// Header defines the signature header +type Header struct { + Raw []byte `json:"-"` + Type string `json:"typ"` +} + +// Claims contains the claims to be signed +type Claims struct { + Manifest + Expiration int64 `json:"exp,omitempty"` + IssuedAt int64 `json:"iat,omitempty"` + NotBefore int64 `json:"nbf,omitempty"` +} + +// Manifest to be signed +type Manifest struct { + Descriptor + References []string `json:"references,omitempty"` +} + +// Descriptor describes the basic information of the target content +type Descriptor struct { + MediaType string `json:"mediaType,omitempty"` + Digest string `json:"digest"` + Size int64 `json:"size"` +} diff --git a/pkg/signature/scheme.go b/pkg/signature/scheme.go index 30b9337b8..d531be54f 100644 --- a/pkg/signature/scheme.go +++ b/pkg/signature/scheme.go @@ -3,6 +3,7 @@ package signature import ( "encoding/json" "fmt" + "strings" "time" ) @@ -30,61 +31,87 @@ func (s *Scheme) RegisterVerifier(verifier Verifier) { s.verifiers[verifier.Type()] = verifier } -// Sign signs content by a signer -func (s *Scheme) Sign(signerID string, content Content) (Signature, error) { - bytes, err := json.Marshal(content) +// Sign signs claims by a signer +func (s *Scheme) Sign(signerID string, claims Claims) (string, error) { + bytes, err := json.Marshal(claims) if err != nil { - return Signature{}, err + return "", err } return s.SignRaw(signerID, bytes) } // SignRaw signs raw content by a signer -func (s *Scheme) SignRaw(signerID string, content []byte) (Signature, error) { +func (s *Scheme) SignRaw(signerID string, content []byte) (string, error) { signer, found := s.signers[signerID] if !found { - return Signature{}, ErrUnknownSigner + return "", ErrUnknownSigner } - return signer.Sign(content) -} -// Verify verifies signed data -func (s *Scheme) Verify(signed Signed) (Content, Signature, error) { - sig, err := s.verifySignature(signed) + signed, sig, err := signer.Sign(EncodeSegment(content)) if err != nil { - return Content{}, sig, err + return "", nil } - var content Content - if err := json.Unmarshal(signed.Signed, &content); err != nil { - return Content{}, sig, err + return strings.Join([]string{ + signed, + EncodeSegment(sig), + }, "."), nil +} + +// Verify verifies the JWT-like token +func (s *Scheme) Verify(token string) (Claims, error) { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return Claims{}, ErrInvalidToken } - return content, sig, s.verifyContent(content) + if err := s.verifySignature(parts); err != nil { + return Claims{}, err + } + + claims, err := DecodeClaims(parts[1]) + if err != nil { + return Claims{}, err + } + + return claims, s.verifyClaims(claims) } -func (s *Scheme) verifySignature(signed Signed) (Signature, error) { - sig := signed.Signature - verifier, found := s.verifiers[sig.Type] +func (s *Scheme) verifySignature(parts []string) error { + rawHeader, err := DecodeSegment(parts[0]) + if err != nil { + return ErrInvalidToken + } + var header Header + if json.Unmarshal(rawHeader, &header); err != nil { + return ErrInvalidToken + } + header.Raw = rawHeader + + verifier, found := s.verifiers[header.Type] if !found { - return Signature{}, ErrUnknownSignatureType + return ErrUnknownSignatureType } - content := []byte(signed.Signed) - if err := verifier.Verify(content, sig); err != nil { - return Signature{}, err + sig, err := DecodeSegment(parts[2]) + if err != nil { + return ErrInvalidToken } - return sig, nil + return verifier.Verify( + header, + strings.Join(parts[:2], "."), + sig, + ) } -func (s *Scheme) verifyContent(content Content) error { +func (s *Scheme) verifyClaims(claims Claims) error { now := time.Now().Unix() - if content.Expiration != 0 && now > content.Expiration { - return fmt.Errorf("content expired: %d: current: %d", content.Expiration, now) + if claims.Expiration != 0 && now > claims.Expiration { + return fmt.Errorf("content expired: %d: current: %d", claims.Expiration, now) } - if content.NotBefore != 0 && now < content.NotBefore { - return fmt.Errorf("content is not available yet: %d: current: %d", content.NotBefore, now) + if claims.NotBefore != 0 && now < claims.NotBefore { + return fmt.Errorf("content is not available yet: %d: current: %d", claims.NotBefore, now) } return nil } diff --git a/pkg/signature/signature.go b/pkg/signature/signature.go deleted file mode 100644 index f116d9450..000000000 --- a/pkg/signature/signature.go +++ /dev/null @@ -1,35 +0,0 @@ -package signature - -import ( - "encoding/json" -) - -// Signed is the high level, partially deserialized metadata object -type Signed struct { - Signed json.RawMessage `json:"signed"` - Signature Signature `json:"signature"` -} - -// Content contains the contents to be signed -type Content struct { - Manifest - Expiration int64 `json:"exp,omitempty"` - NotBefore int64 `json:"nbf,omitempty"` - IssuedAt int64 `json:"iat,omitempty"` -} - -// Manifest to be signed -type Manifest struct { - Digest string `json:"digest"` - Size int64 `json:"size"` - References []string `json:"references,omitempty"` -} - -// Signature to verify the content -type Signature struct { - Type string `json:"typ"` - Signature []byte `json:"sig"` - Algorithm string `json:"alg,omitempty"` - KeyID string `json:"kid,omitempty"` - X5c [][]byte `json:"x5c,omitempty"` -} diff --git a/pkg/signature/util.go b/pkg/signature/util.go deleted file mode 100644 index 0a62bfd06..000000000 --- a/pkg/signature/util.go +++ /dev/null @@ -1,17 +0,0 @@ -package signature - -import ( - "encoding/json" -) - -// Pack packs content with its signature -func Pack(content Content, signature Signature) (Signed, error) { - signed, err := json.Marshal(content) - if err != nil { - return Signed{}, err - } - return Signed{ - Signed: signed, - Signature: signature, - }, nil -} diff --git a/pkg/signature/x509/header.go b/pkg/signature/x509/header.go new file mode 100644 index 000000000..fb5ebdddf --- /dev/null +++ b/pkg/signature/x509/header.go @@ -0,0 +1,18 @@ +package x509 + +import ( + "github.com/notaryproject/nv2/pkg/signature" +) + +// Header defines the signature header +type Header struct { + signature.Header + Parameters +} + +// Parameters defines the signature parameters +type Parameters struct { + Algorithm string `json:"alg,omitempty"` + KeyID string `json:"kid,omitempty"` + X5c [][]byte `json:"x5c,omitempty"` +} diff --git a/pkg/signature/x509/signer.go b/pkg/signature/x509/signer.go index f5e8de755..eb39882d0 100644 --- a/pkg/signature/x509/signer.go +++ b/pkg/signature/x509/signer.go @@ -1,10 +1,12 @@ package x509 import ( - "bytes" "crypto" "crypto/x509" + "encoding/json" "errors" + "io" + "strings" "github.com/docker/libtrust" cryptoutil "github.com/notaryproject/nv2/internal/crypto" @@ -66,27 +68,46 @@ func NewSigner(key libtrust.PrivateKey, certs []*x509.Certificate) (signature.Si return s, nil } -func (s *signer) Sign(raw []byte) (signature.Signature, error) { +func (s *signer) Sign(claims string) (string, []byte, error) { if s.cert != nil { - if err := verifyReferences(raw, s.cert); err != nil { - return signature.Signature{}, err + if err := verifyReferences(claims, s.cert); err != nil { + return "", nil, err } } - sig, alg, err := s.key.Sign(bytes.NewReader(raw), s.hash) + // Generate header + // We have to sign an empty string for the proper algorithm string first. + _, alg, err := s.key.Sign(io.MultiReader(), s.hash) if err != nil { - return signature.Signature{}, err + return "", nil, err } - sigma := signature.Signature{ - Type: Type, - Algorithm: alg, - Signature: sig, + header := Header{ + Header: signature.Header{ + Type: Type, + }, + Parameters: Parameters{ + Algorithm: alg, + }, } - if s.cert != nil { - sigma.X5c = s.rawCerts + header.X5c = s.rawCerts } else { - sigma.KeyID = s.keyID + header.KeyID = s.keyID + } + headerJSON, err := json.Marshal(header) + if err != nil { + return "", nil, err + } + + // Generate signature + signed := strings.Join([]string{ + signature.EncodeSegment(headerJSON), + claims, + }, ".") + + sig, _, err := s.key.Sign(strings.NewReader(signed), s.hash) + if err != nil { + return "", nil, err } - return sigma, nil + return signed, sig, nil } diff --git a/pkg/signature/x509/verifier.go b/pkg/signature/x509/verifier.go index e906e896c..97eb17c99 100644 --- a/pkg/signature/x509/verifier.go +++ b/pkg/signature/x509/verifier.go @@ -1,7 +1,6 @@ package x509 import ( - "bytes" "crypto" "crypto/x509" "encoding/json" @@ -58,27 +57,37 @@ func (v *verifier) Type() string { return Type } -func (v *verifier) Verify(content []byte, sig signature.Signature) error { - if sig.Type != Type { +func (v *verifier) Verify(header signature.Header, signed string, sig []byte) error { + if header.Type != Type { return signature.ErrInvalidSignatureType } + var params Parameters + if err := json.Unmarshal(header.Raw, ¶ms); err != nil { + return err + } - key, cert, err := v.getVerificationKeyPair(sig) + key, cert, err := v.getVerificationKeyPair(params) if err != nil { return err } - if err := key.Verify(bytes.NewReader(content), sig.Algorithm, sig.Signature); err != nil { + if err := key.Verify(strings.NewReader(signed), params.Algorithm, sig); err != nil { return err } - return verifyReferences(content, cert) + + parts := strings.Split(signed, ".") + if len(parts) != 2 { + return errors.New("invalid signed content") + } + + return verifyReferences(parts[1], cert) } -func (v *verifier) getVerificationKeyPair(sig signature.Signature) (libtrust.PublicKey, *x509.Certificate, error) { +func (v *verifier) getVerificationKeyPair(params Parameters) (libtrust.PublicKey, *x509.Certificate, error) { switch { - case len(sig.X5c) > 0: - return v.getVerificationKeyPairFromX5c(sig.X5c) - case sig.KeyID != "": - return v.getVerificationKeyPairFromKeyID(sig.KeyID) + case len(params.X5c) > 0: + return v.getVerificationKeyPairFromX5c(params.X5c) + case params.KeyID != "": + return v.getVerificationKeyPairFromKeyID(params.KeyID) default: return nil, nil, errors.New("missing verification key") } @@ -127,14 +136,14 @@ func (v *verifier) getVerificationKeyPairFromX5c(x5c [][]byte) (libtrust.PublicK return key, cert, nil } -func verifyReferences(raw []byte, cert *x509.Certificate) error { - var content signature.Content - if err := json.Unmarshal(raw, &content); err != nil { +func verifyReferences(seg string, cert *x509.Certificate) error { + claims, err := signature.DecodeClaims(seg) + if err != nil { return err } roots := x509.NewCertPool() roots.AddCert(cert) - for _, reference := range content.Manifest.References { + for _, reference := range claims.Manifest.References { if _, err := cert.Verify(x509.VerifyOptions{ DNSName: strings.SplitN(reference, "/", 2)[0], Roots: roots,