Skip to content

feat: update introduction bundle encoding#43

Merged
osmaczko merged 1 commit intomainfrom
chore/add-introduction-encoding-specs
Feb 11, 2026
Merged

feat: update introduction bundle encoding#43
osmaczko merged 1 commit intomainfrom
chore/add-introduction-encoding-specs

Conversation

@osmaczko
Copy link
Contributor

@osmaczko osmaczko commented Feb 5, 2026

closes: #27

@osmaczko osmaczko requested a review from jazzz February 5, 2026 16:49
Copy link
Collaborator

@jazzz jazzz left a comment

Choose a reason for hiding this comment

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

Technical Content
Looks good. I think Protobufs are a smart direction for now. I see versioning, I see everything I would expect.

Written Spec

The Top half is good. Really like the depth on the Overview, and reasoning for choices. Format specification is 👌 .

Areas that need work

  • Duplicate content in different sections. Examples and Expected size repeat much of the same information. Process and examples are repeated. Would like to see specs treated more like Code in that they are "DRY"
  • Duplication with other Specs. Think this specification should focus on solely the exchanging of payload types over human based channels. Signature mechanics, crypto protocols seem out of scope.
  • This is not the correct location for this. We'll need to move this to another location

Comment on lines 1 to 5
# Introduction Bundle Encoding Specification

**Status:** Draft

## Conventions
Copy link
Collaborator

Choose a reason for hiding this comment

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

[Sand] This doesn't follow the suggested format for Logos specs https://github.com/logos-messaging/specs/blob/master/template.md. Not critical but will eventually need to be in that form

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adapted to Logos specs format.

Comment on lines 125 to 128
**Algorithm:** [XEdDSA][xeddsa] — Ed25519-compatible signatures produced from
X25519 key pairs. This allows the same X25519 installation key to be used for
both Diffie-Hellman key agreement and signing, without maintaining a separate
Ed25519 identity key.
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 think this belongs in the Spec for encoding. This is chat protocol specific, best to not repeat information in multiple places. Recommend removing

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 agree, removed it altogether.

Comment on lines 132 to 134
```
signed_message = domain_separator || ephemeral_pubkey
```
Copy link
Collaborator

Choose a reason for hiding this comment

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

While this is a healthy approach. Message signing has integrity implications for the communication channel - It will be defined elsewhere.

The focus is solely on documenting the format for this interchange data, and the reasons for those choices

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 agree, removed it altogether.

Comment on lines 160 to 166
| `installation_pubkey` protobuf field | 34 (1 tag + 1 length + 32 value) |
| `ephemeral_pubkey` protobuf field | 34 (1 tag + 1 length + 32 value) |
| `signature` protobuf field | 66 (1 tag + 1 length + 64 value) |
| **Total binary payload** | **134 bytes** |
| Base64url encoded (no padding) | 179 characters |
| Prefix `logos_chatintro_1_` | 18 characters |
| **Total encoded string** | **~197 characters** |
Copy link
Collaborator

Choose a reason for hiding this comment

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

While helpful this information seems like it would be hard to manage. And repeats with other sections.

[Dust] I'd cut this down to what is necessary, collapse this into different sections.

  • Focus on good examples section. Input, output, statistics is much more helpful in my opinion.
  • The spec says that key encoding is deferred to protobuf, but then references protobuf internals. I'd choose one path - either the encoding details matter or they do not.

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 agree, removed it altogether.

Focus on good examples section. Input, output, statistics is much more helpful in my opinion.

This is yet to come, after implementation is done.

Comment on lines 186 to 191
Encoding steps:

1. Construct the `IntroBundle` protobuf message with the three fields above.
2. Serialize using proto3 encoding (134 bytes).
3. Encode as base64url without padding (179 characters).
4. Prepend the version prefix:
Copy link
Collaborator

@jazzz jazzz Feb 5, 2026

Choose a reason for hiding this comment

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

[Sand] The process ought to be listed clearly enough. If a reader cannot easily follow along, then the instructions ought to be made clearer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

4. Prepend the version prefix:

```
logos_chatintro_1_<179-character base64url payload>
Copy link
Collaborator

Choose a reason for hiding this comment

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

[Pebble] Examples are very helpful for implementors to at least smoke test their implementations. I'd convert this section to a TestVector approach.

Clear explicit bytes, so that implementers can verify.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed it altogether, will add examples once implementation is ready.

Comment on lines 73 to 74
A V1 Introduction Bundle carries the cryptographic material needed for an
[X3DH][x3dh] key agreement:
Copy link
Collaborator

Choose a reason for hiding this comment

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

[Sand] This seems hyper specific. The spec should theoretically work for another protocol stack that matches the same structure.

Since this protocol treats the internal bytes as blobs then they theoretically could be anything.

High Level I see this spec being referred to by a spec like Inbox. Inbox defines the keys and types, it then references this spec for its encoding.

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 agree, removed.

The `_` character is present in the alphabets of common binary-to-text
encodings (hex, base32, base64url) and may therefore appear inside
`<payload>`. Parsers MUST split the encoded string on the **first three** `_`
characters; everything after the third `_` is the payload verbatim.
Copy link
Collaborator

Choose a reason for hiding this comment

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

everything after the third _ is the payload verbatim.

After some further thought, you may want to consider:

[Dust] Using two delimiters would make the implementations less prone to parsing issues.

Intro = [Preamble]<delim_1>[Bytes]
Preamble = [Prefix]<delim_2>[Namespace]<delim_2>[Version]

That way the parsing rules become:

  • Introduction MUST contains one and only one <delimiter_1>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What you meant is something like logos_chatintro_1~<payload>, where _ separates preamble fields and ~ separates preamble from payload. Then parsing becomes: split on ~ once -> left side is preamble, right side is payload. No "first three" counting. Did I get that right?

That's a good suggestion. However, imo, it adds complexity for marginal benefit. The "split on first 3 underscores" rule is simple and unambiguous, typically one line of code in any language. Two delimiters means two characters to document, two things to validate, and slightly more visual noise.

I'd lean toward keeping the single delimiter if you're fine with it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd lean toward keeping the single delimiter if you're fine with it.

Sounds good. Its a [Dust] do with it what you will.

And yes, you got that right. It's namely a style choice between being prescriptive, or being flexible.

I like the direction you are going.

@jazzz
Copy link
Collaborator

jazzz commented Feb 5, 2026

I'd lets work on the "spec writing" and the implementation is parallel. I don't see any blockers on the content, and the written aspect can be iterated on separately.

@osmaczko osmaczko force-pushed the chore/add-introduction-encoding-specs branch from a5ca4cd to 2f9963a Compare February 6, 2026 19:48
@osmaczko
Copy link
Contributor Author

osmaczko commented Feb 9, 2026

This is not the correct location for this. We'll need to move this to another location

Moved specs to logos-messaging/specs#101.

@osmaczko osmaczko force-pushed the chore/add-introduction-encoding-specs branch 3 times, most recently from 0ca963e to 025b5d6 Compare February 9, 2026 21:09
@osmaczko osmaczko changed the title chore: add introduction bundle encoding specs feat: update introduction bundle encoding Feb 9, 2026
@osmaczko
Copy link
Contributor Author

osmaczko commented Feb 9, 2026

@jazzz please re-review 🙏. The specs have been moved out, and this PR is now focused on the implementation.

@osmaczko osmaczko requested review from jazzz and kaichaosun February 9, 2026 21:12
@osmaczko osmaczko force-pushed the chore/add-introduction-encoding-specs branch from 025b5d6 to 9d55933 Compare February 9, 2026 21:20
Copy link
Collaborator

@jazzz jazzz left a comment

Choose a reason for hiding this comment

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

This Looks good.

I see intentional decisions around

  • safe signature practices
  • Type conversions and
  • Testing

Theres some ways to make this more rusty but I don't see that as a blocker.

The main limitations of this work are caused by the existing code framework that needs a refactor, and is not in scope for this change-set.

Comment on lines 10 to 12
const BUNDLE_PREFIX: &str = "logos_chatintro_1_";

const INTRO_DOMAIN_SEPARATOR: &[u8] = b"logos_chatintro_v1_bind";
Copy link
Collaborator

Choose a reason for hiding this comment

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

So close but so different. Any reason to not use the same prefix?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Used BUNDLE_PREFIX for both 👍

fn intro_binding_message(identity: &PublicKey, ephemeral: &PublicKey) -> Vec<u8> {
let mut message = Vec::with_capacity(INTRO_DOMAIN_SEPARATOR.len() + 64);
message.extend_from_slice(INTRO_DOMAIN_SEPARATOR);
message.extend_from_slice(identity.as_bytes());
Copy link
Collaborator

Choose a reason for hiding this comment

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

[Pebble] Adding the Identity key is redundant. Authenticity is provided by the signature it self.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True, removed identity key from the message.

crate-type = ["staticlib","dylib"]

[dependencies]
base64ct = { version = "1.6", features = ["alloc"] }
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the alloc feature here makes sense - the clarity and simplicity is an asset.

This means that the library cannot run in wasm or other environments so we may have to revisit in the future.

@@ -1,64 +1,180 @@
use base64ct::{Base64UrlUnpadded, Encoding};
Copy link
Collaborator

Choose a reason for hiding this comment

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

This line set off alarm bells. Is key material being encoded in base64?

Is a constant time operation required here or being cautious?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The encoded data has no secret material, using regular base64 would be fine as well as there's nothing to leak via timing. I used constant-time as a reasonable default, as far as I know, there is no meaningful penalty.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not an issue now, but definitely in the realm of "crypto-smell" we will want to cleanup before release. We want our libraries and primitive decisions to be thoughtful, and intentional.

A reasonable default implies uncertainty in the approach taken. Either we need a constant time operation, or we do not.

Choosing it implies that its needed, which causes red flags in external reviewers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's fair, I'll follow principle of least surprise and move to non-constat-time version.

.signature
.as_ref()
.try_into()
.map_err(|_| ChatError::BadBundleValue("invalid signature length".into()))?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

[Dust] From a defensive code perspective. I'd consider validating the signature here.

Its possible for this type to create an invariant that Introduction always contains a valid ephemeral key.
This would make reasoning about this object easier if it was never possible to create an invalid Introduction.

That would contain the validation logic within this file and make it harder for future contributors to make mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, adapted the code 👍

Comment on lines 66 to 71
PrekeyBundle {
identity_key: self.ident.public_key(),
signed_prekey: signed_prekey,
signature: [0u8; 64],
signed_prekey,
signature,
onetime_prekey: None,
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

While not strictly related to this work, I think it would make sense to change this to return an Introduction, rather than using the PrekeyBundle Type. The dependence on PrekeyBundle ought to be contained as tight as possible.

Not required for this PR mostly flagging follow up pieces of work

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've reworked it to return Introduction.


use rand_core::{CryptoRng, RngCore};
use x25519_dalek::{PublicKey, StaticSecret};
use xeddsa::{xed25519, Sign, Verify};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Great work solving the key-conversion issue.

Though from a protocol POV I don't love using XEdDSA here.

Out-of-scope for now, but a cleaner approach would be to update Identity to use an Ed25519 SigningKey and derive the X25519 StaticSecret as needed.

hex::encode(self.installation_key.as_bytes()),
hex::encode(self.ephemeral_key.as_bytes()),
);
impl From<Introduction> for Vec<u8> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice catch

secret: &StaticSecret,
ephemeral: &PublicKey,
rng: R,
) -> [u8; 64] {
Copy link
Collaborator

Choose a reason for hiding this comment

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

[Pebble] I'd create a Newtype using struct Ed25519Signature { sig: [u8; 64] }

That way the context is bound with the datatype as well as allowing convenience functions to be added in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🫡

@osmaczko osmaczko force-pushed the chore/add-introduction-encoding-specs branch from 9d55933 to 4f44f3d Compare February 10, 2026 17:58
@osmaczko osmaczko changed the base branch from main to fix/ci-checks February 10, 2026 17:59
@osmaczko osmaczko force-pushed the chore/add-introduction-encoding-specs branch 2 times, most recently from 2ed31d1 to 25d5cd8 Compare February 10, 2026 19:11
@osmaczko osmaczko changed the base branch from fix/ci-checks to main February 10, 2026 19:12
@osmaczko osmaczko force-pushed the chore/add-introduction-encoding-specs branch 4 times, most recently from f7313e1 to 3d6ac7d Compare February 10, 2026 20:36
@osmaczko
Copy link
Contributor Author

@jazzz thanks for review! I've addressed your comments, please re-check 🙏

@osmaczko osmaczko requested a review from jazzz February 10, 2026 20:38
Copy link
Collaborator

@jazzz jazzz left a comment

Choose a reason for hiding this comment

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

A few comments, but looks its much improved. Love it.

@osmaczko osmaczko force-pushed the chore/add-introduction-encoding-specs branch from 3d6ac7d to c1902ed Compare February 11, 2026 18:47
@osmaczko osmaczko merged commit 57fe656 into main Feb 11, 2026
3 checks passed
@osmaczko osmaczko deleted the chore/add-introduction-encoding-specs branch February 11, 2026 18:51
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

Successfully merging this pull request may close these issues.

Introduction encoding

2 participants