From 514427721e1fedcd65e74e4721e1eee9840ec890 Mon Sep 17 00:00:00 2001 From: Yi-Hsuan Deng Date: Fri, 21 Feb 2025 02:08:51 +0000 Subject: [PATCH] [sw] Change cert template engine to pre-generated method. The current certificate builder library consumes ~7KB. Additionally, the immutable ROM extension duplicates the ASN.1/CBOR library, adding another ~1.5KB. Given the proximity to the 64KB size limit, this change aims to provide a more lightweight certificate construction approach. Change-Id: I11a10ca1c833d238cae67ef30daf9ffcac40e9ae Signed-off-by: Yi-Hsuan Deng --- rules/certificates.bzl | 15 +- sw/device/silicon_creator/lib/cert/BUILD | 11 + .../silicon_creator/lib/cert/cdi_0.hjson | 23 +- .../silicon_creator/lib/cert/cdi_1.hjson | 25 +- sw/device/silicon_creator/lib/cert/cert.c | 4 +- sw/device/silicon_creator/lib/cert/dice.c | 122 ++- sw/device/silicon_creator/lib/cert/template.c | 50 ++ sw/device/silicon_creator/lib/cert/template.h | 238 ++++++ sw/device/silicon_creator/lib/cert/tpm.c | 15 +- .../silicon_creator/lib/cert/tpm_ek.hjson | 22 +- sw/device/silicon_creator/lib/cert/uds.hjson | 22 +- sw/host/ot_certs/README.md | 26 + sw/host/ot_certs/src/asn1/codegen.rs | 770 +++++++++++------- sw/host/ot_certs/src/asn1/der.rs | 27 +- sw/host/ot_certs/src/codegen.rs | 402 +++++---- sw/host/ot_certs/src/template/mod.rs | 185 ++++- sw/host/ot_certs/src/template/subst.rs | 123 ++- sw/host/ot_certs/src/template/testgen.rs | 69 +- sw/host/ot_certs/tests/example_data.json | 4 +- sw/host/ot_certs/tests/generic.hjson | 49 +- 20 files changed, 1561 insertions(+), 641 deletions(-) create mode 100644 sw/device/silicon_creator/lib/cert/template.c create mode 100644 sw/device/silicon_creator/lib/cert/template.h diff --git a/rules/certificates.bzl b/rules/certificates.bzl index 6658892dcad84..c1d1c9b7dc53b 100644 --- a/rules/certificates.bzl +++ b/rules/certificates.bzl @@ -117,14 +117,21 @@ def certificate_template(name, template, cert_format = "x509"): output_group = "unittest", ) + if cert_format == "x509": + runtime_deps = [ + "@//sw/device/silicon_creator/lib/cert:asn1", + "@//sw/device/silicon_creator/lib/cert:template", + ] + else: + runtime_deps = [ + "@//sw/device/silicon_creator/lib/cert:cbor", + ] + native.cc_library( name = "{}_library".format(name), srcs = [":{}_srcs".format(name)], hdrs = [":{}_hdrs".format(name)], - deps = [ - "@//sw/device/silicon_creator/lib/cert:asn1", - "@//sw/device/silicon_creator/lib/cert:cbor", - ], + deps = runtime_deps, ) native.cc_test( diff --git a/sw/device/silicon_creator/lib/cert/BUILD b/sw/device/silicon_creator/lib/cert/BUILD index d6ba8a5bec347..38766c8a8ead7 100644 --- a/sw/device/silicon_creator/lib/cert/BUILD +++ b/sw/device/silicon_creator/lib/cert/BUILD @@ -83,6 +83,17 @@ cc_test( ], ) +cc_library( + name = "template", + srcs = ["template.c"], + hdrs = ["template.h"], + deps = [ + "//sw/device/lib/base:macros", + "//sw/device/lib/base:memory", + "//sw/device/lib/base:status", + ], +) + cc_library( name = "cert", srcs = ["cert.c"], diff --git a/sw/device/silicon_creator/lib/cert/cdi_0.hjson b/sw/device/silicon_creator/lib/cert/cdi_0.hjson index d9aee5d03fa8b..3e9ddd9400960 100644 --- a/sw/device/silicon_creator/lib/cert/cdi_0.hjson +++ b/sw/device/silicon_creator/lib/cert/cdi_0.hjson @@ -10,43 +10,46 @@ // (x and y) which are 32-bytes integers. owner_intermediate_pub_key_ec_x: { type: "integer", - size: 32, + exact-size: 32, }, owner_intermediate_pub_key_ec_y: { type: "integer", - size: 32, + exact-size: 32, }, // Owner intermediate public key ID: this is a 20-byte hash // derived from the owner intermediate public key. owner_intermediate_pub_key_id: { type: "byte-array", - size: 20, + exact-size: 20, + tweak-msb: true, }, // This is the identifier of the public key used to // sign this certificate (creator key). creator_pub_key_id: { type: "byte-array", - size: 20, + exact-size: 20, + tweak-msb: true, }, // Hash of the ROM_EXT (SHA256). rom_ext_hash: { type: "byte-array", - size: 32, + exact-size: 32, }, // ROM_EXT security version, used to prevent rollback. rom_ext_security_version: { - type: "integer", - size: 4, + type: "byte-array", + exact-size: 4, + tweak-msb: true, } // Certificate signature: the result of signing with ECDSA // are two integers named "r" and "s" cert_signature_r: { type: "integer", - size: 32, + range-size: [24, 32], }, cert_signature_s: { type: "integer", - size: 32, + range-size: [24, 32], }, }, @@ -85,7 +88,7 @@ type: "dice_tcb_info", vendor: "OpenTitan", model: "ROM_EXT", - svn: { var: "rom_ext_security_version" }, + svn: { var: "rom_ext_security_version", convert: "big-endian" }, layer: 1, fw_ids: [ { hash_algorithm: "sha256", digest: { var: "rom_ext_hash" } }, diff --git a/sw/device/silicon_creator/lib/cert/cdi_1.hjson b/sw/device/silicon_creator/lib/cert/cdi_1.hjson index 7c0a196ee9696..d0f4dacca319d 100644 --- a/sw/device/silicon_creator/lib/cert/cdi_1.hjson +++ b/sw/device/silicon_creator/lib/cert/cdi_1.hjson @@ -10,48 +10,51 @@ // (x and y) which are 32-bytes integers. owner_pub_key_ec_x: { type: "integer", - size: 32, + exact-size: 32, }, owner_pub_key_ec_y: { type: "integer", - size: 32, + exact-size: 32, }, // Owner public key ID: this is a 20-byte hash // derived from the owner public key. owner_pub_key_id: { type: "byte-array", - size: 20, + exact-size: 20, + tweak-msb: true, }, // This is the identifier of the public key used to // sign this certificate (owner intermediate key). owner_intermediate_pub_key_id: { type: "byte-array", - size: 20, + exact-size: 20, + tweak-msb: true, }, // Hash of the owner stage firmware (SHA256). owner_hash: { type: "byte-array", - size: 32, + exact-size: 32, }, // Hash of the owner manifest (SHA256). owner_manifest_hash: { type: "byte-array", - size: 32, + exact-size: 32, }, // Owner security version, used to prevent rollback. owner_security_version: { - type: "integer", - size: 4, + type: "byte-array", + exact-size: 4, + tweak-msb: true, } // Certificate signature: the result of signing with ECDSA // are two integers named "r" and "s" cert_signature_r: { type: "integer", - size: 32, + range-size: [24, 32], }, cert_signature_s: { type: "integer", - size: 32, + range-size: [24, 32], }, }, @@ -90,7 +93,7 @@ type: "dice_tcb_info", vendor: "OpenTitan", model: "Owner", - svn: { var: "owner_security_version" }, + svn: { var: "owner_security_version", convert: "big-endian" }, layer: 2, fw_ids: [ { hash_algorithm: "sha256", digest: { var: "owner_hash" } }, diff --git a/sw/device/silicon_creator/lib/cert/cert.c b/sw/device/silicon_creator/lib/cert/cert.c index 3e9d99eae3464..1c349b72db5bd 100644 --- a/sw/device/silicon_creator/lib/cert/cert.c +++ b/sw/device/silicon_creator/lib/cert/cert.c @@ -102,8 +102,10 @@ rom_error_t cert_x509_asn1_check_serial_number(const uint8_t *cert_page_buffer, &cert_page_buffer[sn_bytes_offset], asn1_integer_length); // Check the serial number in the certificate matches what was expected. + // The first byte is skipped since the MSb might be modified for achieving + // fixed-length certificate. *matches = kHardenedBoolFalse; - for (size_t i = 0; i < kCertX509Asn1SerialNumberSizeInBytes; ++i) { + for (size_t i = 1; i < kCertX509Asn1SerialNumberSizeInBytes; ++i) { if (launder32(actual_serial_number[i]) != expected_sn_bytes[i]) { HARDENED_CHECK_NE(actual_serial_number[i], expected_sn_bytes[i]); return kErrorOk; diff --git a/sw/device/silicon_creator/lib/cert/dice.c b/sw/device/silicon_creator/lib/cert/dice.c index ed0c8ac247f1f..2867bd5182a9a 100644 --- a/sw/device/silicon_creator/lib/cert/dice.c +++ b/sw/device/silicon_creator/lib/cert/dice.c @@ -26,12 +26,14 @@ static ecdsa_p256_signature_t curr_tbs_signature = {.r = {0}, .s = {0}}; static uint8_t cdi_0_tbs_buffer[kCdi0MaxTbsSizeBytes]; static cdi_0_sig_values_t cdi_0_cert_params = { .tbs = cdi_0_tbs_buffer, - .tbs_size = kCdi0MaxTbsSizeBytes, + .cert_signature_r = (unsigned char *)curr_tbs_signature.r, + .cert_signature_s = (unsigned char *)curr_tbs_signature.s, }; static uint8_t cdi_1_tbs_buffer[kCdi1MaxTbsSizeBytes]; static cdi_1_sig_values_t cdi_1_cert_params = { .tbs = cdi_1_tbs_buffer, - .tbs_size = kCdi1MaxTbsSizeBytes, + .cert_signature_r = (unsigned char *)curr_tbs_signature.r, + .cert_signature_s = (unsigned char *)curr_tbs_signature.s, }; const dice_cert_format_t kDiceCertFormat = kDiceCertFormatX509TcbInfo; @@ -40,6 +42,15 @@ static_assert(kDiceMeasurementSizeInBytes == 32, "The DICE attestation measurement size should equal the size of " "the keymgr binding registers."); +/** + * Helpers for writing static assertions on variable array sizes. + */ +#define ASSERT_SIZE_EQ(actual, expected) \ + static_assert(actual == expected, "Invalid variable size."); + +#define ASSERT_SIZE_GE(actual, expected) \ + static_assert(actual >= expected, "Invalid variable size."); + /** * Returns true if debug (JTAG) access is exposed in the current LC state. */ @@ -58,29 +69,38 @@ rom_error_t dice_uds_tbs_cert_build( hmac_digest_t *otp_rot_creator_auth_state_measurement, cert_key_id_pair_t *key_ids, ecdsa_p256_public_key_t *uds_pubkey, uint8_t *tbs_cert, size_t *tbs_cert_size) { + ASSERT_SIZE_EQ(sizeof(otp_creator_sw_cfg_measurement->digest), + kUdsExactOtpCreatorSwCfgHashSizeBytes); + ASSERT_SIZE_EQ(sizeof(otp_owner_sw_cfg_measurement->digest), + kUdsExactOtpOwnerSwCfgHashSizeBytes); + ASSERT_SIZE_EQ(sizeof(otp_rot_creator_auth_codesign_measurement->digest), + kUdsExactOtpRotCreatorAuthCodesignHashSizeBytes); + ASSERT_SIZE_EQ(sizeof(otp_rot_creator_auth_state_measurement->digest), + kUdsExactOtpRotCreatorAuthStateHashSizeBytes); + ASSERT_SIZE_GE(sizeof(key_ids->cert->digest), + kUdsExactCreatorPubKeyIdSizeBytes); + ASSERT_SIZE_EQ(kCertKeyIdSizeInBytes, kUdsExactCreatorPubKeyIdSizeBytes); + ASSERT_SIZE_GE(sizeof(key_ids->endorsement->digest), + kUdsExactAuthKeyKeyIdSizeBytes); + ASSERT_SIZE_EQ(kCertKeyIdSizeInBytes, kUdsExactAuthKeyKeyIdSizeBytes); + ASSERT_SIZE_EQ(sizeof(uds_pubkey->x), kUdsExactCreatorPubKeyEcXSizeBytes); + ASSERT_SIZE_EQ(sizeof(uds_pubkey->y), kUdsExactCreatorPubKeyEcYSizeBytes); + // Generate the TBS certificate. uds_tbs_values_t uds_cert_tbs_params = { .otp_creator_sw_cfg_hash = (unsigned char *)otp_creator_sw_cfg_measurement->digest, - .otp_creator_sw_cfg_hash_size = kHmacDigestNumBytes, .otp_owner_sw_cfg_hash = (unsigned char *)otp_owner_sw_cfg_measurement->digest, - .otp_owner_sw_cfg_hash_size = kHmacDigestNumBytes, .otp_rot_creator_auth_codesign_hash = (unsigned char *)otp_rot_creator_auth_codesign_measurement->digest, - .otp_rot_creator_auth_codesign_hash_size = kHmacDigestNumBytes, .otp_rot_creator_auth_state_hash = (unsigned char *)otp_rot_creator_auth_state_measurement->digest, - .otp_rot_creator_auth_state_hash_size = kHmacDigestNumBytes, .debug_flag = is_debug_exposed(), .creator_pub_key_id = (unsigned char *)key_ids->cert->digest, - .creator_pub_key_id_size = kCertKeyIdSizeInBytes, .auth_key_key_id = (unsigned char *)key_ids->endorsement->digest, - .auth_key_key_id_size = kCertKeyIdSizeInBytes, .creator_pub_key_ec_x = (unsigned char *)uds_pubkey->x, - .creator_pub_key_ec_x_size = kEcdsaP256PublicKeyCoordBytes, .creator_pub_key_ec_y = (unsigned char *)uds_pubkey->y, - .creator_pub_key_ec_y_size = kEcdsaP256PublicKeyCoordBytes, }; HARDENED_RETURN_IF_ERROR( uds_build_tbs(&uds_cert_tbs_params, tbs_cert, tbs_cert_size)); @@ -93,36 +113,50 @@ rom_error_t dice_cdi_0_cert_build(hmac_digest_t *rom_ext_measurement, cert_key_id_pair_t *key_ids, ecdsa_p256_public_key_t *cdi_0_pubkey, uint8_t *cert, size_t *cert_size) { + ASSERT_SIZE_EQ(sizeof(rom_ext_measurement->digest), + kCdi0ExactRomExtHashSizeBytes); + ASSERT_SIZE_GE(sizeof(key_ids->cert->digest), + kCdi0ExactOwnerIntermediatePubKeyIdSizeBytes); + ASSERT_SIZE_EQ(kCertKeyIdSizeInBytes, + kCdi0ExactOwnerIntermediatePubKeyIdSizeBytes); + ASSERT_SIZE_GE(sizeof(key_ids->endorsement->digest), + kCdi0ExactOwnerIntermediatePubKeyIdSizeBytes); + ASSERT_SIZE_EQ(kCertKeyIdSizeInBytes, kCdi0ExactCreatorPubKeyIdSizeBytes); + ASSERT_SIZE_EQ(sizeof(cdi_0_pubkey->x), + kCdi0ExactOwnerIntermediatePubKeyEcXSizeBytes); + ASSERT_SIZE_EQ(sizeof(cdi_0_pubkey->y), + kCdi0ExactOwnerIntermediatePubKeyEcYSizeBytes); + + uint32_t rom_ext_security_version_be = + __builtin_bswap32(rom_ext_security_version); + // Generate the TBS certificate. cdi_0_tbs_values_t cdi_0_cert_tbs_params = { .rom_ext_hash = (unsigned char *)rom_ext_measurement->digest, - .rom_ext_hash_size = kDiceMeasurementSizeInBytes, - .rom_ext_security_version = rom_ext_security_version, + .rom_ext_security_version = (unsigned char *)&rom_ext_security_version_be, .owner_intermediate_pub_key_id = (unsigned char *)key_ids->cert->digest, - .owner_intermediate_pub_key_id_size = kCertKeyIdSizeInBytes, .creator_pub_key_id = (unsigned char *)key_ids->endorsement->digest, - .creator_pub_key_id_size = kCertKeyIdSizeInBytes, .owner_intermediate_pub_key_ec_x = (unsigned char *)cdi_0_pubkey->x, - .owner_intermediate_pub_key_ec_x_size = kEcdsaP256PublicKeyCoordBytes, .owner_intermediate_pub_key_ec_y = (unsigned char *)cdi_0_pubkey->y, - .owner_intermediate_pub_key_ec_y_size = kEcdsaP256PublicKeyCoordBytes, }; + + size_t tbs_size = kCdi0MaxTbsSizeBytes; HARDENED_RETURN_IF_ERROR(cdi_0_build_tbs(&cdi_0_cert_tbs_params, - cdi_0_cert_params.tbs, - &cdi_0_cert_params.tbs_size)); + cdi_0_cert_params.tbs, &tbs_size)); // Sign the TBS and generate the certificate. hmac_digest_t tbs_digest; - hmac_sha256(cdi_0_cert_params.tbs, cdi_0_cert_params.tbs_size, &tbs_digest); + hmac_sha256(cdi_0_cert_params.tbs, tbs_size, &tbs_digest); HARDENED_RETURN_IF_ERROR( otbn_boot_attestation_endorse(&tbs_digest, &curr_tbs_signature)); util_p256_signature_le_to_be_convert(curr_tbs_signature.r, curr_tbs_signature.s); - cdi_0_cert_params.cert_signature_r = (unsigned char *)curr_tbs_signature.r; - cdi_0_cert_params.cert_signature_r_size = kAttestationSignatureBytes / 2; - cdi_0_cert_params.cert_signature_s = (unsigned char *)curr_tbs_signature.s; - cdi_0_cert_params.cert_signature_s_size = kAttestationSignatureBytes / 2; + ASSERT_SIZE_EQ(sizeof(curr_tbs_signature.r), + kCdi0ExactCertSignatureRSizeBytes); + ASSERT_SIZE_EQ(sizeof(curr_tbs_signature.s), + kCdi0ExactCertSignatureSSizeBytes); + HARDENED_RETURN_IF_ERROR( cdi_0_build_cert(&cdi_0_cert_params, cert, cert_size)); @@ -140,39 +174,53 @@ rom_error_t dice_cdi_1_cert_build(hmac_digest_t *owner_measurement, cert_key_id_pair_t *key_ids, ecdsa_p256_public_key_t *cdi_1_pubkey, uint8_t *cert, size_t *cert_size) { + ASSERT_SIZE_EQ(sizeof(owner_measurement->digest), + kCdi1ExactOwnerHashSizeBytes); + ASSERT_SIZE_EQ(sizeof(owner_manifest_measurement->digest), + kCdi1ExactOwnerManifestHashSizeBytes); + ASSERT_SIZE_GE(sizeof(key_ids->cert->digest), + kCdi1ExactOwnerPubKeyIdSizeBytes); + ASSERT_SIZE_EQ(kCertKeyIdSizeInBytes, kCdi1ExactOwnerPubKeyIdSizeBytes); + ASSERT_SIZE_GE(sizeof(key_ids->endorsement->digest), + kCdi1ExactOwnerIntermediatePubKeyIdSizeBytes); + ASSERT_SIZE_EQ(kCertKeyIdSizeInBytes, + kCdi1ExactOwnerIntermediatePubKeyIdSizeBytes); + ASSERT_SIZE_EQ(sizeof(cdi_1_pubkey->x), kCdi1ExactOwnerPubKeyEcXSizeBytes); + ASSERT_SIZE_EQ(sizeof(cdi_1_pubkey->y), kCdi1ExactOwnerPubKeyEcYSizeBytes); + + uint32_t owner_security_version_be = + __builtin_bswap32(owner_security_version); + // Generate the TBS certificate. cdi_1_tbs_values_t cdi_1_cert_tbs_params = { .owner_hash = (unsigned char *)owner_measurement->digest, - .owner_hash_size = kDiceMeasurementSizeInBytes, .owner_manifest_hash = (unsigned char *)owner_manifest_measurement->digest, - .owner_manifest_hash_size = kDiceMeasurementSizeInBytes, - .owner_security_version = owner_security_version, + .owner_security_version = (unsigned char *)&owner_security_version_be, .owner_pub_key_id = (unsigned char *)key_ids->cert->digest, - .owner_pub_key_id_size = kCertKeyIdSizeInBytes, .owner_intermediate_pub_key_id = (unsigned char *)key_ids->endorsement->digest, - .owner_intermediate_pub_key_id_size = kCertKeyIdSizeInBytes, .owner_pub_key_ec_x = (unsigned char *)cdi_1_pubkey->x, - .owner_pub_key_ec_x_size = kEcdsaP256PublicKeyCoordBytes, .owner_pub_key_ec_y = (unsigned char *)cdi_1_pubkey->y, - .owner_pub_key_ec_y_size = kEcdsaP256PublicKeyCoordBytes, }; + + size_t tbs_size = kCdi1MaxTbsSizeBytes; HARDENED_RETURN_IF_ERROR(cdi_1_build_tbs(&cdi_1_cert_tbs_params, - cdi_1_cert_params.tbs, - &cdi_1_cert_params.tbs_size)); + cdi_1_cert_params.tbs, &tbs_size)); // Sign the TBS and generate the certificate. hmac_digest_t tbs_digest; - hmac_sha256(cdi_1_cert_params.tbs, cdi_1_cert_params.tbs_size, &tbs_digest); + hmac_sha256(cdi_1_cert_params.tbs, tbs_size, &tbs_digest); HARDENED_RETURN_IF_ERROR( otbn_boot_attestation_endorse(&tbs_digest, &curr_tbs_signature)); util_p256_signature_le_to_be_convert(curr_tbs_signature.r, curr_tbs_signature.s); - cdi_1_cert_params.cert_signature_r = (unsigned char *)curr_tbs_signature.r; - cdi_1_cert_params.cert_signature_r_size = kAttestationSignatureBytes / 2; - cdi_1_cert_params.cert_signature_s = (unsigned char *)curr_tbs_signature.s; - cdi_1_cert_params.cert_signature_s_size = kAttestationSignatureBytes / 2; + + ASSERT_SIZE_EQ(sizeof(curr_tbs_signature.r), + kCdi0ExactCertSignatureRSizeBytes); + ASSERT_SIZE_EQ(sizeof(curr_tbs_signature.s), + kCdi0ExactCertSignatureSSizeBytes); + HARDENED_RETURN_IF_ERROR( cdi_1_build_cert(&cdi_1_cert_params, cert, cert_size)); diff --git a/sw/device/silicon_creator/lib/cert/template.c b/sw/device/silicon_creator/lib/cert/template.c new file mode 100644 index 0000000000000..dc7aedddd9248 --- /dev/null +++ b/sw/device/silicon_creator/lib/cert/template.c @@ -0,0 +1,50 @@ +// Copyright lowRISC contributors (OpenTitan project). +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#include "sw/device/silicon_creator/lib/cert/template.h" + +#include + +static const char kLowercaseHexChars[16] = {'0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'a', 'b', + 'c', 'd', 'e', 'f'}; + +uint8_t *template_push_hex_impl(uint8_t *out, const uint8_t *bytes, + size_t size) { + while (size > 0) { + *out++ = (uint8_t)kLowercaseHexChars[bytes[0] >> 4]; + *out++ = (uint8_t)kLowercaseHexChars[bytes[0] & 0xf]; + bytes++; + size--; + } + return out; +} + +uint8_t *template_asn1_integer_impl(uint8_t *out, uint8_t tag, bool tweak_msb, + const uint8_t *bytes_be, size_t size) { + *out++ = tag; + uint8_t *size_ptr = out++; + if (tweak_msb) { + *out++ = 0; // additional zero prefix; + } else { + while (size > 0 && *bytes_be == 0) { + ++bytes_be; + --size; + } + if (size == 0 || *bytes_be > 0x7f) { + *out++ = 0; + } + } + memcpy(out, bytes_be, size); + if (tweak_msb) + out[0] |= 0x80; + out += size; + *size_ptr = (uint8_t)(out - size_ptr) - 1; + return out; +} + +void template_patch_size_be_impl(template_pos_t memo, uint8_t *out_end) { + *(uint16_t *)memo = __builtin_bswap16(__builtin_bswap16(*(uint16_t *)memo) + + (uint16_t)(out_end - (uint8_t *)memo)); +} diff --git a/sw/device/silicon_creator/lib/cert/template.h b/sw/device/silicon_creator/lib/cert/template.h new file mode 100644 index 0000000000000..b8b1cb06e502d --- /dev/null +++ b/sw/device/silicon_creator/lib/cert/template.h @@ -0,0 +1,238 @@ +// Copyright lowRISC contributors (OpenTitan project). +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#ifndef OPENTITAN_SW_DEVICE_SILICON_CREATOR_LIB_CERT_TEMPLATE_H_ +#define OPENTITAN_SW_DEVICE_SILICON_CREATOR_LIB_CERT_TEMPLATE_H_ + +#include "sw/device/lib/base/macros.h" +#include "sw/device/lib/base/memory.h" +#include "sw/device/silicon_creator/lib/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Structure holding the state of the template engine. + * + * The fields in this structure should be considered + * private and not be read or written directly. + * + * The following diagram shows the pointers location during encoding. + * Takes Cdi0 TBS as as example: + * + * Template Const Bytes: + * `kTemplateConstBytes[]` `const_end` `+ sizeof(kTemplateConstBytes)` + * | Bytes Copied to Output | Unconsumed Template Const Bytes | + * + * `const_end` pointer will start from offset zero, and increase to + * `sizeof(kTemplateConstBytes)` at the end of encoding. + * + * Output Buffer: + * `out_begin` `out_end` `+ kCdi0MaxTbsSizeBytes` + * | Used Bytes | Remaining Unused Space | + * + * `out_begin` pointer will not change. + * `out_end` pointer starts from `out_begin` to the actual output size. + * The actual size is between [kCdi0MinTbsSizeBytes, kCdi0MaxTbsSizeBytes]. + */ +typedef struct template_state { + // Points to the remaining pre-computed const array from codegen. + const uint8_t *const_end; + // Points to the original start of the output buffer. + const uint8_t *out_begin; + // Points to the end of bytes already outputted. + uint8_t *out_end; +} template_state_t; + +/** + * Pointer to a specific output location. + */ +typedef void *template_pos_t; + +/** + * Initialize the template engine. + * + * @param state Pointer to the template engine state. + * @param out_buf Pointer to a user-provided output buffer. + * @param const_bytes Pointer to a pre-generated template bytes. + */ +static inline void template_init(template_state_t *state, uint8_t *out_buf, + const uint8_t *const_bytes) { + state->out_begin = state->out_end = out_buf; + state->const_end = const_bytes; +} + +/** + * Finalize template engine and set the actual output size. + * + * @param state Pointer to the template engine state. + * @return The generated size in bytes. + */ +static inline uint16_t template_finalize(template_state_t *state) { + return (uint16_t)(state->out_end - state->out_begin); +} + +/** + * Set the `value` to the `bit_offset` bit of previous `byte_offset` byte. + * + * This function assume the corresponding bit is zero before calling, which + * is ensured by the template code generator. + * + * @param state Pointer to the template engine state. + * @param byte_offset Number of bytes before the last output bytes. + * @param bit_offset Index of the bit to be set. + * @param value Set the bit to one when true. + */ +static inline void template_set_bit(template_state_t *state, int byte_offset, + int bit_offset, bool value) { + *(state->out_end - byte_offset) |= ((uint8_t) !!value) << bit_offset; +} + +/** + * Output a ASN1 DER boolean. + * + * @param state Pointer to the template engine state. + * @param val Value to be output. + */ +static inline void template_push_asn1_bool(template_state_t *state, bool val) { + *state->out_end = val ? 0xff : 0x00; + state->out_end += 1; +} + +/** + * Private implementation of `template_push_hex`. + * + * @param out Pointer to the output buffer. + * @param inp Pointer to a byte array. + * @param size Number of the bytes in the array. + * @return the new end of the output buffer. + */ +uint8_t *template_push_hex_impl(uint8_t *out, const uint8_t *inp, size_t size); + +/** + * Output the buffer as a hex-encoded string. + * + * @param state Pointer to the template engine state. + * @param buf Pointer to a byte array. + * @param size Number of the bytes in the array. + */ +static inline void template_push_hex(template_state_t *state, + const uint8_t *buf, size_t size) { + state->out_end = template_push_hex_impl(state->out_end, buf, size); +} + +/** + * Output the buffer as raw bytes. + * + * @param state Pointer to the template engine state. + * @param buf Pointer to a byte array. + * @param size Number of the bytes in the array. + */ +static inline void template_push_bytes(template_state_t *state, + const uint8_t *buf, size_t size) { + memcpy(state->out_end, buf, size); + state->out_end += size; +} + +/** + * Output `size` bytes from the pre-generated template. + * + * @param state Pointer to the template engine state. + * @param size Number of the bytes to output. + */ +static inline void template_push_const(template_state_t *state, size_t size) { + memcpy(state->out_end, state->const_end, size); + state->out_end += size; + state->const_end += size; +} + +/** + * Private implementation of `template_asn1_integer`. + * + * @param out Pointer to the output buffer. + * @param tag Identifier octet of the tag. + * @param tweak_msb Set the MSB before encoding when true. + * @param bytes_be Pointer to a byte array holding an integer in big-endian + * format. + * @param size Size of the `bytes_be` array in bytes. + * @return the new end of the output buffer. + */ +uint8_t *template_asn1_integer_impl(uint8_t *out, uint8_t tag, bool tweak_msb, + const uint8_t *bytes_be, size_t size); + +/** + * Output a tagged integer. + * + * This function allows the caller to set the tag to a non-standard value which + * can be useful for IMPLICIT integers. Use ASN1_TAG_INTEGER for standard + * integers. + * + * @param state Pointer to the template engine state. + * @param tag Identifier octet of the tag. + * @param tweak_msb Set the MSB before encoding when true. + * @param bytes_be Pointer to a byte array holding an integer in big-endian + * format. + * @param size Size of the `bytes_be` array in bytes. + */ +static inline void template_asn1_integer(template_state_t *state, uint8_t tag, + bool tweak_msb, + const uint8_t *bytes_be, size_t size) { + state->out_end = template_asn1_integer_impl(state->out_end, tag, tweak_msb, + bytes_be, size); +} + +/** + * U32 version of the `template_asn1_integer`. + * + * @param state Pointer to the template engine state. + * @param tag Identifier octet of the tag. + * @param tweak_msb Set the MSB before encoding when true. + * @param value Integer value. + */ +static inline void template_asn1_uint32(template_state_t *state, uint8_t tag, + bool tweak_msb, uint32_t value) { + uint32_t _value = __builtin_bswap32(value); + state->out_end = template_asn1_integer_impl(state->out_end, tag, tweak_msb, + (uint8_t *)&_value, 4); +} + +/** + * Memorize a location to be patched with the actual output size. + * + * The function will saved the address (last output byte + offset) for + * patching later, and it should be paired with a `template_patch_size_*`. + * + * @param state Pointer to the template engine state. + * @param offset Number of bytes after the last output byte. + * @return the memorized location for calling `template_patch_size_be`. + */ +static inline template_pos_t template_save_pos(template_state_t *state, + ptrdiff_t offset) { + return (template_pos_t *)(state->out_end + offset); +} + +/** + * Private implementation of `template_patch_size_be`. + */ +void template_patch_size_be_impl(template_pos_t memo, uint8_t *out_end); + +/** + * Add the actual output size after the memo to the patch location. + * + * The function will perform addition as BE u16 (i.e. mod 65536). + * + * @param state Pointer to the template engine state. + * @param memo The memorized location from `template_save_pos`. + */ +static inline void template_patch_size_be(template_state_t *state, + template_pos_t memo) { + template_patch_size_be_impl(memo, state->out_end); +} + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif // OPENTITAN_SW_DEVICE_SILICON_CREATOR_LIB_CERT_TEMPLATE_H_ diff --git a/sw/device/silicon_creator/lib/cert/tpm.c b/sw/device/silicon_creator/lib/cert/tpm.c index 3fed39e84c433..79ddf984d22f1 100644 --- a/sw/device/silicon_creator/lib/cert/tpm.c +++ b/sw/device/silicon_creator/lib/cert/tpm.c @@ -38,15 +38,22 @@ rom_error_t tpm_ek_tbs_cert_build(cert_key_id_pair_t *key_ids, ecdsa_p256_public_key_t *tpm_ek_pubkey, uint8_t *tpm_ek_tbs, size_t *tpm_ek_tbs_size) { + static_assert(kTpmEkExactAuthKeyKeyIdSizeBytes == kCertKeyIdSizeInBytes, + "Invalid variable size."); + static_assert( + kTpmEkExactTpmEkPubKeyEcXSizeBytes == kEcdsaP256PublicKeyCoordBytes, + "Invalid variable size."); + static_assert( + kTpmEkExactTpmEkPubKeyEcYSizeBytes == kEcdsaP256PublicKeyCoordBytes, + "Invalid variable size."); + static_assert(kTpmEkExactTpmEkPubKeyIdSizeBytes == kCertKeyIdSizeInBytes, + "Invalid variable size."); + tpm_ek_tbs_values_t tpm_ek_tbs_params = { .auth_key_key_id = (unsigned char *)key_ids->endorsement, - .auth_key_key_id_size = kCertKeyIdSizeInBytes, .tpm_ek_pub_key_ec_x = (unsigned char *)tpm_ek_pubkey->x, - .tpm_ek_pub_key_ec_x_size = kEcdsaP256PublicKeyCoordBytes, .tpm_ek_pub_key_ec_y = (unsigned char *)tpm_ek_pubkey->y, - .tpm_ek_pub_key_ec_y_size = kEcdsaP256PublicKeyCoordBytes, .tpm_ek_pub_key_id = (unsigned char *)key_ids->cert, - .tpm_ek_pub_key_id_size = kCertKeyIdSizeInBytes, .tpm_version = "0.0.1", .tpm_version_len = 5, .tpm_vendor = "Nuvoton", diff --git a/sw/device/silicon_creator/lib/cert/tpm_ek.hjson b/sw/device/silicon_creator/lib/cert/tpm_ek.hjson index 5f5cfd9196675..6866a5ae96f4e 100644 --- a/sw/device/silicon_creator/lib/cert/tpm_ek.hjson +++ b/sw/device/silicon_creator/lib/cert/tpm_ek.hjson @@ -8,48 +8,52 @@ // EK certificate public key: this is an EC point on // the prime256v1 curve so it has two components // (x and y) which are 32-bytes integers. + // Also, 32-byte unsigned integers may be encoded to + // 33-byte in ASN.1. tpm_ek_pub_key_ec_x: { type: "integer", - size: 32, + exact-size: 32, }, tpm_ek_pub_key_ec_y: { type: "integer", - size: 32, + exact-size: 32, }, // Intermediate CA public key ID: this is a 20-byte hash // derived from the creator public key. tpm_ek_pub_key_id: { type: "byte-array", - size: 20, + exact-size: 20, + tweak-msb: true, }, // Authority key ID: this is the identifier of // the key that will be used to sign this certificate // offline. auth_key_key_id: { type: "byte-array", - size: 20, + exact-size: 20, + tweak-msb: true, }, // Certificate signature: the result of signing with ECDSA // are two integers named "r" and "s" cert_signature_r: { type: "integer", - size: 32, + range-size: [24, 32], }, cert_signature_s: { type: "integer", - size: 32, + range-size: [24, 32], }, tpm_vendor: { type: "string", - size: 100, + range-size: [0, 19], }, tpm_model: { type: "string", - size: 100, + range-size: [0, 19], }, tpm_version: { type: "string", - size: 100, + range-size: [0, 19], }, }, diff --git a/sw/device/silicon_creator/lib/cert/uds.hjson b/sw/device/silicon_creator/lib/cert/uds.hjson index 238a672f1b4ce..e6485c427a582 100644 --- a/sw/device/silicon_creator/lib/cert/uds.hjson +++ b/sw/device/silicon_creator/lib/cert/uds.hjson @@ -10,44 +10,46 @@ // (x and y) which are 32-bytes integers. creator_pub_key_ec_x: { type: "integer", - size: 32, + exact-size: 32, }, creator_pub_key_ec_y: { type: "integer", - size: 32, + exact-size: 32, }, // Creator public key ID: this is a 20-byte hash // derived from the creator public key. creator_pub_key_id: { type: "byte-array", - size: 20, + exact-size: 20, + tweak-msb: true, }, // Authority key ID: this is the identifier of // the key that will be used to sign this certificate // offline. auth_key_key_id: { type: "byte-array", - size: 20, + exact-size: 20, + tweak-msb: true, }, // Hash of the creator_sw_cfg OTP partition (SHA256). otp_creator_sw_cfg_hash: { type: "byte-array", - size: 32, + exact-size: 32, }, // Hash of the owner_sw_cfg OTP partition (SHA256). otp_owner_sw_cfg_hash: { type: "byte-array", - size: 32, + exact-size: 32, }, // Hash of the rot_creator_auth_codesign OTP partition (SHA256). otp_rot_creator_auth_codesign_hash: { type: "byte-array", - size: 32, + exact-size: 32, }, // Hash of the rot_creator_auth_state OTP partition (SHA256). otp_rot_creator_auth_state_hash: { type: "byte-array", - size: 32, + exact-size: 32, }, // Debug (whether LC state exposes JTAG access or not). debug_flag: { @@ -57,11 +59,11 @@ // are two integers named "r" and "s" cert_signature_r: { type: "integer", - size: 32, + range-size: [24, 32], }, cert_signature_s: { type: "integer", - size: 32, + range-size: [24, 32], } }, diff --git a/sw/host/ot_certs/README.md b/sw/host/ot_certs/README.md index 33f8fed93a3c6..c585b36eb0d16 100644 --- a/sw/host/ot_certs/README.md +++ b/sw/host/ot_certs/README.md @@ -12,3 +12,29 @@ This crate will generate code capable of running on the device to get/set these fields. More detailed documentation on OpenTitan certificates will be published soon. + + +### ASN1 Variable Typing + +The asn1 template supports four variable types: byte-array, string, integer, and +boolean. Array types (byte-array, string, and integer) require the size in bytes +of the value it represents to be specified. + +```javascript +type: "integer", // or "string" or "byte-array" +exact-size: 20, // or `range-size: [min, max]` if size could be variable +``` + +When the variable is an integer, the size is determined using its big-endian +array representation without any additional zero prefixes. + +The byte-array can also be encoded as an integer. This requires the `tweak-msb` +field to be set, and the builder will always set the MSb when encoding as an +integer to ensure a fixed size encoding. If MSb tweak is not possible, please +consider typing it as an integer instead. + +```javascript +type: "byte-array", +exact-size: 20, // or `range-size: [min, max]` if size could be variable +[tweak-msb: true,] // if destination could be an integer +``` diff --git a/sw/host/ot_certs/src/asn1/codegen.rs b/sw/host/ot_certs/src/asn1/codegen.rs index aa161be2cdeb3..5bdb6ca4183a8 100644 --- a/sw/host/ot_certs/src/asn1/codegen.rs +++ b/sw/host/ot_certs/src/asn1/codegen.rs @@ -3,50 +3,15 @@ // SPDX-License-Identifier: Apache-2.0 use anyhow::{bail, ensure, Result}; -use heck::{ToSnakeCase, ToUpperCamelCase}; -use indexmap::IndexMap; +use itertools::Itertools; use num_bigint_dig::BigUint; +use std::cmp::max; use crate::asn1::builder::Builder; +use crate::asn1::der::Der; use crate::asn1::{Oid, Tag}; use crate::template::{Conversion, Value, Variable, VariableType}; -struct ConstantEntry { - var_name: String, - c_decl: String, -} - -/// Constant pool for code generation. -#[derive(Default)] -pub struct ConstantPool { - constants: IndexMap, ConstantEntry>, -} - -impl ConstantPool { - pub fn new() -> ConstantPool { - ConstantPool { - constants: IndexMap::new(), - } - } - - pub fn codestring(&self) -> String { - self.constants - .values() - .map(|x| x.c_decl.clone()) - .collect::>() - .join("") - } - - pub fn get_var_name(&self, data: &Vec) -> Option { - self.constants.get(data).map(|ent| ent.var_name.clone()) - } - - pub fn add_entry(&mut self, data: Vec, var_name: String, c_decl: String) { - self.constants - .insert(data, ConstantEntry { var_name, c_decl }); - } -} - /// Information about how to refer to a variable in the code. #[derive(Debug, Clone)] pub enum VariableCodegenInfo { @@ -58,7 +23,7 @@ pub enum VariableCodegenInfo { size_expr: String, }, /// Variable is an integer. - Int32 { + Uint32 { // Expression generating the value. value_expr: String, }, @@ -78,113 +43,262 @@ pub struct VariableInfo { pub codegen: VariableCodegenInfo, } +// Each CodegenStructure maps to a CodeOutput. +type CodeOutput = Vec; + +#[derive(Debug, Clone)] +struct CodeChunkWithComment { + code: CodeChunk, + comment: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum CodeChunk { + // A code snippet. + Code(String), + // If all the arguments are constants, derive its resulting bytes directly. + ConstBytes(Vec), + // Patch the last two bytes with length delta. + SizePatchMemo(i16), + // Create a new nested block with left curly brace. + OpenBlock, + // Close a nested block. It should be paired with OpenBlock. + CloseBlock, +} + +impl CodeChunk { + fn is_constant(&self) -> bool { + match self { + CodeChunk::Code(_) => false, + CodeChunk::ConstBytes(_) => true, + CodeChunk::SizePatchMemo(_) => true, + CodeChunk::OpenBlock => true, + CodeChunk::CloseBlock => true, + } + } + fn len(&self) -> usize { + match self { + CodeChunk::Code(_) => panic!("Variable has no size"), + CodeChunk::ConstBytes(x) => x.len(), + _ => 0, + } + } +} + /// ASN1 code generator. pub struct Codegen<'a> { /// Output buffer. - output: String, - /// Constant pool. - constants: &'a mut ConstantPool, - /// Indentation string. - indent: String, - /// Current indentation level. - indent_lvl: usize, + output: CodeOutput, /// Variable types: return information about a variable by name. variable_info: &'a dyn Fn(&str) -> Result, - /// Index of next tag (to guarantee unique names). - tag_idx: usize, + /// Minimum size of the output. + min_out_size: usize, /// Maximum size of the output. max_out_size: usize, } +/// ASN1 code generator output. +pub struct CodegenOutput { + /// Output code. + pub code: String, + /// Minimum size of the produced cert/TBS. + pub min_size: usize, + /// Maximum size of the produced cert/TBS. + pub max_size: usize, +} + impl Codegen<'_> { /// Generate code that corresponds to an ASN1 document described by a closure acting on a Builder. - /// Returns the generated code and the maximum possible size of the output. + /// Returns the generated code and the min/max possible size of the output. /// /// # Arguments /// /// * `buf_name` - Name of the variable holding the pointer to the output buffer. /// * `buf_size_name` - Name of the variable holding the size of the output buffer. /// This variable is updated by the code to hold the actual size of the data after production. - /// * `indent` - Identation string (one level). - /// * `indent_lvl` - Initial identation level. - /// * `constants` - Constant pool used to create necessary constants for the code. /// * `variables` - Description of the variable types used when producing the output. /// * `gen` - Closure generating the ASN1 document. pub fn generate( buf_name: &str, buf_size_name: &str, - indent: &str, - indent_lvl: usize, - constants: &mut ConstantPool, variable_info: &dyn Fn(&str) -> Result, gen: impl FnOnce(&mut Codegen) -> Result<()>, - ) -> Result<(String, usize)> { + ) -> Result { let mut builder = Codegen { - output: String::new(), - constants, - indent: indent.to_string(), - indent_lvl, + output: CodeOutput::new(), variable_info, - tag_idx: 0, + min_out_size: 0, max_out_size: 0, }; - // Create an ASN1 state, start with the provided buffer and carry on with - // building the document with closure. - builder.push_str_with_indent("asn1_state_t state;\n"); - builder.push_str_with_indent(&format!( - "RETURN_IF_ERROR(asn1_start(&state, {buf_name}, *{buf_size_name}));\n" - )); gen(&mut builder)?; - // Finish the document and update the size variable. - builder.push_str_with_indent(&format!( - "RETURN_IF_ERROR(asn1_finish(&state, {buf_size_name}));\n" - )); - Ok((builder.output, builder.max_out_size)) - } - /// Push indentation into the output buffer. - fn push_indent(&mut self) { - let indent = self.indent.to_string(); - for _ in 0..self.indent_lvl { - self.push_str(&indent); + let mut output = String::new(); + let mut const_bytes = Vec::::new(); + let mut size_patches = Vec::<(usize, i16)>::new(); + + // For neighboring const chunks, we collect them into a single array and write them at once. + for (is_code, chunk) in &builder + .output + .iter() + .chunk_by(|inst| !inst.code.is_constant()) + { + if !is_code { + let mut nbytes = 0; + + for CodeChunkWithComment { code, comment } in chunk { + nbytes += &code.len(); + match &code { + CodeChunk::OpenBlock => output.push_str("{\n"), + CodeChunk::CloseBlock => output.push_str("};\n"), + CodeChunk::SizePatchMemo(delta) => { + size_patches.push((const_bytes.len(), *delta)); + + // The bytes in (nbytes-2..nbytes) will be patched. + let patch_offset = nbytes - 2; + output.push_str(&format!( + " + /* Size patch with delta {delta} */ + template_pos_t memo = template_save_pos(&state, {patch_offset}); + " + )); + } + CodeChunk::ConstBytes(data) => { + // Add comment about the const in-place. + if let Some(comment) = &comment { + output.push_str(&format!("/* {comment} */\n")); + } + + // But collect the actual bytes to the shared pool. + if !data.is_empty() { + const_bytes.extend(data); + let data = data.iter().map(|ch| format!("0x{:02x}", ch)).join(", "); + output.push_str(&format!("/* {data} */\n")); + } + } + _ => unreachable!(), + } + } + + // Output the combined const segment. + if nbytes > 0 { + output.push_str(&format!( + "template_push_const(&state, /*nbytes=*/{nbytes});\n" + )); + } + } else { + /* is_code == true */ + for CodeChunkWithComment { code, comment } in chunk { + if let Some(comment) = comment { + output.push_str(&format!("/* {comment} */\n")); + } + match &code { + CodeChunk::Code(inner) => output.push_str(inner), + _ => unreachable!(), + } + } + } + } + + // Apply the size delta to the pregenerated const bytes in the reversed order. + for (idx, value) in size_patches.iter().rev() { + let patch_point = &mut const_bytes[idx - 2..*idx]; + let old_value = u16::from_be_bytes(patch_point.try_into().unwrap()); + let new_value = old_value.wrapping_add_signed(*value); + patch_point.copy_from_slice(&new_value.to_be_bytes()); } + + let const_bytes = const_bytes + .iter() + .map(|ch| format!("0x{:02x}", ch)) + .join(", "); + let max_size = builder.max_out_size; + output = format!( + " + if (*{buf_size_name} < {max_size}) {{ + return kErrorCertInvalidSize; + }} + + const static uint8_t kTemplateConstBytes[] = {{{const_bytes}}}; + template_state_t state; + + template_init(&state, {buf_name}, kTemplateConstBytes); + + {output} + + *{buf_size_name} = template_finalize(&state); + " + ); + + Ok(CodegenOutput { + code: output, + min_size: builder.min_out_size, + max_size: builder.max_out_size, + }) } - /// Push raw string into the output buffer. - fn push_str(&mut self, s: &str) { - self.output.push_str(s); + /// Push a chunk to the output stream. + fn push_chunk(&mut self, code: CodeChunk, comment: Option) { + self.output.push(CodeChunkWithComment { code, comment }); } - /// Push raw string with indentation into the output buffer. - fn push_str_with_indent(&mut self, s: &str) { - self.push_indent(); - self.push_str(s); + /// Start a new nested block in the output stream. + /// It should be paired with an `unindent()` call. + fn indent(&mut self) { + self.push_chunk(CodeChunk::OpenBlock, None); } - /// Register a constant byte array. - fn add_constant_byte_array(&mut self, name_hint: Option, data: &[u8]) -> String { - // If constant already exist, do not recreate it - if let Some(name) = self.constants.get_var_name(&data.to_vec()) { - return name; - } + /// Close one level of nested block. + /// It should be paired with an `indent()` call. + fn unindent(&mut self) { + self.push_chunk(CodeChunk::CloseBlock, None); + } - let const_name = format!( - "kConstant{}", - name_hint - .map(|x| x.to_upper_camel_case()) - .unwrap_or("".into()) - ); - let bytes = data - .iter() - .map(|b| format!("{:#04x}", b)) - .collect::>() - .join(", "); - self.constants.add_entry( - data.to_vec(), - const_name.clone(), - format!("static const uint8_t {const_name}[] = {{ {} }};\n", bytes), + /// Push raw const bytes into the output stream. + fn push_const(&mut self, comment: Option, value: &[u8]) { + self.min_out_size += value.len(); + self.max_out_size += value.len(); + self.push_chunk(CodeChunk::ConstBytes(value.to_owned()), comment); + } + + /// Push a constified DER built by the `gen` function. + fn push_der( + &mut self, + comment: Option, + gen: impl FnOnce(&mut Der) -> Result<()>, + ) -> Result<()> { + let content = Der::generate(gen)?; + self.push_const(comment, &content); + Ok(()) + } + + /// Push raw code with size difference into the output stream. + fn push_function_call(&mut self, min_size: usize, max_size: usize, s: &str) { + self.min_out_size += min_size; + self.max_out_size += max_size; + self.push_chunk(CodeChunk::Code(s.to_owned()), None); + } + + /// Get the placeholder for length octet encoded with DER. + fn get_size_placeholder(min_size: usize, max_size: usize) -> Result> { + // DER requires the length octet encoded in minimum bytes. + let tag_size = Self::tag_size(max_size); + ensure!( + Self::tag_size(min_size) == tag_size, + "Var-sized length octet is not supported,\ + max={max_size},\ + min={min_size}" ); - const_name + + let mut len_enc = Vec::::new(); + if max_size <= 0x7f { + len_enc.push(0); + } else { + let nbytes = Self::tag_size(max_size) - 2; + len_enc.push(0x80 | (nbytes as u8)); + len_enc.extend(std::iter::repeat(0).take(nbytes)); + } + + Ok(len_enc) } /// Return the maximum size of ASN1 tag, ie the tag itself, the length. @@ -210,57 +324,6 @@ impl Codegen<'_> { }; tag_bytes + len_bytes } - - // Same as `tag_size` but also count the content size. - fn tag_and_content_size(size: usize) -> usize { - Self::tag_size(size) + size - } - - /// Push a tagged raw OID into the ASN1 output, the buffer and its size are arbitrary C expressions. - fn push_raw_oid(&mut self, expr: &str, expr_size: &str, max_size: usize) { - // A tagged OID needs a tag, up to 3 bytes of length and the OID itself. - // Don't try to exactly compute how many bytes we need for the length. - self.max_out_size += Self::tag_and_content_size(max_size); - self.push_str_with_indent(&format!( - "asn1_push_oid_raw(&state, {expr}, {expr_size});\n" - )) - } - - /// Push a bit in a bitstring. - fn push_bit(&mut self, bitstring_tagname: &str, val: &Value) -> Result<()> { - match val { - Value::Literal(x) => { - self.push_str_with_indent(&format!( - "asn1_bitstring_push_bit({bitstring_tagname}, {x});\n" - )); - } - Value::Variable(Variable { name, convert }) => { - let VariableInfo { - codegen, - var_type: source_type, - } = (self.variable_info)(name)?; - match source_type { - VariableType::Boolean => { - ensure!( - convert.is_none(), - "cannot use a convertion from boolean to boolean" - ); - let VariableCodegenInfo::Boolean { value_expr } = codegen else { - bail!("internal error: boolean not represented by a VariableCodegenInfo::Boolean"); - }; - self.push_str_with_indent(&format!( - "asn1_bitstring_push_bit({bitstring_tagname}, {value_expr});\n" - )); - } - _ => bail!( - "conversion from to {:?} to boolean is not supported", - source_type - ), - } - } - } - Ok(()) - } } impl Tag { @@ -292,21 +355,17 @@ impl Tag { impl Builder for Codegen<'_> { /// Push a byte into the ASN1 output, the value can be any C expression. fn push_byte(&mut self, val: u8) -> Result<()> { - self.push_str_with_indent(&format!("asn1_push_byte(&state, {val});\n")); - self.max_out_size += 1; + self.push_const(Some("Byte".into()), &[val]); Ok(()) } /// Push a tagged boolean into the ASN1 output. fn push_boolean(&mut self, tag: &Tag, val: &Value) -> Result<()> { match val { - Value::Literal(x) => { - let bool_str = if *x { "true" } else { "false" }; - self.push_str_with_indent(&format!( - "asn1_push_bool(&state, {}, {});\n", - tag.codestring(), - bool_str - )); + Value::Literal(_) => { + self.push_der(Some(tag.codestring()), |builder| { + builder.push_boolean(tag, val) + })?; } Value::Variable(Variable { name, convert }) => { let VariableInfo { @@ -323,17 +382,20 @@ impl Builder for Codegen<'_> { } match codegen { VariableCodegenInfo::Boolean { value_expr } => { - self.push_str_with_indent(&format!( - "asn1_push_bool(&state, {}, {value_expr});\n", - tag.codestring() - )) + self.push_tag(Some(tag.codestring()), tag, |builder| { + // A boolean only requires one byte of data (plus the tag). + builder.push_function_call( + 1, + 1, + &format!("template_push_asn1_bool(&state, {value_expr});\n",), + ); + Ok(()) + })?; } _ => bail!("internal error: boolean represented by a {source_type:?}"), } } } - // A boolean only requires one byte of data (plus the tag). - self.max_out_size += Self::tag_and_content_size(1); Ok(()) } @@ -346,29 +408,10 @@ impl Builder for Codegen<'_> { val: &Value, ) -> Result<()> { match val { - // For a literal, try to use `asn1_push_uint32` if possible, otherwise - // create a constant in the pool to hold the encoding and use `asn1_push_integer`. - // For unsigned integers, we might need to push one more byte of data then indicated - // by the length since they are represented in two's completement so an unsigned number - // with the MSB bit set needs to be padded with a 0x00 byte so that it is not interpreted - // as a negative number. Therefore, always add one to estimate. - Value::Literal(x) => { - if x.bits() <= 32 { - self.push_str_with_indent(&format!( - "asn1_push_uint32(&state, {}, {x});\n", - tag.codestring() - )); - self.max_out_size += Self::tag_and_content_size(1 + (x.bits() + 7) / 8); - } else { - let bytes = x.to_bytes_be(); - let const_name = self.add_constant_byte_array(name_hint, &bytes); - self.push_str_with_indent( - &format!( - "asn1_push_integer(&state, {}, false, {const_name}, sizeof({const_name}));\n", - tag.codestring()) - ); - self.max_out_size += Self::tag_and_content_size(1 + bytes.len()) - } + Value::Literal(_) => { + self.push_der(name_hint.clone(), |builder| { + builder.push_integer(name_hint, tag, val) + })?; } Value::Variable(Variable { name, convert }) => { let VariableInfo { @@ -376,43 +419,67 @@ impl Builder for Codegen<'_> { var_type: source_type, } = (self.variable_info)(name)?; // Get the maximum size and verify that types and conversion are correct. - let size = match source_type { - VariableType::Integer { size } => { + match source_type { + VariableType::Integer { .. } => { ensure!(convert.is_none(), "using an integer variable for an integer field cannot specify a conversion"); - size - } - VariableType::ByteArray { size } => { + }, + VariableType::ByteArray { .. } => { match convert { None => bail!("using a byte array variable for an integer field must specify a conversion"), Some(Conversion::BigEndian) => (), _ => bail!("conversion {:?} from byte array to integer is not supported", convert), } - size - } + }, _ => bail!( "using a variable of type {source_type:?} for an integer field is not supported" ), }; - self.max_out_size += Self::tag_and_content_size(1 + size); + + // ASN1 integer will add one extra byte when positive integers + // have MSB = 1. + let (min_size, max_size) = source_type.int_size(1); + + // Value zero will be encoded as one-byte 0x00. + let min_size = max(1, min_size); + + ensure!( + max_size < 126, + "Integer more than 126 bytes is not supported." + ); + + // Two extra bytes for tag and length. + let (min_size, max_size) = (min_size + 2, max_size + 2); + // For variables, an integer can either be represented by a pointer to a big-endian - // byte array, or by a `uint32_t` for a very small integers. Use `asn1_push_uint32` - // or `asn1_push_integer` depending on the case. + // byte array, or by a `uint32_t` for a very small integers. Use `template_push_uint32` + // or `template_push_integer` depending on the case. + let msb_tweak = source_type.use_msb_tweak(); + ensure!( + !msb_tweak || source_type.has_constant_array_size(), + "MSb tweak is only supported with constant array size.", + ); + + let tag_str = tag.codestring(); match codegen { - VariableCodegenInfo::Int32 { value_expr } => { - self.push_str_with_indent(&format!( - "asn1_push_uint32(&state, {}, {value_expr});\n", - tag.codestring() - )) - } + VariableCodegenInfo::Uint32 { value_expr } => self.push_function_call( + min_size, + max_size, + &format!( + "template_asn1_uint32(&state, {tag_str}, {msb_tweak}, {value_expr});\n", + ), + ), VariableCodegenInfo::Pointer { ptr_expr, size_expr, } => { // Make sure the type is correct and get the size. - self.push_str_with_indent(&format!( - "asn1_push_integer(&state, {}, false, {ptr_expr}, {size_expr});\n", - tag.codestring() - )) + self.push_function_call( + min_size, + max_size, + &format!( + "template_asn1_integer(&state, {tag_str}, {msb_tweak}, {ptr_expr}, {size_expr});\n", + ), + ) } _ => bail!("internal error: integer represented by a {source_type:?}"), } @@ -430,15 +497,10 @@ impl Builder for Codegen<'_> { size: usize, ) -> Result<()> { match val { - Value::Literal(x) => { - let data = &x.to_bytes_be(); - let data = [vec![0; size - data.len()], data.clone()].concat(); - let const_name = self.add_constant_byte_array(name_hint, &data); - self.push_str_with_indent(&format!( - "asn1_push_bytes(&state, {const_name}, sizeof({const_name}));\n" - )); - // There is not tag, we are just pushing the data itself. - self.max_out_size += data.len(); + Value::Literal(_) => { + self.push_der(name_hint.clone(), |builder| { + builder.push_integer_pad(name_hint, val, size) + })?; } Value::Variable(Variable { name, convert }) => { let VariableInfo { @@ -446,7 +508,7 @@ impl Builder for Codegen<'_> { var_type: source_type, } = (self.variable_info)(name)?; match source_type { - VariableType::Integer { size } => { + VariableType::Integer { .. } => { ensure!(convert.is_none(), "using an integer variable for an integer field cannot specify a conversion"); let VariableCodegenInfo::Pointer { ptr_expr, @@ -455,10 +517,13 @@ impl Builder for Codegen<'_> { else { bail!("the codegen backend does not support small integers for padded integer fields"); }; + + let (min_size, max_size) = source_type.array_size(); + ensure!(min_size == max_size, "Padding is not supported"); + // There is not tag, we are just pushing the data itself. - self.max_out_size += size; - self.push_str_with_indent(&format!( - "asn1_push_integer_pad(&state, false, {ptr_expr}, {size_expr}, {size});\n" + self.push_function_call(min_size, max_size, &format!( + "template_push_bytes(&state, {ptr_expr}, {size_expr});\n" )) } _ => bail!( @@ -474,13 +539,10 @@ impl Builder for Codegen<'_> { /// the ASN1 output. fn push_byte_array(&mut self, name_hint: Option, val: &Value>) -> Result<()> { match val { - Value::Literal(x) => { - let const_name = self.add_constant_byte_array(name_hint, x); - self.push_str_with_indent(&format!( - "asn1_push_bytes(&state, {const_name}, sizeof({const_name}));\n" - )); - // There is not tag, we are just pushing the data itself. - self.max_out_size += x.len(); + Value::Literal(_) => { + self.push_der(name_hint.clone(), |builder| { + builder.push_byte_array(name_hint, val) + })?; } Value::Variable(Variable { name, convert }) => { let VariableInfo { @@ -488,19 +550,19 @@ impl Builder for Codegen<'_> { var_type: source_type, } = (self.variable_info)(name)?; match source_type { - VariableType::ByteArray { size } => { + VariableType::ByteArray { .. } => { ensure!(convert.is_none(), "using a byte-array variable for a byte-array field cannot specify a conversion"); + let (min_size, max_size) = source_type.array_size(); let VariableCodegenInfo::Pointer { ptr_expr, size_expr, } = codegen else { - bail!("internal error: byte-array represented by a VariableCodegenInfo::Int32"); + bail!("internal error: byte-array represented by a VariableCodegenInfo::Uint32"); }; // There is not tag, we are just pushing the data itself. - self.max_out_size += size; - self.push_str_with_indent(&format!( - "asn1_push_bytes(&state, {ptr_expr}, {size_expr});\n" + self.push_function_call(min_size, max_size, &format!( + "template_push_bytes(&state, {ptr_expr}, {size_expr});\n" )) } _ => bail!( @@ -515,34 +577,30 @@ impl Builder for Codegen<'_> { /// Push an optionally tagged string into the ASN1 output. fn push_string( &mut self, - _name_hint: Option, + name_hint: Option, str_type: &Tag, val: &Value, ) -> Result<()> { - let str_type = str_type.codestring(); match val { - Value::Literal(x) => { - let len = x.len(); - self.push_str_with_indent(&format!( - "asn1_push_string(&state, {str_type}, \"{x}\", {len});\n" - )); - // A tagged string needs a tag (up to 3 bytes of length) and the string itself. - // Don't try to exactly compute how many bytes we need for the length. - self.max_out_size += Self::tag_and_content_size(x.len()); + Value::Literal(_) => { + self.push_der(name_hint.clone(), |builder| { + builder.push_string(name_hint, str_type, val) + })?; } Value::Variable(Variable { name, convert }) => { let VariableInfo { codegen, var_type: source_type, } = (self.variable_info)(name)?; - // When pushing a variable, it can either a string (use asn1_push_string) or a byte array - // that needs to converted (use asn1_push_hexstring). + // When pushing a variable, it can either a string (use template_push_bytes) or a byte array + // that needs to converted (use template_push_hex). match source_type { - VariableType::String { size } => { + VariableType::String { .. } => { ensure!( convert.is_none(), "cannot use a convertion from string to string" ); + let (min_size, max_size) = source_type.array_size(); let VariableCodegenInfo::Pointer { ptr_expr, size_expr, @@ -550,12 +608,17 @@ impl Builder for Codegen<'_> { else { bail!("internal error: string not represented by a VariableCodegenInfo::Pointer"); }; - self.push_str_with_indent(&format!( - "asn1_push_string(&state, {str_type}, {ptr_expr}, {size_expr});\n" - )); - self.max_out_size += Self::tag_and_content_size(size); + self.push_tag(Some(name.into()), str_type, |builder| { + builder.push_function_call( + min_size, + max_size, + &format!("template_push_bytes(&state, (const uint8_t*){ptr_expr}, {size_expr});\n"), + ); + Ok(()) + })?; } - VariableType::ByteArray { size } => { + VariableType::ByteArray { .. } => { + let (min_size, max_size) = source_type.array_size(); match convert { None => bail!("using a byte array variable for an string field must to specify a conversion"), Some(Conversion::LowercaseHex) => { @@ -563,9 +626,12 @@ impl Builder for Codegen<'_> { bail!("internal error: string not represented by a VariableCodegenInfo::Pointer"); }; // The conversion doubles the size. - self.max_out_size += Self::tag_and_content_size(2 * size); - self.push_str_with_indent( - &format!("asn1_push_hexstring(&state, {str_type}, {ptr_expr}, {size_expr});\n")) + self.push_tag(Some(name.into()), str_type, |builder| { + builder.push_function_call(2 * min_size, 2 * max_size, &format!( + "template_push_hex(&state, {ptr_expr}, {size_expr});\n" + )); + Ok(()) + })?; } _ => bail!("conversion {convert:?} from byte array to string is not supported"), } @@ -583,40 +649,71 @@ impl Builder for Codegen<'_> { tag: &Tag, bits: &[Value], ) -> Result<()> { - self.push_tag(name_hint.clone(), tag, |builder| { - let tag_name = format!( - "bit{}_{}", - builder.tag_idx, - name_hint.map(|x| x.to_snake_case()).unwrap_or("".into()) - ); - builder.tag_idx += 1; - builder.push_str_with_indent(&format!("asn1_bitstring_t {tag_name};\n")); - builder.push_str_with_indent(&format!("asn1_start_bitstring(&state, &{tag_name});\n")); - builder.push_str_with_indent("{\n"); - builder.indent_lvl += 1; - for bit in bits { - builder.push_bit(&format!("&{tag_name}"), bit)?; + let bit_consts = bits + .iter() + .map(|x| match x { + Value::Literal(x) => x, + _ => &false, + }) + .collect::>(); + // See X.690 spec section 8.6 for encoding details. + // Note: the encoding of an empty bitstring must be the number of unused bits to 0 and have no content. + let nr_bytes = (bit_consts.len() + 7) / 8; + let mut bytes = vec![0u8; nr_bytes]; + for (i, bit) in bit_consts.iter().enumerate() { + bytes[i / 8] |= (**bit as u8) << (7 - (i % 8)); + } + + let bitstring_name = format!("{}-bit BitString {}", bits.len(), tag.codestring(),); + + self.push_as_bit_string( + Some(bitstring_name), + tag, + bytes.len() * 8 - bits.len(), + |builder| builder.push_byte_array(name_hint.clone(), &Value::Literal(bytes.clone())), + )?; + + for (i, bit) in bits.iter().enumerate() { + let byte_offset = bytes.len() - i / 8; + let bit_offset = 7 - (i % 8); + + if let Value::Variable(Variable { name, convert }) = bit { + let VariableInfo { + codegen, + var_type: source_type, + } = (self.variable_info)(name)?; + match source_type { + VariableType::Boolean => { + ensure!( + convert.is_none(), + "cannot use a convertion from boolean to boolean" + ); + let VariableCodegenInfo::Boolean { value_expr } = codegen else { + bail!("internal error: boolean not represented by a VariableCodegenInfo::Boolean"); + }; + self.push_chunk( + CodeChunk::Code(format!( + "template_set_bit(&state, {byte_offset}, {bit_offset}, {value_expr});\n" + )), + Some(name.clone()), + ); + } + _ => bail!( + "conversion from to {:?} to boolean is not supported", + source_type + ), + } } - // One byte for the unused bits and then one byte per 8 bits. - builder.max_out_size += 1 + (bits.len() + 7) / 8; - builder.indent_lvl -= 1; - builder.push_str_with_indent("}\n"); - builder.push_str_with_indent(&format!("asn1_finish_bitstring(&{tag_name});\n")); - Ok(()) - }) + } + + Ok(()) } fn push_oid(&mut self, oid: &Oid) -> Result<()> { - // Create constant. - let bytes = oid.to_der()?; - let oid_const_name = self.add_constant_byte_array(Some(format!("oid_{}", oid)), &bytes); - self.push_raw_oid( - &oid_const_name, - &format!("sizeof({oid_const_name})"), - bytes.len(), - ); - - Ok(()) + // Serialize oid with tag to const. + self.push_der(Some(format!("Oid of {}", oid)), |builder| { + builder.push_oid(oid) + }) } // Helper function for outputting ASN1 tags. @@ -626,29 +723,84 @@ impl Builder for Codegen<'_> { tag: &Tag, gen: impl FnOnce(&mut Self) -> Result<()>, ) -> Result<()> { - let tag_name = format!( - "tag{}_{}", - self.tag_idx, - name_hint.map(|x| x.to_snake_case()).unwrap_or("".into()) + let tag_name = name_hint.unwrap_or("Unnamed tag".into()); + + self.indent(); + + self.push_const( + Some(format!("Tag {} of {tag_name}", tag.codestring())), + &tag.to_der()?, ); - self.tag_idx += 1; - self.push_str_with_indent(&format!("asn1_tag_t {tag_name};\n")); - self.push_str_with_indent(&format!( - "asn1_start_tag(&state, &{tag_name}, {});\n", - tag.codestring() - )); - self.push_str_with_indent("{\n"); - self.indent_lvl += 1; + + // Allocate placeholders in the output stream. + // This placeholder will be replaced by the encoded length later. + let len_idx = self.output.len(); + self.push_const(None, &[]); + // This placeholder will be replaced by the location memo code later. + let memo_idx = self.output.len(); + self.push_const(None, &[]); + + let content_idx = self.output.len(); + // We do not yet know how many bytes the content will use: remember the current // value of the estimate and see by how much it increases during generation // to obtain a bound. + let old_min_size = self.min_out_size; let old_max_size = self.max_out_size; + self.indent(); gen(self)?; - let max_size = self.max_out_size - old_max_size; - self.max_out_size += Self::tag_size(max_size); - self.indent_lvl -= 1; - self.push_str_with_indent("}\n"); - self.push_str_with_indent(&format!("asn1_finish_tag(&{tag_name});\n")); + self.unindent(); + let min_size: usize = self.min_out_size - old_min_size; + let max_size: usize = self.max_out_size - old_max_size; + + // Generate size patching code. + let len_enc = Self::get_size_placeholder(min_size, max_size)?; + self.min_out_size += len_enc.len(); + self.max_out_size += len_enc.len(); + + if min_size == max_size { + self.output[len_idx].comment = Some(format!("Length of fixed-sized {tag_name}")); + + // Sanity check for all constants. + if self.output[content_idx..] + .iter() + .all(|x| x.code.is_constant()) + { + let total_size: usize = self.output[content_idx..] + .iter() + .map(|x| x.code.len()) + .sum(); + if total_size != min_size || total_size != max_size { + println!("{:?}", &self.output[content_idx..]); + panic!( + "Invalid const size total={total_size}, \ + min={min_size}, M={max_size}" + ); + } + } + + let len_enc = Der::encode_size(max_size); + self.output[len_idx].code = CodeChunk::ConstBytes(len_enc); + // The memo placeholder is left empty / no-op in this branch. + } else { + /* var-sized tag */ + self.output[len_idx].comment = Some(format!( + "Length of {tag_name} between {min_size} ~ {max_size}" + )); + + self.output[len_idx].code = CodeChunk::ConstBytes(len_enc); + + // Add the memo statement. + self.output[memo_idx] = CodeChunkWithComment { + code: CodeChunk::SizePatchMemo(-2), + comment: Some(format!("Start of {tag_name}")), + }; + self.push_chunk( + CodeChunk::Code("template_patch_size_be(&state, memo);\n".to_string()), + Some(format!("End of {tag_name}")), + ); + } + self.unindent(); Ok(()) } } diff --git a/sw/host/ot_certs/src/asn1/der.rs b/sw/host/ot_certs/src/asn1/der.rs index ecae9e86cd6e9..ed1cf755f12ec 100644 --- a/sw/host/ot_certs/src/asn1/der.rs +++ b/sw/host/ot_certs/src/asn1/der.rs @@ -63,6 +63,23 @@ impl Der { Ok(der.output) } + pub fn encode_size(size: usize) -> Vec { + // Push length, see X.690 section 8.1.3. + let mut encoded = Vec::::new(); + if size <= 0x7f { + encoded.push(size as u8); + } else { + let mut remaining = size; + while remaining != 0 { + encoded.push((remaining & 0xff) as u8); + remaining >>= 8; + } + encoded.push(0x80 | (encoded.len() as u8)); + encoded.reverse(); + } + encoded + } + fn get_value_or_error(val: &Value) -> Result<&T> { match val { Value::Literal(x) => Ok(x), @@ -198,14 +215,8 @@ impl Builder for Der { if content.output.len() <= 0x7f { self.push_byte(content.output.len() as u8)?; } else { - let mut len = content.output.len(); - let mut bytes = Vec::::new(); - while len != 0 { - bytes.push((len & 0xff) as u8); - len >>= 8; - } - self.push_byte(0x80 | (bytes.len() as u8))?; - bytes.reverse(); + let len = content.output.len(); + let bytes = Self::encode_size(len); self.push_bytes(&bytes)?; } // Push content diff --git a/sw/host/ot_certs/src/codegen.rs b/sw/host/ot_certs/src/codegen.rs index 9159eddc27807..dbfa03c5f96d9 100644 --- a/sw/host/ot_certs/src/codegen.rs +++ b/sw/host/ot_certs/src/codegen.rs @@ -5,18 +5,22 @@ //! This module is capable of generating C code for generating a binary X.509 //! certificate according to a [`Template`]. -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result}; use heck::ToUpperCamelCase; use indexmap::IndexMap; +use itertools::Itertools; use std::fmt::Write; -use crate::asn1::codegen::{self, ConstantPool, VariableCodegenInfo, VariableInfo}; +use crate::asn1::codegen::{self, CodegenOutput, VariableCodegenInfo, VariableInfo}; use crate::asn1::x509::X509; use crate::template::subst::{Subst, SubstValue}; -use crate::template::{EcdsaSignature, Signature, Template, Value, Variable, VariableType}; +use crate::template::{ + EcdsaSignature, Signature, SizeRange, Template, Value, Variable, VariableType, +}; use crate::x509; -const INDENT: &str = " "; +/// The amount of test cases to generate for covering more corner cases. +const TEST_CASE_COUNT: u32 = 100; pub struct Codegen { /// Header. @@ -80,6 +84,7 @@ pub fn generate_cert(from_file: &str, tmpl: &Template) -> Result { source_c.push('\n'); writeln!(source_c, "#include \"{}.h\"", tmpl.name)?; source_c.push_str("#include \"sw/device/silicon_creator/lib/cert/asn1.h\"\n\n"); + source_c.push_str("#include \"sw/device/silicon_creator/lib/cert/template.h\"\n\n"); source_h.push_str("#include \"sw/device/lib/base/status.h\"\n\n"); @@ -99,18 +104,14 @@ pub fn generate_cert(from_file: &str, tmpl: &Template) -> Result { source_h.push_str(&generate_value_struct(&tbs_value_struct_name, &tbs_vars)); let tbs_value_struct_name = tbs_value_struct_name + "_t"; - // Create a constant pool to share between the two functions. - let mut const_pool = ConstantPool::new(); - // Generate TBS function. let generate_tbs_fn_name = format!("{}_build_tbs", tmpl.name); let generate_tbs_fn_params = - format!("{tbs_value_struct_name} *values, uint8_t *tbs, size_t *tbs_inout_size"); - let (generate_tbs_fn_def, generate_tbs_fn_impl, max_tbs_size) = generate_builder( + format!("{tbs_value_struct_name} *values, uint8_t *out_buf, size_t *inout_size"); + let (generate_tbs_fn_def, generate_tbs_fn_impl) = generate_builder( CertificateComponent::Tbs, &generate_tbs_fn_name, &generate_tbs_fn_params, - &mut const_pool, &tbs_vars, |builder| X509::push_tbs_certificate(builder, &tmpl.certificate), )?; @@ -119,7 +120,13 @@ pub fn generate_cert(from_file: &str, tmpl: &Template) -> Result { let tbs_binary_val_name = "tbs"; sig_vars.insert( tbs_binary_val_name.to_string(), - VariableType::ByteArray { size: max_tbs_size }, + VariableType::ByteArray { + size: SizeRange::RangeSize( + generate_tbs_fn_impl.min_size, + generate_tbs_fn_impl.max_size, + ), + tweak_msb: None, + }, ); let tbs_binary_val = Value::Variable(Variable { name: tbs_binary_val_name.to_string(), @@ -134,38 +141,48 @@ pub fn generate_cert(from_file: &str, tmpl: &Template) -> Result { // Generate sig function. let generate_cert_fn_name = format!("{}_build_cert", tmpl.name); let generate_cert_fn_params = - format!("{sig_value_struct_name} *values, uint8_t *cert, size_t *cert_inout_size"); - let (generate_cert_fn_def, generate_cert_fn_impl, max_cert_size) = generate_builder( + format!("{sig_value_struct_name} *values, uint8_t *out_buf, size_t *inout_size"); + let (generate_cert_fn_def, generate_cert_fn_impl) = generate_builder( CertificateComponent::Certificate, &generate_cert_fn_name, &generate_cert_fn_params, - &mut const_pool, &sig_vars, |builder| X509::push_certificate(builder, &tbs_binary_val, &tmpl.certificate.signature), )?; - // Create two constants for the maximum possible size of TBS and cert. - // Also generate a comment stating how this size was computed. - let max_tbs_size_const_name = format!("k{}MaxTbsSizeBytes", tmpl.name.to_upper_camel_case()); - let max_cert_size_const_name = format!("k{}MaxCertSizeBytes", tmpl.name.to_upper_camel_case()); - source_h.push_str("// Maximum possible size of a TBS and a certificate assuming:\n"); + // Create constants for the variable size range. + source_h.push_str("enum {\n"); for (var_name, var_type) in tbs_vars.iter().chain(sig_vars.iter()) { // Only consider variables whose size can vary, ie pointers. let (codegen, _) = c_variable_info(var_name, "", var_type); if let VariableCodegenInfo::Pointer { .. } = codegen { - let size = match var_type { - VariableType::ByteArray { size } - | VariableType::Integer { size } - | VariableType::String { size } => *size, - VariableType::Boolean => bail!("internal error: boolean represented by a pointer"), - }; - writeln!(source_h, "// - {var_name} is of size at most {size} bytes.")?; + let tmpl_name = tmpl.name.to_upper_camel_case(); + let const_name = var_name.to_upper_camel_case(); + let (min_size, max_size) = var_type.array_size(); + writeln!( + source_h, + "k{tmpl_name}Min{const_name}SizeBytes = {min_size}," + )?; + writeln!( + source_h, + "k{tmpl_name}Max{const_name}SizeBytes = {max_size}," + )?; + + if var_type.has_constant_array_size() { + writeln!( + source_h, + "k{tmpl_name}Exact{const_name}SizeBytes = {max_size}," + )?; + } } } + source_h.push_str("};\n"); + + let max_cert_size_const_name = format!("k{}MaxCertSizeBytes", tmpl.name.to_upper_camel_case()); + source_h.push_str("// Maximum possible size of a certificate\n"); source_h.push_str(&indoc::formatdoc! {"enum {{ - {INDENT}{max_tbs_size_const_name} = {max_tbs_size}, - {INDENT}{max_cert_size_const_name} = {max_cert_size}, - }};"}); + {max_cert_size_const_name} = {}, + }};", generate_cert_fn_impl.max_size}); // Output definition of the functions. source_h.push_str("\n\n"); @@ -173,19 +190,14 @@ pub fn generate_cert(from_file: &str, tmpl: &Template) -> Result { source_h.push_str(&generate_cert_fn_def); source_h.push('\n'); - // Output constant pool and the implementation of the functions. - source_c.push_str(&const_pool.codestring()); - source_c.push('\n'); - source_c.push_str(&generate_tbs_fn_impl); - source_c.push_str(&generate_cert_fn_impl); + // Output the implementation of the functions. + source_c.push_str(&generate_tbs_fn_impl.code); + source_c.push_str(&generate_cert_fn_impl.code); source_c.push('\n'); writeln!(source_h, "\n#endif /* __{}__ */", preproc_guard_include)?; // Generate unittest. - let unittest_data = tmpl.random_test()?; - let expected_cert = x509::generate_certificate(&tmpl.subst(&unittest_data)?)?; - source_unittest.push_str(&license_and_warning); source_unittest.push('\n'); source_unittest.push_str("extern \"C\" {\n"); @@ -193,13 +205,63 @@ pub fn generate_cert(from_file: &str, tmpl: &Template) -> Result { source_unittest.push_str("}\n"); source_unittest.push_str("#include \"gtest/gtest.h\"\n\n"); + for idx in 0..TEST_CASE_COUNT { + let test_case = generate_test_case( + &format!("Verify{idx}"), + &tbs_vars, + &sig_vars, + generate_tbs_fn_impl.min_size, + generate_tbs_fn_impl.max_size, + generate_cert_fn_impl.max_size, + tmpl, + )?; + source_unittest.push_str(&test_case); + } + + Ok(Codegen { + source_h, + source_c, + source_unittest, + }) +} + +// Generate a unit test test case with random variables. +fn generate_test_case( + test_name: &str, + tbs_vars: &IndexMap, + sig_vars: &IndexMap, + min_tbs_size: usize, + max_tbs_size: usize, + max_cert_size: usize, + tmpl: &Template, +) -> Result { + let mut source_unittest = String::new(); + let unittest_data = tmpl.random_test()?; + let expected_cert = x509::generate_certificate(&tmpl.subst(&unittest_data)?)?; + + let tmpl_name = tmpl.name.to_upper_camel_case(); + let generate_tbs_fn_name = format!("{}_build_tbs", tmpl.name); + let generate_cert_fn_name = format!("{}_build_cert", tmpl.name); + let tbs_value_struct_name = format!("{}_tbs_values_t", tmpl.name); + let sig_value_struct_name = format!("{}_sig_values_t", tmpl.name); + + source_unittest.push_str(&format! { r#" + TEST({tmpl_name}, {test_name}) + "#}); + + source_unittest.push_str( + " + { + ", + ); + // Generate constants holding the data. for (var_name, data) in unittest_data.values { match data { SubstValue::ByteArray(bytes) => { writeln!( source_unittest, - "uint8_t g_{var_name}[] = {{ {} }};", + "static uint8_t g_{var_name}[] = {{ {} }};", bytes .iter() .map(|x| format!("{:#02x}", x)) @@ -207,26 +269,27 @@ pub fn generate_cert(from_file: &str, tmpl: &Template) -> Result { .join(", ") )?; } - SubstValue::String(s) => writeln!(source_unittest, "char g_{var_name}[] = \"{s}\";")?, - SubstValue::Int32(val) => writeln!(source_unittest, "uint32_t g_{var_name} = {val};")?, + SubstValue::String(s) => { + let s = s.chars().map(|c| format!("'{c}'")).join(", "); + writeln!(source_unittest, "static char g_{var_name}[] = {{{s}}};")? + } + + SubstValue::Uint32(val) => writeln!(source_unittest, "uint32_t g_{var_name} = {val};")?, SubstValue::Boolean(val) => writeln!(source_unittest, "bool g_{var_name} = {val};")?, } } // Generate structure to hold the TBS data. source_unittest.push('\n'); writeln!(source_unittest, "{tbs_value_struct_name} g_tbs_values = {{")?; - source_unittest.push_str(&generate_value_struct_assignment(&tbs_vars)?); + source_unittest.push_str(&generate_value_struct_assignment(tbs_vars)?); source_unittest.push_str("};\n"); // Generate buffer for the TBS data. source_unittest.push('\n'); - writeln!( - source_unittest, - "uint8_t g_{tbs_binary_val_name}[{max_tbs_size}];" - )?; + writeln!(source_unittest, "uint8_t g_tbs[{max_tbs_size}];")?; // Generate structure to hold the certificate data. source_unittest.push('\n'); writeln!(source_unittest, "{sig_value_struct_name} g_sig_values = {{")?; - source_unittest.push_str(&generate_value_struct_assignment(&sig_vars)?); + source_unittest.push_str(&generate_value_struct_assignment(sig_vars)?); source_unittest.push_str("};\n"); // Generate buffer for the certificate data. source_unittest.push('\n'); @@ -238,29 +301,47 @@ pub fn generate_cert(from_file: &str, tmpl: &Template) -> Result { expected_cert.len(), expected_cert .iter() - .map(|x| format!("{:#02x}", x)) + .map(|x| format!("0x{:02x}", x)) .collect::>() .join(", ") )?; source_unittest.push('\n'); + + // Comment out tbs size setter if the size is constant. + let no_tbs_size = if min_tbs_size == max_tbs_size { + "// " + } else { + "" + }; + // Generate the body of the test. - source_unittest.push_str(&indoc::formatdoc!{ r#" - TEST({}, Verify) {{ - {INDENT}EXPECT_EQ(kErrorOk, {generate_tbs_fn_name}(&g_tbs_values, g_{tbs_binary_val_name}, &g_sig_values.{tbs_binary_val_name}_size)); - {INDENT}size_t cert_size = sizeof(g_cert_data); - {INDENT}EXPECT_EQ(kErrorOk, {generate_cert_fn_name}(&g_sig_values, g_cert_data, &cert_size)); - {INDENT}EXPECT_EQ(cert_size, sizeof(kExpectedCert)); - {INDENT}EXPECT_EQ(0, memcmp(g_cert_data, kExpectedCert, cert_size)); - }} + source_unittest.push_str(&format! { r#" + size_t tbs_size = sizeof(g_tbs); + EXPECT_EQ(kErrorOk, {generate_tbs_fn_name}(&g_tbs_values, g_tbs, &tbs_size)); + EXPECT_GE(tbs_size, {min_tbs_size}); + EXPECT_LE(tbs_size, {max_tbs_size}); + + {no_tbs_size} g_sig_values.tbs_size = tbs_size; + + size_t cert_size = sizeof(g_cert_data); + EXPECT_EQ(kErrorOk, {generate_cert_fn_name}(&g_sig_values, g_cert_data, &cert_size)); + EXPECT_EQ(cert_size, sizeof(kExpectedCert)); + printf("Generated cert: \n"); + for (size_t i=0; i) ptr_expr, size_expr, } => { - writeln!(source, "{INDENT}.{ptr_expr} = g_{var_name},")?; - writeln!(source, "{INDENT}.{size_expr} = sizeof(g_{var_name}),")?; + writeln!(source, ".{ptr_expr} = g_{var_name},")?; + if !var_type.has_constant_array_size() { + writeln!(source, ".{size_expr} = sizeof(g_{var_name}),")?; + } } - VariableCodegenInfo::Int32 { value_expr } + VariableCodegenInfo::Uint32 { value_expr } | VariableCodegenInfo::Boolean { value_expr } => { - writeln!(source, "{INDENT}.{value_expr} = g_{var_name},\n")?; + writeln!(source, ".{value_expr} = g_{var_name},\n")?; } } } @@ -320,22 +403,17 @@ enum CertificateComponent { Tbs, } -// Generate a function that generates a TBS/cert. This functions returns three -// elements: the header definition, the implementation and the maximum size of -// the produced TBS/cert. +/// Generate a function that generates a TBS/cert. +/// +/// This functions returns the header definition, the generated implementation +/// code and the produced TBS/cert size range. fn generate_builder( component: CertificateComponent, fn_name: &str, fn_params_str: &str, - constants: &mut ConstantPool, variables: &IndexMap, gen: impl FnOnce(&mut codegen::Codegen) -> Result<()>, -) -> Result<(String, String, usize)> { - let mut generate_fn_impl = String::new(); - writeln!( - generate_fn_impl, - "rom_error_t {fn_name}({fn_params_str}) {{" - )?; +) -> Result<(String, CodegenOutput)> { let get_var_info = |var_name: &str| -> Result { let var_type = variables .get(var_name) @@ -345,8 +423,7 @@ fn generate_builder( Ok(VariableInfo { var_type, codegen }) }; let generate_fn_def: String; - let implementation: String; - let max_size: usize; + let mut generated_code: CodegenOutput; if component == CertificateComponent::Tbs { generate_fn_def = indoc::formatdoc! { r#" /** @@ -354,9 +431,9 @@ fn generate_builder( * * @param values Pointer to a structure giving the values to use to generate the TBS * portion of the certificate. - * @param[out] tbs Pointer to a user-allocated buffer that will contain the TBS portion of + * @param[out] out_buf Pointer to a user-allocated buffer that will contain the TBS portion of * the certificate. - * @param[in,out] tbs_inout_size Pointer to an integer holding the size of + * @param[in,out] inout_size Pointer to an integer holding the size of * the provided buffer; this value will be updated to reflect the actual size of * the output. * @return The result of the operation. @@ -365,12 +442,9 @@ fn generate_builder( "# }; - (implementation, max_size) = codegen::Codegen::generate( - /* buf_name */ "tbs", - /* buf_size_name */ "tbs_inout_size", - /* indent */ INDENT, - /* indent_lvl */ 1, - constants, + generated_code = codegen::Codegen::generate( + /* buf_name */ "out_buf", + /* buf_size_name */ "inout_size", &get_var_info, gen, )?; @@ -381,9 +455,9 @@ fn generate_builder( * * @param values Pointer to a structure giving the values to use to generate the * certificate (TBS and signature). - * @param[out] cert Pointer to a user-allocated buffer that will contain the + * @param[out] out_buf Pointer to a user-allocated buffer that will contain the * result. - * @param[in,out] cert_inout_size Pointer to an integer holding the size of + * @param[in,out] inout_size Pointer to an integer holding the size of * the provided buffer, this value will be updated to reflect the actual size of * the output. * @return The result of the operation. @@ -392,21 +466,25 @@ fn generate_builder( "# }; - (implementation, max_size) = codegen::Codegen::generate( - /* buf_name */ "cert", - /* buf_size_name */ "cert_inout_size", - /* indent */ INDENT, - /* indent_lvl */ 1, - constants, + generated_code = codegen::Codegen::generate( + /* buf_name */ "out_buf", + /* buf_size_name */ "inout_size", &get_var_info, gen, )?; } - generate_fn_impl.push_str(&implementation); + + let mut generate_fn_impl = String::new(); + writeln!( + generate_fn_impl, + "rom_error_t {fn_name}({fn_params_str}) {{" + )?; + generate_fn_impl.push_str(&generated_code.code); generate_fn_impl.push_str(" return kErrorOk;\n"); generate_fn_impl.push_str("}\n\n"); + generated_code.code = generate_fn_impl; - Ok((generate_fn_def, generate_fn_impl, max_size)) + Ok((generate_fn_def, generated_code)) } // Decide whether a integer should use a special C type instead @@ -425,58 +503,106 @@ fn c_variable_info( var_type: &VariableType, ) -> (VariableCodegenInfo, String) { match var_type { - VariableType::ByteArray { .. } => ( - VariableCodegenInfo::Pointer { - ptr_expr: format!("{struct_expr}{name}"), - size_expr: format!("{struct_expr}{name}_size"), - }, - indoc::formatdoc! {r#" - {INDENT}// Pointer to an array of bytes. - {INDENT}uint8_t *{name}; - {INDENT}// Size of this array in bytes. - {INDENT}size_t {name}_size; - "# - }, - ), - VariableType::Integer { size } => match c_integer_for_length(*size) { + VariableType::ByteArray { .. } => { + if var_type.has_constant_array_size() { + ( + VariableCodegenInfo::Pointer { + ptr_expr: format!("{struct_expr}{name}"), + size_expr: format!("{}", var_type.size()), + }, + indoc::formatdoc! {r#" + // Pointer to an array of bytes. + uint8_t *{name}; + "# + }, + ) + } else { + ( + VariableCodegenInfo::Pointer { + ptr_expr: format!("{struct_expr}{name}"), + size_expr: format!("{struct_expr}{name}_size"), + }, + indoc::formatdoc! {r#" + // Pointer to an array of bytes. + uint8_t *{name}; + // Size of this array in bytes. + size_t {name}_size; + "# + }, + ) + } + } + VariableType::Integer { .. } => match c_integer_for_length(var_type.size()) { Some(c_type) => ( - VariableCodegenInfo::Int32 { + VariableCodegenInfo::Uint32 { value_expr: format!("{struct_expr}{name}"), }, - format!("{INDENT}{c_type} {name};\n"), - ), - None => ( - VariableCodegenInfo::Pointer { - ptr_expr: format!("{struct_expr}{name}"), - size_expr: format!("{struct_expr}{name}_size"), - }, - indoc::formatdoc! {r#" - {INDENT}// Pointer to an unsigned big-endian in integer. - {INDENT}uint8_t *{name}; - {INDENT}// Size of this integer in bytes. - {INDENT}size_t {name}_size; - "# - }, + format!(" {c_type} {name};\n"), ), + None => { + if var_type.has_constant_array_size() { + ( + VariableCodegenInfo::Pointer { + ptr_expr: format!("{struct_expr}{name}"), + size_expr: format!("{}", var_type.size()), + }, + indoc::formatdoc! {r#" + // Pointer to an unsigned big-endian in integer. + uint8_t *{name}; + "# + }, + ) + } else { + ( + VariableCodegenInfo::Pointer { + ptr_expr: format!("{struct_expr}{name}"), + size_expr: format!("{struct_expr}{name}_size"), + }, + indoc::formatdoc! {r#" + // Pointer to an unsigned big-endian in integer. + uint8_t *{name}; + // Size of this array in bytes. + size_t {name}_size; + "# + }, + ) + } + } }, - VariableType::String { .. } => ( - VariableCodegenInfo::Pointer { - ptr_expr: format!("{struct_expr}{name}"), - size_expr: format!("{struct_expr}{name}_len"), - }, - indoc::formatdoc! {r#" - {INDENT}// Pointer to a (not necessarily zero-terminated) string. - {INDENT}char *{name}; - {INDENT}// Length of this string. - {INDENT}size_t {name}_len; - "# - }, - ), + VariableType::String { .. } => { + if var_type.has_constant_array_size() { + ( + VariableCodegenInfo::Pointer { + ptr_expr: format!("{struct_expr}{name}"), + size_expr: format!("{}", var_type.size()), + }, + indoc::formatdoc! {r#" + // Pointer to a (not necessarily zero-terminated) string. + char *{name}; + "# + }, + ) + } else { + ( + VariableCodegenInfo::Pointer { + ptr_expr: format!("{struct_expr}{name}"), + size_expr: format!("{struct_expr}{name}_len"), + }, + indoc::formatdoc! {r#" + // Pointer to a (not necessarily zero-terminated) string. + char *{name}; + // Length of this string. + size_t {name}_len; + "# + }, + ) + } + } VariableType::Boolean => ( VariableCodegenInfo::Boolean { value_expr: format!("{struct_expr}{name}"), }, - format!("{INDENT}bool {name};\n"), + format!("bool {name};\n"), ), } } diff --git a/sw/host/ot_certs/src/template/mod.rs b/sw/host/ot_certs/src/template/mod.rs index 0f9cda40dc258..c9cc972d350a4 100644 --- a/sw/host/ot_certs/src/template/mod.rs +++ b/sw/host/ot_certs/src/template/mod.rs @@ -368,30 +368,121 @@ pub enum HashAlgorithm { Sha256, } +/// SizeRange sets the range of the variable it represented. +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum SizeRange { + /// The [min, max] size of the variable in bytes. + RangeSize(usize, usize), + /// Equivalent to RangeSize(size, size). + ExactSize(usize), +} + +impl SizeRange { + pub fn range(self) -> (usize, usize) { + match self { + Self::RangeSize(min_size, max_size) => (min_size, max_size), + Self::ExactSize(size) => (size, size), + } + } +} + /// Declaration of a variable that can be filled into the template. #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(tag = "type", rename_all = "kebab-case")] pub enum VariableType { /// Raw array of bytes. + #[serde(rename_all = "kebab-case")] ByteArray { - /// Length in bytes for this variable. - size: usize, + #[serde(flatten)] + size: SizeRange, + + /// The MSb will always be set when encoded as an integer. + tweak_msb: Option, }, /// Signed integer: such an integer is represented by an array of /// in big-endian. Integer { - /// Maximum size in bytes for this variable. - size: usize, + #[serde(flatten)] + size: SizeRange, }, /// UTF-8 encoded String. String { - /// Maximum size in bytes for this variable. - size: usize, + #[serde(flatten)] + size: SizeRange, }, /// Boolean variable. Boolean, } +impl VariableType { + /// Return the maximum array size of the variable. + pub fn size(&self) -> usize { + self.array_size().1 + } + + /// Return true if the variable uses msb tweak trick. + pub fn use_msb_tweak(&self) -> bool { + use VariableType::*; + matches!( + self, + ByteArray { + tweak_msb: Some(true), + .. + } + ) + } + + /// Return true if the user guarantees to pass a fixed-length array for + /// this variable. + pub fn has_constant_array_size(&self) -> bool { + let (min_size, max_size) = self.array_size(); + min_size == max_size + } + + /// Return the the user's guarantee on the array size passing for this + /// variable. + /// + /// The result is the closed range [min, max]. + pub fn array_size(&self) -> (usize, usize) { + use VariableType::*; + match self { + ByteArray { size, .. } | String { size, .. } => size.range(), + // The array buffer for integer should always have the maximum size. + Integer { size, .. } => (size.range().1, size.range().1), + Boolean => panic!("Boolean variable has no array size"), + } + } + + /// Return the the user's guarantee on the size of the integer value + /// represented by the u8 array. + /// + /// `extra_bytes` argument specifies the amount of extra bytes that + /// will be added when the MSb is set. + /// + /// The result is the closed range [min, max]. + pub fn int_size(&self, extra_bytes: usize) -> (usize, usize) { + use VariableType::*; + match self { + ByteArray { + tweak_msb: Some(true), + .. + } => { + if !self.has_constant_array_size() { + panic!("Tweak MSb of var-sized ByteArray is not supported"); + } + (self.size() + extra_bytes, self.size() + extra_bytes) + } + ByteArray { .. } => panic!( + "Encoding ByteArray variable without tweak-msb as an integer is not supported" + ), + Integer { size, .. } => (size.range().0, size.range().1 + extra_bytes), + String { .. } => panic!("String variable has no integer size"), + Boolean => panic!("Boolean variable has no integer size"), + } + } +} + impl Template { pub fn from_hjson_str(content: &str) -> Result