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 the public API to remove footguns, and document it. #52

Merged
merged 1 commit into from
Mar 22, 2021
Merged
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
7 changes: 3 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ece"
version = "1.3.0"
version = "1.4.0-alpha1"
authors = ["Firefox Sync Team <[email protected]>", "JR Conlin <[email protected]>"]
license = "MPL-2.0"
edition = "2018"
Expand All @@ -12,20 +12,19 @@ keywords = ["http-ece", "web-push"]
byteorder = "1.3"
thiserror = "1.0"
base64 = "0.12"
hex = "0.4"
hkdf = { version = "0.9", optional = true }
lazy_static = { version = "1.4", optional = true }
once_cell = "1.4"
openssl = { version = "0.10", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
sha2 = { version = "0.9", optional = true }

[dev-dependencies]
hex = "0.4"

[features]
default = ["backend-openssl", "serializable-keys"]
serializable-keys = ["serde"]
backend-openssl = ["openssl", "lazy_static", "hkdf", "sha2"]
backend-test-helper = []

[package.metadata.release]
no-dev-version = true
98 changes: 87 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,99 @@
[Latest Version]: https://img.shields.io/crates/v/ece.svg
[crates.io]: https://crates.io/crates/ece

*This crate has not been security reviewed yet, use at your own risk ([tracking issue](https://github.com/mozilla/rust-ece/issues/18))*.
*This crate has not been security reviewed yet, use at your own risk
([tracking issue](https://github.com/mozilla/rust-ece/issues/18))*.

[ece](https://crates.io/crates/ece) is a Rust implementation of the HTTP Encrypted Content-Encoding standard (RFC 8188). It is a port of the [ecec](https://github.com/web-push-libs/ecec) C library.
This crate is destined to be used by higher-level Web Push libraries, both on the server and the client side.
The [ece](https://crates.io/crates/ece) crate is a Rust implementation of Message Encryption for Web Push
([RFC8291](https://tools.ietf.org/html/rfc8291)) and the HTTP Encrypted Content-Encoding scheme
([RFC8188](https://tools.ietf.org/html/rfc8188)) on which it is based.

[Documentation](https://docs.rs/ece/)
It provides low-level cryptographic "plumbing" and is destined to be used by higher-level Web Push libraries, both on
the server and the client side. It is a port of the [ecec](https://github.com/web-push-libs/ecec) C library.

## Cryptographic backends

This crate is designed to be used with different crypto backends. At the moment only [openssl](https://github.com/sfackler/rust-openssl) is supported.
[Full Documentation](https://docs.rs/ece/)

## Implemented schemes

Currently, two HTTP ece schemes are available to consumers of the crate:
- The newer [RFC8188](https://tools.ietf.org/html/rfc8188) `aes128gcm` standard.
- The legacy [draft-03](https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-03) `aesgcm` scheme.
This crate implements both the published Web Push Encryption scheme, and a legacy scheme from earlier drafts
that is still widely used in the wild:

* `aes128gcm`: the scheme described in [RFC8291](https://tools.ietf.org/html/rfc8291) and
[RFC8188](https://tools.ietf.org/html/rfc8188)
* `aesgcm`: the draft scheme described in
[draft-ietf-webpush-encryption-04](https://tools.ietf.org/html/draft-ietf-webpush-encryption-04) and
[draft-ietf-httpbis-encryption-encoding-03](https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-03_)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would really be in favor of dropping the oldest one of these. I'll file a separate ticket. Most of what we're seeing is either 'aes128gcm' or 'aesgcm'.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, we can definitely consider dropping the really old aesgcm128 format:

image

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 don't think the code actually supports that format, this could just be a documentation issue or misunderstanding on my side.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jrconlin I'm not very familiar with the various versions of these specs, could you please help me understand what you mean here? The two linked "draft" documents only seem to mention "aesgcm", which IIUC is the "legacy" line in your graph and the corresponding "legacy" API in this crate. Neither contains the string "aesgcm128" that I can see.

I suspect we don't have anything to remove in the code, but I'd be happy to update the doc references here to make things more clear, if you can suggest any revisions.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Heh. The fun of a long lived and somewhat popular draft is that you get stuff like this. I don't expect that this PR should drop support for the very old protocol, but I do think we should add it as a future issue.

In essence, three "versions" of the encryption format started being used over time. The very first version "aesgcm128" used a combination of header and body content in order to encrypt/decrypt the message. It was reasonably early so not a lot of libraries adopted it, but firefox said that they'd support it as part of the WebPush pre-cursor. The next version cleaned things up, but still used header/body format (albeit, with different headers and formats), and was labeled "aesgcm". This version was WIDELY adopted by third party encoder libraries and (as you can see) is still very much in use. The final RFC proposed "aes128gcm", which made encryption/decryption self contained within the body of the message. While folks have been encouraging subscription providers to update their libraries to use the RFC format (since it's both more efficient and far more secure), a lot of subscription services have not.

There are not a lot of changes between "aesgcm128" and "aesgcm", but they are present. Supporting the very old format just adds unneeded complexity and potential surface area for the library. For most client side / decryption models, we can definitely control this from the Push Server by simply blocking messages that use "aesgcm128" with a 415 or something. For encryption, however, it would be best to remove that particular format as a potential option.

All things must come to an end, and for "aesgcm128", it's way overdue.

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've dug in a bit more here, and I don't believe this crate has support for aesgcm128. I've been using the desktop implementation for reference to help get my head around it.

In order to support aesgcm128, we would need to be:

  • Using a KDF info string of "Content-Encoding: aesgcm128"; I can't find such a string anywhere in the repo.
  • Emitting an Encryption-Key header as part of the encryption process; I can only find support for emitting the Crypto-Key and Encryption headers of the newer-but-still-legacy aesgcm scheme.

I've made the following updates to help clarify what I believe the situation is, but if I've misunderstood, please correct me:

  • I've delete a reference to the Encryption-Key header in one of the comments, which was the only place I could find such a string, and which is incorrect since the comment is specifically in the context of aesgcm which uses the Encryption field.
  • I've added an explicit note in the readme saying this scheme is not and will never be supported, along with a link to the older draft RFC for reference (draft-thomson-http-encryption-02, which I dug out of the comments in the desktop implementation).


It does not support, and we have no plans to ever support, the obsolete `aesgcm128` scheme
from [earlier drafts](https://tools.ietf.org/html/draft-thomson-http-encryption-02).

## Usage

To receive messages via WebPush, the receiver must generate an EC keypair and a symmetric authentication secret,
then distribute the public key and authentication secret to the sender:

```
let (keypair, auth_secret) = ece::generate_keypair_and_auth_secret()?;
let pubkey = keypair.pub_as_raw();
// Base64-encode the `pubkey` and `auth_secret` bytes and distribute them to the sender.
```

The sender can encrypt a Web Push message to the receiver's public key:

```
let ciphertext = ece::encrypt(&pubkey, &auth_secret, b"payload")?;
```

And the receiver can decrypt it using their private key:

```
let plaintext = ece::decrypt(&keypair, &auth_secret, &ciphertext)?;
```

That's pretty much all there is to it! It's up to the higher-level library to manage distributing the encrypted payload,
typically by arranging for it to be included in a HTTP response with `Content-Encoding: aes128gcm` header.

### Legacy `aesgcm` encryption

The legacy `aesgcm` scheme is more complicated, because it communicates some encryption parameters in HTTP header fields
rather than as part of the encrypted payload. When used for encryption, the sender must deal with `Encryption` and
`Crypto-Key` headers in addition to the ciphertext:

```
let encrypted_block = ece::legacy::encrypt_aesgcm(pubkey, auth_secret, b"payload")?;
for (header, &value) in encrypted_block.headers().iter() {
// Set header to corresponding value
}
// Send encrypted_block.body() as the body
```

When receiving an `aesgcm` message, the receiver needs to parse encryption parameters from the `Encryption`
and `Crypto-Key` fields:

```
// Parse `rs`, `salt` and `dh` from the `Encryption` and `Crypto-Key` headers.
// You'll need to consult the spec for how to do this; we might add some helpers one day.
let encrypted_block = ece::AesGcmEncryptedBlock::new(dh, rs, salt, ciphertext);
let plaintext = ece::legacy::decrypt_aesgcm(keypair, auth_secret, encrypted_block)?;
```

### Unimplemented Features

* We do not implement streaming encryption or decryption, although the ECE scheme is designed to permit it.
* We only support encrypting or decrypting across multiple records for `aes128gcm`; messages using the
legacy `aesgcm` scheme must fit in a single record.
* We do not support customizing the record size parameter during encryption, but do check it during decryption.
* The default record size is 4096 bytes.
* We do not support customizing the number of padding bytes added during encryption.
* We currently select the padding length at random for each encryption, but this is an implementation detail and
should not be relied on.

These restrictions might be lifted in future, if it turns out that we need them.

## Cryptographic backends

This crate is designed to use pluggable backend implementations of low-level crypto primitives. different crypto
backends. At the moment only [openssl](https://github.com/sfackler/rust-openssl) is supported.

## Release process

Expand All @@ -34,4 +111,3 @@ make sure you have it installed and then:
it's proposing to do seem sensible.
3. Run `cargo release [major|minor|patch]` to prepare, commit, tag and publish the release.
4. Make a PR from your `release-vX.Y.Z` branch to request it be merged to the main branch.

25 changes: 5 additions & 20 deletions src/aes128gcm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,8 @@ const ECE_AES128GCM_NONCE_INFO: &str = "Content-Encoding: nonce\0";
/// Web Push encryption structure for the AES128GCM encoding scheme ([RFC8591](https://tools.ietf.org/html/rfc8291))
///
/// This structure is meant for advanced use. For simple encryption/decryption, use the top-level [`encrypt`](crate::encrypt) and [`decrypt`](crate::decrypt) functions.
pub struct Aes128GcmEceWebPush;
pub(crate) struct Aes128GcmEceWebPush;
impl Aes128GcmEceWebPush {
/// Encrypts a Web Push message using the "aes128gcm" scheme. This function
/// automatically generates an ephemeral ECDH key pair.
pub fn encrypt(
remote_pub_key: &dyn RemotePublicKey,
auth_secret: &[u8],
plaintext: &[u8],
params: WebPushParams,
) -> Result<Vec<u8>> {
let cryptographer = crypto::holder::get_cryptographer();
let local_prv_key = cryptographer.generate_ephemeral_keypair()?;
Self::encrypt_with_keys(
&*local_prv_key,
remote_pub_key,
auth_secret,
plaintext,
params,
)
}

/// Encrypts a Web Push message using the "aes128gcm" scheme, with an explicit
/// sender key. The sender key can be reused.
pub fn encrypt_with_keys(
Expand Down Expand Up @@ -136,6 +117,10 @@ impl EceWebPush for Aes128GcmEceWebPush {
false
}

fn allow_multiple_records() -> bool {
true
}

fn pad_size() -> usize {
ECE_AES128GCM_PAD_SIZE
}
Expand Down
49 changes: 19 additions & 30 deletions src/aesgcm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,20 @@ const ECE_WEBPUSH_RAW_KEY_LENGTH: usize = 65;
const ECE_WEBPUSH_IKM_LENGTH: usize = 32;

pub struct AesGcmEncryptedBlock {
pub dh: Vec<u8>,
pub salt: Vec<u8>,
pub rs: u32,
pub ciphertext: Vec<u8>,
pub(crate) dh: Vec<u8>,
pub(crate) salt: Vec<u8>,
pub(crate) rs: u32,
pub(crate) ciphertext: Vec<u8>,
}

impl AesGcmEncryptedBlock {
pub fn aesgcm_rs(rs: u32) -> u32 {
fn aesgcm_rs(rs: u32) -> u32 {
if rs > u32::max_value() - ECE_TAG_LENGTH as u32 {
return 0;
}
rs + ECE_TAG_LENGTH as u32
}

/// Create a new block from the various header strings and body content.
pub fn new(
dh: &[u8],
salt: &[u8],
Expand Down Expand Up @@ -87,35 +86,20 @@ impl AesGcmEncryptedBlock {
}

/// Encode the body as a String.
/// If you need the bytes, probably just call .ciphertext directly
pub fn body(&self) -> String {
base64::encode_config(&self.ciphertext, base64::URL_SAFE_NO_PAD)
}
}
/// Web Push encryption structure for the legacy AESGCM encoding scheme ([Web Push Encryption Draft 4](https://tools.ietf.org/html/draft-ietf-webpush-encryption-04))

/// Web Push encryption structure for the legacy AESGCM encoding scheme
/// ([Web Push Encryption Draft 4](https://tools.ietf.org/html/draft-ietf-webpush-encryption-04))
///
/// This structure is meant for advanced use. For simple encryption/decryption, use the top-level [`encrypt_aesgcm`](crate::legacy::encrypt_aesgcm) and [`decrypt_aesgcm`](crate::legacy::decrypt_aesgcm) functions.
pub struct AesGcmEceWebPush;
impl AesGcmEceWebPush {
/// Encrypts a Web Push message using the "aesgcm" scheme. This function
/// automatically generates an ephemeral ECDH key pair.
pub fn encrypt(
remote_pub_key: &dyn RemotePublicKey,
auth_secret: &[u8],
plaintext: &[u8],
params: WebPushParams,
) -> Result<AesGcmEncryptedBlock> {
let cryptographer = crypto::holder::get_cryptographer();
let local_prv_key = cryptographer.generate_ephemeral_keypair()?;
Self::encrypt_with_keys(
&*local_prv_key,
remote_pub_key,
auth_secret,
plaintext,
params,
)
}
/// This structure is meant for advanced use. For simple encryption/decryption, use the top-level
/// [`encrypt_aesgcm`](crate::legacy::encrypt_aesgcm) and [`decrypt_aesgcm`](crate::legacy::decrypt_aesgcm)
/// functions.
pub(crate) struct AesGcmEceWebPush;

impl AesGcmEceWebPush {
/// Encrypts a Web Push message using the "aesgcm" scheme, with an explicit
/// sender key. The sender key can be reused.
pub fn encrypt_with_keys(
Expand Down Expand Up @@ -175,6 +159,11 @@ impl EceWebPush for AesGcmEceWebPush {
ciphertextlen as u32 % rs == 0
}

/// Don't allow multiple records for this legacy scheme.
fn allow_multiple_records() -> bool {
false
}

fn pad_size() -> usize {
ECE_AESGCM_PAD_SIZE
}
Expand All @@ -201,7 +190,7 @@ impl EceWebPush for AesGcmEceWebPush {
Ok(&block[(2 + padding_size)..])
}

/// Derives the "aesgcm" decryption keyn and nonce given the receiver private
/// Derives the "aesgcm" decryption key and nonce given the receiver private
/// key, sender public key, authentication secret, and sender salt.
fn derive_key_and_nonce(
ece_mode: EceMode,
Expand Down
Loading