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

Signature spec updates #158

Merged
merged 10 commits into from
Jun 2, 2022
165 changes: 165 additions & 0 deletions signature-envelope-jws.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# JWS Signature Envelope

This specification implements the [Notary v2 Signature specification](signature-specification.md) using JSON Web Signature (JWS). JWS ([RFC7515](https://datatracker.ietf.org/doc/html/rfc7515)) is a JSON based envelope format for digital signatures over any type of payload (e.g. JSON, binary). JWS is a Notary v2 supported signature format and specifically uses the *JWS JSON Serialization* representation.

## Storage

A JWS signature envelope will be stored in an OCI registry as a blob, and referenced in the signature manifest as a blob with `mediaType` of `"application/jose+json"`.

Signature Manifest Example

```jsonc
{
"artifactType": "application/vnd.cncf.notary.signature",
Copy link
Contributor

Choose a reason for hiding this comment

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

need version here: application/vnd.cncf.notary.signature.v2

"blobs": [
{
"mediaType": "application/jose+json",
"digest": "sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0",
"size": 32654
}
],
"subject": {
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:73c803930ea3ba1e54bc25c2bdc53edd0284c62ed651fe7b00369da519a3c333",
"size": 16724
},
"annotations": {
"io.cncf.notary.x509chain.thumbprint#S256":
"[\"B7A69A70992AE4F9FF103EBE04A2C3BA6C777E439253CE36562E6E98375068C3\",\"932EB6F5598435D4EF23F97B0B5ACB515FAE2B8D8FAC046AB813DDC419DD5E89\"]"
}
}
```

## JWS Payload

The JWS envelope contains a [Notary v2 Payload](./signature-specification.md#payload).

Example of Notary v2 payload

```jsonc
{
"targetArtifact": {
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:73c803930ea3ba1e54bc25c2bdc53edd0284c62ed651fe7b00369da519a3c333",
"size": 16724,
"annotations": {
"io.wabbit-networks.buildId": "123" // user defined metadata
}
}
}
```

## Protected Headers

The JWS envelope for Notary v2 uses following headers

- Registered headers - `alg`, `cty`, and `crit`
- [Public headers](https://datatracker.ietf.org/doc/html/rfc7515#section-4.2) with collision resistant names - `io.cncf.notary.signingTime`, `io.cncf.notary.expiry`

Example

```jsonc
{
"alg": "PS384",
"cty": "application/vnd.cncf.notary.payload.v1+json",
"io.cncf.notary.signingTime": "2022-04-06 07:01:20Z",
Copy link
Contributor

Choose a reason for hiding this comment

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

Not RFC 3339, missing the T.

Copy link
Contributor Author

@gokarnm gokarnm May 28, 2022

Choose a reason for hiding this comment

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

RFC 3339 allows a space separator [1]. I'll update the example though to commonly used representation.

https://datatracker.ietf.org/doc/html/rfc3339#section-5.6

NOTE: ISO 8601 defines date and time separated by "T".
Applications using this syntax may choose, for the sake of
readability, to specify a full-date and full-time separated by
(say) a space character.

"io.cncf.notary.expiry": "2022-10-06 07:01:20Z",
"crit":["io.cncf.notary.expiry"]
}
```

- **[`alg`](https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.1)**(*string*): This REQUIRED header defines which signing algorithm was used to generate the signature. JWS specification defines `alg` as a required header, that MUST be present and MUST be understood and processed by verifier. The signature algorithm of the signing key (first certificate in `x5c`) is the source of truth, and during signing the value of `alg` MUST be set corresponding to signature algorithm of the signing key using [this mapping](#supported-alg-header-values) that lists the Notary v2 allowed subset of `alg` values supported by JWS. Similarly verifier of the signature MUST match `alg` with signature algorithm of the signing key to mitigate algorithm substitution attacks.
- **[`cty`](https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.10)**(*string*): The REQUIRED header content-type is used to declare the media type of the secured content (the payload). The supported value is `application/vnd.cncf.notary.payload.v1+json`.
gokarnm marked this conversation as resolved.
Show resolved Hide resolved
- **`io.cncf.notary.signingTime`**(*string*): This REQUIRED header specifies the time at which the signature was generated. This is an untrusted timestamp, and therefore not used in trust decisions. Its value is a RFC 3339 formatted date time, the optional fractional second ([time-secfrac](https://datatracker.ietf.org/doc/html/rfc3339#section-5.6)[[1](https://datatracker.ietf.org/doc/html/rfc3339#section-5.3)]) SHOULD NOT be used.
- **`io.cncf.notary.expiry`**(*string*): This OPTIONAL header provides a “best by use” time for the artifact, as defined by the signer. Its value is a RFC 3339 formatted date time, the optional fractional second ([time-secfrac](https://datatracker.ietf.org/doc/html/rfc3339#section-5.6)[[1](https://datatracker.ietf.org/doc/html/rfc3339#section-5.3)]) SHOULD NOT be used.
- **[`crit`](https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.11)**(*array of strings*): This OPTIONAL header lists the headers that implementation MUST understand and process. It MUST only contain headers apart from registered headers (e.g. `alg`, `cty`) in JWS specification, therefore this header is only present when the optional `io.cncf.notary.expiry` header is present in the protected headers collection.
If present, the value MUST be `["io.cncf.notary.expiry"]`.

## Unprotected Headers

Notary v2 supports following unprotected headers: `timestamp`, `x5c` and `io.cncf.notary.signingAgent`

```jsonc
{
"x5c": ["<Base64(DER(leafCert))>", "<Base64(DER(intermediateCACert))>", "<Base64(DER(rootCert))>"],
"io.cncf.notary.timestamptoken": "<Base64(TimeStampToken)>",
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"io.cncf.notary.timestamptoken": "<Base64(TimeStampToken)>",
"io.cncf.notary.timestamp": "<Base64(DER(TimeStampToken))>",

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got some more feedback on this, plan to rename to io.cncf.notary.timestampSignature to indicate it's a countersignature.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Isn't the value of RFC3161 TimeStampToken already DER encoded, do we need to specify it additionally here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess it's confusing because we mention DER encoding for the cert chain (x5c), which could originally be in PEM or DER format.

Copy link
Contributor

Choose a reason for hiding this comment

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

TimeStampToken is defined in terms of ASN.1, and section 3 specifies encodings for multiple transports like email, files, HTTP. All of them use DER encoding, and then some of them base64 encode after that. So yes, it's very likely DER already, but given that we define a new context I think we need to specify the ASN.1 encoding as well.

"io.cncf.notary.signingAgent": "notation/1.0.0"
}
```

- **[`x5c`](https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6)** (*array of strings*): This REQUIRED header contains the ordered list of X.509 certificate or certificate chain([RFC5280](https://datatracker.ietf.org/doc/html/rfc5280)) corresponding to the key used to digitally sign the JWS. The certificate chain is represented as a JSON array of certificate value strings, each string in the array is a base64-encoded DER certificate value. The certificate containing the public key corresponding to the key used to digitally sign the JWS MUST be the first certificate, followed by the intermediate and root certificates in the correct order. Refer [*Certificate Chain* unsigned attribute](signature-specification.md#unsigned-attributes) for more details.
- **`io.cncf.notary.timestamp`** (*string*): This OPTIONAL header is used to store countersignature that provides trusted signing time. Only [RFC3161]([rfc3161](https://datatracker.ietf.org/doc/html/rfc3161#section-2.4.2)) compliant `TimeStampToken` are supported.
- **TODO** Define the opaque datum (hash of envelope) that is sent to TSA, and how TSA response (time stamp token) is represented in this header.
- **`io.cncf.notary.signingAgent`**(*string*): This OPTIONAL header provides the identifier of a client (e.g. Notation) that produced the signature. E.g. “notation/1.0.0”. Refer [*Signing Agent* unsigned attribute](signature-specification.md#unsigned-attributes) for more details.

## Signature

In JWS signature is calculated by combining JWSPayload and protected headers.
The process is described below:

### Create the *JWS Signing Input*

1. Compute the Base64Url value of ProtectedHeaders, this is the value of `protected` property in the signature envelope.
1. Compute the Base64Url value of JWSPayload, this is the value of `payload` property in the signature envelope.
1. Build *JWS Signing Input* to be signed by concatenating the values generated in step 1 and step 2 using '.'
`ASCII(BASE64URL(UTF8(ProtectedHeaders)) ‘.’ BASE64URL(JWSPayload))`
Comment on lines +102 to +105
Copy link
Contributor

Choose a reason for hiding this comment

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

The Base64Url here includes paddings or not? It seems Base64Url is never defined in this documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, clarified that JWS uses base64url without padding as defined in Base64url Encoding in RFC 7515 section 2.


### Generate the signature

1. Compute the signature on the *JWS Signing Input* constructed in the previous step by using the signature algorithm of the signing key, which MUST match the corresponding protected header `alg`.
2. Compute the Base64Url value of the signature produced in the previous step.
This is the value of the `signature` property in the signature envelope.

## Signature Envelope

The final signature envelope comprises of Payload, ProtectedHeaders, UnprotectedHeaders, and Signature, no additional top level fields are supported.

Since Notary v2 restricts one signature per signature envelope, the compliant signature envelope MUST be in flattened JWS JSON format.

```jsonc
{
"payload": "<Base64Url(JWSPayload)>",
"protected": "<Base64Url(ProtectedHeaders)>",
"header": {
"io.cncf.notary.timestamp": "<Base64(TimeStampToken)>",
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"io.cncf.notary.timestamp": "<Base64(TimeStampToken)>",
"io.cncf.notary.timestamp": "<Base64(DER(TimeStampToken))>",

Copy link
Contributor

Choose a reason for hiding this comment

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

Time stamp token is a CMS signed-data and is BER encoded. If we require DER here, the client needs to convert the BER encoded timestamp token to DER encoded. Is it intended?

Copy link
Contributor

Choose a reason for hiding this comment

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

You may be right, I was looking at RFC 3161 which only says DER, not BER. Let's postpone any timestamp-related changes to a follow-up PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Time-Stamp Protocol via HTTP in RFC 3161 specifies that the TSA response is ASN.1 DER-encoded. Validated with a freetsa.org example here.

So the current content "io.cncf.notary.timestamp": "<Base64(TimeStampToken)>" is accurate, we don't specifically require DER, it's whichever format TSA servers respond with. I'm going to defer changes to timestamp related details, we can refine when we do the implementation. From the signature format's perspective it's an opaque token that is separately validated.

"x5c": ["<Base64(DER(leafCert))>", "<Base64(DER(intermediateCACert))>", "<Base64(DER(rootCert))>"]

Choose a reason for hiding this comment

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

Is cert ordering in the chain suggested or required?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's in order from leaf to root, will clarify in description.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need the root in the envelope? Isn't it redundant?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The root cert isn't generally required for verification. The intent is for signature producers to always include full chain, which will be validated by signing tool (notation), so that consumers can set trusted root as root cert (our recommendation). We want to avoid partial chains in the envelope as tooling cannot determine if the partial chain was only one level below the root. We can relax this requirement if there is a pressing reason in future.

Copy link
Contributor

Choose a reason for hiding this comment

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

What if we have multiple intermediate certs and CA certs? Are they allowed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you are referring to cross signed certificates, we currently don't support them, as they are used less frequently and are more difficult to validate using standard libraries.

},
"signature": "Base64Url( sign( ASCII( <Base64Url(ProtectedHeader)>.<Base64Url(JWSPayload)> )))"
}
```

## Implementation Constraints

### Supported `alg` header values

Notary v2 implementation MUST enforce the following constraints on signature generation and verification:

1. `alg` header value MUST NOT be `none` or any symmetric-key algorithm such as `HMAC`.
1. `alg` header value MUST be same as that of signature algorithm identified using signing certificate's public key algorithm and size.
1. `alg` header values for various signature algorithms is a subset of values supported by [JWS][jws-alg-values].

**Mapping of Notary v2 approved algorithms to JWS `alg` header values**

| Signature Algorithm | `alg` Header Value|
| ------------------------------- | ----------------- |
| RSASSA-PSS with SHA-256 | PS256 |
| RSASSA-PSS with SHA-384 | PS384 |
| RSASSA-PSS with SHA-512 | PS512 |
| ECDSA on secp256r1 with SHA-256 | ES256 |
| ECDSA on secp384r1 with SHA-384 | ES384 |
| ECDSA on secp521r1 with SHA-512 | ES512 |

1. Signing certificate MUST be a valid codesigning certificate.
1. Only JWS JSON flattened format is supported.

## FAQ

**Q:** Why JWT is not used as the signature envelope format?

**A:** JWT uses JWS compact serialization which do not support unsigned attributes. Notary v2 signature requires support for unsigned attributes. Instead we use the *JWS JSON Serialization* representation, which supports unsigned attributes.

**Q:** Why JWT `exp` and `iat` claims are not used?

**A:** Unlike JWT which always contains a JSON payload, Notary v2 envelope can support payloads other than JSON, like binary. Reusing the JWT payload structure and claims, limits the Notary v2 JWS envelope to only support JSON payload, which is undesirable. Also, reusing JWT claims requires following same claim semantics as defined in JWT specifications. The [`exp`](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4) claim requires that verifier MUST reject the signature if current time equals or is greater than `exp`, where as Notary v2 allows verification policy to define how expiry is handled.

[jws-alg-values]: https://datatracker.ietf.org/doc/html/rfc7518#section-3.1
Loading