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

Update data model with latest notation spec #62

Closed
wants to merge 1 commit 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
28 changes: 15 additions & 13 deletions jws.go
Original file line number Diff line number Diff line change
@@ -1,39 +1,41 @@
package notation

import "time"

const (
// MediaTypeJWSEnvelope describes the media type of the JWS envelope.
MediaTypeJWSEnvelope = "application/vnd.cncf.notary.v2.jws.v1"
)

// JWSPayload contains the set of claims used by Notary V2.
type JWSPayload struct {
// Private claim.
Subject Descriptor `json:"subject"`

// Identifies the number of seconds since Epoch at which the signature was issued.
IssuedAt int64 `json:"iat"`

// Identifies the number of seconds since Epoch at which the signature must not be considered valid.
ExpiresAt int64 `json:"exp,omitempty"`
}

// JWSProtectedHeader contains the set of protected headers.
type JWSProtectedHeader struct {
// Defines which algorithm was used to generate the signature.
Algorithm string `json:"alg"`

// Media type of the secured content (the payload).
ContentType string `json:"cty"`

// Lists the headers that implementation MUST understand and process.
Critical []string `json:"crit"`

// The time at which the signature was generated.
SigningTime time.Time `json:"io.cncf.notary.signingTime"`

// The "best by use" time for the artifact, as defined by the signer.
Expiry time.Time `json:"io.cncf.notary.expiry,omitempty"`
}

// JWSUnprotectedHeader contains the set of unprotected headers.
type JWSUnprotectedHeader struct {
// RFC3161 time stamp token Base64-encoded.
TimeStampToken []byte `json:"timestamp,omitempty"`
TimestampSignature []byte `json:"io.cncf.notary.timestampSignature,omitempty"`

// List of X.509 Base64-DER-encoded certificates
// as defined at https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6.
CertChain [][]byte `json:"x5c"`

// The identifier of a client that produced the signature.
SigningAgent string `json:"io.cncf.notary.signingAgent,omitempty"`
}

// JWSEnvelope is the final signature envelope.
Expand Down
6 changes: 6 additions & 0 deletions notation.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ import (
// Media type for Notary payload for OCI artifacts, which contains an artifact descriptor.
const MediaTypePayload = "application/vnd.cncf.notary.payload.v1+json"

// Payload contains the set of claims used by Notary V2.
type Payload struct {
// The descriptor of the target artifact manifest that is being signed.
TargetArtifact Descriptor `json:"targetArtifact"`
}

// Descriptor describes the content signed or to be signed.
type Descriptor struct {
// The media type of the targeted content.
Expand Down
62 changes: 45 additions & 17 deletions signature/jws/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"errors"
"fmt"
"time"

"github.com/golang-jwt/jwt/v4"
"github.com/notaryproject/notation-go"
Expand Down Expand Up @@ -105,17 +106,10 @@ func (s *pluginSigner) generateSignature(ctx context.Context, desc notation.Desc
return nil, fmt.Errorf("keySpec %q for key %q is not supported", key.KeySpec, key.KeyID)
}

// Generate payload to be signed.
payload := packPayload(desc, opts)
if err := payload.Valid(); err != nil {
return nil, err
}

// Generate signing string.
token := jwtToken(alg.JWS(), payload)
payloadToSign, err := token.SigningString()
// Generate JWS signing input.
payloadToSign, err := signingString(alg.JWS(), desc, opts)
if err != nil {
return nil, fmt.Errorf("failed to marshal signing payload: %v", err)
return nil, err
}

// Execute plugin sign command.
Expand Down Expand Up @@ -190,19 +184,20 @@ func (s *pluginSigner) mergeConfig(config map[string]string) map[string]string {
}

func (s *pluginSigner) generateSignatureEnvelope(ctx context.Context, desc notation.Descriptor, opts notation.SignOptions) ([]byte, error) {
rawDesc, err := json.Marshal(desc)
rawPayload, err := json.Marshal(notation.Payload{
TargetArtifact: desc,
})
if err != nil {
return nil, err
}
// Execute plugin sign command.
req := &plugin.GenerateEnvelopeRequest{
ContractVersion: plugin.ContractVersion,
KeyID: s.keyID,
Payload: rawDesc,
Payload: rawPayload,
SignatureEnvelopeType: notation.MediaTypeJWSEnvelope,
// TODO: Update payload type once https://github.com/notaryproject/notaryproject/pull/158 is approved.
PayloadType: notation.MediaTypePayload,
PluginConfig: s.mergeConfig(opts.PluginConfig),
PayloadType: notation.MediaTypePayload,
PluginConfig: s.mergeConfig(opts.PluginConfig),
}
out, err := s.runner.Run(ctx, req)
if err != nil {
Expand Down Expand Up @@ -242,12 +237,12 @@ func (s *pluginSigner) generateSignatureEnvelope(ctx context.Context, desc notat
}

// Check descriptor subject is honored.
var payload notation.JWSPayload
var payload notation.Payload
err = decodeBase64URLJSON(envelope.Payload, &payload)
if err != nil {
return nil, fmt.Errorf("envelope payload can't be decoded: %w", err)
}
if !descriptorPartialEqual(desc, payload.Subject) {
if !descriptorPartialEqual(desc, payload.TargetArtifact) {
return nil, errors.New("descriptor subject has changed")
}

Expand Down Expand Up @@ -283,6 +278,15 @@ func descriptorPartialEqual(original, newDesc notation.Descriptor) bool {
return true
}

func encodeBase64URLJSON(v interface{}) (string, error) {
data, err := json.Marshal(v)
if err != nil {
return "", err
}
enc := base64.RawURLEncoding.EncodeToString(data)
return enc, nil
}

func decodeBase64URLJSON(str string, v interface{}) error {
dec, err := base64.RawURLEncoding.DecodeString(str)
if err != nil {
Expand All @@ -308,3 +312,27 @@ func verifyJWT(sigAlg string, payload string, sig string, signingCert *x509.Cert
method := jwt.GetSigningMethod(sigAlg)
return method.Verify(payload, sig, signingCert.PublicKey)
}

func signingString(alg string, desc notation.Descriptor, opts notation.SignOptions) (string, error) {
protected := notation.JWSProtectedHeader{
Algorithm: alg,
ContentType: notation.MediaTypePayload,
SigningTime: time.Now().Truncate(time.Second), // Truncate current time to avoid fractional second.
Expiry: opts.Expiry,
}
if !protected.Expiry.IsZero() {
protected.Critical = []string{"io.cncf.notary.expiry,omitempty"}
}
protectedRaw, err := encodeBase64URLJSON(&protected)
if err != nil {
return "", fmt.Errorf("failed to encode protected header: %v", err)
}
payloadRaw, err := encodeBase64URLJSON(&notation.Payload{
TargetArtifact: desc,
})
if err != nil {
return "", fmt.Errorf("failed to encode payload: %v", err)
}

return protectedRaw + "." + payloadRaw, nil
}
61 changes: 19 additions & 42 deletions signature/jws/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"reflect"
"strings"
"testing"
"time"

"github.com/golang-jwt/jwt/v4"
"github.com/notaryproject/notation-go"
Expand Down Expand Up @@ -130,21 +129,6 @@ func TestSigner_Sign_KeySpecNotSupported(t *testing.T) {
testSignerError(t, signer, "keySpec \"custom\" for key \"1\" is not supported")
}

func TestSigner_Sign_PayloadNotValid(t *testing.T) {
signer := pluginSigner{
runner: &mockRunner{[]interface{}{
&validMetadata,
&plugin.DescribeKeyResponse{KeyID: "1", KeySpec: notation.RSA_2048},
}, []error{nil, nil}, 0},
keyID: "1",
}
_, err := signer.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{Expiry: time.Now().Add(-100)})
wantEr := "token is expired"
if err == nil || !strings.Contains(err.Error(), wantEr) {
t.Errorf("Signer.Sign() error = %v, wantErr %v", err, wantEr)
}
}

func TestSigner_Sign_GenerateSignatureKeyIDMismatch(t *testing.T) {
signer := pluginSigner{
runner: &mockRunner{[]interface{}{
Expand Down Expand Up @@ -336,13 +320,13 @@ func TestSigner_Sign_Valid(t *testing.T) {
t.Fatal(err)
}
want := notation.JWSEnvelope{
Protected: "eyJhbGciOiJQUzI1NiIsImN0eSI6ImFwcGxpY2F0aW9uL3ZuZC5jbmNmLm5vdGFyeS5wYXlsb2FkLnYxK2pzb24ifQ",
Payload: "eyJ0YXJnZXRBcnRpZmFjdCI6eyJtZWRpYVR5cGUiOiIiLCJkaWdlc3QiOiIiLCJzaXplIjowfX0",
Header: notation.JWSUnprotectedHeader{
CertChain: [][]byte{cert.Raw},
},
}
if got.Protected != want.Protected {
t.Errorf("Signer.Sign() Protected %v, want %v", got.Protected, want.Protected)
if got.Payload != want.Payload {
t.Errorf("Signer.Sign() Payload %v, want %v", got.Payload, want.Payload)
}
if _, err = base64.RawURLEncoding.DecodeString(got.Signature); err != nil {
t.Errorf("Signer.Sign() Signature %v is not encoded as Base64URL", got.Signature)
Expand Down Expand Up @@ -390,34 +374,27 @@ func (s *mockEnvelopePlugin) Run(ctx context.Context, req plugin.Request) (inter
}
alg := keySpec.SignatureAlgorithm().JWS()
req1 := req.(*plugin.GenerateEnvelopeRequest)
t := &jwt.Token{
Method: jwt.GetSigningMethod(alg),
Header: map[string]interface{}{
"alg": alg,
"cty": notation.MediaTypePayload,
},
Claims: struct {
jwt.RegisteredClaims
Subject json.RawMessage `json:"subject"`
}{
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()),
},
Subject: req1.Payload,
},
var payload notation.Payload
err = json.Unmarshal(req1.Payload, &payload)
if err != nil {
return nil, err
}
signed, err := t.SignedString(key)
payloadToSign, err := signingString(alg, payload.TargetArtifact, notation.SignOptions{})
if err != nil {
return nil, err
}
parts := strings.Split(signed, ".")
if len(parts) != 3 {
return nil, errors.New("invalid compact serialization")
sig, err := jwt.GetSigningMethod(alg).Sign(payloadToSign, key)
if err != nil {
return "", err
}
parts := strings.Split(payloadToSign, ".")
if len(parts) != 2 {
return nil, errors.New("invalid signing string")
}
envelope := notation.JWSEnvelope{
Protected: parts[0],
Payload: parts[1],
Signature: parts[2],
Signature: sig,
}
if s.certChain != nil {
// Override cert chain.
Expand Down Expand Up @@ -520,7 +497,7 @@ func TestPluginSigner_SignEnvelope_CertBasicConstraintCA(t *testing.T) {
keyID: "1",
}
_, err = signer.Sign(context.Background(), notation.Descriptor{
MediaType: notation.MediaTypePayload,
MediaType: "test media type",
Size: 1,
}, notation.SignOptions{})
if err == nil || err.Error() != "signing certificate does not meet the minimum requirements: keyUsage must have the bit positions for digitalSignature set" {
Expand All @@ -538,7 +515,7 @@ func TestPluginSigner_SignEnvelope_SignatureVerifyError(t *testing.T) {
keyID: "1",
}
_, err = signer.Sign(context.Background(), notation.Descriptor{
MediaType: notation.MediaTypePayload,
MediaType: "test media type",
Size: 1,
}, notation.SignOptions{})
if err == nil || err.Error() != "crypto/rsa: verification error" {
Expand All @@ -552,7 +529,7 @@ func TestPluginSigner_SignEnvelope_Valid(t *testing.T) {
keyID: "1",
}
_, err := signer.Sign(context.Background(), notation.Descriptor{
MediaType: notation.MediaTypePayload,
MediaType: "test media type",
Size: 1,
}, notation.SignOptions{})
if err != nil {
Expand Down
12 changes: 1 addition & 11 deletions signature/jws/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,6 @@ func (r *builtinPlugin) Run(ctx context.Context, req plugin.Request) (interface{
}
}

func jwtToken(alg string, claims jwt.Claims) *jwt.Token {
return &jwt.Token{
Header: map[string]interface{}{
"alg": alg,
"cty": notation.MediaTypePayload,
},
Claims: claims,
}
}

func jwsEnvelope(ctx context.Context, opts notation.SignOptions, compact string, certChain [][]byte) ([]byte, error) {
parts := strings.Split(compact, ".")
if len(parts) != 3 {
Expand All @@ -162,7 +152,7 @@ func jwsEnvelope(ctx context.Context, opts notation.SignOptions, compact string,
if err != nil {
return nil, fmt.Errorf("timestamp failed: %w", err)
}
envelope.Header.TimeStampToken = token
envelope.Header.TimestampSignature = token
}

// encode in flatten JWS JSON serialization
Expand Down
24 changes: 0 additions & 24 deletions signature/jws/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,8 @@ import (
"crypto/x509"
"errors"
"fmt"
"time"

"github.com/golang-jwt/jwt/v4"
"github.com/notaryproject/notation-go"
)

type notaryClaim struct {
jwt.RegisteredClaims
Subject notation.Descriptor `json:"subject"`
}

// packPayload generates JWS payload according the signing content and options.
func packPayload(desc notation.Descriptor, opts notation.SignOptions) jwt.Claims {
var expiresAt *jwt.NumericDate
if !opts.Expiry.IsZero() {
expiresAt = jwt.NewNumericDate(opts.Expiry)
}
return notaryClaim{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: expiresAt,
IssuedAt: jwt.NewNumericDate(time.Now()),
},
Subject: desc,
}
}

var (
oidExtensionKeyUsage = []int{2, 5, 29, 15}
)
Expand Down
Loading