Skip to content

Commit

Permalink
Merge pull request #8 from shizhMSFT/main
Browse files Browse the repository at this point in the history
Based on our [Notary v2 call this week](https://hackmd.io/_vrqBGAOSUC_VWvFzWruZw?view), we agreed to merge this into a `prototype-1` branch.
This will give us a baseline to which we can open issues and submit new PRs.
  • Loading branch information
SteveLasker authored Aug 26, 2020
2 parents 560d4f8 + a42acd8 commit f4765d1
Show file tree
Hide file tree
Showing 30 changed files with 1,854 additions and 1 deletion.
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.

### 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

0 comments on commit f4765d1

Please sign in to comment.