Digital signatures for RP2350 boards

I’m having flashbacks from engineering school (and indeed, my earlier engineering years) of lab notebooks with the table of contents starting in the back…

That’s a great idea - make life easier for the application programmer since the API code already knows the important part

I like taking the typical FS iterator approach to walking the table, as well as the find_otp_entry() type-specific function.

1 Like

I see the serial number can be set from the command line. Would using the chip id make sense here?

Other info that might make sense?

  • manufacture date
  • hardware version/revision
  • manufacture location (could we use subject field for this?)

How does the validity date work? Should it be set to the manufacturing date plus a few days? In that case I can use it to stop the programming tool from burning an older cert from a previous batch. This isn’t security though, just a extra guardrail in production.

As several have mentioned, it would be super useful to have hardware revision accessible in OTP. Currently we do stuff like charge the cap on the low pass filter for the adjustable VREG, flip it input, if it’s still high it’s rev 10, else rev 8. This will be an issue again when going from 6 to 7 on the same chip.

However, if we lock this in the cert then it starts to smell like a sleazy lock down. So this info should definitely be stored in the clear in the file system described by @henrygab as well, and the firmware should use that as the true correct source, with a fallback.

Silliness

To install the info and cert in existing BP6:

  • at startup prompt to test E9 bug.
  • if bug, it’s first batch
  • if not prompt “didya fix ur six”, most people will probably member that experience. If yes, first batch.
  • if no, then second batch

I would like a “warranty voider” row to burn bits when certain potentially dangerous things are done, like changing core speed or voltage. We don’t really question replacements and getting them back is more trouble than its worth, but it might be useful debugging data.

I had both in my example from previous discussions. The “Dangerous Prototypes” serial is assigned to that BP unit at manufacturing. The rPI serial number would be read from OTP for that same unit. That exposes a case where someone counterfeits the product - they would most likely need to copy the entire OTP from their authentic sample.

BP firmware would read the cert, validate cert with public key, extract the extensions. Read the rPI from OTP and compare to what was in the cert as a second-layer verification.

Even if everything else is good (public key verification, etc.), a certificate won’t validate if the current date is outside of the validity date; that’s why in the past I’ve used 10 or 20 years later for the “not after” date. (ICS crap can sit in production for decades, sigh).

As far as the other info? Sometimes, I’ve put stuff into the subject field like:

  • C (country name): the country, would you use NL or CN?
  • O (organization): Dangerous Prototypes
  • OU (organization unit): Bus Pirate 6 ← whatever model
  • CN (common name): Rev n ← hardware rev

That makes it really quick to get an idea of what it is. Then you could encode mfg date and location along with serial number(s) in the x509 extensions. (It gets confusing that the “serial number” in the subject field is for the certificate, not what the certificate is for).

In practice, I never had to put manufacturing date into the cert, because it was in the serial number database; but I would add it here so anyone could see it.

Also keep in mind there are other encodings for x509 extensions; I just picked ASCII for the example so we could see it. For example, when signing firmware, I would put the SHA256 hash of the firmware binary in one of the extensions as a bit string.

1 Like

One suggestion:

Use a smaller keysize instead of the 2048 bits here. 512 bits is the smallest one that openssl supports.

As discussed earlier we just use the X.509 format because it is a common format for this kind of task and there are many tools and libraries available for it. We do not need all of it’s feature, especially not the private key part of it.

So reducing the keysize to the minium will reduce the size we need to store in OTP.

In a short test I got the DER format certificate down to a bit over 500 bytes by using 512 bits. This of course also depends on what kind of properties you put in the certificate.

You can probably go even lower by using ECDSA with 256 bits, but this doesn’t have the broad support across all libraries as RSA.

2 Likes

You are absolutely correct. I used 2048 out of habit; I think it’s kind of automatic muscle memory for me. Also agree that ECDSA is a better algorithm, but I had hit-or-miss luck with support, as you point out.

The downside? A threat actor would have more luck cracking the private key, but I don’t think it really matters in our use case here.

One other caveat - at some point, the crypto libs might mark that keysize as deprecated and not support it. Like crypto algorithms that have been proven insecure (DES, 3DES, etc.)

3 Likes

The private key is thrown away and ignored in this usecase. So it doesn’t matter if it is cracked or not, nothing depends on it.

What is important though is the signature on the certificate. This is where you want to have SHA256 for.

Oh, and you do not want to use 512 bits for the root certification authority of course. Because you don’t want to have that being cracked. The 512 bits are just for the key of the individual bus pirates that is thrown away.

3 Likes

Okay, that makes sense. I would probably set the issuer to the company (where labs llc, USA, etc), and the subject to the manufacturing location and ver/rev as you mention (CN, Shenzhen, BPn, REVn). I’m in terested in reusing the existing fields to save space/simplify access.

I kind of grasp what this means, but I’ll need to sit down with Matt’s guide and figure out how that works.

It was not on my radar that this would be my spring festival project, but I’ve really enjoyed learning about it and I’ve started some docs to go with it.

1 Like

Basically this command from above creates a self-signed certificate:

You do not want that for the individual Bus Pirates, because then you don’t have a common root certificate to verify all Bus Pirates against.

Instead you create one self signed root certificate with 2048 first. Then when you create the certs for the individual Bus Pirates, you sign them with the root certificate instead of the private key itself.

This works with the same “openssl x509 -req …” command, but you add the “-CA cacert.pem -CAkey cakey.pem” options pointing to your root certificate and root key.

1 Like

They are self-signed using the ./private.pem key that is created in an earlier step. That’s the signing key that’s common to all of them (and the one that needs to be protected). The matching public key is what’s used to validate the certificate (and would be hard-coded into the BP firmware).

1 Like

So you are proposing not to use the common CA mechanism where you have one root CA with key and cert and the individual Bus Pirates get their own key and cert that are signed by the CA?

Isn’t it more common practice to use a CA setup? I’d say the whole architecture of the X.509 system leans towards using CAs, so I would also use that here.
Or do you have a special reason not to do that?

1 Like

EDIT - what I’m suggesting is a CA; but a private one. Nothing that validates up to some authoritative public root CA, like a webserver’s certificate.
/EDIT

Yes it is, for things like websites, banks; real world things where there’s a large amount of trust involved.

The problem with using a real CA (certificate authority) is that you’d need to validate all signings back up to the root CA. That means you either encode each certificate along the way into the firmware. There would need to be a reliable, secure store for the root CA on the BP (or, it would have to go online to find a root CA).

That’s a lot of complication for what we’re doing here. For simplicity’s sake, there can be a private key that is secured by DP and used to sign the birth certificates.

If we want to add one layer, the “main” key can be used to sign an intermediary signing certificate that signs the birth certificates. If one of those gets compromised, then it can be replaced; but again, the BP would need a way to verify the chain. That could simply be having the public key for the “main” signing keypair and the intermediate’s public key.

1 Like

But with the way you suggest the implementation this is not obvious. You are not using the fields that X.509 suggests to use for this, like the issuer and subject. This makes it harder to understand the concepts and I don’t really see an advantage in doing it this way. All the fields are still encoded in the cert, they just don’t mean what they usually mean.

Your proposal doesn’t support using several levels, like intermediate certificates at all. So let’s say we don’t need or want them. Then the firmware just needs to store the one root ca certificate and can then directly validate each individual BP cert against that. No additional complications.

If you decide you want to introduce intermediate certs then you can do that. You would have to also store them in the firmware or download them from somewhere though, that is right. That is the added complexity coming from intermediate certs.

But just using the CA fields as suggested by the X.509 structures doesn’t really add extra complexity.

1 Like

The code really just needs the DP public key to validate the birth certificate was signed by the DP private key. Once that’s verified, the BP firmware can use what it finds in the birth certificate and know that it is unaltered since being signed.

Maybe it’s confusing, because the x509 is being used as a “birth certificate” and not to share keys or other things like you’d normally see with a web server or something.

1 Like

But what do you win by doing it not the usual way with a CA, what is the advantage?

You suggested above to use standard tools and standard practices like X.509 to profit from the existing tooling and knowledge. So why not go the full way and do it like it is commonly done?

You lose the ability to use 512 bit private keys that I suggested above to save OTP space. Because this key then matters, if you crack it you can forge the signatures.

The 2048 bit CA public key wouldn’t have to be stored in each certificate it signed, just in the CA cert. So it wouldn’t have to go into OTP, but regular firmware where space is not an issue.

2 Likes

This method isn’t misusing x509; this is just another way to leverage the technology. It’s used this way to sign firmware (which is checked before upgrades), and to ensure authenticity of devices.

EDIT
What I’m trying to describe is a private CA, but I really didn’t say it properly. Simply self-signing certs with a private key held by DP. That cert contains the data we care about for that particular BP. Using the public key to validate the cert to guarantee the source and integrity of that data.
/EDIT

I was proposing something self-contained and simple enough for the purpose while still being secure.

2 Likes

Using openssl from the command line was getting really annoying, so I looked for a Python wrapper. There is one, but they recommend using the cryptography library instead if one doesn’t actually need to do client/server stuff.

Handy Python Script
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography import x509
from cryptography.x509.oid import NameOID, ObjectIdentifier
from cryptography.hazmat.primitives import hashes
import datetime
import os

class CertificateManager:
    def __init__(self, key_path, cert_path, passphrase):
        self.key_path = os.path.expanduser(key_path)
        self.cert_path = os.path.expanduser(cert_path)
        self.passphrase = passphrase.encode()

    def generate_key(self):
        key = rsa.generate_private_key(
            public_exponent=65537,
            key_size=2048,
        )
        os.makedirs(os.path.dirname(self.key_path), exist_ok=True)
        with open(self.key_path, "wb") as f:
            f.write(key.private_bytes(
                encoding=serialization.Encoding.PEM,
                format=serialization.PrivateFormat.TraditionalOpenSSL,
                encryption_algorithm=serialization.BestAvailableEncryption(self.passphrase),
            ))
        return key

    def load_key(self):
        with open(self.key_path, "rb") as key_file:
            key = serialization.load_pem_private_key(
                key_file.read(),
                password=self.passphrase,
            )
        return key

    def create_certificate(self, key, subject, issuer, serial_number, valid_days, extensions):
        cert = x509.CertificateBuilder().subject_name(
            subject
        ).issuer_name(
            issuer
        ).public_key(
            key.public_key()
        ).serial_number(
            serial_number
        ).not_valid_before(
            datetime.datetime.now(datetime.timezone.utc)
        ).not_valid_after(
            datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=valid_days)
        )
        
        for ext in extensions:
            cert = cert.add_extension(ext['extension'], critical=ext['critical'])

        cert = cert.sign(key, hashes.SHA256())
        os.makedirs(os.path.dirname(self.cert_path), exist_ok=True)
        with open(self.cert_path, "wb") as f:
            f.write(cert.public_bytes(serialization.Encoding.PEM))
        return cert

    def print_certificate_info(self, cert):
        buf = cert.subject.rfc4514_string()
        print(f"Subject: {buf}")
        buf = cert.issuer.rfc4514_string()
        print(f"Issuer: {buf}")
        print(f"Valid from: {cert.not_valid_before}")
        print(f"Valid to: {cert.not_valid_after}")
        print(f"Serial Number: {cert.serial_number}")
        #show the x509 extensions we added
        for ext in cert.extensions:
            print(f"Extension: {ext.oid}")
            print(f"Critical: {ext.critical}")
            print(f"Value: {ext.value}")

        public_key = cert.public_key().public_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PublicFormat.SubjectPublicKeyInfo
        )
        print(f"Public Key: {public_key.decode()}")

# Usage example
if __name__ == "__main__":
    key_path = "~/cert/pykey.pem"
    cert_path = "~/cert/pycert.pem"
    passphrase = "123456789"

    manager = CertificateManager(key_path, cert_path, passphrase)

    # Generate or load key
    if not os.path.exists(manager.key_path):
        key = manager.generate_key()
        print("Key generated")
    else:
        key = manager.load_key()
        print("Key loaded")

    # Define subject and issuer
    issuer = x509.Name([
        x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
        x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Iowa"),
        x509.NameAttribute(NameOID.LOCALITY_NAME, "Muscatine"),
        x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Where Labs LLC"),
        x509.NameAttribute(NameOID.COMMON_NAME, "https://buspirate.com"),
    ])

    subject = x509.Name([
        x509.NameAttribute(NameOID.COUNTRY_NAME, "CN"),
        x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Guangdong"),
        x509.NameAttribute(NameOID.LOCALITY_NAME, "Shenzhen"),
        x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Bus Pirate 6"),
        x509.NameAttribute(NameOID.COMMON_NAME, "Rev 2"),
    ])    

    # Define custom OIDs for the extensions
    bpsn_oid = ObjectIdentifier("1.3.6.1.4.1.11129.2.1.1")
    rpsn_oid = ObjectIdentifier("1.3.6.1.4.1.11129.2.1.2")
    mfgdate_oid = ObjectIdentifier("1.3.6.1.4.1.11129.2.1.3")

    # Define extensions
    extensions = [
        {'extension': x509.UnrecognizedExtension(bpsn_oid, b"BP serial BP123456"), 'critical': False},
        {'extension': x509.UnrecognizedExtension(rpsn_oid, b"rPI serial RP123456"), 'critical': False},
        {'extension': x509.UnrecognizedExtension(mfgdate_oid, b"MFG date 20230101"), 'critical': False},
    ]

    # Create certificate
    cert = manager.create_certificate(key, subject, issuer, 0x1337, (365*100), extensions)

    # Print certificate info
    manager.print_certificate_info(cert)

This creates a cert from a private key.

ian@DESKTOP-7VKKLTO:~/cert$ openssl x509 -in pycert.pem --inform PEM --text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 4919 (0x1337)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = US, ST = Iowa, L = Muscatine, O = Where Labs LLC, CN = https://buspirate.com
        Validity
            Not Before: Jan 30 14:12:59 2025 GMT
            Not After : Jan  6 14:12:59 2125 GMT
        Subject: C = CN, ST = Guangdong, L = Shenzhen, O = Bus Pirate 6, CN = Rev 2
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:da:1c:42:d3:d2:d5:23:00:34:86:70:cb:95:e2:
                    ...
                    c5:63
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            1.3.6.1.4.1.11129.2.1.1:
                BP serial BP123456
            1.3.6.1.4.1.11129.2.1.2:
                rPI serial RP123456
            1.3.6.1.4.1.11129.2.1.3:
                MFG date 20230101
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        bf:f3:0d:13:a2:b5:17:44:6d:47:5d:03:97:10:65:ef:98:f2:
...
        45:32:cf:39
Actual cert
-----BEGIN CERTIFICATE-----
MIIDqjCCApKgAwIBAgICEzcwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UEBhMCVVMx
DTALBgNVBAgMBElvd2ExEjAQBgNVBAcMCU11c2NhdGluZTEXMBUGA1UECgwOV2hl
cmUgTGFicyBMTEMxHjAcBgNVBAMMFWh0dHBzOi8vYnVzcGlyYXRlLmNvbTAgFw0y
NTAxMzAxNDEyNTlaGA8yMTI1MDEwNjE0MTI1OVowWzELMAkGA1UEBhMCQ04xEjAQ
BgNVBAgMCUd1YW5nZG9uZzERMA8GA1UEBwwIU2hlbnpoZW4xFTATBgNVBAoMDEJ1
cyBQaXJhdGUgNjEOMAwGA1UEAwwFUmV2IDIwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQDaHELT0tUjADSGcMuV4qqfkbgYyvVjb9txdlR2Dsag88fp5Zhx
vrqHVj1hPsU6DPk7umBlj7bHLUCW4/oajgTwA2ZGRC6kQOgVhVDJJAUdQOY1m5Yu
P25ykRM6P/FiJKpIjPHNXaD3/20CVT7y6VhmreOdxxYMgfeLnCNTh4aVGaHJYVkP
icl2OePLKal8uGjQQndL7BLCKM304UpaN1yF+cfzb03uUyfZiC1cQKr8+9gliTv7
WifwmywMelj6u23tLuOnEUoSMGCP2iUPwaQR7K27tbRNsboblqTB3PpO7aVDrOBP
mp0E1sLPKpBuHu9e9hbEqkAkPPsVMnbLCMVjAgMBAAGjaDBmMCAGCisGAQQB1nkC
AQEEEkJQIHNlcmlhbCBCUDEyMzQ1NjAhBgorBgEEAdZ5AgECBBNyUEkgc2VyaWFs
IFJQMTIzNDU2MB8GCisGAQQB1nkCAQMEEU1GRyBkYXRlIDIwMjMwMTAxMA0GCSqG
SIb3DQEBCwUAA4IBAQC/8w0TorUXRG1HXQOXEGXvmPJRT8M9NPc36rSZ3xN7Nmut
MdaMVNkJ96Wa2usR2TwsvR46MkYcEvasxNdilG3sFYbdSlFFGjXessYDYTc0xudn
eWzzMVd8B00tnXrHi1uOGUWa0PIJcc7X/wm5EEpd/He5438j9LpbATe+S7u18dEi
npz5J1/Dn/MuecubHyYWgpGxTPG4jhFqbB/mlfVAdLfA3qLDhBIBXuFWsMHMbFdY
HghRbBxnR2QYc812RpHmAypE01eIOHfIuxMlyXgxxRjvb3UpFcKqQXevsjXkkC53
hooP3neT9rq1Qj7QkqwJ2KtnJllWI2jcv8ZFMs85
-----END CERTIFICATE-----

With public key:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2hxC09LVIwA0hnDLleKq
n5G4GMr1Y2/bcXZUdg7GoPPH6eWYcb66h1Y9YT7FOgz5O7pgZY+2xy1AluP6Go4E
8ANmRkQupEDoFYVQySQFHUDmNZuWLj9ucpETOj/xYiSqSIzxzV2g9/9tAlU+8ulY
Zq3jnccWDIH3i5wjU4eGlRmhyWFZD4nJdjnjyympfLho0EJ3S+wSwijN9OFKWjdc
hfnH829N7lMn2YgtXECq/PvYJYk7+1on8JssDHpY+rtt7S7jpxFKEjBgj9olD8Gk
Eeytu7W0TbG6G5akwdz6Tu2lQ6zgT5qdBNbCzyqQbh7vXvYWxKpAJDz7FTJ2ywjF
YwIDAQAB
-----END PUBLIC KEY-----

These don’t verify on the Bus Pirate which led me to an issue in the cert command…

    uint32_t flags;
    ret = mbedtls_x509_crt_verify(&cert, &cert, NULL, NULL, &flags, NULL, NULL);
    if (ret != 0) {
        char error_buf[100];
        mbedtls_strerror(ret, error_buf, 100);
        printf("Failed to verify certificate: %s\r\n", error_buf);
        return;
    }

Yesterday I cribbed this from an example, but today I realize it is verifying the cert against the cert, and not the public key.

    ret = mbedtls_pk_verify(
        &public_key,
        MBEDTLS_MD_SHA256,
        cert.raw.p, cert.raw.len - cert.sig.len,
        cert.sig.p, cert.sig.len
    );
    if (ret != 0) {
        char error_buf[100];
        mbedtls_strerror(ret, error_buf, 100);
        printf("Failed to verify certificate: %s\r\n", error_buf);
        return;
    }

This is the correct function to verify against a public key.

Failed to verify certificate: PK - Bad input parameters to function

Both today’s cert and yesterdays cert now fail verification.

    if (ctx->pk_info == NULL ||
        pk_hashlen_helper(md_alg, &hash_len) != 0) {
        return MBEDTLS_ERR_PK_BAD_INPUT_DATA;
    }

This seems to be the code that is returning the error.

I’m really confused at this point.

I think the next step is to verify the python generated cert against the python generated public key using openssl, but I haven’t figured out that part yet.

Update to cert command pushed.

Current code

    // Compute the SHA-256 hash of the TBS (to-be-signed) part of the certificate
    printf("Verifying the SHA-256 signature");
    const mbedtls_md_info_t *mdinfo = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
    ret = mbedtls_md(
        //mbedtls_md_info_from_type(MBEDTLS_MD_SHA256),
        mdinfo,
        cert.tbs.p, cert.tbs.len, hash);
    if (ret != 0) {
        char error_buf[100];
        mbedtls_strerror(ret, error_buf, 100);
        printf("Failed to create hash: %s\r\n", error_buf);
        return;
    }

    printf("Hash done\r\n");

    // Verify the certificate signature using the public key
    ret = mbedtls_pk_verify(
        &public_key,
        mdinfo->type,
        hash, 0,
        cert.sig.p, cert.sig.len
    );
    if (ret != 0) {
        char error_buf[100];
        mbedtls_strerror(ret, error_buf, 100);
        printf("Failed to verify: %s\r\n", error_buf);
        return;
    }

Aparently it is a two step process:

  1. Generate a hash of the cert
  2. Then pass this hash to mbedtls_pk_verify

Currently mbedtls_pk_verify causes a full system crash of both cores :slight_smile:

Ugh.

Yeah, that’s what x509 verification looks like. The signer hashes the contents of the cert, encrypts the hash with their private key, and appends it to the data to make a signed certificate.

To verify, pull the encrypted hash off and decrypt with the public key of the signer. Hash the x509 data and compare the two. If they match, you know who signed it, and also that the data hasn’t been tempered with it corrupted since it was signed.

Crypto libraries are ugly; they need to manage context, allocate or manage data/pointers. It seems like it takes a couple of tries to fully understand the implementation.

I’m not at a computer right now, but can look later.

1 Like

I’ve been giving this some thought, and I think it can be skipped under our use cases. Specifically, the below solution is permissible because OTP is not a rewritable medium, only appendable.

Thus, have the following situations:

  1. Rev0 applied to all directory entry items
  2. Rev1 (or higher) applied to all directory entry items
  3. Rev0 applied to first N entries, later entries appended with a higher revision

The use of the iterator allows a simple solution:

  • Today, no special code needed.
  • If non-compatible change needed, then define a record type that increases the version of subsequent records.

Here’s what it would look like:

  1. [Rev0 item] [Rev0 item] [Rev0 item] ...
  2. [Rev1 ID->] [Rev1 item] [Rev1 item] ...
  3. [Rev0 item] [Rev0 item] [Rev1 ID->] [Rev1 item] [Rev1 item] ...

Benefits:

  • YAGNI … don’t write code we don’t need.
  • Callers are insulated from the changes when using the helper functions (which use an iterator).
  • Really simple way to allow OTP directory programmed with earlier revision to support later revision.

So, no specific changes need to be added today for this support. Also, forcing use through the iterator allows many other changes to the OTP directory entry in future (e.g., entry to create gaps in the directory, etc.).

2 Likes

@ian - I got to a computer earlier, but got pulled away for something else. I’ll get a chance to look more into it in the next few days - sorry for not getting back sooner

1 Like