POSIX-compatible litmus.sh
March 2024
Introduction
One of the utilites resulting from the FFA project is the RSA signature-checking script called litmus.sh (Datskovskiy 2017). It is written using a Shell Scripting Language and calls Peh and a variety of other programs usually found in major operating system distributions. However, the reader might be misled by the #!/bin/sh shebang string pointing to the traditional location of the Bourne shell (sh(1)), thinking it may be POSIX-compatible (IEEE and The Open Group 2018). Closer inspection reveals it is actually written for the GNU system shell (GNU Bash) and uses incompatible syntax in some cases. Since both FFA and Peh have been proven to be quite portable to other systems, even the ones not directly supported by the popular software development stacks (Gregory 2024), the author believes it would be beneficial for the litmus.sh script to conform to the POSIX standard, as it may be more convinient for the users who would not necessarily have a copy of GNU Bash installed, but were instead left with a smaller POSIX-compatible shell, either by the limitations of their system, its design, or their own choice.
All the changes presented in this article are available in the following vpatch: ffa_litmus_width_fix_and_posix.vpatch (seal).
POSIX compatibility
A naïve approach would be to just run the script under an incompatible shell and see where it fails, but it really would not guarantee us full compatibility (as the incompatible shell in question can have its own Bash-compatible extensions or maybe not even fully conform to the standard). A more sophisticated approach would, of course, be properly analizing the code to find all the deviations from the standard using either a tool made for this purpose or one’s own eyes. Either way, we can reveal several inconsistencies. Armed with the documentation, let us check the first culprit.
RET_EGGOG=-1
POSIX-compatible shells operate on unsigned 8-bit exit codes (with the value being truncated to 8 bits in case of a wider exit code), so the first thing we do is change the RET_EGGOG constant.
RET_EGGOG=255
Another thing we have to watch out for are the C-like variable assignments. An example presents itself when our Peh tape is being built.
turd+=$r
A simple string concatenation is sufficient in this case (and in all other similar cases not listed here).
turd="${turd}${r}"
There is one section with three consecutive assignments.
turd+=$sig_version
turd+="FF"
turd+=$(printf "%08x" $hashed_header_len)
We can just change it to a single line using the same method.
turd="${turd}${sig_version}FF$(printf "%08x" $hashed_header_len)"
== being used in the script for string comparison is another example of incompatible syntax.
if [ "$start_ln" == "" ] || [ "$end_ln" == "" ]
We can safely change it (and all other occurences of it) to =, as is even recommended in the GNU Bash manual when POSIX compatibility is desired (Free Software Foundation 2022).
if [ "$start_ln" = "" ] || [ "$end_ln" = "" ]
POSIX-compatible shells do not support arbitrary base conversions for number literals.
r=$((16#$r))
Fortunately, the only base conversion in litmus.sh is the hexadecimal one, so we can get away with a simple change.
r=$((0x$r))
Another non-standard operation we have to get rid of is the extended command substitution.
peh_res=$((cat $PUBFILE; echo $tape) | \ peh $peh_width $peh_height $tape_len $peh_life $PEH_RNG_DEV);
We can solve this one using various methods, but if we want to keep the single line cat(1) command, we can simply do the following.
peh_res=$(echo $tape | cat $PUBFILE - | \ peh $peh_width $peh_height $tape_len $peh_life $PEH_RNG_DEV);
The same principle applies in another place.
hash=$((cat $DATAFILE; xxd -r -p <<< $turd) | $HASHER | cut -d ' ' -f1)
This can obviously be changed the same way to the following line.
hash=$(echo $turd | xxd -r -p | cat $DATAFILE - | $HASHER | cut -d ' ' -f1)
Arithmetic for loops are another GNU Bash extension, so we have to get rid of them. Fortunately, there is only one we have to watch out for.
# Attach the padding bytes: for ((x=1; x<=$pkcs_pad_bytes; x++)); do pkcs+="FF" done
We can clearly rewrite it as a while loop without sacrificing any logic.
# Attach the padding bytes: x=1 while [ $x -le $pkcs_pad_bytes ]; do pkcs="${pkcs}FF" x=$((x + 1)) done
Another kind of unsupported looping constructs is a for loop over the elements of an array. It should not be surprising, since POSIX does not define arrays in the Shell Command Language at all. Its only instance is the loop veryfing the existence of the files litmus.sh operates on.
# Verify that each of the given input files exists: FILES=($PUBFILE $SIGFILE $DATAFILE) for f in ${FILES[@]}; do
There is no need to use arrays in this case and we can simply iterate over a regular string the same way litmus.sh checks for the external programs.
# Verify that each of the given input files exists: FILES="$PUBFILE $SIGFILE $DATAFILE" for f in $FILES do
Another case of an array is the sig_bytes variable.
sig_bytes=($(echo $sig_payload | base64 -d | hexdump -ve '1/1 "%.2x "'))
If we remove the parentheses, we can actually get the raw content instead of an array.
sig_bytes=$(echo $sig_payload | base64 -d | hexdump -ve '1/1 "%.2x "')
This poses a problem, since we need the length of the array we just lost.
sig_len=${#sig_bytes[@]}
Fortunatelly, wc(1) is already on our list of dependencies, so we can use it on our raw variable the following way to achieve the same result.
sig_len=$(echo $sig_bytes | wc -w)
Another thing we lack is the array slicing mechanism.
r=$(echo ${sig_bytes[@]:$sig_pos:$count} | sed "s/ //g" | tr 'a-z' 'A-Z')
This time we have to be a bit more clever, but we can achieve the same effect by using cut(1) (which is also, conveniently, on our list of dependencies).
r=$(echo $sig_bytes | cut -d\ -f$(($sig_pos + 1))-$(($sig_pos + $count)) \ | sed "s/ //g" | tr 'a-z' 'A-Z')
According to author’s knowledge, these changes are the only ones needed to ensure POSIX compatibility. The above changes have been tested and properly run on the Almquist Shell (ash(1), included in the BusyBox software suite), which does not support most of the GNU Bash extensions. The author believes it should run on any POSIX-compatible shell.
Proper RSA signature length
Another thing worth noting is a subtle bug in determining the signature length.
# RSA Bitness for use in determining required Peh width: rsa_width=$(($rsa_byteness * 8))
rsa_byteness is a variable containing the amount of octets needed to be read from the signature file, containing the actual RSA signature. While the amount of octets in bits is always a multiple of 8, the actual signature is not guaranteed to share the length with the RSA key used to sign it, e.g., a 4096-bit key can generate a 4088-bit valid signature which is full octet short (and the signature file will contain precisely 511 octets of storage for the signature itself). Multiplying this value by 8 will obviously result in an illegal Peh width, so we have to find a way to calculate the minimal legal width on which our signature will fit (i.e., the next integer with its Hamming weight equal 1).
Fortunately, an appropriate and easy to implement method was devised by Pete Hart and William Lewis (1997). The reader is encouraged to analyze the example presented by the authors to understand what exactly is going on with the rsa_width variable when being subjected to the following operations. Instead of relying on rsa_byteness (which only applies to the file), we start with the rsa_bitness (being the exact length of the signature in bits).
# RSA Bitness for use in determining required Peh width: rsa_width=$((rsa_bitness - 1)) rsa_width=$((rsa_width | rsa_width >> 1)) rsa_width=$((rsa_width | rsa_width >> 2)) rsa_width=$((rsa_width | rsa_width >> 4)) rsa_width=$((rsa_width | rsa_width >> 8)) rsa_width=$((rsa_width + 1))
We can safely assume four shifts, since the actual signature is always a 16-bit value.
Another small change we need to do is change the amount of padding, since it still uses the encoded length.
# Compute necessary number of padding FF bytes : pkcs_pad_bytes=$(($rsa_byteness - $MD_LEN - $ASN_LEN - 3))
The obvious solution is replacing the rsa_byteness variable with our newly calculated rsa_width and simply dividing it by 8.
# Compute necessary number of padding FF bytes : pkcs_pad_bytes=$(($rsa_width / 8 - $MD_LEN - $ASN_LEN - 3))
The pkcs_pad_bytes variable should now contain the exact amount of padding needed.
Conclusions
The changes we made can be tested on any Version 4 PGP signature conforming to RFC 4880 on any POSIX-compatible shell, including GNU Bash—the original shell litmus.sh was written for—as all the changes we made are fully upwards-compatible.
References
Datskovskiy, Stanislav [asciilifeform, pseud.]. 2017. “Finite Field Arithmetic,” Loper OS (December). http://www.loper-os.org/?p=1913.
Free Software Foundation. 2022. GNU Bash manual. https://www.gnu.org/software/bash/manual/.
Gregory, David [hapax, pseud.]. 2024. “PEH.EXE: Running Peh on DOS,” Hapax Legomenon (February). http://146.190.13.172/articles/peh-exe-running-peh-on-dos/.
Hart, Pete, and Lewis, William. 1997. Replies to “Possibly better stringobject allocator.” Usenet newsgroup comp.lang.python (February). https://groups.google.com/g/comp.lang.python/c/xNOq4N-RffU.
IEEE and The Open Group. 2018. The Open Group Base Specifications Issue 7: 2018 edition. https://pubs.opengroup.org/onlinepubs/9699919799/.