-
Notifications
You must be signed in to change notification settings - Fork 84
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
Closed
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,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", | ||
} | ||
) |
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.1.2", | ||
Authors: []*cli.Author{ | ||
{ | ||
Name: "Shiwei Zhang", | ||
Email: "[email protected]", | ||
}, | ||
}, | ||
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,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) | ||
} |
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,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: "[<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, | ||
}, | ||
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 | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this indicate that an image cannot be renamed? Or that a publisher should assign a "canonical" reference before signing the image?
There was a problem hiding this comment.
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)
There was a problem hiding this 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)?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 byexample.com
?Is it reasonable that when the signature by
example.com
is revoked, the signature byregistry.corp.io
should be invalid immediately?There was a problem hiding this comment.
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?