Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SSL support to mug #10

Open
wants to merge 65 commits into
base: main
Choose a base branch
from
Open

Add SSL support to mug #10

wants to merge 65 commits into from

Conversation

arnu515
Copy link

@arnu515 arnu515 commented Nov 12, 2024

Hi! I've added some SSL support to mug using Erlang's ssl module. It lives in the mug/ssl module and has an API very similar to the existing TCP API.

Here's an example of connecting to an SSL host:

import gleam/bit_array
import gleam/io
import mug
import mug/ssl

pub fn main() {
  let assert Ok(socket) =
    mug.new("localhost", port: 12345)  // connect to a TCP/SSL echo server
    |> ssl.with_ssl()
    |> ssl.with_cacerts(ssl.NoVerification)  // don't verify if certs are valid (useful for local debugging)
    |> ssl.connect()

  let assert Ok(Nil) = ssl.send(socket, <<"Hello, Joe!\n":utf8>>)
  let assert Ok(packet) = ssl.receive(socket, timeout_milliseconds: 100)
  io.println(bit_array.to_string(packet))  // "Hello, Joe!\n"
}

There are still a few bits and bobs missing, like the ability to upgrade from a regular TCP connection to an encrypted TLS one. There is also an unimplemented function for adding the certs_keys option since I'm not able to figure out how to do it right now (I'll give it a try soon, I'm new to both Gleam and Erlang).

I've added a few tests in test/mug_ssl_test.gleam which are built off of test/mug_test.gleam. One of them isn't currently working because I'm not able to figure out some ca certificate nonsense (hence, it has been commented out).

Please let me know if any changes have to be done, and I'd request you check my doc comments for any grammatical errors, since English is not my first language. Thanks!

@arnu515
Copy link
Author

arnu515 commented Nov 14, 2024

Hi! I've added the ability to upgrade a TCP connection to a TLS connection via upgrade and upgrade3.

There are still two things left:

  • Supporting certs_keys
  • Fixing that CA certificate test

(closed by mistake, sorry!)

@arnu515 arnu515 closed this Nov 14, 2024
@arnu515 arnu515 reopened this Nov 14, 2024
Copy link
Owner

@lpil lpil left a comment

Choose a reason for hiding this comment

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

This is looking really nice! Very clean!! I've left some minor notes inline.

When you are ready for a review please un-draft this PR. Thank you!

@lpil lpil marked this pull request as draft November 14, 2024 17:11
@arnu515
Copy link
Author

arnu515 commented Dec 5, 2024

I've merged the latest changes from main, but looks like GitHub isn't showing it here for some reason

Copy link
Owner

@lpil lpil left a comment

Choose a reason for hiding this comment

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

Great work, thank you! I've left notes on the API largely. The TLS side I'm less qualified to speak about, but it looks good from what I can tell.


// For connect only
/// An invalid option was passed
Options(opt: Dynamic)
Copy link
Owner

Choose a reason for hiding this comment

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

What's this dynamic?

Copy link
Author

Choose a reason for hiding this comment

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

The docs types it as any, and doesn't explain what it's supposed to be, but from conventions I assume that this is raised if a passed option is invalid.

NoApplicationProtocol
}

pub type Error {
Copy link
Owner

Choose a reason for hiding this comment

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

This is large a duplication of the existing error type, let's merge them into one.

Cacertfile,
dynamic.from(string.to_utf_codepoints(cacerts)),
)
True, None -> #(Cacerts, dynamic.from(get_system_cacerts()))
Copy link
Owner

Choose a reason for hiding this comment

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

Why do we load the certs rather than have the SSL application do that? It would be able to cache them, for example.

Copy link
Author

Choose a reason for hiding this comment

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

I don't quite understand what you mean. Does the SSL application automatically load system cacerts?

Copy link
Author

Choose a reason for hiding this comment

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

And caching is done by erlang's ssl module automatically

@@ -0,0 +1,630 @@
import gleam/bytes_builder.{type BytesBuilder}
Copy link
Owner

Choose a reason for hiding this comment

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

This module seems to be almost exactly the same as the existing module. We could merge them into one rather than having duplicate functions for the programmer to pick between.

@arnu515
Copy link
Author

arnu515 commented Dec 7, 2024

For merging them into one, I wonder how common methods like send and recv can be realised. Do I add prefixes, like ssl_send, or do I have some kind of record that holds metadata and match on it?

I'd request you to give me another review of the code, and bring to my notice any other problems, which I'll fix before doing this refactoring.

@lpil
Copy link
Owner

lpil commented Dec 7, 2024

That sounds good! I presume we cannot cheaply and reliably introspect the connection to determine what mode it is in.

@arnu515
Copy link
Author

arnu515 commented Dec 22, 2024

Hi! I've merged the two files into one. There's a unified API now for both TCP and TLS.

A simple example:

let assert Ok(socket) =
  mug.new("echo.example.com", port: 443)
  |> mug.timeout(milliseconds: 10_000)
  |> mug.with_tls()  // enable TLS!
  |> mug.connect()
let assert Ok(Nil) = mug.send(socket, <<"Hello, Joe!\n":utf8>>)
let assert Ok(data) = mug.receive(socket, 500)
should.equal(data, <<"Hello, Joe!\n":utf8>>)
let assert Ok(_) = mug.shutdown(socket)

Copy link
Owner

@lpil lpil left a comment

Choose a reason for hiding this comment

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

Thank you! Great work here! Here's some notes:

The tests fail cryptically when run now due to certs being missing. The developer-experience needs to be good, so people don't get stuck there not knowing what to do. It would be good if it was automatic, or for the script being at the root (so it's more easy to run) and for instructions to be given to the programmer telling them what to do, instead of a cryptic crash.

The terms TLS and SSL are both used here, but it's not clear to me why either is picked in each case or what the difference is in this package. Could you expand on this a little please? 🙏

Some of the functions are missing type annotations, could you ensure they all have all their annotations please.

For a few names abbreviations have been used and wordshavebeenwrittenlikethis. Please use full words always and put underscores between words, with the exception for existing established abbreviations or initialisms or acronyms such as TLS.

Remove all the use of catch all patterns (_ -> ...) please, they cause bugs when types change as the logic doesn't get updated.

The builder API can silently fail to configure TLS if you give it certificates but TLS has not been enabled, which seems like it could be error prone. Let's remove these builder functions and have the programmer explicitly construct the TLS configuration, to make it clear as possible.

And thank you for waiting while I dealt with some family matters.

Comment on lines +237 to +238
certfile: String,
keyfile: String,
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
certfile: String,
keyfile: String,
certificate_path: String,
key_path: String,

pub type Key {
/// A der-encoded key.
/// `alg` is one of 'RSAPrivateKey' | 'DSAPrivateKey' | 'ECPrivateKey' | 'PrivateKeyInfo'.
DerEncodedKey(alg: String, key: BitArray)
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
DerEncodedKey(alg: String, key: BitArray)
DerEncodedKey(algorithm: String, key: BitArray)

If the string has to be one of a known finite variants then this should be a custom type rather than a string.

Certificates(
use_system_cacerts: Bool,
cacerts: Option(CaCertificates),
certs_keys: List(CertsKeys),
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
certs_keys: List(CertsKeys),
certificate_keys: List(CertsKeys),

PemEncodedCaCertificates(cacertfile: String)
}

pub type CertsKeys {
Copy link
Owner

Choose a reason for hiding this comment

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

This custom type is plural, but the data within is singular. Is this correct?

Copy link
Author

Choose a reason for hiding this comment

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

That's because the certfile has a chain of certificates inside it.

/// certs override PEM-encoded ones. Therefore, if you wish to use a PEM encoded CA cert along
/// with the system's CA, you should decode the PEM into a DER using (`public_key:pem_decode`)[https://www.erlang.org/doc/apps/public_key/public_key.html#pem-api]
/// and use the DerEncodedCaCertificates variant instead.
Certificates(
Copy link
Owner

Choose a reason for hiding this comment

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

It looks like one can use system certs wit custom certs, but the custom certs must all be der or pem. How come that specific restriction?

Copy link
Author

Choose a reason for hiding this comment

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

If PEM certificates are passed, then the ssl application ignores DER certificates, including system cacerts, which are DER encoded. Thus if the user passes like a hundred DER certificates, passing just one PEM certificate will result in all those DER certs being ignored.

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.

5 participants