From fbf5f369005f9c6bcabe5391016b20fe2f7ac581 Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Mon, 7 Sep 2026 16:52:18 -0500 Subject: [PATCH 1/2] Add Matrix cross-signing upload endpoints --- NAMESPACE | 2 + R/keys.R | 75 ++++++++++++++++++++++++++++ inst/tinytest/test_keys.R | 44 ++++++++++++++++ man/mx_keys_device_signing_upload.Rd | 43 ++++++++++++++++ man/mx_keys_signatures_upload.Rd | 26 ++++++++++ 5 files changed, 190 insertions(+) create mode 100644 man/mx_keys_device_signing_upload.Rd create mode 100644 man/mx_keys_signatures_upload.Rd diff --git a/NAMESPACE b/NAMESPACE index 9d59b04..31a8483 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -8,7 +8,9 @@ export(mx_get_account_data) export(mx_get_state) export(mx_guess_mime) export(mx_keys_claim) +export(mx_keys_device_signing_upload) export(mx_keys_query) +export(mx_keys_signatures_upload) export(mx_keys_upload) export(mx_login) export(mx_logout) diff --git a/R/keys.R b/R/keys.R index cdb036e..355fd9c 100644 --- a/R/keys.R +++ b/R/keys.R @@ -167,3 +167,78 @@ mx_send_to_device <- function(session, event_type, messages, txn_id = NULL) { invisible(NULL) } + +#' Upload cross-signing public keys +#' +#' POST \code{/_matrix/client/v3/keys/device_signing/upload}. The key +#' objects must already carry the signatures required by the Matrix +#' cross-signing specification. Homeservers normally require user-interactive +#' authentication (UIA); the initial 401 response contains a session id which +#' the caller supplies in a completed \code{auth} object on retry. +#' +#' @param session An \code{mx_session}. +#' @param master_key A signed Matrix CrossSigningKey object or NULL. +#' @param self_signing_key A CrossSigningKey signed by the master key, or NULL. +#' @param user_signing_key A CrossSigningKey signed by the master key, or NULL. +#' @param auth Completed UIA authentication object or NULL. +#' @return Parsed homeserver response. +#' @examples +#' \dontrun{ +#' mx_keys_device_signing_upload(s, master_key = master, +#' self_signing_key = self, user_signing_key = user, +#' auth = list(type = "m.login.password", session = uia_session, +#' identifier = list(type = "m.id.user", user = "bot"), +#' password = "secret")) +#' } +#' @export +mx_keys_device_signing_upload <- function(session, master_key = NULL, + self_signing_key = NULL, + user_signing_key = NULL, + auth = NULL) { + body <- list() + if (!is.null(master_key)) { + body$master_key <- master_key + } + if (!is.null(self_signing_key)) { + body$self_signing_key <- self_signing_key + } + if (!is.null(user_signing_key)) { + body$user_signing_key <- user_signing_key + } + if (!is.null(auth)) { + body$auth <- auth + } + if (!length(body) || (length(body) == 1L && !is.null(body$auth))) { + stop("mx_keys_device_signing_upload: no signing keys supplied", + call. = FALSE) + } + mx_http(session$server, "POST", + "/_matrix/client/v3/keys/device_signing/upload", + body = body, token = session$token) +} + +#' Upload signatures over device or cross-signing keys +#' +#' POST \code{/_matrix/client/v3/keys/signatures/upload}. The body maps a +#' user id to key ids and the complete signed key objects. mx.api transports +#' the objects unchanged and performs no signature validation itself. +#' +#' @param session An \code{mx_session}. +#' @param signatures Named list mapping user ids to signed key objects. +#' @return Parsed response, including any per-signature \code{failures}. +#' @examples +#' \dontrun{ +#' mx_keys_signatures_upload(s, list( +#' "@bot:example.org" = list(DEVICE = signed_device))) +#' } +#' @export +mx_keys_signatures_upload <- function(session, signatures) { + if (!is.list(signatures) || is.null(names(signatures)) || + !length(signatures)) { + stop("mx_keys_signatures_upload: 'signatures' must be a non-empty named list", + call. = FALSE) + } + mx_http(session$server, "POST", + "/_matrix/client/v3/keys/signatures/upload", + body = signatures, token = session$token) +} diff --git a/inst/tinytest/test_keys.R b/inst/tinytest/test_keys.R index 08dd4d9..77c7d37 100644 --- a/inst/tinytest/test_keys.R +++ b/inst/tinytest/test_keys.R @@ -2,9 +2,14 @@ library(tinytest) # Smoke: functions exist with the expected signatures. expect_true(is.function(mx.api::mx_keys_upload)) +expect_true(is.function(mx.api::mx_keys_device_signing_upload)) +expect_true(is.function(mx.api::mx_keys_signatures_upload)) expect_true(is.function(mx.api::mx_keys_query)) expect_true(is.function(mx.api::mx_keys_claim)) expect_true(is.function(mx.api::mx_send_to_device)) +expect_equal(names(formals(mx.api::mx_keys_device_signing_upload)), + c("session", "master_key", "self_signing_key", "user_signing_key", "auth")) +expect_equal(names(formals(mx.api::mx_keys_signatures_upload)), c("session", "signatures")) expect_equal( names(formals(mx.api::mx_keys_upload)), @@ -31,11 +36,50 @@ fake <- mx.api::mx_session( device_id = "DEV" ) expect_error(mx.api::mx_keys_upload(fake)) # nothing to upload +expect_error(mx.api::mx_keys_device_signing_upload(fake)) +expect_error(mx.api::mx_keys_device_signing_upload( + fake, auth = list(type = "m.login.password") +)) +expect_error(mx.api::mx_keys_signatures_upload(fake, list())) expect_error(mx.api::mx_keys_query(fake, device_keys = "not a list")) expect_error(mx.api::mx_keys_claim(fake, one_time_keys = list())) # unnamed expect_error(mx.api::mx_send_to_device(fake, "m.room.encrypted", messages = list())) # unnamed +# Endpoint bodies are passed through exactly, including UIA on the retry. +local({ + ns <- asNamespace("mx.api") + original <- get("mx_http", envir = ns, inherits = FALSE) + calls <- list() + assignInNamespace("mx_http", function(base_url, method, path, body = NULL, + query = NULL, token = NULL) { + calls[[length(calls) + 1L]] <<- list(base_url = base_url, method = method, + path = path, body = body, token = token) + list(failures = list()) + }, ns = "mx.api") + on.exit(assignInNamespace("mx_http", original, ns = "mx.api"), add = TRUE) + + master <- list(user_id = "@u:example", usage = list("master"), + keys = list("ed25519:master" = "master")) + auth <- list(type = "m.login.password", session = "uia-1", + identifier = list(type = "m.id.user", user = "u"), + password = "secret") + mx_keys_device_signing_upload(fake, master_key = master, auth = auth) + expect_identical(calls[[1]]$method, "POST") + expect_identical(calls[[1]]$path, + "/_matrix/client/v3/keys/device_signing/upload") + expect_identical(calls[[1]]$body$master_key, master) + expect_identical(calls[[1]]$body$auth, auth) + expect_identical(calls[[1]]$token, "tok") + + signed <- list("@u:example" = list(DEV = list(device_id = "DEV"))) + mx_keys_signatures_upload(fake, signed) + expect_identical(calls[[2]]$method, "POST") + expect_identical(calls[[2]]$path, + "/_matrix/client/v3/keys/signatures/upload") + expect_identical(calls[[2]]$body, signed) +}) + # Live round-trip gated on at_home() + env vars. Reuses mx.crypto if # present so the upload payload is real (signed) — falls back to a # format-only smoke test otherwise. diff --git a/man/mx_keys_device_signing_upload.Rd b/man/mx_keys_device_signing_upload.Rd new file mode 100644 index 0000000..6c6ed67 --- /dev/null +++ b/man/mx_keys_device_signing_upload.Rd @@ -0,0 +1,43 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{mx_keys_device_signing_upload} +\alias{mx_keys_device_signing_upload} +\title{Upload cross-signing public keys} +\usage{ +mx_keys_device_signing_upload( + session, + master_key = NULL, + self_signing_key = NULL, + user_signing_key = NULL, + auth = NULL +) +} +\arguments{ +\item{session}{An \code{mx_session}.} + +\item{master_key}{A signed Matrix CrossSigningKey object or NULL.} + +\item{self_signing_key}{A CrossSigningKey signed by the master key, or NULL.} + +\item{user_signing_key}{A CrossSigningKey signed by the master key, or NULL.} + +\item{auth}{Completed UIA authentication object or NULL.} +} +\value{ +Parsed homeserver response. +} +\description{ +POST \code{/_matrix/client/v3/keys/device_signing/upload}. The key +objects must already carry the signatures required by the Matrix +cross-signing specification. Homeservers normally require user-interactive +authentication (UIA); the initial 401 response contains a session id which +the caller supplies in a completed \code{auth} object on retry. +} +\examples{ +\dontrun{ +mx_keys_device_signing_upload(s, master_key = master, + self_signing_key = self, user_signing_key = user, + auth = list(type = "m.login.password", session = uia_session, + identifier = list(type = "m.id.user", user = "bot"), + password = "secret")) +} +} diff --git a/man/mx_keys_signatures_upload.Rd b/man/mx_keys_signatures_upload.Rd new file mode 100644 index 0000000..a527bad --- /dev/null +++ b/man/mx_keys_signatures_upload.Rd @@ -0,0 +1,26 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{mx_keys_signatures_upload} +\alias{mx_keys_signatures_upload} +\title{Upload signatures over device or cross-signing keys} +\usage{ +mx_keys_signatures_upload(session, signatures) +} +\arguments{ +\item{session}{An \code{mx_session}.} + +\item{signatures}{Named list mapping user ids to signed key objects.} +} +\value{ +Parsed response, including any per-signature \code{failures}. +} +\description{ +POST \code{/_matrix/client/v3/keys/signatures/upload}. The body maps a +user id to key ids and the complete signed key objects. mx.api transports +the objects unchanged and performs no signature validation itself. +} +\examples{ +\dontrun{ +mx_keys_signatures_upload(s, list( + "@bot:example.org" = list(DEVICE = signed_device))) +} +} From 6cf73cc88de3f96a4374048b9500afb85839cdf6 Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Mon, 7 Sep 2026 16:52:18 -0500 Subject: [PATCH 2/2] Bump version to 0.3.0.2 --- DESCRIPTION | 4 ++-- NEWS.md | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index e36256b..8f90330 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Package: mx.api Type: Package Title: Minimal Matrix Client-Server API -Version: 0.3.0.1 -Date: 2026-06-10 +Version: 0.3.0.2 +Date: 2026-09-04 Authors@R: c( person("Troy", "Hernandez", role = c("aut", "cre"), email = "troy@cornball.ai", diff --git a/NEWS.md b/NEWS.md index 120fb74..3af9272 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,10 @@ +# mx.api 0.3.0.2 + +* New: `mx_keys_device_signing_upload()` publishes Matrix master, + self-signing, and user-signing keys with explicit UIA passthrough. +* New: `mx_keys_signatures_upload()` publishes signatures over device and + cross-signing key objects. + # mx.api 0.3.0.1 * `mx_room_create()` gains `creation_content`, merged into the