Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Specification and Prototype - JWT based signature #2

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 109 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,109 @@
# nv2
# 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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the digest and references block are signed

Does this indicate that an image cannot be renamed? Or that a publisher should assign a "canonical" reference before signing the image?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An image can be renamed, moved to a different registry, different tag, etc.
What this block shows is: what was it when it was signed.
When you run example.com/hello-world:v1 from myregistry.io/hello-you:v1-blah, the validation would pass as long as the digest hasn't changed.
However, you can also add another registry.corp.io signature for: registry.corp.io/hello-you:v1-blah, and then limit deployments to being signed by registry.corp.io and pulled from registry.corp.io. The signature validation would come from Notary v2. The additional "policy" would be something incorporated into a policy managmenet solution like OPA or something else.
Think of it as an extra layer of security, we may like. Or, we may not. See comment above: #2 (comment)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Roughly: the signature attests that the signer (Foo) called the image by this name (foo.example.com/myimage:latest), and a validator can implement a policy such that it only trusts the name (foo.example.com/myimage:*) if it was signed by this key (Foo)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, But, I suspect companies would more likely re-sign for themselves and trust their own registry. The main point is an x509 signature can only sign artifacts where the CN matches the registry name.
As we're discussing the larger question of key types, this may become less possible.
But, is it interesting?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SteveLasker
I think we need clarification regarding the "re-sign" scenario you mentioned.
More specifically, should the signature by registry.corp.io be independent of the signature by example.com?
Is it reasonable that when the signature by example.com is revoked, the signature by registry.corp.io should be invalid immediately?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Daniel,
Great question. Today, the signatures are independent. The reason a signature may get revoked may or may not have anything to do with subsequent signatures. But, the information is super important. I'd suggest this is more of a policy managmenet decision that makes use of the information, rather than a hard rule the spec would enforce.
To your point, once we've all gotten comfortable with the prototype, we should absolutely clarify this in the spec.
I think the other question buried here is the continual question of policy vs. data separation.
I'm suggesting signatures are data, enabling policy by external systems.
We must support the ability to invalidate some data - such as the wabbit-networks signature may be revoked. If acme-rockets wants to keep or invalidate their signature is a policy decision.
Thoughts?


### 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
25 changes: 25 additions & 0 deletions cmd/nv2/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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",
}
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",
}
)
29 changes: 29 additions & 0 deletions cmd/nv2/main.go
Original file line number Diff line number Diff line change
@@ -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.2.0",
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)
}
}
71 changes: 71 additions & 0 deletions cmd/nv2/manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package main

import (
"fmt"
"io"
"math"
"net/url"
"os"
"strings"

"github.com/notaryproject/nv2/pkg/registry"
"github.com/notaryproject/nv2/pkg/signature"
"github.com/opencontainers/go-digest"
"github.com/urfave/cli/v2"
)

func getManifestFromContext(ctx *cli.Context) (signature.Manifest, error) {
if uri := ctx.Args().First(); uri != "" {
return getManfestsFromURI(ctx, uri)
}
return getManifestFromReader(os.Stdin, ctx.String(mediaTypeFlag.Name))
}

func getManifestFromReader(r io.Reader, mediaType string) (signature.Manifest, error) {
lr := &io.LimitedReader{
R: r,
N: math.MaxInt64,
}
digest, err := digest.SHA256.FromReader(lr)
if err != nil {
return signature.Manifest{}, err
}
return signature.Manifest{
Descriptor: signature.Descriptor{
MediaType: mediaType,
Digest: digest.String(),
Size: math.MaxInt64 - lr.N,
},
}, nil
}

func getManfestsFromURI(ctx *cli.Context, uri string) (signature.Manifest, error) {
parsed, err := url.Parse(uri)
if err != nil {
return signature.Manifest{}, err
}
var r io.Reader
switch strings.ToLower(parsed.Scheme) {
case "file":
path := parsed.Path
if parsed.Opaque != "" {
path = parsed.Opaque
}
file, err := os.Open(path)
if err != nil {
return signature.Manifest{}, err
}
defer file.Close()
r = file
case "docker", "oci":
remote := registry.NewClient(nil, &registry.ClientOptions{
Username: ctx.String(usernameFlag.Name),
Password: ctx.String(passwordFlag.Name),
Insecure: ctx.Bool(insecureFlag.Name),
})
return remote.GetManifestMetadata(parsed)
default:
return signature.Manifest{}, fmt.Errorf("unsupported URI scheme: %s", parsed.Scheme)
}
return getManifestFromReader(r, ctx.String(mediaTypeFlag.Name))
}
129 changes: 129 additions & 0 deletions cmd/nv2/sign.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package main

import (
"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 OCI Artifacts",
ArgsUsage: "[<scheme://reference>]",
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,
mediaTypeFlag,
},
Action: runSign,
}

func runSign(ctx *cli.Context) error {
// initialize
scheme, err := getSchemeForSigning(ctx)
if err != nil {
return err
}

// core process
claims, err := prepareClaimsForSigning(ctx)
if err != nil {
return err
}
sig, err := scheme.Sign(signerID, claims)
if err != nil {
return err
}

// write out
path := ctx.String("output")
if path == "" {
path = strings.Split(claims.Manifest.Digest, ":")[1] + ".nv2"
}
if err := ioutil.WriteFile(path, []byte(sig), 0666); err != nil {
return err
}

fmt.Println(claims.Manifest.Digest)
return nil
}

func prepareClaimsForSigning(ctx *cli.Context) (signature.Claims, error) {
manifest, err := getManifestFromContext(ctx)
if err != nil {
return signature.Claims{}, err
}
manifest.References = ctx.StringSlice("reference")
now := time.Now()
nowUnix := now.Unix()
claims := signature.Claims{
Manifest: manifest,
IssuedAt: nowUnix,
}
if expiry := ctx.Duration("expiry"); expiry != 0 {
claims.NotBefore = nowUnix
claims.Expiration = now.Add(expiry).Unix()
}

return claims, 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
}
Loading