Upgrade to Pro — share decks privately, control downloads, hide ads and more …

End-to-End Encryption Saves Lives. You Can Star...

Avatar for sylph01 sylph01
April 17, 2025
73

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.

Avatar for sylph01

sylph01

April 17, 2025
Tweet

Transcript

  1. 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
  2. ようおいでたなもし! Welcome to Matsuyama! The Japanese part is (an antiquated

    phrase for) "Welcome" in the Iyo dialect spoken in Ehime 4
  3. 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
  4. 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
  5. 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
  6. 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
  7. 9

  8. I happen to be known as the SMTP をやめろ-guy SMTP

    をやめろ = lit. "stop using SMTP"; more like "SMTP is dead" 10
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. Melos https:/ /github.com/sylph01/melos Taken from a novel that is in

    most Japanese middle school/high school textbooks 22
  16. 23

  17. 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
  18. 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
  19. 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
  20. 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
  21. 3+ person is difficult Extend 2-person method? Number of edges

    in an n-node complete graph is O(n^2) 30
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 58

  43. 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
  44. 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
  45. Optional values struct { uint8 present; select (present) { case

    0: struct{}; case 1: T value; }; } optional<T>; 62
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. Trees with arrays The algorithms are described in Appendix C

    of RFC 9420 Also this comes with a serialization/deserialization format! 72
  56. 73

  57. 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
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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
  65. 85

  66. Why do you need End-to-End Encryption in Ruby? Because... This

    is a DanceDanceRevolution reference. see: MAX.(period), Over the "Period" 87
  67. 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
  68. 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
  69. 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
  70. 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