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

Encryption processing is under-specified #31

Closed
bifurcation opened this issue Jan 26, 2022 · 17 comments
Closed

Encryption processing is under-specified #31

bifurcation opened this issue Jan 26, 2022 · 17 comments

Comments

@bifurcation
Copy link
Contributor

The current description in {{encryption-procedure}} is not sufficient to interoperably implement this, as it allows multiple interpretations. It is also unnecessarily complicated to implement.

On multiple interpretations: With a stream cipher like AES-CTR, it is not clear whether the 4-byte extension header advances the keystream or not. That is, does the cipher need to generate 4*CC + ext.length + payload.length bytes of keystream, which it splits over the two segments, or does it generate 4*CC + 4 + ext.length + payload.length bytes, and not use the four where the plaintext extension header goes (as in RFC 6904). Likewise, for AEAD ciphers like AES-GCM, the cipher authenticates the plaintext as a single contiguous byte string, so we need to specify what that byte string is.

On unnecessarily complicated: Most encryption APIs in the world take a single "plaintext" argument, comprising all of the plaintext to be encrypted. This is especially true for AEAD ciphers. The reassembly required by the current scheme forces implementations to do a bunch of copies to and from a temporary buffer on either side of the encryption.

There are basically two ways to address this problem:

  1. Clearly define the plaintext, but leave it discontiguous (don't solve the complexity problem). The interoperability problem would be fixed if you just defined that the plaintext is the concatenation of (1) the CSRCs, (2) the extension header data, and (3) the RTP payload. That would still be a hassle to implement, but at least you would have a chance of interop.

  2. Make the plaintext contiguous. If I understand correctly, the need for the plaintext header extension header is to allow a cryptex-encrypted RTP header to parse in the same way as an unencrypted one. Assuming this constraint, I don't really see an easy way to make the plaintext contiguous. You would basically have to have set X=0 and CC=<encrypted header size>, but then you would have to add the real X and CC values in the plaintext, which is again messy.

  3. Use a stream cipher to allow hole-punching. You could do this in the style of RFC 6904: Leave the main SRTP protection unchanged, but use a stream cipher to encrypt the CSRCs and header extension. This also makes it simple to punch a hole for the header extension header, in the same way RFC 6904 does.

My preference would be for (3) here. It's a larger change to the document, but I think it ends up being simpler to implement:

pkt[12..csrc_end] ^= stream_cipher.keystream(0, 4*cc);
pkt[ext_start..ext_end] ^= cryptex_keystream(4*cc + 4, 4*cc + 4 + ext.len);
@murillo128
Copy link
Collaborator

murillo128 commented Jan 26, 2022

We considered adding the transformed byte format to which apply the encryption/decryption to the spec:

      0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+<+
   |V=2|P|X|  CC   |M|     PT      |       sequence number         | |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
   |                           timestamp                           | |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
   |           synchronization source (SSRC) identifier            | |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
   |  0xC0 or 0xC2 |    0xDE       |           length=3            | |
 +>+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ |
 | |            contributing source (CSRC) identifiers             | |
 | |                               ....                            | |
 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
 | |                  RFC 8285 header extensions                   | |
 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
 | |                          payload  ...                         | |
 | |                               +-------------------------------+ |
 | |                               | RTP padding   | RTP pad count | |
 +>+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+<+
 | ~                     SRTP MKI (OPTIONAL)                       ~ |
 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
 | :                 authentication tag (RECOMMENDED)              : |
 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
 |                                                                   |
 +- Encrypted Portions*                     Authenticated Portion ---+

But it was agreed that:

The "cryptex reordered packet" is a conceptual description which shouldn't ever appear anywhere outside the srtp libraries. Depending on your cryptography implementation, it should be perfectly possible to implement cryptex without ever actually reordering bytes in memory, but rather just feeding the appropriate bytes to your crypto engine.

#18

Regarding complexity of the temporal buffers, having implemented it, it only took a couple of hours to add it to the libsrtp library and it only requires an 32bits temporal register plus 2xnum_of_csrcs 32 bits mem moves. I would not even know how to start implementing option (3) so hard to consider it as an easier to implement solution.

@bifurcation
Copy link
Contributor Author

It sounds like the intent of what was agreed in #18 was basically option (1) in the original comment here. The "cryptex reordered packet" sounds like it is the plaintext I refer to. But that notion never made it to the document, so even if the WG's decision is to go with this pattern, the document needs to be updated.

But you would still have the problem that you have to do all the moves you describe, which wouldn't be necessary with option (3). To your point about how to implement -- in libsrtp terms, you would just do what srtp_process_header_encryption does, but the method would be a lot shorter because (1) no loops and (2) no branch for one-byte vs. two-byte extensions.

@bifurcation
Copy link
Contributor Author

Here's a mockup of how you would do the encryption with option (3) in libsrtp:

bifurcation/libsrtp@c7cd74c

@juberti
Copy link
Owner

juberti commented Mar 7, 2022

Agree that we need to explain more about what we're doing here to ensure everyone is on the same page. Test vectors however should be sufficient to ensure proper interop.

Regarding 1) vs 3), there's about the same amount of code either way. I do agree 3) is slightly easier to reason about, since you just skip over bytes in a way that I suppose isn't possible with a block cipher. The fact that there is precedent for this in 6904 is also nice.

@JonathanLennox, thoughts on this?

@JonathanLennox
Copy link
Contributor

Since the existing test vectors (and thus the PRs for both libsrtp and jitsi-srtp) use option 1, I'm personally inclined to leave it that way, though I agree it could be better described.

I confess I don't quite understand how hole-punching can work for AEAD ciphers, since ciphertext from the hole isn't available for the verifier when verifying the authentication tag.

@bifurcation
Copy link
Contributor Author

@JonathanLennox The only way to "hole-punch" with AEAD is to assemble a byte string without the bytes you want to skip (either omitted or set to zero or something). Either way, there's some memcpy going on. This is what I meant by "a hassle to implement".

This is why my preference is for (3), because there are well-defined ways to do hole-punching with stream ciphers.

@JonathanLennox
Copy link
Contributor

As you say with AEAD you have to assemble the ciphertext as a byte stream without the bytes you want to skip, so it seems to me that it'd be simplest for CTR to do the same thing, rather than have separate mechanisms for CTR and AEAD.

@bifurcation
Copy link
Contributor Author

Well, the question is basically whether we always do the AEAD thing or always do the CTR thing (where in the latter case you would use some stream cipher corresponding to the AEAD, as with RFC 6904). These basically correspond to (1) and (3).

@JonathanLennox
Copy link
Contributor

The whole point of cryptex is that the header extensions and the payload (and the CSRCs) are encrypted in a single operation, so it's not clear to me what "using a stream cipher corresponding to the AEAD" would mean?

@bifurcation
Copy link
Contributor Author

To be concrete here about the proposals (imagining the primary encryption is AEAD and assuming the same ultimate packet layout):

Option 1: AEAD-like

  • AEAD encrypt into a ciphertext buffer:
    • plaintext = CSRCs || original-extension || payload
    • AAD = Header || shim-extension
  • Copy the first len(CSRCs) bytes of ciphertext back to the CSRCs block
  • Add the shim extension
  • Copy the remaining ciphertext to the payload

(You could make this more elegant by making the ciphertext contiguous, e.g., by setting CC=0 in the header and representing the real CC in the "defined by profile" of the shim header.)

Option 3: RFC 6904-like

  • Insert shim extension
  • Encrypt CSRCs and payload with stream cipher
  • AEAD encrypt with:
    • plaintext = []
    • AAD = whole-packet

Also, since both of those are kind of gross, here's a proposal with a different packet format that works more nicely with AEAD:

Option 4: AEAD but nicer

  • Add shim header containing original CC in "defined by profile"
  • Set CC=0 in header
  • AEAD-encrypt with:
    • plaintext = CSRCs || original-extension || payload
    • AAD = modified-header || shim-extension

So your packet format would look like:

     0                   1                   2                   3
   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+<+
  |V=2|P|X| CC=0  |M|     PT      |       sequence number         | |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
  |                           timestamp                           | |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
  |           synchronization source (SSRC) identifier            | |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
  |          0xCOD        |  CC   |           length=0            | |
+>+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ |
| |            contributing source (CSRC) identifiers             | |
| |                               ....                            | |
+>+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| |                  RFC 8285 header extensions                   | |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| |                          payload  ...                         | |
| |                               +-------------------------------+ |
| |                               | RTP padding   | RTP pad count | |
+>+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+<+
| ~                     SRTP MKI (OPTIONAL)                       ~ |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| :                 authentication tag (RECOMMENDED)              : |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
|                                                                   |
+- Encrypted Portions*                     Authenticated Portion ---+

@JonathanLennox
Copy link
Contributor

I still feel like option 1 is the best. It's just a buffer copy of 4*(cc+1) bytes (only when cc != 0), and my implementation https://github.com/jitsi/jitsi-srtp/pull/29/files#diff-5e0923c6a3d2fde412050f213229e9c3dc69b8c3b35d895ffecd66364994b2cfR411 is like 15 lines of simple code.

@JonathanLennox
Copy link
Contributor

But really at this point I feel like this is a topic big enough that it should get WG consensus. Will you be in person in Vienna?

@bifurcation
Copy link
Contributor Author

Assuming you == me, I will not be in Vienna in person, but I'm happy to dial in.

Re: buffer copy of 4*(cc+1) -- Fair point. That does make it seem less awful. There is a bit of extra complexity because you have to do the swap before and after encryption. In other words, the logical patch looks like:

 csrc_start = pkt_start + fixed_hdr_size;
 ext_hdr_start = csrc_start + (4 * cc);
 ext_data_start = ext_hdr + fixed_ext_hdr_size;
 payload_start = ext_data_start + ext_data_size;
 
 encrypt_start = payload_start;
 encrypt_size = payload_size;
+if (cryptex) {
+  encrypt_start = csrc_start + 4;
+  encrypt_size = (4 * cc) + ext_data_size + payload_size;
+  move_ext_hdr_up(csrc_start, ext_hdr_start);
+}
 
 aead_encrypt(aad=pkt[..encrypt_start], encrypt_start, encrypt_size);
 
+if (cryptex) {
+  move_ext_hdr_down(csrc_start, ext_hdr_start);
+}

Does that look right?

In any case, it would probably be good to describe that in the document as a suggestion to implementers.

@murillo128
Copy link
Collaborator

I can try to prepare a PR to add the "internal" packet structure for AED and how to generate it from and to an RTP packet as a note to the implemementators.

I would however not able to present the alternatives in the meeting as I think my crypto knowledge is not good enough to do a proper job and lead the discussions.

@bifurcation
Copy link
Contributor Author

FWIW, I wouldn't phrase it as the "internal packet structure", but as "the inputs to the encryption", roughly as above:

plaintext = CSRCs || extension-data || payload
AAD = header || extension-header

With an explanatory note that you can transform a packet to make the plaintext continuous by swapping the CSRCs and the extension header (which might be clearer with a picture).

If it would be helpful, I could present the alternatives in the meeting.

@murillo128
Copy link
Collaborator

I have created a PR for clarifying the plaintext and associated data in use for the encryption context. Would be good if you can check if this approach works for you or you would like to present a alternative encryption method.

@murillo128
Copy link
Collaborator

fixed in #45

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants