Share
Embed
End-to-End Encryption Saves Lives. You Can Start Saving Lives With Ruby, Too
2025/4/17 @ RubyKaigi 2025 https://rubykaigi.org/2025/
https://rubykaigi.org/2025/presentations/s01.html#day2
"Why do you need End-to-end Encryption in Ruby?"
This talk will cover the Ruby implementation of the Messaging Layer Security protocol (RFC 9420), which enables authenticated key exchange in group messaging systems. By learning how end-to-end encryption in group messaging works, you could be more confident about the security of your daily messages that are sent through your messaging apps. And yes, it does save actual lives.
This talk covers how the protocol works, details of the Ruby implementation, why it is important for Ruby, and the ongoing work on the future of modern cryptography in Ruby.
sylph01
April 17, 2025
Transcript
92 slides
-
End-to-End Encryption Saves Lives. You Can Start Saving Lives With
Ruby, Too Ryo Kajiwara/ 梶原 龍 (sylph01) 2025/4/17 @ RubyKaigi 2025 Yes, this is the right talk; the intended title was too long to fit in the CfP form 1 -
ようおいでたなもし! Welcome to Matsuyama! The Japanese part is (an antiquated
phrase for) "Welcome" in the Iyo dialect spoken in Ehime 4 -
I do stuff Play rhythm games (especially DanceDanceRevolution) Play the
bassoon/contrabassoon Play DJ at clubs (also at RubyMusicMixin!) Ride a lot of trains (Rails!) (travelled on 99% of JR) Build keyboards if anything catches your interest let's talk! 5 -
And I do stuff that is more relevant to this
talk: Freelance web developer focused on Digital Identity and Security Worked/ing on writing/editing and implementing standards HTTPS in Local Network CG / Web of Things WG @ W3C, OAuth / Messaging Layer Security WG @ IETF Worked as an Officer of Internet Society Japan Chapter (2020-23) 6 -
The Usual Disclaimer Cryptographic API can be very easy to
misuse Operational Security is also very difficult I've done my research, but I don't consider myself a cryptography expert a.k.a. "I am not djb" If you're not sure, please have your system audited by a security expert before going to production also I don't have a PhD/Master's degree in this field so yeah... 7 -
Notes on Production Readiness The MLS implementation is still in
progress Passes most test vectors "Passive Clients - Random" is not passing yet Lacks validation on error cases I am planning to release the complete version before the next IETF (2025/7) 8 -
I happen to be known as the SMTP をやめろ-guy SMTP
をやめろ = lit. "stop using SMTP"; more like "SMTP is dead" 10 -
SMTP mail lacks two things: 1) Strong identity 2) End-to-end
Encryption MLS also helps with the identity part (with the full architecture), but that is service dependent 11 -
End-to-End Encryption = The people who run the service cannot
read your messages LINE, Facebook Messenger, Signal, WhatsApp, ... They each have their own version of E2EE Some apps' E2EE are opt-in 12 -
E2EE is more important than ever Increasing tension in international
affairs Powerful people wanting to control the Internet even in democratic countries! Provides a safe method to communicate under oppression / in war zones 14 -
Hey, but doesn't E2EE help criminals? E2EE is being targeted
by authorities even in democratic countries! Even if you ban E2EE, criminals will use it anyways Banning E2EE disproportionately harms vulnerable people 15 -
HTTPS(TLS) also helps criminals, but are you going to ban
HTTPS(TLS)? 17 -
Messaging Layer Security More interoperable version of doing E2EE MLS
provides key exchange Protocol (RFC 9420), Architecture (to be an RFC soon) Today's topic is the implementation of the Protocol Now we (kinda) have RFC 9420 in Ruby! 19 -
mls gem already exists (for a defunct website...) It exists
since 2012, so it predates the 00 Internet-Draft of Messaging Layer Security (2018/2) 21 -
Melos https:/ /github.com/sylph01/melos Taken from a novel that is in
most Japanese middle school/high school textbooks 22 -
Building blocks: HPKE "Encrypt with Public Key, Decrypt with Private
Key", formalized A combination of the following: Key Encapsulation Mechanism (KEM) ( ≒ asymmetric crypto) Key Derivation Function (KDF) ( ≒ hash) Authenticated Encryption with Associated Data (AEAD) (= symmetric crypto) Available in Ruby: hpke gem I have talked about this in detail at RubyConf Taiwan 2023 25 -
Security Characteristics Forward Secrecy: messages sent at a certain point
in time are secure in the face of later compromise of a group member secure against "harvest now, decrypt later" attack RFC 9420, Section 16.6 26 -
Security Characteristics Post-Compromise Security: messages are secure even if a
group member was compromised at some point in the past Each member updates their key so that group secrets are not always encrypted with private keys that have been compromised RFC 9420, Section 16.6 27 -
2 person is easy # both party starts with chain
key 0 chain_key[0] = "some common secret" # when sending a message... message_key[n] = hmac_sha256(chain_key[n], 0x02) # encrypt the message #(n) using message_key[n] chain_key[n+1] = hmac_sha256(chain_key[n], 0x01) Symmetric Key Ratchet part of the Double Ratchet algorithm 29 -
3+ person is difficult Extend 2-person method? Number of edges
in an n-node complete graph is O(n^2) 30 -
Secret Tree def self.populate_tree_impl(suite, tree, index, secret) tree.array[index] = {
'handshake_ratchet_secret' => Melos::Crypto.expand_with_label(suite, secret, "handshake", "", suite.kdf.n_h), 'application_ratchet_secret' => Melos::Crypto.expand_with_label(suite, secret, "application", "", suite.kdf.n_h), 'next_handshake_ratchet_secret_generation' => 0, 'next_application_ratchet_secret_generation' => 0 } unless Melos::Tree.leaf?(index) left_secret = Melos::Crypto.expand_with_label(suite, secret, "tree", "left", suite.kdf.n_h) right_secret = Melos::Crypto.expand_with_label(suite, secret, "tree", "right", suite.kdf.n_h) populate_tree_impl(suite, tree, Melos::Tree.left(index), left_secret) populate_tree_impl(suite, tree, Melos::Tree.right(index), right_secret) end end Each user is assigned a leaf in the tree From the base encryption secret, we recursively populate the tree down to its leaves file lib/melos/secret_tree.rb 35 -
Secret Tree The actual ratcheting part: def self.ratchet_application(suite, tree, leaf_index)
node_index = leaf_index * 2 generation = tree.array[node_index]['next_application_ratchet_secret_generation'] application_ratchet_secret = tree.array[node_index]['application_ratchet_secret'] application_nonce = Melos::Crypto.derive_tree_secret(suite, application_ratchet_secret, "nonce", generation, suite.hpke.n_n) application_key = Melos::Crypto.derive_tree_secret(suite, application_ratchet_secret, "key", generation, suite.hpke.n_k) next_application_ratchet_secret = Melos::Crypto.derive_tree_secret(suite, application_ratchet_secret, "secret", generation, suite.kdf.n_h) tree.array[node_index]['next_application_ratchet_secret_generation'] = generation + 1 tree.array[node_index]['application_ratchet_secret'] = next_application_ratchet_secret tree.array[node_index]['application_nonce'] = application_nonce tree.array[node_index]['application_key'] = application_key end file lib/melos/secret_tree.rb 36 -
Secret Tree It essentially boils down to this: # for
generation n # derive_tree_secret is essentially a hash function key = Crypto.derive_tree_secret(ratchet_secret[n], "key", n) nonce = Crypto.derive_tree_secret(ratchet_secret[n], "nonce", n) ratchet_secret[n + 1] = Crypto.derive_tree_secret(ratchet_secret[n], "secret", n) This gives you the key and nonce to encrypt the actual messages. 37 -
TreeKEM Users are assigned a leaf node Users have their
leaf key pair 39 -
TreeKEM When a user wants to update the group secret,
user creates an UpdatePath 40 -
TreeKEM User generates a random path_secret at their leaf The
parent's path_secret is calculated using the child's path_secret Calculate that up to the root The commit_secret is calculated from the root's path_secret 41 -
TreeKEM When 0 creates its UpdatePath (yellow) We find the
copath nodes along the UpdatePath (green) If all ParentNode s in the tree are populated, we encrypt the path secret to the key of its copath node 42 -
TreeKEM We can see that's possible in a 2-leaf tree
0 creates an UpdatePath, node 1 has a path_secret the path secret is encrypted to 2 's public key node 2 knows the private key, so 2 can decrypt 43 -
TreeKEM What happens if there are blanks in the tree?
We calculate the resolution of the copath node to figure out which node's keys are available. Then, we encrypt the path secret of the UpdatePath node to each key 44 -
TreeKEM The resolution of the node indicated left is [3,
2] The indicated node has unmerged leaves so its resolution is itself + list of unmerged leaves 45 -
TreeKEM The resolution of the node indicated left is [3,
2, 9, 14] The resolution is used to figure out the set of keys that are necessary to encrypt to every node under that node 46 -
TreeKEM So actually what you do is: Calculate path_secret s
for each node on UpdatePath then encrypt the path_secret s to each key on the resolution of the copath node 47 -
Evolution of a Group A group is updated through messages
called proposals and commits Proposals Add and remove users Notifies an update of user's leaf key Injects Pre-Shared Keys Commits will fix those information and advances the group's epoch UpdatePath s are conveyed in the Commit 48 -
Evolution of a Group Users who want to join the
group publishes their identity (including its public key) to a Directory using a KeyPackage Figure 2 in RFC 9420 Section 3.2. The Delivery Service is out of scope for this implementation 49 -
Evolution of a Group User A creates an initial group
To add User B , A sends an Add proposal adding B , then Commit s A sends a Welcome message to introduce B into the group Same for adding C Figure 3 in RFC 9420 Section 3.2. 50 -
Evolution of a Group When Add ing: Add the leaf_node
from the KeyPackage in the proposal to the leftmost empty node Then add the leaf index to intermediate nodes' unmerged list 51 -
Evolution of a Group User B sends an Update proposal
notifying the group of its leaf key update Then User A Commit s that update to include it in the epoch advancement Figure 4 in RFC 9420 Section 3.2. 52 -
Evolution of a Group When Update ing: Replace the leaf
node of the sender with the leaf_node inside the Update proposal Then blank all nodes on its direct path up to the root 53 -
Evolution of a Group When removing a user: Z sends
a Remove proposal and a Commit B cannot decrypt the path_secret included in the Commit 's UpdatePath , so B will not know the next epoch_secret 54 -
Evolution of a Group When Remove ing: Blank the specified
node in the proposal Then blank all nodes on its direct path up to the root If the right half of the tree is fully empty, the tree is shrunk 3 becomes the new root 55 -
Evolution of a Group When are intermediate parent nodes filled?
During the processing of a Commit When processing a Commit , the UpdatePath inside the Commit is merged into the ratchet tree ParentNode s are created based on the UpdatePath content 56 -
Stuff we are omitting here Injection of Pre-Shared Keys Transcript
Hashes Hashes that summarize the proposals/commits taken place in the last epoch Signing/Verification of messages There are public messages and private messages 57 -
It's not just a pack and unpack MLS has variable
length vectors and optional values in their structs struct { opaque group_id<V>; uint64 epoch; ContentType content_type; opaque authenticated_data<V>; opaque encrypted_sender_data<V>; opaque ciphertext<V>; } PrivateMessage; 60 -
Variable length vectors struct { uint32 fixed<0..255>; opaque variable<V>; }
StructWithVectors; Based on variable-length integer encoding in RFC 9000, Section 16 2-bit prefix if 00 , length is encoded with 6 bits if 01 , length is encoded with 14 bits if 11 , length is encoded with 30 bits 61 -
Optional values struct { uint8 present; select (present) { case
0: struct{}; case 1: T value; }; } optional<T>; 62 -
Define structs like this class Melos::Struct::PrivateMessage < Melos::Struct::Base attr_reader :group_id,
:epoch, :content_type, :authenticated_data, :encrypted_sender_data, :ciphertext STRUCT = [ [:group_id, :vec], [:epoch, :uint64], [:content_type, :uint8], [:authenticated_data, :vec], [:encrypted_sender_data, :vec], [:ciphertext, :vec] ] 63 -
Melos::Struct::Base #initialize , .new_and_rest : deserializer #raw : serializer These
operate based on the STRUCT constant of the class lib/melos/struct/base.rb 64 -
Deserialize each element like this def deserialize_elem(buf, type, type_param) case
type when :uint8 value = buf.byteslice(0, 1).unpack1('C') buf = buf.byteslice(1..) when :uint16 value = buf.byteslice(0, 2).unpack1('S>') buf = buf.byteslice(2..) (snip) when :vec value, buf = Melos::Vec.parse_vec(buf) (snip) lib/melos/struct/base.rb; I'm likely rewriting this with a StringIO instead of using strings as buffers 65 -
Sometimes a class is nested class Melos::Struct::FramedContent < Melos::Struct::Base (snip)
STRUCT = [ [:group_id, :vec], [:epoch, :uint64], [:sender, :class, Melos::Struct::Sender], [:authenticated_data, :vec], [:content_type, :uint8], (snip) ] lib/melos/struct/structs.rb 66 -
So we recursively call new when :class value, buf =
type_param.send(:new_and_rest, buf) when :classes # prefix, length = buf.get_prefix_and_length # puts "#{prefix}, #{length}" vec, buf = Melos::Vec.parse_vec(buf) value = [] while (vec.bytesize > 0) current_instance, vec = type_param.send(:new_and_rest, vec) value << current_instance end lib/melos/struct/structs.rb 67 -
We sometimes have values that depend on other values class
Melos::Struct::Sender < Melos::Struct::Base attr_reader :sender_type, :leaf_index, :sender_index STRUCT = [ [:sender_type, :uint8], [:leaf_index, :select, ->(ctx){ctx[:sender_type] == 0x01}, :uint32], [:sender_index, :select, ->(ctx){ctx[:sender_type] == 0x02}, :uint32], ] lib/melos/struct/structs.rb 68 -
We call the Proc with the current context to handle
that def deserialize_select_elem_with_context( buf, context, predicate, type, type_param) if predicate.(context) deserialize_elem(buf, type, type_param) else [nil, buf] end end lib/melos/struct/base.rb 69 -
Trees with arrays Complete balanced trees can be described with
a flat array For example, the tree on the right will be written as: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, nil, 11, nil, nil, nil] 70 -
Trees with arrays Leaf nodes are always even indexed Parent
nodes are always odd indexed We define operations such as left , right , parent , sibling , ... based on the array-based tree's characteristics 71 -
Trees with arrays The algorithms are described in Appendix C
of RFC 9420 Also this comes with a serialization/deserialization format! 72 -
HPKE using OpenSSL's API HPKE is nice, and OpenSSL itself
has APIs that do this Talked about it at RubyConf Taiwan 2023 Haven't worked on it much since 75 -
HPKE using OpenSSL's API But is it a good idea?
Protocols need the whole cipher suite Not only encap/decap and open/seal Also want access to constants such as hash/key length OpenSSL's HPKE context remembers which algorithm to use, but to use them separately you have to call them separately 76 -
Some APIs of OpenSSL gem are intentionally undocumented ...but I'm
not gonna fix it. You have to know what you're doing to use them 77 -
Missing features in OpenSSL? Checking correspondence of key pairs def
self.signature_key_pair_corresponds?(suite, private_key, public_key) private_pkey = suite.pkey.deserialize_private_signature_key(private_key) public_pkey = suite.pkey.deserialize_public_signature_key(public_key) if suite.pkey.equal?(Melos::Crypto::CipherSuite::X25519) || suite.pkey.equal?(Melos::Crypto::CipherSuite::X448) # is an Edwards curve; check equality of the raw public key private_pkey.raw_public_key == public_pkey.raw_public_key else # is an EC; check equality of the public key Point private_pkey.public_key == public_pkey.public_key end end 78 -
Missing features in OpenSSL? Getting EC public/private keys in UncompressedPointRepresentation
form def self.derive_key_pair(suite, secret) pkey = suite.hpke.kem.derive_key_pair(secret) if suite.pkey.equal?(Melos::Crypto::CipherSuite::X25519) || suite.pkey.equal?(Melos::Crypto::CipherSuite::X448) # is an Edwards curve [pkey.raw_private_key, pkey.raw_public_key] else # is an EC [pkey.private_key.to_s(2), pkey.public_key.to_bn.to_s(2)] end end 79 -
Additional features in OpenSSL OpenSSL 3.5.0 ships with post-quantum cryptography
ML-KEM, ML-DSA, SLH-DSA There is an Internet-Draft that adds ML-KEM in MLS 80 -
Grand Unifying Cryptography API Different platforms depend on different cryptographic
libraries Desktop: OpenSSL Compatible (to an extent) with LibreSSL, BoringSSL, and the like Browser: Web Crypto API Embedded: Mbed TLS, wolfSSL, ... We have Mbed TLS support in PicoRuby Can we have a unified API wrapper for these libraries? 83 -
Grand Unifying Cryptography API Very quick example of calling Web
Crypto API's random number generator require "js" array = JS::eval('return new Uint8Array(16)') JS.global[:window][:crypto].getRandomValues(array) p array 84 -
Why do you need End-to-End Encryption in Ruby? Because... This
is a DanceDanceRevolution reference. see: MAX.(period), Over the "Period" 87 -
Ruby needs them to stay relevant or else people would
just use Python, Go, Rust, whatever the cool kids use these days Actually, Python doesn't have an MLS implementation yet, so it's a win for Ruby 88 -
Ruby needs Freedom on the Internet and the Free Internet
needs Ruby free as in freedom 89 -
Shoutouts The Messaging Layer Security Working Group @ IETF Protocol
implementers in Ruby (the list is growing!) RubyKaigi 2025 organizer team, esp. the local organizers 90 -
More Shoutouts Sponsors of RubyKaigi, esp: (ones I have personal
connections with) codeTakt Inc. I'm at the Day2 and Day4 drinkup, let's talk! Bloomo Securities Inc. 91 -
Questions? / Comments? Twitter: @s01 or Fediverse: @[email protected] also find
me in the venue / at drinkups! I am at: codeTakt Day2/Day4 Drinkup, RubyMusicMixin @ Day3 92