Skip to content

Commit

Permalink
Sketch tls config class
Browse files Browse the repository at this point in the history
  • Loading branch information
wlandau-lilly committed Sep 15, 2023
1 parent 3b014af commit c69bbc2
Show file tree
Hide file tree
Showing 16 changed files with 488 additions and 85 deletions.
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export(crew_class_controller_group)
export(crew_class_launcher)
export(crew_class_launcher_local)
export(crew_class_schedule)
export(crew_class_tls)
export(crew_clean)
export(crew_client)
export(crew_controller)
Expand All @@ -22,6 +23,7 @@ export(crew_launcher_local)
export(crew_random_name)
export(crew_retry)
export(crew_schedule)
export(crew_tls)
export(crew_worker)
export(ends_with)
export(everything)
Expand Down
31 changes: 6 additions & 25 deletions R/crew_client.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,12 @@
#' If `NULL`, the host defaults to the local IP address.
#' @param port TCP port to listen for the workers. If `NULL`,
#' then an available ephemeral port is automatically chosen.
#' @param tls_enable Logical of length 1, whether to use transport layer
#' security (TLS) to secure connections between the client and workers.
#' Only supported for `mirai` version 0.9.0.9020 and above and
#' `nanonext` version 0.9.0.9034 and above.
#' Uses an automatically generated one-time self-signed certificate by
#' default. To guard against man-in-the-middle attacks, consider
#' generating a one-time certificate yourself, requesting a trusted
#' certificate authority (CA) to sign it, and then supplying the
#' keys to the `tls_config` argument. Enabling TLS requires `mirai`
#' version 0.9.0.9027 or above, and a `NULL` value for `tls_enable`
#' will enable TLS if and only if the `mirai` version is sufficient.
#' @param tls_config Optional and only relevant if TLS is enabled
#' (see the `tls_config` argument). The `tls_config` argument
#' controls how transport layer security (TLS) is configured,
#' and it is directly forwarded to the `tls` argument of
#' `mirai::daemons()`. If `tls_config` is `NULL`,
#' then `mirai` will generate a one-time
#' self-signed certificate. This default approach is protects against
#' the simplest attempts at packet sniffing, but it is still vulnerable
#' to man-in-the-middle attacks. When greater security is required,
#' consider generating a PEM-encoded certificate and associated
#' private key yourself and using a trusted certificate authority (CA)
#' to sign the former. The documentation of `mirai`, including the
#' `tls` arguments of the `mirai::daemons()` and `mirai::daemon()`
#' functions, has more details.
#' @param tls Either `NULL` to disable transport layer security (TLS)
#' or a TLS configuration object from [crew_tls()].
#' @param tls_enable Deprecated on 2023-09-15 in version 0.4.1.
#' Use argument `tls` instead.
#' @param tls_config Deprecated on 2023-09-15 in version 0.4.1.
#' Use argument `tls` instead.
#' @param seconds_interval Number of seconds between
#' polling intervals waiting for certain internal
#' synchronous operations to complete. If `space_poll` is `TRUE`, then
Expand Down
246 changes: 246 additions & 0 deletions R/crew_tls.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
#' @title Configure TLS.
#' @export
#' @family user
#' @description Create an `R6` object with transport layer security (TLS)
#' configuration for `crew`.
#' @details See <https://wlandau.github.io/crew/articles/risks.html> for
#' details.
#' @return An `R6` object with TLS configuration settings and methods.
#' @param mode Character of length 1. Must be one of the following:
#' * `"none"`: disable TLS configuration.
#' * `"automatic"`: let `mirai` create a one-time key pair with a
#' self-signed certificate.
#' * `"custom"`: manually supply a private key pair, an optional
#' password for the private key, a certificate,
#' an optional revocation list.
#' @param key If `mode` is `"none"` or `"automatic"`, then `key` is `NULL`.
#' If `mode` is `"custom"`, then `key` is a character of length 1
#' with the file path to the private key file.
#' @param password If `mode` is `"none"` or `"automatic"`,
#' then `password` is `NULL`.
#' If `mode` is `"custom"` and the private key is not encrypted, then
#' `password` is still `NULL`.
#' If `mode` is `"custom"` and the private key is encrypted,
#' then `password` is a character of length 1 the the password of the private
#' key. In this case, DO NOT SAVE THE PASSWORD IN YOUR R CODE FILES.
#' See the `keyring` R package for solutions.
#' @param certificates If `mode` is `"none"` or `"automatic"`,
#' then `certificates` is `NULL`.
#' If `mode` is `"custom"`, then `certificates` is a character vector
#' of file paths to certificate files (signed public keys).
#' If the certificate is self-signed or if it is
#' directly signed by a certificate authority (CA),
#' then only the certificate of the CA is needed. But if you have a whole
#' certificate chain which begins at your own certificate and ends with the
#' CA, then you can supply the whole certificate chain as a character vector
#' which begins at your own certificate and ends with
#' the certificate of the CA.
#' @examples
#' crew_tls(mode = "automatic")
crew_tls <- function(
mode = "none",
key = NULL,
password = NULL,
certificates = NULL
) {
tls <- crew_class_tls$new(
name = crew::crew_random_name(),
mode = mode,
key = key,
password = password,
certificates = certificates
)
tls$validate()
tls
}

#' @title `R6` TLS class.
#' @export
#' @family class
#' @description `R6` class for TLS configuration.
#' @details See [crew_tls()].
#' @examples
#' crew_tls(mode = "automatic")
crew_class_tls <- R6::R6Class(
classname = "crew_class_tls",
cloneable = FALSE,
public = list(
#' @field name Name of the [crew_client()] object paired with this TLS
#' object. Automatically set in [crew_client()].
name = NULL,
#' @field mode See [crew_tls()].
mode = NULL,
#' @field key See [crew_tls()].
key = NULL,
#' @field password See [crew_tls()].
password = NULL,
#' @field certificates See [crew_tls()].
certificates = NULL,
#' @description TLS configuration constructor.
#' @return An `R6` object with TLS configuration.
#' @param name Name of the [crew_tls()] object paired with this TLS
#' object. Automatically set in [crew_tls()].
#' @param mode Argument passed from [crew_tls()].
#' @param key Argument passed from [crew_tls()].
#' @param password Argument passed from [crew_tls()].
#' @param certificates Argument passed from [crew_tls()].
#' @examples
#' crew_tls(mode = "automatic")
initialize = function(
name = NULL,
mode = NULL,
key = NULL,
password = NULL,
certificates = NULL
) {
self$name <- name
self$mode <- mode
self$key <- key
self$password <- password
self$certificates <- certificates
},
#' @description Validate the object.
#' @return `NULL` (invisibly).
validate = function() {
for (field in c("name", "mode")) {
crew_assert(
self[[field]],
is.character(.),
length(.) == 1L,
nzchar(.),
!anyNA(.),
message = paste(
"crew_tls() argument",
field,
"must be a character of length 1"
)
)
}
crew_assert(
crew_assert(
self$mode %in% c("none", "automatic", "custom"),
message = "TLS mode must be \"none\", \"automatic\", or \"custom\"."
)
)
if_any(
self$mode %in% c("none", "automatic"),
self$validate_mode_automatic(),
self$validate_mode_custom()
)
invisible()
},
#' @description Validation for non-custom modes.
#' @return `NULL` (invisibly).
validate_mode_automatic = function() {
for (field in c("key", "password", "certificates")) {
crew_assert(
is.null(self[[field]]),
message = paste(
"If mode is not \"custom\" in crew_tls(), then",
field,
"must be NULL."
)
)
}
invisible()
},
#' @description Validation for custom mode.
#' @return `NULL` (invisibly).
validate_mode_custom = function() {
for (field in c("key", "password", "certificates")) {
crew_assert(
self[[field]],
is.character(.),
length(.) >= 1L,
nzchar(.),
!anyNA(.),
message = paste(
"If mode is \"custom\", then crew_tls() argument",
field,
"must be of type character and be non-missing and nonempty."
)
)
}
for (field in c("key", "password")) {
crew_assert(
length(self[[field]]) == 1L,
message = paste(
"If mode is \"custom\", then crew_tls() argument",
field,
"must have length 1."
)
)
}
files <- c(self$key, self$certificates)
for (file in files) {
crew_assert(
file.exists(file),
message = paste("file not found:", file)
)
}
crew_tls_assert_key(self$key)
for (certificate in self$certificates) {
crew_tls_assert_certificate(certificate)
}
invisible()
}
)
)

crew_tls_assert_key <- function(key) {
crew_assert(
file.exists(key),
message = "private key file not found"
)
lines <- readLines(key)
crew_assert(
length(lines) > 0L,
message = "private key file is empty"
)
crew_assert(
lines[1L] == "-----BEGIN PRIVATE KEY-----" ||
lines[1L] == "-----BEGIN ENCRYPTED PRIVATE KEY-----",
message = paste(
"private key file must begin with the line",
"-----BEGIN PRIVATE KEY----- or -----BEGIN ENCRYPTED PRIVATE KEY-----.",
"please make sure you have a valid private key in PEM format."
)
)
crew_assert(
lines[length(lines)] == "-----END PRIVATE KEY-----" ||
lines[length(lines)] == "-----END ENCRYPTED PRIVATE KEY-----",
message = paste(
"private key file must end with the line",
"-----END PRIVATE KEY----- or -----END ENCRYPTED PRIVATE KEY-----.",
"please make sure you have a valid private key in PEM format."
)
)
}

crew_tls_assert_certificate <- function(certificate) {
crew_assert(
file.exists(certificate),
message = paste("certificate file not found:", certificate)
)
lines <- readLines(certificate)
crew_assert(
length(lines) > 0L,
message = paste("certificate file is empty:", certificate)
)
crew_assert(
lines[1L] == "-----BEGIN CERTIFICATE-----",
message = paste(
"certificate file must begin with the line",
"-----BEGIN CERTIFICATE-----.",
"please make sure you have a valid certificate in PEM format."
)
)
crew_assert(
lines[length(lines)] == "-----BEGIN CERTIFICATE-----",
message = paste(
"certificate file must end with the line",
"-----BEGIN CERTIFICATE-----.",
"please make sure you have a valid certificate in PEM format."
)
)
}
1 change: 1 addition & 0 deletions _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ reference:
- '`crew_controller_local`'
- '`crew_controller_group`'
- '`crew-package`'
- '`crew_tls`'
- title: Launcher plugin development
contents:
- '`crew_client`'
Expand Down
3 changes: 2 additions & 1 deletion man/crew-package.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion man/crew_class_client.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion man/crew_class_controller.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion man/crew_class_controller_group.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion man/crew_class_launcher.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion man/crew_class_schedule.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c69bbc2

Please sign in to comment.