-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #8 from shizhMSFT/main
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
Showing
30 changed files
with
1,854 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, ®istry.ClientOptions{ | ||
Username: ctx.String(usernameFlag.Name), | ||
Password: ctx.String(passwordFlag.Name), | ||
Insecure: ctx.Bool(insecureFlag.Name), | ||
}) | ||
return remote.GetManifestMetadata(parsed) | ||
default: | ||
return signature.Manifest{}, fmt.Errorf("unsupported URI scheme: %s", parsed.Scheme) | ||
} | ||
return getManifestFromReader(r, ctx.String(mediaTypeFlag.Name)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.