Format of mb1_t194_prod.bin OEM signature header

Hello.

Currently I’m worning on a server software which manages flashing and OTA update of Jetson devices, which includes, of course, generation of signed files. For ordinary images (like kernel or firmwares) I’m understand the format of whole signature block, but MB1 image is rather special.

There is a block of data added by flashing tool during image generation. It is different from similar blocks in other images: these blocks contain only magic header and file length, but not in MB1:

00000bb0  4d 42 31 42 e0 bd 03 00  00 00 02 40 00 00 02 40  |MB1B.......@...@|
00000bc0  00 31 04 00 08 00 00 00  12 00 00 00 00 00 00 00  |.1..............|

MB1B is a magic value, e0 bd 03 00 is little-endinan file length, but what is the meaning of other fields? Is there any documentation on format of this block?

This header is also present in nvidia-signed part, but it is slightly different there:

00000f00  4d 42 31 42 e0 bd 03 00  00 00 02 40 00 00 02 40  |MB1B.......@...@|
00000f10  00 31 04 00 00 00 00 00  00 00 00 00 00 00 00 00  |.1..............|

As you can see, there are extra 08 00 00 00 12 bytes in the OEM-signed part. What do they mean?

hello initrd.img,

may I know why you’re concerned with that, mb1 only release with binary files.

Because when I’m making OTA release on my server, I"m need to sign MB1 with PKC key and encrypt with SBK. Flashing tool in Jetpack treats MB1 differently from all other binary images, so to generate valid MB1 binary for SBK/PKC secured device I need to recreate that process on my server.

there’s l4t_sign_image.sh script file you may sign/encrypt the binary file locally.

Yes, it is there (and I’m already reproduced that with 40 lines of Java code), but it does not work with MB1 binary.

Just compare execution logs: test.bin is just random binary, mb1_test.bin is a copy of mb1_t194_prod.bin:

Logs
jetpack@jetpack-vm:~/jetpack$ ./l4t_sign_image.sh --key ~/share/keys/pkc.pem --encrypt_key ~/share/keys/sbk.txt --chip 0x19 --file test.bin --split False
/home/jetpack/jetpack/bootloader/tegraflash.py --chip 0x19 --key /home/jetpack/share/keys/pkc.pem --encrypt_key /home/jetpack/share/keys/sbk.txt --cmd sign test.bin 
Welcome to Tegra Flash
version 1.0.0
Type ? or help for help and q or quit to exit
Use ! to execute system commands
 
[   0.0030 ] Generating signature
[   0.0099 ] tegrasign_v3.py --getmode mode.txt --key /home/jetpack/share/keys/pkc.pem
[   0.0134 ] Key size i[   0.0193 ] s 384 bytes
[   0.0233 ] tegrasign_v3.py --getmontgomeryvalues montgomery.bin --key /home/jetpack/share/keys/pkc.pem
[   0.0267 ] K[   0.0288 ] ey size is 384 bytes
[   0.0290 ] Saving Montgomery values in montgomery.bin
[   0.0288 ] header_magic: 75922893
[   0.0289 ] Encrypting file
[   0.0318 ] tegrahost_v2 --chip 0x19 --align 1_test.bin
[   0.0339 ] 
[   0.0342 ] header_magic: 75922893
[   0.0396 ] tegrasign_v3.py --key /home/jetpack/share/keys/sbk.txt --list 1_test.bin_list.xml
[   0.0547 ] Key is a SBK key
[   0.0547 ] Key Size is 16 bytes
[   0.0556 ] 22240
[   0.0682 ] tegrahost_v2 --chip 0x19 0 --updatesigheader 1_test.bin.encrypt 1_test.bin.hash zerosbk
[   0.0712 ] 
[   0.0739 ] tegrahost_v2 --chip 0x19 --align 1_test.bin.encrypt
[   0.0752 ] 
[   0.0752 ] header_magic: 42058c1e
[   0.0782 ] tegrahost_v2 --appendsigheader 1_test.bin.encrypt oem-rsa-sbk --chip 0x19 0 --magicid DATA
[   0.0798 ] adding BCH for 1_test.bin.encrypt
[   0.0814 ] 
[   0.0850 ] tegrasign_v3.py --key /home/jetpack/share/keys/pkc.pem --list 1_test.bin_sigheader.encrypt_list.xml --pubkeyhash pub_key.key
[   0.0895 ] K[   0.0911 ] ey size is 384 bytes
[   0.1636 ] Saving pkc public key in pub_key.key
[   0.1665 ] tegrahost_v2 --chip 0x19 0 --updatesigheader 1_test.bin_sigheader.encrypt.signed 1_test.bin_sigheader.encrypt.sig oem-rsa --pubkeyhash pub_key.key --setmontgomeryvalues montgomery.bin
[   0.1694 ] 
[   0.1699 ] Signed and encrypted file: /home/jetpack/nvidia/nvidia_sdk/JetPack_4.6_Linux_JETSON_AGX_XAVIER_TARGETS/Linux_for_Tegra/test.bin_sigheader.encrypt.signed
l4t_sign_image.sh: Generate header for test.bin_sigheader.encrypt.signed
l4t_sign_image.sh: chip 0x19: add 0x56da to offset  0x8 in sig file
l4t_sign_image.sh: Generate 16-byte-size-aligned base file for test.bin_sigheader.encrypt.signed
l4t_sign_image.sh: the signed file is /home/jetpack/nvidia/nvidia_sdk/JetPack_4.6_Linux_JETSON_AGX_XAVIER_TARGETS/Linux_for_Tegra/test_sigheader.bin.encrypt.signed
jetpack@jetpack-vm:~/jetpack$ ./l4t_sign_image.sh --key ~/share/keys/pkc.pem --encrypt_key ~/share/keys/sbk.txt --chip 0x19 --file mb1_test.bin --split False
/home/jetpack/jetpack/bootloader/tegraflash.py --chip 0x19 --key /home/jetpack/share/keys/pkc.pem --encrypt_key /home/jetpack/share/keys/sbk.txt --cmd sign mb1_test.bin 
Welcome to Tegra Flash
version 1.0.0
Type ? or help for help and q or quit to exit
Use ! to execute system commands
 
[   0.0026 ] Generating signature
[   0.0066 ] tegrasign_v3.py --getmode mode.txt --key /home/jetpack/share/keys/pkc.pem
[   0.0241 ] Key size is 38[   0.0262 ] 4 bytes
[   0.0303 ] tegrasign_v3.py --getmontgomeryvalues montgomery.bin --key /home/jetpack/share/keys/pkc.pem
[   0.0334 ] K[   0.0359 ] ey size is 384 bytes
[   0.0364 ] Saving Montgomery values in montgomery.bin
[   0.0363 ] header_magic: 4e564441
l4t_sign_image.sh: Error: Unable to find the signed file generated by tegraflash.py

Probably this is due to fact that MB1 already has a signature header, but I’m not sure.

hello initrd.img,

there’re two approaches to test this further. (1) flash script with --no-flash options, (2) the massflashing tool to generate a “massflash blob”. I could have both ways to create the mb1_t194_prod_sigheader.bin.encrypt, which writes to mb1 partition.

Yes, they do, but this is not a solution, unfortunately. Signing has to be performed on a secure server, and these megabytes of bash scripts interspersed with Python interspersed with bash again, having full root access to a host system is a huge attack surface, compared to relatively small server on a high-level language such as Java.

This becomes more problematic when you realize that these scripts don’t actualy perform some complex and elaborate tasks: you can replace whole l4t_sign_image.sh with all tegrasign, tegraflash and tegrahost binaries by 45 lines of Java which will do the same for regular binaries.

I can even attach a concrete example to prove my words:

package some.pkg;

import org.bouncycastle.util.encoders.Hex;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;

public class XavSign {
    private static byte[] reverseArray(byte[] x) {
        byte[] r = new byte[x.length];
        for (int i = 0; i < x.length; i++) r[x.length - 1 - i] = x[i];
        return r;
    }

    private static byte[] toLittleEndian(BigInteger x, int byteLength) {
        byte[] out = new byte[byteLength];
        byte[] rawBytes = x.toByteArray();
        for (int i = 0; i < Math.min(byteLength, rawBytes.length); i++) out[i] = rawBytes[rawBytes.length - 1 - i];
        return out;
    }

    /**
     * In-place encrypt {@code dataBuffer} with SBK and sign it with PKC, returning detached signature header.
     * Function assumes that buffer is already padded to 16 bytes.
     *
     * @param pkc        Private key for RSA signature
     * @param sbk        Symmetric 128-bit key for SBK encryption
     * @param dataBuffer Data buffer
     * @param magic      4-character magic value
     * @return           Detached signature header
     */
    public static byte[] encryptAndSign(RSAPrivateKey pkc, byte[] sbk, ByteBuffer dataBuffer, String magic)
            throws Exception
    {
        Cipher aes = Cipher.getInstance("AES/CBC/NoPadding");
        aes.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sbk, "AES"), new IvParameterSpec(new byte[16]));
        aes.doFinal(dataBuffer.clear(), dataBuffer.duplicate());

        MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
        sha256.update(dataBuffer.clear());
        byte[] dataHash = sha256.digest();

        byte[] headerArray = new byte[4096];
        ByteBuffer header = ByteBuffer.wrap(headerArray).order(ByteOrder.LITTLE_ENDIAN);
        ByteBuffer signedHeader = ByteBuffer.wrap(headerArray, 0xb90, 0x470).slice().order(ByteOrder.LITTLE_ENDIAN);

        signedHeader.put(16, (byte) 1);
        for (int i = 0; i < 4; i++) signedHeader.put(32 + i, (byte) magic.charAt(i));
        signedHeader.putInt(36, dataBuffer.capacity());
        signedHeader.put(80, dataHash);

        sha256.update(signedHeader.clear());
        byte[] signedHeaderHash = sha256.digest();

        header.putInt(0, 0x4144564e); // 'NVDA' signature
        header.put(16, signedHeaderHash);

        Signature sig = Signature.getInstance("RSASSA-PSS");
        sig.setParameter(new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1));
        sig.initSign(pkc);
        sig.update(signedHeader.clear());
        header.put(48, reverseArray(sig.sign()));

        int bitLength = pkc.getModulus().bitLength();
        int byteLength = bitLength / 8;
        header.put(0x1f0, toLittleEndian(pkc.getModulus(), byteLength));

        if (bitLength > 2048) {
            // Montgomery values:
            BigInteger R = BigInteger.ONE.shiftLeft(bitLength);
            BigInteger N = pkc.getModulus();
            BigInteger Ni = R.multiply(R.modInverse(N)).subtract(BigInteger.ONE).divide(N);
            BigInteger RR = R.multiply(R).mod(N);

            header.put(0x1f0 + byteLength, toLittleEndian(Ni, byteLength));
            header.put(0x1f0 + 2 * byteLength, toLittleEndian(RR, byteLength));
        }

        return headerArray;
    }

    public static void main(String[] args) throws Exception {
        RSAPrivateKey key = (RSAPrivateKey) CryptoUtils.parseKey(Files.readString(Paths.get("pkc.pem")));
        byte[] sbk = Hex.decode(Files.readString(Paths.get("sbk.txt")));

        byte[] data = Files.readAllBytes(Paths.get("eks.img"));
        byte[] sigHeader = encryptAndSign(key, sbk, ByteBuffer.wrap(data), "EKSB");

        try (OutputStream out = Files.newOutputStream(Paths.get("eks_sigheader.img.java_out"))) {
            out.write(sigHeader);
            out.write(data);
        }
    }
}

This snippet of code produces a byte-exact copy of images signed by original flash.sh, with exception of a non-deterministic randomized RSASSA-PSS signature.

For security perspective it’s a total nightmare to use few megabytes of bash mess that requires root privileges instead of 45 lines of Java. Especially to use it in places that handle sensitive key material. Needless to say that this also implies deployment and maintainance difficulties - you have to use outdated VM with Ubuntu 18.04 LTS, you have to download everything with SDK Manager every time, and all that stuff.

That snippet works perfectly for same set of binaries as l4t_sign_image.sh does. With slight modification it will also work for pre-signed binaries. The only thing that remains a bit unclear (but I didn’t yet tested whether it will affect the operation of signed binaries) is a meaning of few bytes in OEM-signed part, at offsets 0xbc4 and 0xbc8.