Repository navigation
Check the device certificate in an uploaded KeyPackage (GRYT-1509) - #243
Merged
Merged
Conversation
sivert-io
marked this pull request as ready for review
September 28, 2026 08:05
On upload the server now does what @gryt/crypto's readMlsKeyPackage does, minus the scope. After the lifetime check from #242, the KeyPackage has to be signed by its own leaf key. Its credential has to hold a device certificate the person key signed, for that same leaf key and for the deviceId the upload is under (device_mismatch otherwise). The lifetime goes first. It's a few integer compares, where the rest is two Ed25519 verifications, so junk that's also expired costs nothing. And a device whose clock is off hears about its clock, with serverTime to retry against, and not about a signature that was never the problem. The certificate is read by @gryt/crypto 0.7.0, already on main from #245, through its ./* export so neither index loads. crypto's reader wants the scope to expect, and the server can't know which one a client used, so it passes back the scope the certificate names. Clients check it against their pin. The bytes are copied first, because crypto reads a Node Buffer's lengths off the shared pool (GRYT-1520). The tests sign certificates with crypto, and still read one a crypto build signed at 272bb78. Co-Authored-By: Claude Opus 5.5 <[email protected]>
sivert-io
force-pushed
the
claude/GRYT-1509-keypackage-certs
branch
from
September 28, 2026 08:13
87a20b9 to
9a6c491
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What to look at
parseKeyPackagechecks Refuse and prune expired KeyPackages (GRYT-1510) #242's lifetime first and then the signatures. The lifetime is a few integer compares where the rest is two Ed25519 verifications, and a device whose clock is off gets told about its clock, withserverTime, rather than about a signature. @gryt/crypto 0.7.0 was already on main from Somewhere to publish the MLS person key binding (GRYT-1515) #245, so package.json and yarn.lock don't change here.require("@gryt/crypto/mls-device-certificate"). That's the package's./*export, so crypto's index and ts-mls's index stay out, and it leans on Node'srequire(esm)likemlsWire.tsalready does.readDeviceCertificatewants the scope to expect. The server can't know it, soclaimedScopereads the scope out of the certificate (version byte, u16 length, UTF-8) and passes it back. Everything else, the format and the signature, is crypto's.Uint8Arrayfirst. crypto reads a NodeBuffer's lengths off the shared pool and refuses a good certificate, and ts-mls hands the credential back as a Buffer when it decoded one. That's GRYT-1520, filed against crypto.verifyKeyPackagewith Ed25519 fromnode:crypto, since crypto 0.7.0 only exports that check insidereadMlsKeyPackage, which also wants the scope and applies the lifetime rules from Refuse and prune expired KeyPackages (GRYT-1510) #242.srv:plus the first key id it saw for this server, or the bare address if it has no pin. The server can't always know which, and a wrong guess would lock that client out of MLS here for good. Clients check the scope themselves.mls:keypackages:publishhas three new refusals:invalid_key_packagewhen the leaf key didn't sign it,invalid_device_certificate, anddevice_mismatchwhen the certificate names another device than the upload.What's in it
On upload the server now does what crypto's
readMlsKeyPackagedoes, minus the scope. The KeyPackage has to be signed by its own leaf key. Its credential has to hold a device certificate the person key signed, for that same leaf key and for thedeviceIdit's uploaded under.The rest of the chain, that the person key is bound by the member's identity, needs the binding, and the server never gets it. That's GRYT-1515. Once the binding is published the upload can check it too.
Tests
The handler tests now give each device a real certificate, signed with crypto's
signDeviceCertificate. The fixture test still reads a certificate a crypto build signed at 272bb78, and reads it again as a Buffer. New ones: crypto's own certificate reads and names its device, a certificate with one byte changed is refused, a credential that's just a name is refused, a certificate moved onto another leaf key is refused, a KeyPackage its leaf didn't sign is refused, and an upload under another device's id getsdevice_mismatch. Full suite: 1757 pass.Task: GRYT-1509. Docs: Gryt-chat/docs#141.
🤖 Generated with Claude Code