Skip to content

Feature: MQTT Fallbacks + Alternative Auth#29

Open
DavidArthurCole wants to merge 4 commits into
ClusterM:masterfrom
DavidArthurCole:feat/invesitgation-imps
Open

Feature: MQTT Fallbacks + Alternative Auth#29
DavidArthurCole wants to merge 4 commits into
ClusterM:masterfrom
DavidArthurCole:feat/invesitgation-imps

Conversation

@DavidArthurCole

Copy link
Copy Markdown

While reverse engineering the stock plugin's traffic, I captured and decrypted the HTTPS sessions to map out which cloud endpoints it actually uses and what the responses look like.

A few of the existing stubs were pointed at the wrong endpoints or parsing fields by the wrong names. This PR fixes those, adds a cloud firmware fallback for when MQTT hasn't delivered version data yet, and wires the isFirmwareBetaOpen account setting through so beta firmware entries are only shown to users who opted in. All changes were validated against the live API with a fresh bearer token.

  • Switch get_user_print_info from /v1/iot-service/api/user/bind to /v1/iot-service/api/user/print?force=true, which returns Studio-native field names directly (dev_name, dev_online, dev_access_code) and eliminates the translation layer
  • Add cloud firmware fallback in get_printer_firmware: when no MQTT get_version data has arrived yet, hit /v1/iot-service/api/user/device/version?dev_id= with the required X-BBL-Client-ID header
  • Filter beta firmware entries from the cloud response unless setting.isFirmwareBetaOpen is set in the user's profile; persist this flag through auth::Store so it survives restarts
  • Add fetch_device_cert() infrastructure for the mTLS cert endpoint (/applications/{token}/cert) - endpoint and response shape fully confirmed, wiring to MQTT connect is a follow-on
  • Fix beta firmware filter to use lowercase "beta" (the API does not use "BETA")
  • Fix crl field parsing - the response wraps it in an array (["..."]), not a bare string
  • Add hermetic unit tests for auth::Store persistence covering firmware_beta_open round-trips and backwards compatibility with credential files written before the field existed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the cloud integration layer to better match observed Bambu cloud API behavior, adds a cloud fallback for firmware data when MQTT hasn’t populated versions yet, and persists the account-level “beta firmware opt-in” flag across restarts. It also introduces initial (not yet wired) support for fetching mTLS device certificates for cloud MQTT.

Changes:

  • Switch get_user_print_info to use /v1/iot-service/api/user/print?force=true and adjust field mapping logic accordingly.
  • Add cloud firmware fallback in bambu_network_get_printer_firmware, including beta firmware filtering controlled by setting.isFirmwareBetaOpen persisted via auth::Store.
  • Add cloud::fetch_device_cert() and document the mTLS certificate retrieval endpoint/shape; add unit tests for auth store persistence/back-compat.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/auth_persist_test.cpp New hermetic tests for auth::Store JSON persistence, including legacy-file behavior.
src/cloud_auth.cpp Parse refreshExpiresIn, plumb isFirmwareBetaOpen, and add fetch_device_cert() REST call.
src/bind_cloud.cpp Adjust printer rename payload field to dev_name.
src/auth.cpp Persist/load firmware_beta_open and add update_firmware_beta() API.
src/agent.cpp Add has_firmware_data() and persist beta flag after profile fetch.
src/abi_http.cpp Update print-info endpoint and add cloud firmware fallback + beta filtering.
NETWORK_PLUGIN.md Document mTLS auth endpoint and confirm ticket/profile/firmware response shapes + headers.
include/obn/cloud_auth.hpp Extend auth/profile structs and add DeviceCertResult + fetch_device_cert() decl.
include/obn/auth.hpp Add firmware_beta_open to session and store API to update it.
include/obn/agent.hpp Declare Agent::has_firmware_data().
CMakeLists.txt Add auth_persist_test target and CTest registration.
Comments suppressed due to low confidence (2)

src/abi_http.cpp:260

  • The beta-filter logic only filters dev.firmware entries and ignores dev.ams[].firmware entries from the cloud response. If the user hasn’t opted into beta firmware, AMS beta entries (or all AMS entries if you rebuild the object) may still leak through or be dropped inconsistently. Apply the same filtering to all firmware arrays (printer + AMS) while keeping the rest of each device object intact.
                                out << "{\"dev_id\":" << obn::json::escape(dev.find("dev_id").as_string());
                                out << ",\"firmware\":[";
                                auto fw_v = dev.find("firmware");
                                const auto& fw = fw_v.as_array();
                                bool first_fw = true;
                                for (const auto& entry : fw) {
                                    if (entry.find("status").as_string() == "beta") continue;
                                    if (!first_fw) out << ',';
                                    first_fw = false;
                                    out << entry.dump();
                                }

src/abi_http.cpp:170

  • This switches get_user_print_info to /user/print?force=true. Per the repo’s own NETWORK_PLUGIN.md notes, this endpoint can return a success envelope with no "devices" key (e.g. when the queue is empty). Currently that maps to an empty device list, which would make Studio show no devices. Consider detecting a missing/empty devices array and falling back to /user/bind (or otherwise preserving the previous device list) instead of returning an empty list.
    const std::string url = obn::cloud::api_host(a->cloud_region())
                          + "/v1/iot-service/api/user/print?force=true";
    std::map<std::string, std::string> hdrs{
        {"Authorization", "Bearer " + s.access_token},
    };

    auto resp = obn::http::get_json(url, hdrs);
    if (http_code) *http_code = static_cast<unsigned int>(resp.status_code);

    if (!resp.error.empty()) {
        OBN_WARN("get_user_print_info: transport: %s", resp.error.c_str());
        if (http_body) *http_body = resp.body;
        return BAMBU_NETWORK_ERR_GET_USER_PRINTINFO_FAILED;
    }
    if (resp.status_code != 200) {
        OBN_WARN("get_user_print_info: HTTP %ld body=%s",
                 resp.status_code, resp.body.c_str());
        if (http_body) *http_body = resp.body;
        return BAMBU_NETWORK_ERR_GET_USER_PRINTINFO_FAILED;
    }

    std::vector<std::string> dev_ids;
    std::string mapped = remap_bind_payload(resp.body, &dev_ids);
    OBN_INFO("get_user_print_info: mapped %zu -> %zu bytes, %zu device(s)",
             resp.body.size(), mapped.size(), dev_ids.size());

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/abi_http.cpp
Comment thread src/abi_http.cpp Outdated
Comment thread src/cloud_auth.cpp Outdated
@ClusterM

Copy link
Copy Markdown
Owner

Hi!
Great work! What do you think about Copilot’s comments?

I’m also afraid that many cloud-related functions will have to be either commented out or removed, since a lot of this is essentially dead code. Without a digital signature, we are still stuck in Developer Mode anyway, and this code could easily create legal issues.

@DavidArthurCole

Copy link
Copy Markdown
Author

Hi! Great work! What do you think about Copilot’s comments?

I've fixed them as suggested, it did have a couple good points :)

I’m also afraid that many cloud-related functions will have to be either commented out or removed, since a lot of this is essentially dead code. Without a digital signature, we are still stuck in Developer Mode anyway, and this code could easily create legal issues.

Yes, unfortunately I think you're right. I've left the code in for documentation purposes, however you're right that we're essentially locked out of it for now. The fetch_device_cert code is the closest thing here to towing the line against:
§1201 - circumvention of access controls

Since it touches the mTLS auth flow. Keeping it dead (#if 0) is the right call for now, but I think still having it as a documented function is good.

Side-note: All of these changes were made as an output of my full decomp, which is available here:
https://f.sfconservancy.org/baltobu/reverse-networking/pulls/3

Comment thread NETWORK_PLUGIN.md Outdated
Comment on lines +559 to +596
#### 6.3.1. Cloud MQTT authentication: mutual TLS (mTLS)

Cloud MQTT does **not** use username/password authentication. The stock plugin authenticates with client certificates obtained from the Bambu cloud REST API immediately before connecting to the broker. The following was confirmed via SSLKEYLOGFILE TLS decryption of the stock plugin's HTTPS traffic.

**Certificate retrieval endpoint:**

```
GET /v1/iot-service/api/user/applications/{application_token}/cert?aes256={base64url-key}&ver=1
Authorization: Bearer {access_token}
```

`application_token` is an opaque token from the user's account (distinct from the MQTT bearer token). `aes256` is a base64url-encoded key used to decrypt the returned private key. `ver=1` is the API version.

**Response (200 OK):**

```json
{
"message": "success",
"code": 0,
"error": null,
"cert": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
"crl": ["-----BEGIN X509 CRL-----\n...\n-----END X509 CRL-----\n"],
"key": "{base64-encoded-encrypted-private-key}"
}
```

The `cert` field contains a **3-certificate chain** (PEM-concatenated):

1. **Device leaf cert** — subject: `GLOF{dev_id}-{hex}`, issuer: `GLOF{dev_id}.bambulab.com`
2. **Per-device intermediate CA** — subject: `GLOF{dev_id}.bambulab.com`, issuer: `application_root.bambulab.com`
3. **Root CA** — subject: `application_root.bambulab.com`, issuer: `BBL CA`

**Per-device CA architecture:** each device has its own intermediate CA named `{dev_uid}.bambulab.com`. Client certs are issued under this per-device CA, so the MQTT broker can verify which device a connection belongs to from the certificate alone. Revocation is per-device via the device-specific CRL returned in the `crl` field. Observed CRL validity: 30 days from issue date.

**`key` field:** the private key is AES-256 encrypted and base64-encoded. The `aes256` query parameter in the request URL is the decryption key.

**`crl` field:** a fresh CRL issued at request time, not a cached value.

@j4k0xb j4k0xb May 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cloud MQTT does in fact use username/password authentication and the client only verifies the server's TLS certificate. It's not mTLS.
As said earlier, this is about authorization control and requires the built-in private key of the network plugin.
I have some insight from earlier versions but their implementation is a bit more complex and differs quite substantially from this current documentation. Please let me know whether it's a goal to document everything about it in more detail or if it's out of scope.

Here's also some related information:

Sibling `header` object the stock plugin wraps the command in (cloud and LAN alike when paired against signature-checking firmware):
```json
{
"header": {
"cert_id": "a4e8faaa…CN=GLOF3813734089.bambulab.com",
"payload_len": 1313,
"sign_alg": "RSA_SHA256",
"sign_string": "ycWyeOUZFB…==",
"sign_ver": "v1.0"
},
"print": { }
}
```
`cert_id` identifies the device certificate used for signing (its Subject DN includes `CN=<dev_id>.bambulab.com`); `payload_len` is the byte length of the serialized `print` object; `sign_string` is the Base64 RSA-SHA256 signature of that exact payload computed with the per-install private key shipped inside the stock plugin's obfuscated blob; `sign_ver` versions the canonicalization rules. Non-Developer-Mode firmware rejects unsigned `project_file` (and other privileged) commands with `MQTT Command verification failed` (error `84033543`). Developer Mode bypasses the check entirely (see "Developer Mode requirement" in `README.md`), making the `header` envelope optional.

DavidArthurCole and others added 4 commits May 26, 2026 06:06
Merge CMakeLists test targets from both branches, clarify cloud MQTT
auth docs (open plugin token path vs stock mTLS), fall back from
/user/print to /user/bind when the device list is empty, and filter
beta firmware entries in ams[].firmware as well as dev.firmware.

Co-authored-by: Cursor <cursoragent@cursor.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comment thread src/agent.cpp
Comment on lines 1352 to 1356
auth_store_->update_profile(prof.user_id, prof.user_name,
prof.nick_name, prof.avatar);
OBN_INFO("change_user: hello %s (uid=%s)",
auth_store_->update_firmware_beta(prof.firmware_beta_open);
OBN_INFO("change_user: hello %s (uid=%s, beta_fw=%d)",
prof.user_name.empty() ? prof.nick_name.c_str() : prof.user_name.c_str(),
Comment on lines +105 to +109
// DeviceCertResult::key before the private key can be used with mosquitto.
DeviceCertResult fetch_device_cert(const std::string& region,
const std::string& access_token,
const std::string& application_token,
const std::string& aes256_key);
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.

4 participants