-
Notifications
You must be signed in to change notification settings - Fork 44
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
Changes from 6 commits
3d8617b
33d1ee9
3e93835
f705cb8
59ab1b7
868585e
ca186b4
5e1ccf6
2378235
49e1687
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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", | ||||||
"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", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not RFC 3339, missing the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||
"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)>", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got some more feedback on this, plan to rename to There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)>", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||
"x5c": ["<Base64(DER(leafCert))>", "<Base64(DER(intermediateCACert))>", "<Base64(DER(rootCert))>"] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is cert ordering in the chain suggested or required? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's in order from leaf to root, will clarify in description. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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.
need version here:
application/vnd.cncf.notary.signature.v2