Salut. Je suis Mrinal Wadhwa, CTO chez Ockam . Au début d'Ockam, nous développions une bibliothèque C. C'est l'histoire de la raison pour laquelle, plusieurs mois plus tard, nous avons décidé d'abandonner des dizaines de milliers de lignes de C et de les réécrire en Rust .
Avant de commencer, j'étais dans un webinaire enregistré cette semaine avec Paul Dix, le directeur technique d'InfluxData, où nous avons tous deux discuté des réécritures d'InfluxDB et d'Ockam dans Rust. Pourquoi les deux projets open source ont choisi de réécrire, pourquoi nous avons choisi Rust comme nouveau langage, les leçons que nous avons apprises en cours de route, et plus encore. Vérifiez l'enregistrement . C'était une discussion perspicace.
Ockam permet aux développeurs de créer des applications qui peuvent faire confiance aux données en mouvement. Nous vous donnons des outils simples pour ajouter une communication cryptée de bout en bout et mutuellement authentifiée à - toute application exécutée dans n'importe quel environnement. Vos applications bénéficient de garanties d'intégrité, d'authenticité et de confidentialité des données de bout en bout... sur des réseaux privés, entre plusieurs clouds, via des flux de messages dans Kafka - sur n'importe quelle topologie multi-sauts et multi-protocoles. Toutes les communications deviennent authentifiées de bout en bout et privées.
Nous rendons également les parties difficiles super faciles à mettre à l'échelle - amorcez les relations de confiance, gérez en toute sécurité les clés, faites pivoter/révoquer les informations d'identification de courte durée, appliquez des politiques d'autorisation basées sur les attributs, etc. Le résultat final est - vous pouvez créer des applications qui ont un contrôle granulaire sur chaque décision de confiance et d'accès - des applications privées et sécurisées par conception.
En 2019, nous avons commencé à construire tout cela en C. Nous voulions qu'Ockam s'exécute partout - des appareils périphériques contraints aux puissants serveurs cloud. Nous voulions également qu'Ockam soit utilisable dans n'importe quel type d'application, quel que soit le langage dans lequel l'application est intégrée.
Cela faisait de C un candidat évident. Il peut être compilé pour 99% des ordinateurs et s'exécuter à peu près partout (une fois que vous avez compris comment gérer toutes les chaînes d'outils spécifiques à la cible). Et tous les autres langages populaires peuvent appeler des bibliothèques C via une forme d'interface de fonction native - afin que nous puissions ultérieurement fournir des wrappers idiomatiques de langage pour tous les autres langages : Typescript, Python, Elixir, Java, etc.
L'idée était de garder le cœur de nos protocoles centrés sur la communication découplé de tout comportement spécifique au matériel et d'avoir des adaptateurs enfichables pour le matériel que nous voulons prendre en charge. Par exemple, il y aurait des adaptateurs pour stocker les clés secrètes dans divers HSM, des adaptateurs pour divers protocoles de transport, etc.
Notre plan était d'implémenter notre noyau en tant que bibliothèque C. Nous encapsulerions ensuite cette bibliothèque C avec des wrappers pour d'autres langages et l'exécuterions partout à l'aide d'adaptateurs matériels enfichables.
Mais, nous nous soucions aussi profondément de la simplicité - c'est en notre nom. Nous voulons qu'Ockam soit simple à utiliser, simple à construire et simple à entretenir.
Au cœur d'Ockam se trouve une pile en couches de protocoles cryptographiques et basés sur des messages comme Ockam Secure Channels et Ockam Routing . Ce sont des protocoles de communication asynchrones, à plusieurs étapes et avec état et nous voulions faire abstraction de tous les détails de ces protocoles aux développeurs d'applications. Nous avons imaginé l'expérience utilisateur comme un simple appel de fonction sur une seule ligne pour créer un canal sécurisé authentifié et crypté de bout en bout.
Le code lié à la cryptographie a également tendance à avoir beaucoup de pistolets, un petit faux pas et votre système devient peu sûr. Ainsi, la simplicité n'est pas seulement un idéal esthétique pour nous, nous pensons que c'est une exigence cruciale pour nous assurer que nous pouvons permettre à chacun de construire des systèmes sécurisés. Connaître les détails de chaque protocole impliqué ne devrait pas être nécessaire. Nous voulions cacher ces footguns et fournir des interfaces de développement faciles à utiliser correctement et impossibles/difficiles à utiliser d'une manière qui tirera votre application dans le pied.
C'est là que C manquait cruellement.
Nos tentatives pour exposer des interfaces sûres et simples, en C, n'ont pas abouti. À chaque itération, nous avons constaté que les développeurs d'applications auraient besoin de connaître trop de détails sur l'état du protocole et les transitions d'état.
À cette époque, j'ai écrit un prototype de création d'un Ockam Secure Channel sur Ockam Routing dans Elixir.
Les programmes Elixir fonctionnent sur BEAM, la machine virtuelle Erlang. BEAM fournit des processus Erlang. Les processus Erlang sont des acteurs concurrents avec état légers. Étant donné que les acteurs peuvent s'exécuter simultanément tout en maintenant l'état interne, il est devenu facile d'exécuter une pile simultanée de protocoles avec état Ockam Transports + Ockam Routing + Ockam Secure Channels.
J'ai pu masquer toutes les couches avec état et créer une simple fonction à une ligne que quelqu'un peut appeler pour créer un canal sécurisé chiffré de bout en bout sur une variété de routes multi-sauts et multi-protocoles.
{:ok, channel} = Ockam.SecureChannel.create(route, vault, keypair)
Un développeur d'application invoquerait cette fonction simple et plusieurs acteurs simultanés exécuteraient les couches sous-jacentes des protocoles avec état. La fonction reviendrait lorsque le canal est établi ou s'il y a une erreur. C'est exactement ce que nous voulions dans notre interface.
Mais Elixir n'est pas comme C. Il ne fonctionne pas très bien sur des ordinateurs petits/contraints et ce n'est pas un bon choix pour être enveloppé dans des wrappers idiomatiques spécifiques à la langue.
À ce stade, nous savions que nous voulions implémenter des acteurs légers, mais nous savions également que C ne rendrait pas cela facile. C'est à ce moment-là que j'ai commencé à creuser dans Rust et que j'ai très vite rencontré quelques éléments qui rendaient Rust très attrayant :
Les bibliothèques Rust peuvent exporter une interface compatible avec la convention d'appel de C. Cela signifie que tout langage ou runtime qui peut lier et appeler de manière statique ou dynamique des fonctions dans une bibliothèque C peut également lier et appeler des fonctions dans une bibliothèque Rust - exactement de la même manière. Étant donné que la plupart des langages prennent en charge les fonctions natives en C, ils prennent également déjà en charge les fonctions natives en Rust. Cela a rendu Rust égal à C du point de vue de notre exigence d'avoir des wrappers spécifiques au langage autour de notre bibliothèque principale.
Rust compile à l'aide de LLVM, ce qui signifie qu'il peut cibler un très grand nombre d'ordinateurs. Cet ensemble n'est probablement pas aussi grand que tout ce que C peut cibler avec GCC et divers forks GCC propriétaires, mais il s'agit toujours d'un très grand sous-ensemble et des travaux sont en cours pour faire compiler Rust avec GCC. Avec la prise en charge croissante de nouvelles cibles LLVM et la prise en charge potentielle de GCC dans Rust, cela semblait être un bon pari du point de vue de notre exigence de pouvoir fonctionner partout.
Le système de type de Rust nous permet de transformer les invariants en erreurs de compilation. Cela réduit l'ensemble des erreurs possibles qui peuvent être expédiées à la production en les rendant plus faciles à détecter au moment du développement. Notre équipe et l'utilisateur de notre bibliothèque Rust deviennent moins susceptibles d'envoyer des bogues de comportement ou des vulnérabilités de sécurité à la production.
Les fonctionnalités de sécurité de la mémoire de Rust éliminent la possibilité d'utilisation après libération, de doubles libérations, de débordements, d'accès hors limites, de courses de données et de nombreuses autres erreurs courantes qui sont connues pour causer 60 à 70 % des vulnérabilités de haute gravité dans les grands C ou des bases de code C++. Rust fournit cette sécurité au moment de la compilation sans encourir les coûts de performance liés à la gestion sécurisée de la mémoire lors de l'exécution à l'aide d'un ramasse-miettes. Cela donne à Rust un sérieux avantage pour écrire du code qui doit être hautement performant, exécuté dans des environnements contraints et hautement sécurisé.
La dernière pièce qui m'a convaincu que Rust convient parfaitement à Ockam était async/await
. Nous avions déjà identifié que nous avions besoin d'acteurs légers pour créer une interface simple et sûre avec la pile de protocoles d'Ockam. async/await
signifiait qu'une grande partie du travail acharné pour créer des acteurs avait déjà été effectuée dans des projets comme tokio et async-std. Nous pourrions construire l'implémentation d'acteur d'Ockam sur cette base.
Un autre aspect important qui s'est démarqué était que async/await
dans rust a une différence significative par rapport à async/await
dans d'autres langages comme Javascript. En Javascript, un moteur de navigateur ou nodejs choisit la manière dont il exécutera les fonctions asynchrones. Mais dans Rust, vous pouvez brancher un mécanisme de votre choix. Ceux-ci sont appelés runtimes asynchrones - tokio est un exemple populaire d'un tel runtime optimisé pour les systèmes hautement évolutifs. Mais nous n'avons pas toujours besoin d'utiliser tokio, nous pouvons à la place choisir un runtime asynchrone optimisé pour les petits appareils embarqués ou les microcontrôleurs.
Cela signifiait que l'implémentation d'acteur d'Ockam, que nous avons appelée plus tard Ockam Workers , si nous la basons sur async/await
de Rust, peut présenter exactement la même interface à nos utilisateurs, quel que soit l'endroit où elle s'exécute - gros ordinateurs ou petits ordinateurs. Toutes nos interfaces de protocole qui reposeraient sur Ockam Workers peuvent également présenter exactement la même interface simple, quel que soit l'endroit où elles s'exécutent.
À ce stade, nous étions convaincus que nous devions réécrire Ockam en Rust. Dans la conversation du webinaire , que j'ai mentionnée plus tôt, Paul Dix et moi avons discuté de ce à quoi ressemblait la transition pour nos équipes chez Ockam et InfluxDB après que chaque projet ait décidé de passer à Rust. Nous avons discuté de la façon dont InfluxDB est passé de Go à Rust et comment Ockam est passé de C à Rust. Au cas où vous seriez intéressé, dans cette partie de notre voyage allez écouter l' enregistrement .
De nombreuses itérations plus tard, n'importe qui peut désormais utiliser la caisse Ockam en rouille pour créer un canal sécurisé crypté de bout en bout et mutuellement authentifié avec un simple appel de fonction.
Voici cette seule ligne, que nous avions imaginée lorsque nous avons commencé :
let channel = node.create_secure_channel(&identity, route, options).await?;
Il crée un canal authentifié et crypté sur des routes multi-sauts et multi-protocoles arbitraires qui peuvent s'étendre sur des réseaux privés et des clouds. Nous sommes en mesure de cacher toute la complexité sous-jacente et les armes à pied derrière cet appel de fonction simple et sûr. Le code reste le même quelle que soit la façon dont vous l'utilisez - sur des serveurs évolutifs ou de minuscules microcontrôleurs.
Pour en savoir plus, consultez kout Ockam sur GitHub ou essayez les procédures pas à pas de la bibliothèque Ockam Rust et Ockam Command .
Également publié ici.
Si vous avez fait partie d'un projet qui a été réécrit en Rust, venez partager l'histoire de votre équipe avec nous sur notre serveur Discord . Nous recrutons également pour les rôles Rust et Elixir, venez rejoindre notre équipe de constructeurs - nous cherchons à rendre les logiciels plus sécurisés et privés dès la conception.