Skip to content

Commit 4505047

Browse files
committed
Add Cryptographic Message Syntax (CMS) API Explainer
1 parent 1dcea02 commit 4505047

2 files changed

Lines changed: 344 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
update-toc:
2+
doctoc README.md
Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
# Cryptographic Message Syntax (CMS) API Explainer
2+
3+
Authors: [Jon Choukroun](https://github.com/jonchoukroun), Michael Hashe, [Marcos Cáceres](http://github.com/marcoscaceres/).
4+
5+
This explainer presents a straw-person proposal for a Web API that expose Cryptographic Message Syntax (CMS) functionality to JavaScript. Due to the complexity, and ambiguities in the CMS RFC, this API aim is to reduce the need of third-party JavaScript libraries to perform CMS operations (particularly as they relate to email). As such, the proposed APIs provide what we believe to be the bare minimum API surface to sign, encrypt, decrypt, and verify S/MIME messages, in addition to some utility methods for ease-of-use by developers. How user agents implement CMS encoding and the underlying cryptographic operations (including remote key usage) is beyond the scope of this draft.
6+
7+
## Table of contents
8+
9+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
10+
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
11+
12+
- [Cryptographic Message Syntax (CMS) API Explainer](#cryptographic-message-syntax-cms-api-explainer)
13+
- [Table of contents](#table-of-contents)
14+
- [Extensions to the Crypto interface](#extensions-to-the-crypto-interface)
15+
- [The `CMSEnvelopedData` interface](#the-cmsenvelopeddata-interface)
16+
- [Encrypting](#encrypting)
17+
- [Decrypting](#decrypting)
18+
- [Signing](#signing)
19+
- [Verifying](#verifying)
20+
- [Common CMS Types](#common-cms-types)
21+
- [Content Type](#content-type)
22+
- [Key Handles](#key-handles)
23+
- [CMS Utilities](#cms-utilities)
24+
- [Parsing S/MIME](#parsing-smime)
25+
- [Examples](#examples)
26+
- [Processing a received message that was signed-encrypted-signed](#processing-a-received-message-that-was-signed-encrypted-signed)
27+
- [Signing, encrypting, and signing a message](#signing-encrypting-and-signing-a-message)
28+
29+
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
30+
31+
## Extensions to the Crypto interface
32+
33+
As the API closely mirrors the existing Web Crypto “subtle” functionality, we propose extending the `Crypto` interface to add a `cms` attribute which would expose the needed “CMS” functionality.
34+
35+
```WebIDL
36+
// Extend the Crypto interface to include the cms property
37+
partial interface Crypto {
38+
[SameObject]
39+
readonly attribute CMSCrypto cms;
40+
};
41+
42+
[Exposed=(Window, Worker), SecureContext]
43+
interface CMSCrypto {
44+
// Methods defined below as partial interfaces
45+
};
46+
```
47+
48+
## The `CMSEnvelopedData` interface
49+
50+
```WebIDL
51+
interface CMSEnvelopedData {
52+
readonly attribute CMSContentType contentType;
53+
readonly attribute ContentEncryptionAlgorithm contentEncryptionAlgorithm;
54+
readonly attribute ArrayBuffer encryptedContent;
55+
readonly attribute FrozenArray<RecipientInfo> recipientInfos;
56+
};
57+
```
58+
59+
## Encrypting
60+
61+
The `encrypt()` method would allow the web application to encrypt content with the public key(s) of one or more recipients. The API requires an algorithm specifier for both content encryption (symmetric) and key encryption (asymmetric), which must both be supported by the underlying CMS implementation. Recipient key identifiers must be valid.
62+
63+
```WebIDL
64+
partial interface CMSCrypto {
65+
Promise<CMSEnvelopedData> encrypt(
66+
ContentEncryptionAlgorithm contentEncryptionAlgorithm,
67+
KeyEncryptionAlgorithm keyEncryptionAlgorithm,
68+
sequence<CryptoKeys> recipientKeys,
69+
ArrayBuffer data
70+
);
71+
};
72+
```
73+
74+
- `contentEncryptionAlgorithm`: The algorithm used to encrypt the content. This parameter specifies the content encryption algorithm identifier, which determines how the data (content) will be symmetrically encrypted.
75+
- `keyEncryptionAlgorithm`: The algorithm used to encrypt the symmetric keys. This specifies the key encryption algorithm identifier, which is used to encrypt the symmetric key(s) that were used to encrypt the content. Each recipient's public key is used in conjunction with this algorithm to encrypt the symmetric key, ensuring that only the intended recipients can decrypt the content.
76+
- recipientKeys: An array of `CryptoKey` objects that point to recipients' public keys, which will be used to assymetrically encrypt the generated symmetric key(s). Each `CryptoKey` in the array corresponds to a specific recipient, allowing for the encrypted content to be securely shared with multiple recipients. How the underlying implementation matches a key handle to public key material is a user-agent implementation detail, and out of scope for this draft.
77+
- `data`: The cleartext data to encrypt. This parameter is the actual data (in the form of an `ArrayBuffer`) to be encrypted. The data is symmetrically encrypted using the specified content encryption algorithm, and the result is included in the CMS Enveloped Data structure that the method returns.
78+
79+
The CMS implementation will follow the [content-encryption](https://datatracker.ietf.org/doc/html/rfc5652#section-6.3) and [key-encryption](https://datatracker.ietf.org/doc/html/rfc5652#section-6.4) processes, then wrap the outputs into an enveloped-data as a `CMSEnvelopedData` instance. The encrypted content should be in MIME canonical format.
80+
81+
The CMS spec defines other encryption syntaxes, which are beyond the scope of this document.
82+
83+
## Decrypting
84+
85+
The `decrypt()` method provides a way for web applications to decrypt content that has been encrypted using the Cryptographic Message Syntax (CMS) Enveloped-Data content type. This method allows for the decryption of data using a handle to the user's private key, facilitating secure communication and data exchange in web applications.
86+
87+
```WebIDL
88+
partial interface CMSCrypto {
89+
Promise<ArrayBuffer> decrypt(CryptoKey privateKey, CMSEnvelopedData data);
90+
};
91+
```
92+
93+
Web applications can pass in a parsed CMS enveloped-data type, and a `CryptoKey` object of the user’s private decryption key, to get back the decrypted cleartext as an array of bytes. The message contents or specific S/MIME parts should be parsed using the `parseSMIME()`. Then the output can be passed into this API for decryption.
94+
95+
To successfully decrypt the data, the CMS implementation must get the encrypted content-encryption key from the matching `RecipientInfo` entry. After decrypting this symmetric key using the user’s private asymmetric `CryptoKey` object, the implementation can then decrypt the encrypted content. This cleartext byte array can be converted to a string, then further parsed into a CMS data object if necessary.
96+
97+
## Signing
98+
99+
The `.sign()` method in the `CMSCrypto` interface simplifies the creation of a digital signature for given data. This single API call performs hashing of the data using a specified digest algorithm, followed by signing that hash with a specified signature algorithm. The process culminates in the generation of a CMS signed-data object, which encapsulates the signature, signer information, and the original content.
100+
101+
A successful response will resolve to a CMS signed-data object (`CMSSignedData`), which contains the signature, signer data (including certificates), and the content that was signed.
102+
103+
```WebIDL
104+
partial interface CMSCrypto {
105+
Promise<CMSSignedData> sign(DigestAlgorithm digestAlgorithm,
106+
SignatureAlgorithm signAlgorithm,
107+
CryptoKey signingKey,
108+
ArrayBuffer data);
109+
};
110+
111+
interface CMSSignedData {
112+
readonly attribute CMSContentType contentType;
113+
readonly attribute ArrayBuffer content;
114+
// Should include all SignerInfo.digestAlgorithms
115+
readonly attribute FrozenArray<DigestAlgorithm> digestAlgorithms;
116+
readonly attribute FrozenArray<X509Certificate>;
117+
// One for each signer
118+
readonly attribute FrozenArray<SignerInfo> signerInfos;
119+
}
120+
121+
interface SignerInfo {
122+
// Must reference a certificate in the CMSSignedDataType.certificates list
123+
// See [Signer Info Type (RFC 5652)](https://datatracker.ietf.org/doc/html/rfc5652#section-5.3) for more details
124+
readonly attrbiute SignatureIdentifier signerId;
125+
readonly attrbiute DigestAlgorithm digestAlgorithm;
126+
readonly attrbiute SignatureAlgorithm signatureAlgorithm;
127+
readonly attrbiute ArrayBuffer signature;
128+
}
129+
```
130+
131+
## Verifying
132+
133+
The `.verify()` method is designed to validate the integrity and authenticity of a CMS signed-data object. By analyzing the signed content and the signer's certificate contained within, the method validates the signature and that the content has not been altered. This verification process is crucial for establishing trust in the data received.
134+
135+
```WebIDL
136+
enum VerificationStatus {"pass", "permerror", "neutral", "none"};
137+
138+
partial interface CMSCrypto {
139+
Promise<VerificationStatus> verify(CMSSignedData signedData);
140+
};
141+
142+
```
143+
144+
## Common CMS Types
145+
146+
### Content Type
147+
148+
Identifies the CMS type of a given object. This can be an OID or other implementation, TBD.
149+
150+
### Key Handles
151+
152+
We are making a parallel proposal to extend the [Proposed "remote" `CryptoKey` interface](https://github.com/WebKit/explainers/tree/main/remote-cryptokeys), to support remote key use. This will allow CMS operations to use a pointer to a key that is securely stored on the user’s device, for example in Keychain. The expectation is that the CMS implementation would use the underlying cryptographic operations called by Web Crypto, using the remote `CryptoKey` handle.
153+
154+
## CMS Utilities
155+
156+
### Parsing S/MIME
157+
158+
This API allows the web application to pass in one or more S/MIME parts and get back an array of CMS data objects. The array can contain any CMS data type . The data objects can be passed into CMS APIs to verify, decrypt, and so on.
159+
160+
```
161+
partial interface CMSCrypto {
162+
Promise<sequence<CMSData>> parseSMIME(sequence<ArrayBuffer> parts);
163+
};
164+
165+
typedef (CMSEnvelopedData or CMSSignedData) CMSData;
166+
```
167+
168+
## Examples
169+
170+
### Processing a received message that was signed-encrypted-signed
171+
172+
```JS
173+
const { generateKey } = window.crypto.subtle;
174+
const { cms } = window.crypto;
175+
/**
176+
* Processes a received message that was signed-encrypted-signed.
177+
* Assumes message is already converted to an ArrayBuffer.
178+
* @async
179+
* @returns {Promise<ArrayBuffer[]>} Processed message parts
180+
*/
181+
async function processMessage() {
182+
const msg = readMessageIntoBuffer();
183+
const cmsData = await cms.parseSMIME([msg]);
184+
185+
const processedData = cmsData.map(async (part) => {
186+
let content;
187+
188+
try {
189+
switch (part.contentType) {
190+
case ENVELOPED_DATA_TYPE:
191+
const clearText = await processEncryptedData(part);
192+
193+
// Assuming there's a nested signature
194+
const innerSig = await cms.parseSMIME([clearText]);
195+
// Throws
196+
await cms.verify(innerSig[0]); // Assuming verify() and first part only
197+
content = innerSig[0].content;
198+
break;
199+
200+
case SIGNED_DATA_TYPE:
201+
// Throws
202+
await cms.verify(part);
203+
content = part.content;
204+
break;
205+
206+
default:
207+
throw new Error("Unsupported content type");
208+
}
209+
} catch (error) {
210+
console.error(
211+
"Verification failed or unsupported content type:",
212+
error
213+
);
214+
throw error; // Rethrow or handle as needed
215+
}
216+
217+
return content;
218+
});
219+
220+
return Promise.all(processedData);
221+
};
222+
223+
async function processEncryptedData(envelopedData: CMSEnvelopedData) {
224+
// Get a pointer to the remote decryption key
225+
const decryptKey = await window.crypto.subtle.generateKey(
226+
{
227+
name: "remote",
228+
action: "fetch",
229+
userIdentifier: "alice@icloud.com",
230+
},
231+
false,
232+
["decrypt"]
233+
);
234+
const dataBuf = await cms.decrypt(decryptKey, envelopedData);
235+
return dataBuf;
236+
}
237+
```
238+
239+
### Signing, encrypting, and signing a message
240+
241+
```JS
242+
const { generateKey } = window.crypto.subtle;
243+
const { cms } = window.crypto;
244+
245+
async function composeSESMessage() {
246+
// Message content, assembled into MIME parts and read into an ArrayBuffer
247+
const msgParts = assembleMimeParts();
248+
const msgBuf = convertToBuffer(msgParts);
249+
250+
// Recipients
251+
const recipients = ["alice@example.com", "bob@example.com"];
252+
253+
// Sign contents (only 1 signer)
254+
const [innerSignature] = await handleSigning(msgBuf);
255+
256+
// Wrap contents and signature into multipart/signed MIME part
257+
const signedMsg = createSignedMime(msgParts, innerSignature);
258+
const signedMsgBuf = convertToBuffer(signedMsg);
259+
260+
// Encrypt contents + inner signaure
261+
const { encryptedContent } = await handleEncryption(signedMsgBuf, recipients);
262+
263+
// Create encrypted MIME part
264+
const encryptedMsg = createEncryptedMime(encryptedContent);
265+
const encryptedMsgBuf = convertToBuffer(encryptedMsg);
266+
267+
// Sign encrypted contents
268+
const [outerSignature] = await handleSigning(encryptedMsgBuf);
269+
270+
// Wrap encrypted content and outer signature in MIME part
271+
return createSignedMime(encryptedMsg, outerSignature);
272+
}
273+
274+
async function handleSigning(data: ArrayBuffer) {
275+
// Get a pointer to the remote signing key
276+
const signerKey = await window.crypto.subtle.generateKey(
277+
{
278+
name: "remote",
279+
action: "fetch",
280+
userIdentifier: "alice@icloud.com",
281+
},
282+
false,
283+
["sign"]
284+
);
285+
286+
// Define required algorithms
287+
const digestAlgo = "SHA-256";
288+
const signAlgo = {
289+
name: "RSA-PSS",
290+
saltLength: someNumber,
291+
};
292+
293+
// Sign and return signatures as array
294+
const signedData = await cms.signData(digestAlgo, signAlgo, signerKey, data);
295+
return signedData.signerInfos.map((s) => s.signature);
296+
}
297+
298+
// Assumes a remote key handle accessor based on recipient email addresses
299+
async function handleEncryption(data, recipients) {
300+
// Get recipient key handles, can use remote key interface
301+
const recipientKeys = recipients
302+
.map(fetchRecipientKey)
303+
.map(async (recipientEmail) => {
304+
return await window.crypto.subtle.generateKey(
305+
{
306+
name: "remote",
307+
action: "fetch",
308+
userIdentifier: recipientEmail,
309+
},
310+
// False is required, even for public keys
311+
false,
312+
["encrypt"]
313+
);
314+
});
315+
316+
// Define required algorithms
317+
const contentAlgo = {
318+
name: "AES-CTR",
319+
counter: Uint8Array,
320+
length: someNumber,
321+
};
322+
const keyAlgo = {
323+
name: "ECDSA",
324+
hash: { name: someString }, // eg: SHA-384
325+
};
326+
327+
// Encrypt
328+
const envelopedData = await cms.encrypt(
329+
contentAlgo,
330+
keyAlgo,
331+
recipientKeys,
332+
data
333+
);
334+
335+
// Confirm recipientInfos length matches recipient count
336+
if (envelopedData.recipientInfos.length !== recipients.length) {
337+
// error handling
338+
}
339+
340+
return envelopedData.encryptedContent;
341+
}
342+
```

0 commit comments

Comments
 (0)