No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-02 15:20:55 +02:00
src Implement UpdatePath 2026-10-02 15:20:55 +02:00
tests Add round trip tests for all proposals 2026-09-25 23:03:34 +02:00
.gitignore MLS: Initial commit 2026-09-21 15:51:23 +02:00
composer.json MLS: Initial commit 2026-09-21 15:51:23 +02:00
composer.lock MLS: Initial commit 2026-09-21 15:51:23 +02:00
README.md Implement Proposals 2026-09-24 20:51:55 +02:00

php-mls

A PHP implementation of the Messaging Layer Security protocol defined by RFC 9420. The library is under active development and does not yet implement the complete protocol.

Message wire-format extensions

RFC 9420 reserves wire-format values 0xF000 through 0xFFFF for private use. Applications can add private message formats without extending or modifying the core message classes.

A custom message implements MlsMessageContent. Its wireFormat() method identifies the private wire format, while encodeTo() writes the message body:

use Mls\Codec\Decoder;
use Mls\Codec\Encoder;
use Mls\Message\MlsMessageContent;
use Mls\Message\WireFormat;

final readonly class ExampleMessage implements MlsMessageContent
{
    private const WIRE_FORMAT = 0xF001;

    public function __construct(
        private string $payload,
    ) {
    }

    public function wireFormat(): WireFormat
    {
        return new WireFormat(self::WIRE_FORMAT);
    }

    public function encodeTo(Encoder $encoder): void
    {
        $encoder->writeVariableOpaque($this->payload);
    }

    public static function decode(Decoder $decoder): self
    {
        return new self($decoder->readVariableOpaque());
    }
}

Register its decoder alongside the standard message formats:

use Mls\Codec\Decoder;
use Mls\Message\MlsMessageDecoderRegistry;
use Mls\Message\WireFormat;

$registry = MlsMessageDecoderRegistry::standard();

$registry->register(
    new WireFormat(0xF001),
    static fn (Decoder $decoder): ExampleMessage =>
        ExampleMessage::decode($decoder),
);

Pass the configured registry explicitly when decoding an MlsMessage:

use Mls\Codec\Decoder;
use Mls\Codec\MlsCodec;
use Mls\Message\MlsMessage;

$codec = new MlsCodec();

$message = $codec->decode(
    $bytes,
    static fn (Decoder $decoder): MlsMessage =>
        MlsMessage::decode($decoder, $registry),
);

Unregistered wire formats are rejected. The registry also verifies that a decoder returns content whose declared wire format matches the envelope. Keeping the registry explicit avoids global mutable protocol configuration and lets each application control the private formats it accepts.

Proposal extensions

RFC 9420 allows additional proposal types, with values 0xF000 through 0xFFFF reserved for private use. A non-default proposal defines its own wire syntax and implements ProposalContent:

use Mls\Codec\Decoder;
use Mls\Codec\Encoder;
use Mls\Proposal\ProposalContent;
use Mls\Proposal\ProposalType;

final readonly class ExampleProposal implements ProposalContent
{
    private const PROPOSAL_TYPE = 0xF001;

    public function __construct(
        private string $payload,
    ) {
    }

    public function proposalType(): ProposalType
    {
        return new ProposalType(self::PROPOSAL_TYPE);
    }

    public function encodeTo(Encoder $encoder): void
    {
        $encoder->writeVariableOpaque($this->payload);
    }

    public static function decode(Decoder $decoder): self
    {
        return new self($decoder->readVariableOpaque());
    }
}

Register its decoder alongside the standard proposal types:

use Mls\Codec\Decoder;
use Mls\Proposal\ProposalDecoderRegistry;
use Mls\Proposal\ProposalType;

$proposalRegistry = ProposalDecoderRegistry::standard();

$proposalRegistry->register(
    new ProposalType(0xF001),
    static fn (Decoder $decoder): ExampleProposal =>
        ExampleProposal::decode($decoder),
);

Pass the configured registry whenever a Proposal is decoded:

use Mls\Codec\Decoder;
use Mls\Codec\MlsCodec;
use Mls\Proposal\Proposal;

$codec = new MlsCodec();

$proposal = $codec->decode(
    $bytes,
    static fn (Decoder $decoder): Proposal =>
        Proposal::decode($decoder, $proposalRegistry),
);

Unregistered proposal types are rejected because proposal bodies do not have a generic length-delimited fallback. The registry also rejects the reserved type 0x0000, all RFC 9420 GREASE values, and decoder results whose declared proposal type does not match the envelope. Applications must ensure that all relevant group members support a non-default proposal type before committing it, as required by RFC 9420 Section 13.2.