Category Archives: Data

Multi-Factor (4FA and 5FA) Authentication with FreeBSD and SSH

I was asked on twitter today to explain some recent work I have done in the multi-factor authentication department. I can write this article because I have recently released my work under the title “secure_thumb.”

While many 2FA and beyond methodologies implement SMS as a factor in authentication, I was uncomfortable sending factors over the wire. Over the years I have had one actual instance where a machine got rooted and the SSH private key (although passphrase protected) was stolen from said machine. At the time, I did not have PGP set up, so I had no way of getting a new key trusted by the FreeBSD community. We quickly blacklisted the stolen key and a review of recent commits was performed to check for any malicious activity. Another developer was sent to my house to verify government-issued ID before a new key was generated offline and injected into the cluster through a trusted channel.

From that incident many years ago, I vowed to never let a single factor stand between me and the FreeBSD cluster again (that factor being a passphrase typed at a command-prompt to load an SSH private key that gave access to the FreeBSD cluster and commit access to change any FreeBSD source code).

Since I was actively writing and maintaining the FreeBSD boot loader at the time, there was some mild concern that malicious code could have been injected into places that few others understood (the loader code that I write/maintain is in a language called Forth; a language which causes most to run the opposite direction and is known for being quite unreadable). I did a top-down review of every line of Forth at that time to put minds at-ease (added benefit, I then shortly after rewrote several of the inefficient structures and solved some inherited issues that were long-in-the-tooth as they say).

Post-incident, my SSH private key to the cluster remained secured on a thumb drive encrypted by GELI — an in-kernel FreeBSD system that allows for the encryption of entire filesystems with a myriad of options. We went from a single-factor to 3-factor in a single day, but we will see how a scare later resulted in the development of 4- and 5-factor authentication that is used today.

For many years I used 3FA implemented as a physical USB thumb drive kept in a secure location, offline, encrypted, holding SSH private keys protected with a unique passphrase. One day, it was believed that I lost this key. When it became obvious that we were not going to find it in any time soon, I had the cluster revoke my keys. This time, however, I had PGP properly set up and could quickly re-establish trust by sending a new public key in a PGP encrypted message to the cluster admin team.

Every time I regenerate my FreeBSD.org private keys (years apart) I take the opportunity to think about security. This time I had an entire framework built around my 3FA solution, and I envisioned a scenario where the person that may have found my thumb drive would not be able to decrypt the contents even if they had stolen the passphrase through keystroke logging. My 4FA solution was born.

Since FreeBSD GELI allows you to have multiple keys (which are essentially concatenated together to form one ginormous key), I built a new framework which creates a key (we’ll call this a trust certificate) for the host or hosts that you want to allow decryption. Thus, only if an attacker were able to purloin both my GELI passphrase using a keystroke logger and steal the GELI keyfile from trusted host will they be able to mount the volume. Those 2 factors plus the physical locality of the thumb drive (and being mostly offline), plus the passphrase required to use the SSH private key, altogether form 4FA.

Where does the fifth factor come into play?

Physically locking USB thumb drives that require a code to unlock before they will yield their plug to a computer. I have seen 3 such mechanical devices (e.g., the Cryptex Round Compass Lock by SDI) and a couple digital ones with a keypad/LCD on the thumb drive.

The secure_thumb framework for managing 4FA using securely-stored and encrypted SSH private keys, see https://FrauBSD.org/secure_thumb

As an added bonus, the framework creates one unencrypted partition to contain trust-management software (*cough* shell scripts that make sure the host is trusted and is not sniffing keystrokes ala DTrace for example) and scripts to automate the mounting of the GELI partition. But wait, there’s more …

The framework creates a FreeBSD slice to allow the creation of multiple encrypted partitions each with a different key and trust certificate. You can make a USB thumb drive that has 7 encrypted partitions, each with a different passphrase, and with the trust certificates for each partition sent to different people. The net effect would be a thumb drive that can be used by 7 people but each person can only open their partition.

There are other hidden features, such as the ability to dynamically resize the encrypted slice (both grow and shrink) but you can also add 2 additional partitions to it for making it look like it’s just a drive for another OS with some random unencrypted data on it for TSA to peruse.

D-String, an answer to P-String and C-String limitations

C-String’s are delimited by a NULL byte. P-Strings are preceded by a length identifier. Both have their downsides and I’ve developed the solution (it’s called the D-String; D for Data). The C-String’s downfall is that it cannot contain a NULL (else the interpreting language — C — will prematurely terminate the data). The P-String’s downfall is that it cannot represent more than 255 bytes (unless of course you use a wider length identifier in which case you’ve also increased the overhead). The D-String overcomes both of these limitations with minimal overhead. Let’s have a look at the specifics (free of charge).

NOTE: These are my own internal notes. They will be translated into a full technical explanation in another blog posting. However… the fact is that I’ve sat on this technology for 10 years and want to finally make it public. This is the first step in doing so. Last, this will serve as a backup should my iPhone crash (currently the only machine in the world with a documented example of the methodology). This is not meant to be digested by mere mortals (but if you can, all the more power to you — you’ll have a leg-up on the rest of those waiting on the technical discussion).

shxd.rfc04 — dStr data object

Len
0x00 => 0x00
0x01 => 0x01 0x00 0x00 DATA
0xFF => 0xFF 0x00 0x00 DATA
0x0100 => 0x0101 0x00 0x01 DATA
0x0101 => 0x0101 0x00 0x00 DATA
0xFFFF => 0xFFFF 0x00 0x00 DATA
0x010000 => 0x010101 0x00 0x03 DATA
0x010100 => 0x010101 0x00 0x02 DATA
0x010100 => 0x010101 0x00 0x01 DATA
0xFFFFFF => 0xFFFFFF 0x00 0x00 DATA
0x01000000 => 0x01010101 0x00 0x07 DATA
0x01000001 => 0x01010101 0x00 0x06 DATA
0x01000100 => 0x01010101 0x00 0x05 DATA
0x01000101 => 0x01010101 0x00 0x04 DATA
0x01010000 => 0x01010101 0x00 0x03 DATA
0x01010001 => 0x01010101 0x00 0x02 DATA
0x01010100 => 0x01010101 0x00 0x01 DATA
0xFFFFFFFF => 0xFFFFFFFF 0x00 0x00 DATA
0x0100000000 => 0x0101010101 0x00 0x0F DATA
0x0100000001 => 0x0101010101 0x00 0x0E DATA
0x0100000100 => 0x0101010101 0x00 0x0D DATA
0x0100000101 => 0x0101010101 0x00 0x0C DATA
0x0100010000 => 0x0101010101 0x00 0x0B DATA
0x0100010001 => 0x0101010101 0x00 0x0A DATA
0x0100010100 => 0x0101010101 0x00 0x09 DATA
0x0100010101 => 0x0101010101 0x00 0x08 DATA
0x0101000000 => 0x0101010101 0x00 0x07 DATA
0x0101000001 => 0x0101010101 0x00 0x06 DATA
0x0101000100 => 0x0101010101 0x00 0x05 DATA
0x0101000101 => 0x0101010101 0x00 0x04 DATA
0x0101010000 => 0x0101010101 0x00 0x03 DATA
0x0101010001 => 0x0101010101 0x00 0x02 DATA
0x0101010100 => 0x0101010101 0x00 0x01 DATA
0xFFFFFFFFFF => 0xFFFFFFFFFF 0x00 0x00 DATA
0x010000000000 => 0x010101010101 0x00 0x1F DATA
0x010000000001 => 0x010101010101 0x00 0x1E DATA
0x010000000100 => 0x010101010101 0x00 0x1D DATA
0x010000000101 => 0x010101010101 0x00 0x1C DATA
0x010000010000 => 0x010101010101 0x00 0x1B DATA
0x010000010001 => 0x010101010101 0x00 0x1A DATA
0x010000010100 => 0x010101010101 0x00 0x19 DATA
0x010000010101 => 0x010101010101 0x00 0x18 DATA
0x010001000000 => 0x010101010101 0x00 0x17 DATA
0x010001000001 => 0x010101010101 0x00 0x16 DATA
0x010001000100 => 0x010101010101 0x00 0x15 DATA
0x010001000101 => 0x010101010101 0x00 0x14 DATA
0x010001010000 => 0x010101010101 0x00 0x13 DATA
0x010001010001 => 0x010101010101 0x00 0x12 DATA
0x010001010100 => 0x010101010101 0x00 0x11 DATA
0x010001010101 => 0x010101010101 0x00 0x10 DATA
0x010100000000 => 0x010101010101 0x00 0x0F DATA
0x010100000001 => 0x010101010101 0x00 0x0E DATA
0x010100000100 => 0x010101010101 0x00 0x0D DATA
0x010100000101 => 0x010101010101 0x00 0x0C DATA
0x010100010000 => 0x010101010101 0x00 0x0B DATA
0x010100010001 => 0x010101010101 0x00 0x0A DATA
0x010100010100 => 0x010101010101 0x00 0x09 DATA
0x010100010101 => 0x010101010101 0x00 0x08 DATA
0x010101000000 => 0x010101010101 0x00 0x07 DATA
0x010101000001 => 0x010101010101 0x00 0x06 DATA
0x010101000100 => 0x010101010101 0x00 0x05 DATA
0x010101000101 => 0x010101010101 0x00 0x04 DATA
0x010101010000 => 0x010101010101 0x00 0x03 DATA
0x010101010001 => 0x010101010101 0x00 0x02 DATA
0x010101010100 => 0x010101010101 0x00 0x01 DATA
0xFFFFFFFFFFFF => 0xFFFFFFFFFFFF 0x00 0x00 DATA
.
.
.
0x0100000000000000 => 0x0101010101010101 0x00 0x7F DATA
0xFFFFFFFFFFFFFFFF => 0xFFFFFFFFFFFFFFFF 0x00 0x00 DATA
.
.
.
0x01000000000000000000000000000000 => 0x01010101010101010101010101010101 0x00 0x7FFFF DATA
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF => 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 0x00 0x0000 DATA

That’s a length identifier of 2^(8*16) or 2^128 or 3.4028236692094e+38 or 340,282 thousand Decillion bytes long. The length identifier is valid with only 3 bytes of overhead preceding the actual DATA (compared to 16 bytes for the length identifier).

Scaling this higher (to 512 bit integers — 64-bytes wide), the overhead would be 9 bytes.

The overhead is always the length of the length-identifier (in bytes) divided by 8 plus one (with the minimum overhead being two bytes at the low end).

If a dStr contains 0 bytes of data, the dStr will be 0x00.

If a dStr contains 1-15 bytes of data, the dStr will be 0xLL 0x00 0xNN DATA (header is 3 bytes). LL is the length of DATA. NN is the encode register.

If a dStr contains 65536-16777215 bytes of data, the dStr will be 0xLLLLLL 0x00 0xNN DATA (header is 5 bytes).

If a dStr contains 16777216-4294967295 bytes of data, the dStr will be 0xLLLLLLLL 0x00 0xNN DATA (header of 6 bytes).

If a dStr contains 4294967296-1099511627775 bytes of data, the dStr will be 0xLLLLLLLLLL 0x00 0xNN DATA (header of 7 bytes).

If a dStr contains 1099511627776-281474976710655 bytes of data, the dStr will be 0xLLLLLLLLLLLL 0x00 0xNN DATA (header of 8 bytes).

If a dStr contains 281474976710655-7.2057594037928e+16 bytes of data, the dStr will be 0xLLLLLLLLLLLLLL 0x00 0xNN DATA (header of 9 bytes).

If a dStr contains 7.2057594037928e+16-1.844674407371e+19 bytes of data, the dStr will be 0xLLLLLLLLLLLLLLLL 0x00 0xNN DATA (header of 10 bytes).

If a dStr contains 1.844674407371e+19-4.7223664828696e+21 bytes of data, the dStr will be 0xLLLLLLLLLLLLLLLLLL 0x00 0xNNNN DATA (header of 12 bytes).

If the dStr contains 4.7223664828696e+21-1.2089258196146e+24 bytes of data, the dStr will be 0xLLLLLLLLLLLLLLLLLLLL 0x00 0xNNNN DATA (header of 13 bytes).

Ad nausea to infinitum.