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

Unambiguous signatures #526

Merged
merged 7 commits into from Jan 26, 2022
Merged

Unambiguous signatures #526

merged 7 commits into from Jan 26, 2022

Conversation

TWal
Copy link
Contributor

@TWal TWal commented Dec 16, 2021

Signatures as specified in the MLS protocol document are ambiguous, in the sense that signatures produced for one purpose in the protocol can be (maliciously) used in a different place of the protocol.
Indeed, we found that for example, a KeyPackage can have the same serialization as MLSPlaintextTBS, which proves that one signature could be accepted for different structures.

The collisions we have found so far are quite artificial, so it is not super clear how to exploit this, but at least it makes it impossible to have provable security.

The solution is pretty simple, we simply prefix each signed value with a label, in a fashion similar to ExpandWithLabel.
That way, when calling SignWithLabel you can give the structure name in the Label argument.

Copy link
Contributor

@kkohbrok kkohbrok left a comment

Choose a reason for hiding this comment

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

Great catch and a neat solution!

@TWal
Copy link
Contributor Author

TWal commented Dec 16, 2021

Something that is still bothering me is the MLSPlaintextTBS structure:

struct {
    select (MLSPlaintextTBS.sender.sender_type) {
        case member:
            GroupContext context;

        case preconfigured:
        case new_member:
            struct{};
    }

    WireFormat wire_format;
    opaque group_id<0..255>;
    uint64 epoch;
    Sender sender;
    opaque authenticated_data<0..2^32-1>;

    ContentType content_type;
    select (MLSPlaintextTBS.content_type) {
        case application:
          opaque application_data<0..2^32-1>;

        case proposal:
          Proposal proposal;

        case commit:
          Commit commit;
    }
} MLSPlaintextTBS;

Since the first select is done on a field defined after it, this structure is not parsable. We could say that's not a problem since we only need to serialize it, but for security purposes I would recommend to put it at the end, like this:

struct {
    WireFormat wire_format;
    opaque group_id<0..255>;
    uint64 epoch;
    Sender sender;
    opaque authenticated_data<0..2^32-1>;

    ContentType content_type;
    select (MLSPlaintextTBS.content_type) {
        case application:
          opaque application_data<0..2^32-1>;

        case proposal:
          Proposal proposal;

        case commit:
          Commit commit;
    }

    select (MLSPlaintextTBS.sender.sender_type) {
        case member:
            GroupContext context;

        case preconfigured:
        case new_member:
            struct{};
    }
} MLSPlaintextTBS;

What do you think about it?

@raphaelrobert
Copy link
Member

Nice one! Crafting collisions is probably not straightforward, but eliminating the the risk altogether is the way to go.

@TWal
Copy link
Contributor Author

TWal commented Dec 18, 2021

It is actually quite easy to craft collisions, I made a script do find them: https://github.com/TWal/TLSCollisionFinder
(I wanted to check whether there was accidentally a way do disambiguate the structures)

case preconfigured:
case new_member:
struct{};
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't love the aesthetics here. Maybe we could solve this a little more elegantly by having a context input to SignWithLabel/VerifyWithLabel?

Alternatively: Do we even need this field any more? This field was added before membership_tag, and now the membership_tag already (a) demonstrates knowledge of the GroupContext and (b) prevents a non-member from synthesizing a valid MLSPlaintext.

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 not sure the alternative is a good idea for several reasons, for example the membership_tag is present only in MLSPlaintext and not in MLSCiphertext, also I'm not confident in removing things from the signature without a solid proof that it is not necessary (for example, signing the tree hash seems like a reasonable thing to do?).

I feel like adding a context input is a bit weird. What do you think about this third option: adding an option<GroupContext> group_context; and say something like "group_context MUST contain a value if sender_type = ..." below? This pattern already exists for confirmation_tag for example.

@@ -2536,7 +2561,7 @@ the right.
* Validate the KeyPackage:

* Verify that the signature on the KeyPackage is valid using the public key
in the KeyPackage's credential
in the KeyPackage's credential and the label `"KeyPackage"`
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 prefer that we put these descriptions with the structs they belong to, as opposed to down here in the instructions. We already have prose that says what the signature covers; that should just specify the label as well. You could also include it as a comment in the syntax specification:

struct {
    ...
    // SignWithLabel(., "MLSPlaintextTBS", MLSPlaintextTBS)
    opaque signature<0..2^16-1>;
} KeyPackage;

struct {
    ...
    // SignWithLabel(., "GroupInfoTBS", GroupInfoTBS)
    opaque signature<0..2^16-1>;
} GroupInfo;

struct {
    ...
    // SignWithLabel(., "PublicGroupStateTBS", PublicGroupStateTBS)
    opaque signature<0..2^16-1>;
} PublicGroupState;

struct {
    ...
    // SignWithLabel(., "MLSPlaintextTBS", MLSPlaintextTBS)
    opaque signature<0..2^16-1>;

    // MAC(confirmation_key, GroupContext.confirmed_transcript_hash)
    optional<MAC> confirmation_tag;

    // MAC(membership_key, MLSPlaintextTBM);
    optional<MAC> membership_tag;
} MLSPlaintext;

(Note: I don't think GroupInfoTBS exists, but it probably should.)

Copy link
Contributor

Choose a reason for hiding this comment

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

(#529 adds a GroupInfoTBS)

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 every structure has a TBS associated with it, this seems like a nice formatting of how you must compute the signatures, however for KeyPackage there is no TBS (yet), it is said in the text above that the TBS contains "all of the fields except for the signature field".

Should we add a TBS for every structure that we sign? This is for example done for PublicGroupState where a TBS is explicitly described even if it contains all fields except the signature field in PublicGroupState.

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

Successfully merging this pull request may close these issues.

None yet

4 participants