Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion c/docs/client-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ Defaults used when the matching option is left at 0:

| Macro | Default | Bounds |
| --- | --- | --- |
| `AZ_IOT_SU_MAX_ROOT_KEYS` | 4 | Root keys in the trust store. |
| `AZ_IOT_SU_MAX_ROOT_KEYS` | 8 | Root keys in the trust store. |
| `AZ_IOT_SU_REQUEST_BUFFER_SIZE` | 16384 | Copy of the update metadata (manifest, signature and file URLs) for the current deployment; a one-file offer is about 5 KiB. A larger deployment is refused: `AZ_IOT_SU_EVENT_UPDATE_REFUSED` is raised with `AZ_IOT_ERR_NOT_ENOUGH_SPACE`. Also sizes `AZ_IOT_SU_STATE_BLOB_MAX_SIZE` and the scratch the client verifies the signature in, so the client holds about twice this value. |
| `AZ_IOT_SU_VERIFY_SCRATCH_SIZE` | 8192 | Stack used by `az_iot_su_parse_update_request()` for the decoded signature (JWS header with the signing key, and the signatures). A signature that does not fit fails with `AZ_IOT_ERR_AUTH` and an `ERROR` log naming the part that is too large. |
| `AZ_IOT_SU_WORKFLOW_ID_SIZE` | 64 | Workflow ID kept for reporting, duplicate detection and persistence. A deployment with a longer ID is refused: nothing is processed or reported, and `AZ_IOT_SU_EVENT_UPDATE_REFUSED` is raised with `AZ_IOT_ERR_NOT_ENOUGH_SPACE`. |
Expand Down
2 changes: 1 addition & 1 deletion c/docs/eng/software-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -1241,7 +1241,7 @@ capacity is compile-time configurable:

```c
#ifndef AZ_IOT_SU_MAX_ROOT_KEYS
#define AZ_IOT_SU_MAX_ROOT_KEYS 4
#define AZ_IOT_SU_MAX_ROOT_KEYS 8
#endif
```

Expand Down
4 changes: 3 additions & 1 deletion c/docs/eng/test-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -940,7 +940,7 @@ RS256 signature verification) through the crypto backends in `c/adapters/crypto_
| | Device properties too small is rejected | — | unit | Done | [device_properties_too_small_is_rejected](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/su_client_test.c#L954) |
| | Device properties buffer size matches the need | The reported requirement is exact, not an estimate. | unit | Done | [device_properties_buffer_size_matches_need](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/su_client_test.c#L996) |
| | Build report with too small a buffer is rejected | `az_iot_su_build_report()` bound. | unit | Done | [build_report_with_too_small_a_buffer_is_rejected](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/su_client_test.c#L1373) |
| Manifest & crypto | Microsoft root keys are embedded | The shipped roots match the published values. | unit | Done | [microsoft_root_keys_are_embedded](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/su_client_test.c#L1187) |
| Manifest & crypto | Microsoft root keys are embedded | ADU.200702.R, ADU.200703.R and ADU.241112.R, in that order; 3072-bit moduli; none disabled. | unit | Done | [microsoft_root_keys_are_embedded](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/su_client_test.c#L1187) |
| | Public parser accepts updateMetadata | `az_iot_su_parse_update_request()`; other shapes are NOT_FOUND. | unit | Done | [public_parser_accepts_update_metadata](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/su_client_test.c) |
| | Sha256 oneshot matches known answers | FIPS 180-4 vectors (empty to 1M bytes), through the SDK's one-shot SHA-256. Every crypto backend (OpenSSL; mbedTLS 3.6/4.1/4.2). | unit | Done | [sha256_oneshot_matches_known_answers](../../tests/support/crypto_contract.c) |
| | Sha256 incremental matches known answers for any chunking | Chunks of 1/63/64/65/4096 bytes plus empty updates. | unit | Done | [sha256_incremental_matches_known_answers_for_any_chunking](../../tests/support/crypto_contract.c) |
Expand All @@ -950,6 +950,7 @@ RS256 signature verification) through the crypto backends in `c/adapters/crypto_
| | HMAC-SHA256 matches known answers | RFC 4231 cases 1-4, 6, 7; a one-block key; empty key and data. Composed by the SDK over each backend's SHA-256. | unit | Done | [hmac_sha256_matches_known_answers](../../tests/support/crypto_contract.c) |
| | HMAC-SHA256 rejects bad arguments | NULL key, data, output or backend with a non-zero length. | unit | Done | [hmac_sha256_rejects_bad_arguments](../../tests/support/crypto_contract.c) |
| | Rs256 accepts known good vectors | 2048/3072/4096-bit, e=3, leading-zero modulus/exponent. | unit | Done | [rs256_accepts_known_good_vectors](../../tests/support/crypto_contract.c) |
| | Microsoft root keys verify the root key package | Each compiled-in root verifies its RS256 signature on Microsoft's published production root key package (version 2); a modulus with one byte changed is rejected. Every crypto backend. | unit | Done | [microsoft_root_keys_verify_the_root_key_package](../../tests/support/crypto_contract.c) |
| | Rs256 rejects known bad vectors | Bit flips, wrong key/exponent, e=0/1/even, bad signature length or value, PSS, SHA-1/384/512, malformed PKCS#1 v1.5 encodings. | unit | Done | [rs256_rejects_known_bad_vectors](../../tests/support/crypto_contract.c) |
| | Rs256 rejects missing inputs | NULL or zero-length key or signature; NULL message. | unit | Done | [rs256_rejects_missing_inputs](../../tests/support/crypto_contract.c) |
| | Rs256 rejects oversized keys safely | Moduli and exponents that fill or exceed a fixed DER buffer, up to 70000 bytes; rejected with no out-of-bounds write. | unit | Done | [rs256_rejects_oversized_keys_safely](../../tests/support/crypto_contract.c) |
Expand All @@ -959,6 +960,7 @@ RS256 signature verification) through the crypto backends in `c/adapters/crypto_
| | Init requires a crypto backend that verifies | None, incomplete or other-version backend is INVALID_ARG; no `verify_rs256` is NOT_SUPPORTED; the public init takes the connection's backend. | unit | Done | [init_requires_a_crypto_backend_that_verifies](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/su_client_test.c) |
| | Standalone entry points check the backend they need | `az_iot_su_parse_update_request()`: no backend is INVALID_ARG, no `verify_rs256` is NOT_SUPPORTED, and non-NULL outputs are zeroed on both. `az_iot_su_verify_file_hash()`: no backend is INVALID_ARG; a SHA-256-only backend verifies. | unit | Done | [standalone_entry_points_check_the_backend_they_need](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/su_client_test.c) |
| | Manifest signed by an unknown root key is rejected | End-to-end through `az_iot_su_parse_update_request()`. | unit | Done | [manifest_signed_by_an_unknown_root_key_is_rejected](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/su_client_test.c#L1428) |
| | Manifest under root ADU.241112.R resolves to that key | With `az_iot_su_microsoft_root_keys()`, the SJWK is verified with the ADU.241112.R modulus. | unit | Done | [manifest_under_root_adu_241112_r_resolves_to_that_key](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/su_client_test.c) |
| | Malformed jws is rejected | Wrong segment count, bad base64url, missing header. | unit | Done | [malformed_jws_is_rejected](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/su_client_test.c#L1449) |
| | Near-limit nested signing key is verified | An offer near `AZ_IOT_SU_REQUEST_BUFFER_SIZE`, almost all signing JWK, verifies in the managed client. | unit | Done | [near_limit_nested_signing_key_is_verified](../../tests/unit/su_client_test.c) |
| | Large signature is verified | JWS header and signing key over 2 KiB in an offer over 4 KiB; modulus in standard base64 with escaped slashes, asserted as decoded with the exponent and signature (managed and public parser). | unit | Done | [large_signature_is_verified](../../tests/unit/su_client_test.c) |
Expand Down
2 changes: 1 addition & 1 deletion c/inc/azure/iot/az_iot_su.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ extern "C"

/* Maximum number of RSA root public keys the core trust store holds. */
#ifndef AZ_IOT_SU_MAX_ROOT_KEYS
#define AZ_IOT_SU_MAX_ROOT_KEYS 4
#define AZ_IOT_SU_MAX_ROOT_KEYS 8
#endif

/* Scratch buffer (in-struct) that holds a COPY of the `updateMetadata`
Expand Down
5 changes: 2 additions & 3 deletions c/src/features/su/su_client.c
Original file line number Diff line number Diff line change
Expand Up @@ -2205,9 +2205,8 @@ void az_iot_su_client_deinit(az_iot_su_client* client)
}

/* az_iot_su_microsoft_root_keys() — Microsoft's compiled-in software updates production
* root public keys — is defined in su_root_keys_microsoft.c (generated from the
* official agent's hardcoded key list). Kept in a separate translation unit so
* the large key blobs live apart from the state machine. */
* root public keys — is defined in su_root_keys_microsoft.c. Kept in a separate
* translation unit so the large key blobs live apart from the state machine. */

/* ------------------------------------------------------------------------- */
/* persistence & resume (Phase 5) */
Expand Down
40 changes: 39 additions & 1 deletion c/src/features/su/su_root_keys_microsoft.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
* big-endian public moduli of Microsoft's published software updates signing roots,
* base64url-decoded from the official Device Update agent's hardcoded
* root key list (Azure/iot-hub-device-update,
* src/utils/root_key_utils/src/root_key_list.c, non-test block). These are
* src/utils/root_key_utils/src/root_key_list.c, non-test block), plus
* ADU.241112.R from Microsoft's production root key package (version 2),
* whose signatures verify under both roots above. These are
* PUBLIC keys and safe to embed; they are the immutable trust anchor that
* verifies every software update manifest's signing chain.
*
Expand Down Expand Up @@ -76,6 +78,34 @@ static const uint8_t k_modulus_SU_200703_R[] = {
0x99,
};

static const uint8_t k_modulus_SU_241112_R[] = {
0x00, 0x8b, 0x81, 0xa9, 0xea, 0x3d, 0x4d, 0x07, 0x69, 0x44, 0x1e, 0x1d, 0x2a, 0x5a, 0xed, 0x01,
0xe4, 0xb0, 0x71, 0x52, 0xf1, 0xaa, 0x17, 0x8f, 0x60, 0x6e, 0xfa, 0x93, 0x8a, 0xd6, 0x99, 0xd3,
0xa5, 0xf3, 0x52, 0xe7, 0xd9, 0x23, 0x03, 0xf4, 0x74, 0x5c, 0xd4, 0x2e, 0x5f, 0xed, 0x3d, 0x39,
0x4a, 0x73, 0xcb, 0xde, 0x8e, 0xed, 0xc9, 0x08, 0xae, 0x84, 0xb5, 0x66, 0x82, 0xb0, 0x05, 0xd5,
0xb4, 0x18, 0xfa, 0x0d, 0x5f, 0x24, 0x91, 0x35, 0xef, 0x87, 0x69, 0xc1, 0xdf, 0x8d, 0x14, 0xde,
0xc7, 0x9d, 0x04, 0x6a, 0x39, 0x7d, 0x95, 0xa3, 0xfe, 0xb3, 0x47, 0xdc, 0x70, 0x7c, 0xad, 0x0e,
0x93, 0x06, 0x24, 0xef, 0x15, 0x10, 0x27, 0x19, 0xac, 0x42, 0xa9, 0x08, 0xab, 0x61, 0xbd, 0xfc,
0x5f, 0xb4, 0x81, 0x06, 0x9e, 0x23, 0x96, 0x39, 0xed, 0xf6, 0xec, 0xdc, 0x24, 0x00, 0x76, 0x8b,
0xbd, 0xbb, 0xaa, 0x2e, 0x3c, 0x85, 0xab, 0xd6, 0x58, 0xb8, 0x65, 0x5b, 0x10, 0x50, 0x7d, 0x31,
0xae, 0x53, 0x96, 0xa1, 0xef, 0xd2, 0xac, 0x9c, 0xad, 0x7a, 0x83, 0xf7, 0x86, 0x9c, 0x64, 0x61,
0xf6, 0xd9, 0xc9, 0xf1, 0xa0, 0x80, 0x1b, 0xe7, 0x29, 0xfb, 0x71, 0x17, 0xe1, 0xff, 0x73, 0x41,
0x77, 0xb7, 0xc8, 0x70, 0x26, 0xac, 0x66, 0x26, 0x30, 0x7d, 0xcc, 0xe8, 0x23, 0x9e, 0x31, 0x39,
0xf6, 0x76, 0x2a, 0xa7, 0xe4, 0x26, 0x57, 0xaa, 0xc3, 0x39, 0xba, 0x0b, 0xf5, 0x90, 0x84, 0xb1,
0x25, 0xe2, 0xfb, 0x17, 0x87, 0xed, 0x88, 0x60, 0x81, 0x7b, 0x58, 0xb7, 0x3c, 0xd6, 0x64, 0x77,
0xaa, 0x25, 0x58, 0x52, 0xe7, 0x55, 0xf2, 0x51, 0xf6, 0x61, 0x1e, 0x4d, 0xd0, 0xbd, 0x4b, 0x1e,
0xfc, 0x01, 0xe7, 0x59, 0xb1, 0xf0, 0x4a, 0xe3, 0x75, 0x67, 0xed, 0x1e, 0x8c, 0x95, 0x5b, 0x7b,
0xf3, 0xf6, 0x54, 0xe6, 0x4b, 0xd5, 0x0a, 0x61, 0x49, 0xc2, 0xdb, 0xe4, 0x92, 0x62, 0x09, 0xc5,
0x69, 0xb5, 0xdd, 0xee, 0x85, 0xe7, 0x9d, 0x78, 0x6b, 0x45, 0x10, 0x6f, 0x62, 0xb4, 0x43, 0x98,
0xce, 0x98, 0x0b, 0xb2, 0x52, 0xf6, 0x57, 0xc2, 0x85, 0x44, 0x63, 0x72, 0x40, 0x9d, 0x25, 0x4e,
0x3d, 0x82, 0x6d, 0xac, 0x36, 0x8e, 0x2e, 0x2f, 0x15, 0x54, 0x02, 0x48, 0x17, 0xeb, 0x45, 0x7a,
0xac, 0x19, 0xa5, 0x61, 0x70, 0x37, 0x88, 0x4e, 0xf9, 0x48, 0xb1, 0xdd, 0xd8, 0x41, 0x73, 0xcb,
0x95, 0xa9, 0x51, 0x41, 0x29, 0x42, 0xa8, 0x6b, 0xa8, 0x1c, 0xc1, 0xb9, 0x6b, 0xc0, 0xbc, 0x9f,
0x75, 0xdd, 0xf5, 0x85, 0x98, 0x00, 0x61, 0xbf, 0x20, 0x07, 0xa3, 0x1b, 0x33, 0x6a, 0xd4, 0xea,
0x6f, 0xf7, 0xed, 0x03, 0xd1, 0x82, 0x69, 0x2d, 0x2b, 0x54, 0x0a, 0xd9, 0x56, 0x42, 0x29, 0xd2,
0x6d,
};

static const uint8_t k_exponent_65537[] = { 0x01, 0x00, 0x01 };

static const az_iot_su_root_key k_microsoft_root_keys[] = {
Expand All @@ -95,6 +125,14 @@ static const az_iot_su_root_key k_microsoft_root_keys[] = {
.exponent_len = sizeof(k_exponent_65537),
.disabled = false,
},
{
.kid = "ADU.241112.R",
.modulus = k_modulus_SU_241112_R,
.modulus_len = sizeof(k_modulus_SU_241112_R),
.exponent = k_exponent_65537,
.exponent_len = sizeof(k_exponent_65537),
.disabled = false,
},
};

const az_iot_su_root_key* az_iot_su_microsoft_root_keys(size_t* out_count)
Expand Down
46 changes: 46 additions & 0 deletions c/tests/support/crypto_contract.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "crypto_contract.h"
#include "internal/crypto.h"
#include "su_crypto_vectors.h"
#include "su_root_key_package_vectors.h"

#define SU_ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))

Expand Down Expand Up @@ -364,6 +365,50 @@ static void rs256_accepts_known_good_vectors(void** state)
}
}

/* Each compiled-in Microsoft root verifies its signature on the published root key
* package, so a wrong modulus byte fails here. */
static void microsoft_root_keys_verify_the_root_key_package(void** state)
{
(void)state;
size_t count = 0;
const az_iot_su_root_key* keys = az_iot_su_microsoft_root_keys(&count);
assert_int_equal(count, SU_ARRAY_LEN(k_su_root_key_package_signatures));
for (size_t i = 0; i < count; ++i)
{
const su_root_key_package_signature* s = &k_su_root_key_package_signatures[i];
assert_string_equal(keys[i].kid, s->kid);
assert_int_equal(
g_crypto->verify_rs256(
g_crypto,
keys[i].modulus,
keys[i].modulus_len,
keys[i].exponent,
keys[i].exponent_len,
k_su_root_key_package_signed,
sizeof(k_su_root_key_package_signed),
s->signature,
s->signature_len),
AZ_IOT_OK);

uint8_t modulus[512];
assert_true(keys[i].modulus_len <= sizeof(modulus));
memcpy(modulus, keys[i].modulus, keys[i].modulus_len);
modulus[keys[i].modulus_len - 1] ^= 0x02;
assert_int_not_equal(
g_crypto->verify_rs256(
g_crypto,
modulus,
keys[i].modulus_len,
keys[i].exponent,
keys[i].exponent_len,
k_su_root_key_package_signed,
sizeof(k_su_root_key_package_signed),
s->signature,
s->signature_len),
AZ_IOT_OK);
}
}

static void rs256_rejects_known_bad_vectors(void** state)
{
(void)state;
Expand Down Expand Up @@ -733,6 +778,7 @@ int crypto_contract_run(const char* group_name, const az_iot_crypto* crypto)
cmocka_unit_test(hmac_sha256_matches_known_answers),
cmocka_unit_test(hmac_sha256_rejects_bad_arguments),
cmocka_unit_test(rs256_accepts_known_good_vectors),
cmocka_unit_test(microsoft_root_keys_verify_the_root_key_package),
cmocka_unit_test(rs256_rejects_known_bad_vectors),
cmocka_unit_test(rs256_rejects_missing_inputs),
cmocka_unit_test(rs256_rejects_oversized_keys_safely),
Expand Down
81 changes: 81 additions & 0 deletions c/tests/support/gen_su_root_key_package_vectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
# Copyright (c) Microsoft. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for full license information.

"""Generate tests/support/su_root_key_package_vectors.h from a Microsoft root key package.

The output is committed; tests never run this. It holds the package's signed bytes (the
`protected` object, serialized compactly as the Device Update agent does) and one RS256
signature per root, so the compiled-in roots are checked against the published package.

python3 c/tests/support/gen_su_root_key_package_vectors.py rootkeypackage-<v>.json \
> c/tests/support/su_root_key_package_vectors.h
"""

import base64
import json
import sys


def b64url(s):
return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))


def emit_bytes(name, data):
print(f"static const uint8_t {name}[] = {{")
for i in range(0, len(data), 16):
print(" " + ", ".join(f"0x{b:02x}" for b in data[i : i + 16]) + ",")
print("};\n")


def main():
with open(sys.argv[1], "rb") as f:
package = json.loads(f.read())
protected = package["protected"]
signed = json.dumps(protected, separators=(",", ":")).replace("/", "\\/").encode()
kids = list(protected["rootKeys"])
if len(package["signatures"]) != len(kids):
sys.exit("signature count does not match root count")

print(
"""// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license
// information.

/* SPDX-License-Identifier: MIT */
/**
* @file su_root_key_package_vectors.h
* @brief Microsoft production root key package known answers. GENERATED by
* gen_su_root_key_package_vectors.py; do not edit.
*/
#ifndef SU_ROOT_KEY_PACKAGE_VECTORS_H
#define SU_ROOT_KEY_PACKAGE_VECTORS_H

#include <stddef.h>
#include <stdint.h>

/* clang-format off */
"""
)
print(f"/** @brief Root key package version. */\n#define SU_ROOT_KEY_PACKAGE_VERSION {protected['version']}\n")
print("/** @brief Signed bytes: the package's `protected` object. */")
emit_bytes("k_su_root_key_package_signed", signed)
for i, kid in enumerate(kids):
sig = package["signatures"][i]
if sig["alg"] != "RS256":
sys.exit(f"unexpected alg for {kid}")
emit_bytes(f"k_su_root_key_package_sig_{i}", b64url(sig["sig"]))

print("/** @brief One root's RS256 signature over k_su_root_key_package_signed. */")
print("typedef struct su_root_key_package_signature\n{")
print(" const char* kid;\n const uint8_t* signature;\n size_t signature_len;")
print("} su_root_key_package_signature;\n")
print("static const su_root_key_package_signature k_su_root_key_package_signatures[] = {")
for i, kid in enumerate(kids):
print(f' {{ "{kid}", k_su_root_key_package_sig_{i}, sizeof(k_su_root_key_package_sig_{i}) }},')
print("};\n")
print("/* clang-format on */\n\n#endif /* SU_ROOT_KEY_PACKAGE_VECTORS_H */")


if __name__ == "__main__":
main()
Loading
Loading