IPMI Firmware signature

Supermicro implemented signature firmware for the latest versions of their platform, as they claimed here: https://www.supermicro.com/en/support/security_Cryptographic

but there isn’t all that much detail on that page to understand what is signed in the firmware, and how. Stay along and we’ll discuss that here.

Flash layout

The firmware distributed by Supermicro follows the layout of the flash available on the platform.

On an Aspeed Ast2400/Ast2500 based IPMI, this would look as shown in Figure 1:

u-boot0x00x40000rootfs0x4000000x1400000kernel0x14000000x1500000web0x17000000x1800000
Figure 1: Flash layout

To note, there is a JFFS (flash-optimized file system) located in between the u-boot segment and the rootfs, that is used by the IPMI to store non-volatile data (configuration, certificate, keys, …). That is not distributed, and this is re-created by the firmware if absent from the flash at first boot.

Firmware signature

There is a bunch of space still available, and Supermicro used that space to store public keys and signatures.

The public key used to sign the payload is stored at the address 0x16ff800 in the PEM DER ASN.1 PKCS#1 format (-----BEGIN RSA PUBLIC KEY-----). In the firmware payload distributed, it is stored at the offset 0x16ff804. The key may or may not be the same as the one stored in the flash. This is used to update the signing key should it need to be.

The key used to sign the payload is currently an RSA 2048bits key.

So how is the firmware signed? Two signatures are used.

The first signature (at 0x16ffe00) checks the content of the u-boot, the rootfs, the webfs, the kernel as well as the key located at 0x16ff804. Should this first signature succeed, then the key located in the distributed firmware is loaded, and it would then check the second signature (at 0x16ffc00) of u-boot, the rootfs and the webfs sections for the firmware to be signed with that key.

Below is an implementation of the firmware verification mechanism in python.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
#!/usr/bin/env nix-shell
#!nix-shell -i python3 -p python3 python3Packages.cryptography

import argparse
import os
import struct
import sys
from mmap import mmap, PROT_READ

from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import utils, padding


class InvalidSignature(Exception):
    pass


def check(infile, public_key=None):
    with open(infile, "r+b") as f:
        with mmap(f.fileno(), 0) as mm:
            chosen_hash = hashes.SHA256()
            outer_checksum = hashes.Hash(chosen_hash)
            # Add uboot
            outer_checksum.update(mm[0:0x100000])
            # Add rootfs
            outer_checksum.update(mm[0x400000:0x400000+0x1000000])
            # Add kernel, inner key and signature 
            outer_checksum.update(mm[0x1400000:0x1400000+0x2ffc00])
            # Add webfs 
            outer_checksum.update(mm[0x1700000:0x1700000+0x840000])

            # Load public key
            if public_key:
                with open(public_key, 'r+b') as pk:
                    outer_key = serialization.load_pem_public_key(pk.read())
            else:
                key_len = struct.unpack('<I', mm[0x016ff800:0x016ff804])[0]
                key_data = mm[0x016ff804:0x016ff804+key_len]
                outer_key = serialization.load_pem_public_key(key_data)

            # Load signature
            signature = mm[0x16ffe00:0x16ffe00+0x100]

            # And check the signature
            try:
                outer_key.verify(
                    signature,
                    outer_checksum.finalize(),
                    padding.PKCS1v15(),
                    utils.Prehashed(chosen_hash)
                )
            except Exception:
                raise InvalidSignature('outer signature invalid')

            # outer signature is now verified, and there is a second signature
            # on a subset of the firmware already checked
            # Should the key change, it would be checked with the new key
            inner_checksum = hashes.Hash(chosen_hash)
            # Add uboot
            inner_checksum.update(mm[0:0x40000])
            # Add rootfs
            # Note: length looks wrong here. This is only partially checks
            #       the rootfs. but it's checked by the outer signature already
            #       a typo?
            inner_checksum.update(mm[0x400000:0x400000+0x100000])
            # Add kernel, and a lot more thing.
            inner_checksum.update(mm[0x1400000:0x1400000+0x100000])
            # Add webfs 
            inner_checksum.update(mm[0x1700000:0x1700000+0x100000])
            # add parts of the key segment
            inner_checksum.update(mm[0x16f0000:0x16f0000+0xfc00])

            # Load public key
            key_len = struct.unpack('<I', mm[0x016ff800:0x016ff804])[0]
            key_data = mm[0x016ff804:0x016ff804+key_len]
            inner_key = serialization.load_pem_public_key(key_data)

            # Load signature
            signature = mm[0x16ffc00:0x16ffc00+0x100]

            # And check the signature
            try:
                inner_key.verify(
                    signature,
                    inner_checksum.finalize(),
                    padding.PKCS1v15(),
                    utils.Prehashed(chosen_hash)
                )
            except Exception:
                raise InvalidSignature('inner signature invalid')


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='check ipmi firmware signature')
    parser.add_argument('infile', metavar='FIRMWARE', help='file to check')
    parser.add_argument('--public_key', nargs='?', help='public key used to sign the firmware')
    args = parser.parse_args()

    try:
        check(args.infile, args.public_key)
        print('success')
    except InvalidSignature as e:
        print(e.args[0])
        sys.exit(1)
    except Exception as e:
        print(e)
        sys.exit(2)
Supermicro IPMI Firmware signature verification

In the end, the firmware image looks like:

u-boot0x00x40000rootfs0x4000000x1400000kernel0x14000000x1500000web0x17000000x1800000publickey0x16ff8040x16ff9aeouter signature0x16ffe000x16fff00inner signature0x16ffc000x16ffd00
Figure 2: Firmware layout with signature