Digital signatures for RP2350 boards

Writing a pre-flight check for burning the cert. Let me know if something seems off. I’m not transporting with a CRC or anything because I assume a corrupt cert won’t verify:

  • Is the cert valid against the public key?
  • Does the cert serial match the RPi serial?
  • Are the pages we’re going to use already protected?**
  • Are the rows we plan to use empty?
  • Burn and verify each row (burn is actually very slow!)
Number of OTP pages required: 7
Protection pages: 004 : 00a
First protection row to program: f89
Last protection row to program: f95
Cert write to OTP: 32

**Code to determine what to protect seems to be working. Return status 32 means OTP not blank (because I have a cert there :wink: )

One issue with my current approach is that I use the first row (2 bytes) to store the length of the cert. I don’t believe the length is consistent. So I need to:

  1. Reshuffle all the math to account for an extra row, or
  2. We’ve discussed some kind of directory structure with entries starting from the last free page and moving forward. If that contains the data length, then we wouldn’t need to start the cert with it.

Leading to a few concerns:

  • The protection fuses are not very granular, so the directory table would be unprotected (only soft protection) and could become corrupted rendering everything useless.
  • Perhaps the “really important stuff” like cert, any hardware/manufacturing info should have a fixed location (along with a directory entry) so it can survive a corrupt directory table
  • Looping us back to needing the cert length at in the first row of the cert.

EDIT: it seems like if only the serial and validity change the cert is the same length, but what if we update a field in the future? Think the issue still applies.

1 Like

I’m a fan of the directory structure

The DER format has the length encoded at the beginning!

  • The first byte is 0x30
  • The next byte is the length (maybe):
    • If the length is less than 128 bytes, then the length is second byte (up to 0x80).
    • If the length is 128 to 256, then you’ll have 0x81 for the second byte and the third byte will be the number of bytes from 128 to 256 (0x81 XX)
    • If the length is greater than 256, then byte 2 is 0x82 and the next two bytes are the length: 0x82 XX XX

If I look at the post above where you’re showing the row data, I see:

  • Row 0x101 is 0x8230 - correcting for endianness, that’s 0x30, 0x82. This is a DER (0x30 in first byte), length is > 256 bytes (0x82) in the second byte
  • Row 0x102 is 0x0203; changing endianness makes is 0x0302 or 770 decimal
    • (I think that means read the next 770 bytes as cert info; you’re already past the first four bytes).

I see Row 0x100 is 0x0603, little endian for decimal 774 which matches the above stuff.

Parsing ASN.1 stuff is strange with rules like that :man_shrugging:

For example, the cert I made before:

┌──(matty💊s76)-[~/tmp]
└─$ ll cert.der          
-rw-rw-r-- 1 matty matty 1477 Jan 29 09:19 cert.der
                                                                                                                        
┌──(matty💊s76)-[~/tmp]
└─$ hexdump -C cert.der | head -3  
00000000  30 82 05 c1 30 82 03 a9  a0 03 02 01 02 02 14 74  |0...0..........t|
00000010  bb 7e ac 70 85 84 d7 b7  c9 ca f4 a2 0a 3f 26 ef  |.~.p.........?&.|
00000020  88 49 42 30 0d 06 09 2a  86 48 86 f7 0d 01 01 0b  |.IB0...*.H......|
                                                                            
  • 0x30 == DER
  • 0x82 == len > 256 bytes
  • 0x05c1 == 1473 bytes (cert is 1477, we’ve already read 4 bytes)

EDIT: Is the endianness thing part of OTP?
EDIT 2: Ope, forgot this ARM is little endian :man_facepalming:

1 Like

Oh excellent, thank you! I assume for the pub key as well. Will add that. It also gives a way to search for the cert if the directory is corrupted I suppose.

The packing of the bytes was entirely my choice, but I did so thinking I can do some fancy magic to pull it back together. Currently it’s just << | , and there really isn’t any shame in that either.

1 Like

Yes, that should hold true for all DER encoded things, because that are all ASN.1.

When penetrating and desperate to find secrets in binaries, I’ve searched for 0x3080/81/82. You don’t get hits very often, but it’s worth a try :rofl:

(I mean you get a lot of hits, but not often a key)

2 Likes
Cert length: 774
Pubkey length: 294

Thanks again for the tip! Seems to be working for 0x82.

    if (der[1] <= 0x80) {
        return der[1]+2;
    } else if (der[1] == 0x81) {
        return der[2]+3;
    } else if (der[1] == 0x82) {
        return ((der[2] << 8) | der[3])+4;
    }

As best I can tell, the DER length depends on the number of length fields used? So +2/+3/+4? Does this look ok to you?

1 Like

Yes, that looks good!

You don’t need to store the public key in the OTP. Only the cert needs to be immutable. Public key can (and should) be hard coded in the firmware.

If the key is in OTP, a counterfeiter could make their own keys and certs that look legit, and put their public key in the OTP.

With the key in the firmware, anyone trying to change it has to do a PR, which should be reviewed and caught.

Edit: ope - second case should be der [2] + 128 + 3

1 Like

Great, thank you so much! Updated.

Yes, the public key is in the firmware. I just wanted to be fancy and extract the length from it as well :slight_smile:

1 Like

Oh good; sorry about the confusion. I was answering from my phone and didn’t actually look at the code :slight_smile:

1 Like

Cert pages locked. One full cycle completed, and one PICO2 sacrificed :slight_smile:

The code is an absolute mess, but now I know how to put it all together this weekend.

Some things:

  • Will extract the length from the cert instead of burning it at the beginning of the cert
  • I’m going to run with cert starts at row 0x100 until suggested otherwise
  • Need to look at what Henry proposed for the directory system and implement that

image

  • I’m going to expand the “model” to include the revision, but there’s a little bug somewhere I may need Henry to have a look at, we’ll see
  • We have a field “manufacturing data” in the whitelabel space, it appears as the board-ID. I’m planning to program the RPi unique ID here as ASCII numbers so the board can be identified even if a firmware won’t load.
1 Like

Looked into this. I don’t see the ability to do per file protections in github, but I can make the public key a submodule in a repo where we lock the main branch and don’t accept pull requests.

1 Like

Pushed a big update to otp_ branch:

  • Complete cert verification and burning function that can be used from binmode
  • Updated cert function to read from OTP
  • That big buffer was problematic again, so I wrote my own thin PEM encoder

Todo:

  • Manufacturing binmode to burn cert
  • cert command burn cert from file (for existing users)
  • Check in public key as a git submodule to prevent pull request attacks
  • Remove dump cert&key to file (too nasty, and probably not very useful beyond debugging)?
  • Directory structure format?
  • Location of cert and other standard formatted data in OTP?
  • Format of other data in OTP?

Welcome comments on “?” items.

Edit:

UF2 Bootloader v1.0
Model: Bus Pirate 6
Board-ID: F9:04:9D:32:5E:76:45:4F

Starting fresh with a new PICO2:

  • Whitelabel info burned
  • Manufacturing string burned (unique ID as ASCII)
  • Cert burned from “file” (the file is simulated as a variable in the cert command)

Next I’ll generate the final master private key and check the public key in as a submodule.

2 Likes

My $0.02:

If it’s necessary to get the cert, it would just be a matter of getting all of OTP and pulling it out. Even just displaying it all as hex in the terminal and copy/pasting for a Python script or something to extract. A fun exercise for someone :slightly_smiling_face:

I’m still a fan of @henrygab 's work there.

Starting at row 0x100 makes sense.

Created the “real” private key and stored it safely.

The public key is now in a separate repo, which is included as a submodule. Be sure to init the submodules when working with the otp_ branch.

General cleanup and finalizing. The burn cert from file appears to work, but I have only simulated it and not used an actual Bus Pirate.

For the “other info” page I’d like to store some of the cert info in the clear:

  • 0x01 Device name
  • 0x02 Device version
  • 0x03 Device revision
  • 0x04 Production location
  • 0x05 Manufacturer
  • 0x06 Production date

0x01Bus Pirate0x00 0x02 5XL 0x00 0x03 0 0x00 0x04 2015-2-16 16:39 0x00 0x05China0x00

I’m thinking maybe store these as ASCII text with a beginning token and null termination. It is redundant, but I would prefer not to parse the cert to get at that info. Especially if there are ever clone devices without a valid cert. We could just trust the cert even if not valid, but I would think an invalid cert shouldn’t be trusted.

There are 128bytes in the page, any other info that should go in there?

Gathering as much of the discussion in one place as I can find.

  • Directory Entries are 4 rows
  • First row is EntryType, 0x0000 is end of list (no more entries)
  • Directory items are stored backwards from the last writable address 0xf7c-0xf7e
  • Search of directory items is from back to front, looking for 0x0000 (unprogrammed) as end of list. This allows to add more later.

EntryTypes:
0. EOL

  1. usb_whitelabel (?)
  2. x509 cert
  3. Info strings

The current setup is something like:

   typedef struct _OTP_DIRECTORY_ITEM {
        uint16_t EntryType; // with 0x0000 defined as "end of list"
        uint16_t StartRow;  // row where that entry is stored
        uint16_t RowCount;  // count of consecutive rows
        uint16_t CRC16; // Validates the prior three entries.
    } OTP_DIRECTORY_ITEM;

    enum {
        OTP_DIRECTORY_ITEM_TYPE_END = 0x0000,
        OTP_DIRECTORY_ITEM_TYPE_USB_WHITELABEL = 0x0001,
        OTP_DIRECTORY_ITEM_TYPE_CERT = 0x0001,
        OTP_DIRECTORY_ITEM_TYPE_DEVICE_INFO = 0x0002,
        
    };

    OTP_DIRECTORY_ITEM directory_item[4] = {
        {OTP_DIRECTORY_ITEM_TYPE_END, 0, 0, 0},
        {OTP_DIRECTORY_ITEM_TYPE_CERT, 0x100, 7*64, 0},
        {OTP_DIRECTORY_ITEM_TYPE_DEVICE_INFO, 0x2c0, 1*64, 0},
        {OTP_DIRECTORY_ITEM_TYPE_USB_WHITELABEL, 0xd0, 1*64, 0},
    };

I’m super inclined to put the info page first, before the cert. I know it doesn’t matter, but it feels orderly to me.

Really close to done with with this - for now… I can see the finish line :slight_smile:

1 Like

This should probably move to the other thread now, but the discussion of OTP directory is here so I will continue for now:

A simple opt command to demonstrate:

  • Searching for existing records, backwards
  • 0x0000 is the end of directory indicator
  • Write entry if not present, and verify
  • Read entry and verify CRC

Now for integration I think it will go something like this:

  • Add white label directory entry when programming white label info
  • Parse device info from cert, add device info page and directory entry
  • Burn the cert, add cert directory entry

Then tie it all up in a manufacturing binmode and it should be done :partying_face:

1 Like

So cool to see those ideas come to reality :slightly_smiling_face:

Getting the raw individual OID values turned out to be quite the adventure, but the info section now seems to be parsing out of the cert data correctly:

:0x01:Bus Pirate:0x00::0x02:6:0x00::0x03:2:0x00::0x04:CN:0x00::0x05:Where Labs LLC:0x00::0x06:2025-02-16 13:01:03:0x00:

1 Like

Yeah, ASN.1 can be weird, right? The whole tag value notation with the different encodings is something for sure

1 Like

IIRC, per-file protections are possible (branch protection rules?) … I’ll take a look in the next few days. Submodules increase complexity for non-expert git users … I’ve found it a support headache and generally recommend avoiding it where possible.

2 Likes

Sure, if there’s a better way to do it, I’m all for it. It also slows down my pushes by checking the submodule every time.