- PHP 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| src | ||
| tests | ||
| .gitignore | ||
| composer.json | ||
| composer.lock | ||
| README.md | ||
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.