Public Key Re-Use for Signing both Debug and Production Code

Description

Public Key Re-Use for Signing both Debug and Production Code occurs when a single public key is employed to authenticate both debug and production firmware, creating a critical security vulnerability. Public-key cryptography verifies firmware integrity by decrypting a hash with the public key and comparing it to the computed firmware hash. During development, debug firmware includes extensive debug hooks, modes, and messages for testing. If debug firmware images leak—and they commonly do—attackers can use them on production devices when the same key is used, gaining significant system control capabilities through debug functionality.

Risk

Reusing keys for debug and production has severe implications. Debug firmware leaks enable production attacks. Debug hooks exploitable on production systems. Privilege escalation possible. System compromise achievable. Intellectual property exposed. Unauthorized code execution enabled. Memory read/write access possible. Identity assumption attacks enabled. The root of trust is fundamentally compromised.

Solution

Use different keys for production and debug firmware. Allocate sufficient storage for multiple cryptographic keys. If storage is constrained, consider hardware-assisted key management. Implement key revocation mechanisms. Use separate signing infrastructure for debug and production. Ensure debug firmware cannot be loaded on production devices. Consider using different key sizes or algorithms for additional separation. Implement hardware fuses to permanently disable debug capabilities in production.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Debug modes expose sensitive data and memory contents.
IntegrityScope: Integrity

Debug firmware can modify memory and system state.
AvailabilityScope: Availability

Debug capabilities can disrupt normal system operation.
Access ControlScope: Access Control, Authentication

Privilege escalation and identity assumption through debug interfaces.

Example Code

Vulnerable Code

// Vulnerable: Single key for both debug and production

#include <stdint.h>
#include <stdbool.h>
#include <string.h>

// VULNERABLE: Single public key for all firmware
static const uint8_t FIRMWARE_PUBLIC_KEY[256] = {
    0x30, 0x82, 0x01, 0x0A, 0x02, 0x82, 0x01, 0x01,
    // ... 2048-bit RSA public key (same for debug and production)
    // This key is embedded in silicon/OTP
};

typedef struct {
    uint32_t magic;
    uint32_t version;
    uint32_t flags;  // Bit 0: debug mode
    uint32_t size;
    uint8_t  signature[256];
    uint8_t  payload[];
} firmware_header_t;

// VULNERABLE: No differentiation between debug and production
bool vulnerable_verify_firmware(const firmware_header_t* header) {
    uint8_t computed_hash[32];
    uint8_t decrypted_hash[32];

    // Compute hash of firmware payload
    sha256_hash(header->payload, header->size, computed_hash);

    // VULNERABLE: Uses same key for debug and production
    rsa_decrypt(FIRMWARE_PUBLIC_KEY, header->signature, decrypted_hash);

    // VULNERABLE: Only checks signature, not firmware type
    return memcmp(computed_hash, decrypted_hash, 32) == 0;
}

bool vulnerable_boot_firmware(const firmware_header_t* header) {
    // VULNERABLE: Debug firmware accepted on production device
    if (!vulnerable_verify_firmware(header)) {
        return false;
    }

    // VULNERABLE: Debug mode flag can be set in leaked debug firmware
    if (header->flags & 0x01) {
        enable_debug_mode();  // Attacker gains debug access!
    }

    execute_firmware(header->payload, header->size);
    return true;
}

// Attack scenario:
// 1. Debug firmware image leaks from development
// 2. Attacker extracts debug firmware with debug flag set
// 3. Debug firmware is signed with the same key as production
// 4. Attacker loads debug firmware onto production device
// 5. Debug firmware boots successfully (same key!)
// 6. Attacker has full debug access on production hardware
# Vulnerable: Firmware signing infrastructure with shared keys

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding

class VulnerableSigningInfrastructure:
    def __init__(self):
        # VULNERABLE: Single key pair for everything
        self.private_key = self._load_master_key()
        self.public_key = self.private_key.public_key()

    def _load_master_key(self):
        # VULNERABLE: Same key used for all purposes
        return load_key_from_hsm("master_signing_key")

    def sign_debug_firmware(self, firmware_data):
        """Sign debug firmware - VULNERABLE: Uses production key."""
        # VULNERABLE: Same key as production
        return self._sign_data(firmware_data)

    def sign_production_firmware(self, firmware_data):
        """Sign production firmware - VULNERABLE: Same key as debug."""
        # VULNERABLE: Same key as debug
        return self._sign_data(firmware_data)

    def _sign_data(self, data):
        return self.private_key.sign(
            data,
            padding.PKCS1v15(),
            hashes.SHA256()
        )

# Build system using vulnerable infrastructure
class VulnerableBuildSystem:
    def __init__(self):
        self.signing = VulnerableSigningInfrastructure()

    def build_debug_firmware(self, source_code):
        """Build debug firmware with extensive hooks."""
        firmware = compile_with_debug_flags(source_code)

        # Add debug capabilities
        firmware = add_debug_shell(firmware)
        firmware = add_memory_dump_function(firmware)
        firmware = add_jtag_enable(firmware)

        # VULNERABLE: Signed with same key as production
        signature = self.signing.sign_debug_firmware(firmware)

        return package_firmware(firmware, signature, debug=True)

    def build_production_firmware(self, source_code):
        """Build production firmware."""
        firmware = compile_with_release_flags(source_code)

        # VULNERABLE: Same key as debug, debug firmware works here too
        signature = self.signing.sign_production_firmware(firmware)

        return package_firmware(firmware, signature, debug=False)

Fixed Code

// Fixed: Separate keys for debug and production

#include <stdint.h>
#include <stdbool.h>
#include <string.h>

// FIXED: Separate public keys for different firmware types
static const uint8_t PRODUCTION_PUBLIC_KEY[384] = {
    0x30, 0x82, 0x01, 0x8A, 0x02, 0x82, 0x01, 0x81,
    // ... 3072-bit RSA public key for production
};

static const uint8_t DEBUG_PUBLIC_KEY[384] = {
    0x30, 0x82, 0x01, 0x8A, 0x02, 0x82, 0x01, 0x81,
    // ... Different 3072-bit RSA public key for debug
    // Only installed in debug/development units
};

// FIXED: Device mode stored in hardware fuse
typedef enum {
    DEVICE_MODE_PRODUCTION = 0,
    DEVICE_MODE_DEVELOPMENT = 1
} device_mode_t;

// FIXED: Read device mode from OTP fuses (cannot be changed)
device_mode_t get_device_mode(void) {
    return read_otp_fuse(DEVICE_MODE_FUSE_ADDR);
}

typedef struct {
    uint32_t magic;
    uint32_t version;
    uint32_t firmware_type;  // 0 = production, 1 = debug
    uint32_t size;
    uint8_t  signature[384];
    uint8_t  payload[];
} firmware_header_t;

// FIXED: Verify with appropriate key based on firmware type
bool secure_verify_firmware(const firmware_header_t* header) {
    const uint8_t* public_key;
    device_mode_t device_mode = get_device_mode();

    // FIXED: Select key based on firmware type
    if (header->firmware_type == 0) {
        // Production firmware - always allowed
        public_key = PRODUCTION_PUBLIC_KEY;
    } else if (header->firmware_type == 1) {
        // FIXED: Debug firmware only on development devices
        if (device_mode != DEVICE_MODE_DEVELOPMENT) {
            log_security_event("Debug firmware rejected on production device");
            return false;
        }
        public_key = DEBUG_PUBLIC_KEY;
    } else {
        log_security_event("Unknown firmware type");
        return false;
    }

    uint8_t computed_hash[32];
    uint8_t decrypted_hash[32];

    sha256_hash(header->payload, header->size, computed_hash);
    rsa_decrypt(public_key, header->signature, decrypted_hash);

    return secure_compare(computed_hash, decrypted_hash, 32);
}

bool secure_boot_firmware(const firmware_header_t* header) {
    if (!secure_verify_firmware(header)) {
        return false;
    }

    // FIXED: Debug features only available on development devices
    if (header->firmware_type == 1) {
        // Already verified device is in development mode
        enable_debug_mode();
    }

    execute_firmware(header->payload, header->size);
    return true;
}

// FIXED: Production devices have debug key fuse blown
void secure_production_provisioning(void) {
    // Blow fuse to set production mode (irreversible)
    blow_otp_fuse(DEVICE_MODE_FUSE_ADDR, DEVICE_MODE_PRODUCTION);

    // FIXED: Optionally zero out debug key storage
    secure_erase_key_slot(DEBUG_KEY_SLOT);
}
# Fixed: Firmware signing infrastructure with separate keys

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
from enum import Enum

class FirmwareType(Enum):
    PRODUCTION = "production"
    DEBUG = "debug"
    FACTORY_TEST = "factory_test"

class SecureSigningInfrastructure:
    def __init__(self):
        # FIXED: Separate keys for each firmware type
        self._keys = {
            FirmwareType.PRODUCTION: self._load_production_key(),
            FirmwareType.DEBUG: self._load_debug_key(),
            FirmwareType.FACTORY_TEST: self._load_factory_key(),
        }

    def _load_production_key(self):
        """Load production signing key from secure HSM."""
        # FIXED: Separate key with strict access controls
        return load_key_from_hsm(
            key_id="production_signing_key",
            require_quorum=True,  # Multiple operators required
            audit_log=True
        )

    def _load_debug_key(self):
        """Load debug signing key from development HSM."""
        # FIXED: Different key, different HSM, different access
        return load_key_from_hsm(
            key_id="debug_signing_key",
            hsm="development_hsm",
            require_quorum=False  # Less strict for development
        )

    def _load_factory_key(self):
        """Load factory test signing key."""
        return load_key_from_hsm(
            key_id="factory_test_key",
            hsm="factory_hsm"
        )

    def sign_firmware(self, firmware_data: bytes, firmware_type: FirmwareType) -> bytes:
        """Sign firmware with the appropriate key."""
        if firmware_type not in self._keys:
            raise ValueError(f"Unknown firmware type: {firmware_type}")

        private_key = self._keys[firmware_type]

        # FIXED: Embed firmware type in signed data
        type_prefix = firmware_type.value.encode() + b'\x00'
        data_to_sign = type_prefix + firmware_data

        signature = private_key.sign(
            data_to_sign,
            padding.PSS(
                mgf=padding.MGF1(hashes.SHA384()),
                salt_length=padding.PSS.MAX_LENGTH
            ),
            hashes.SHA384()
        )

        # FIXED: Log signing operation
        audit_log(
            action="firmware_signed",
            firmware_type=firmware_type.value,
            firmware_hash=sha384(firmware_data).hexdigest()
        )

        return signature

class SecureBuildSystem:
    def __init__(self):
        self.signing = SecureSigningInfrastructure()

    def build_debug_firmware(self, source_code):
        """Build debug firmware - only works on debug devices."""
        firmware = compile_with_debug_flags(source_code)
        firmware = add_debug_capabilities(firmware)

        # FIXED: Uses debug-specific key
        signature = self.signing.sign_firmware(
            firmware,
            FirmwareType.DEBUG
        )

        return package_firmware(
            firmware,
            signature,
            firmware_type=FirmwareType.DEBUG
        )

    def build_production_firmware(self, source_code):
        """Build production firmware - works only on production devices."""
        firmware = compile_with_release_flags(source_code)

        # FIXED: Debug firmware signed with different key won't work
        signature = self.signing.sign_firmware(
            firmware,
            FirmwareType.PRODUCTION
        )

        return package_firmware(
            firmware,
            signature,
            firmware_type=FirmwareType.PRODUCTION
        )
// Fixed: Hardware boot ROM with separate key verification

module secure_boot_rom (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        device_mode_fuse,  // 0=production, 1=development
    input  wire [31:0] firmware_type,
    input  wire [3071:0] signature,
    input  wire [255:0] firmware_hash,
    output reg         boot_authorized,
    output reg  [7:0]  boot_status
);

    // FIXED: Separate key storage
    reg [3071:0] production_public_key;  // From OTP
    reg [3071:0] debug_public_key;       // From OTP (zeroed in production)

    // Boot status codes
    localparam STATUS_OK = 8'h00;
    localparam STATUS_INVALID_TYPE = 8'h01;
    localparam STATUS_DEBUG_ON_PROD = 8'h02;
    localparam STATUS_SIGNATURE_FAIL = 8'h03;

    reg [3071:0] selected_key;
    reg key_selected;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            boot_authorized <= 1'b0;
            boot_status <= STATUS_OK;
            key_selected <= 1'b0;
        end else begin
            // FIXED: Key selection based on firmware type and device mode
            case (firmware_type)
                32'h0000_0000: begin  // Production firmware
                    selected_key <= production_public_key;
                    key_selected <= 1'b1;
                end

                32'h0000_0001: begin  // Debug firmware
                    // FIXED: Only allow debug on development devices
                    if (device_mode_fuse == 1'b1) begin
                        selected_key <= debug_public_key;
                        key_selected <= 1'b1;
                    end else begin
                        // FIXED: Reject debug firmware on production
                        boot_authorized <= 1'b0;
                        boot_status <= STATUS_DEBUG_ON_PROD;
                        key_selected <= 1'b0;
                    end
                end

                default: begin
                    boot_authorized <= 1'b0;
                    boot_status <= STATUS_INVALID_TYPE;
                    key_selected <= 1'b0;
                end
            endcase
        end
    end

endmodule

CVE Examples

  • CVE-2020-10713: BootHole vulnerability where signed bootloaders could be replaced, highlighting risks of key management in boot chains.
  • CVE-2018-3665: Debug features enabled through firmware key compromise in certain processors.

  • CWE-693: Protection Mechanism Failure (parent)
  • CWE-321: Use of Hard-coded Cryptographic Key (peer)
  • CWE-1207: Debug and Test Problems (category)
  • CWE-522: Insufficiently Protected Credentials (related)

References

  1. MITRE Corporation. "CWE-1291: Public Key Re-Use for Signing both Debug and Production Code." https://cwe.mitre.org/data/definitions/1291.html
  2. NIST. "Guidelines for the Selection, Configuration, and Use of Transport Layer Security (TLS) Implementations"
  3. ARM. "Platform Security Architecture"