6
$\begingroup$

This question is related to, but not the same as, two others I recently asked about using Mathematica's PGP functionality: "After using GenerateAsymmetricKeyPair[], how to print the two keys in hex?" and "Given a PGP public key, how can I extract its PGP fingerprint in hex, using v12.0?". As before, I am using Mathematica v12.0.

Having generated a PGP key pair using GenerateAsymmetricKeyPair[], how can I print the two key blocks in the format in which they are usually stated, i.e. broken into 64-character lines in base64, like this:

-----BEGIN PGP PUBLIC KEY BLOCK-----
[optional version info]
[blank line]
[several 64-character lines in base64]
[5-character final line in base64, beginning with "="]
-----END PGP PUBLIC KEY BLOCK-----

This is the standard format if you want to send someone your public key in the body text of an email, for example.

A private key block looks similar:

-----BEGIN PGP PRIVATE KEY BLOCK-----
[optional version info]
[blank line]
[several 64-character lines in base64]
[a line in base64 of possibly fewer than 64 characters, ending with "=="]
[5-character final line in base64, beginning with "="]
-----END PGP PUBLIC KEY BLOCK-----

I realise that since the base64 string in the public key block contains other information as well as the public key in the strict sense - and similarly for the private key block, which in fact contains the public key as well as the private key in the strict sense - some information (including name, email address, and passphrase if specified) may need to be processed by Mathematica before GenerateAsymmetricKeyPair[] is called, and if not before then it certainly will after. But how can I actually get Mathematica to produce key blocks in this standard format?

[Edit by @ool: This was not a great description by me of what's in the armoured ASCII versions of the two keys. The canonical source here is section 11 of RFC 4880, which specifies that minimal blocks in each case are constructed from the relevant key packet (public or private) followed by a userid packet, with the combination of those two items then followed by a CRC24 checksum; and which advocates that the userid packet in the private key block "should" be followed by a self-signature because that will allow the extraction of the public key, while commenting that self-signature is not necessary, for example when a transferable public key accompanies the transferable private key.]

$\endgroup$
7
  • $\begingroup$ If there is no way to do this, then we would be in the strange position that Mathematica can generate a keypair of the kind used for PGP public key encryption but it can't print the public key block in the standard form for emailing to someone. I hope this isn't so! $\endgroup$ – ool Mar 21 at 11:04
  • $\begingroup$ Just a quick thought that you could also just use the output of the gpg command, eg RunProcess[{"gpg", "--help"}] rather than trying to force Mathematica to do it. It depends on your goals though, just in case this helps you save time :) $\endgroup$ – Carl Lange Mar 22 at 14:09
  • $\begingroup$ Similar question posted here. $\endgroup$ – Rohit Namjoshi Apr 4 at 20:57
  • $\begingroup$ If gais the output of GenerateAsymetricKeyPair, then BaseEncode[ga["PublicKey"]["PublicByteArray"],"Base64"] returns the key as Base64. In the last line, the = sign is followed by the crc24 hash of the key in Base64 (4 characters). The Mathematica Hashfunction can compute a crc32 hash, but not crc24. So you would need to create the hash code. $\endgroup$ – Jean-Pierre Apr 4 at 22:49
  • $\begingroup$ Thanks, @Jean-Pierre. But before the 5-char final line a key block contains much more info than just the key in the strict sense put into Base64. Or are you saying that the key followed by its 4-char crc24 hash would make a minimal functioning block? (Then there is the further question about the private key block.) $\endgroup$ – ool Apr 5 at 10:05
4
+175
$\begingroup$

UPDATE: Partial implementation of RFC 2440 to generate a Public Key ASCII armor block from GenerateAsymmetricKeyPair[]. The Base64 data will include 3 types of packets: Public Key, User ID, Signature. RFC 2440 can be quite confusing, particularly concerning the signature and the fields to hash and encrypt. I cannot guarantee the accuracy of the code. The purpose is to show the extent of what is involved.

Some functions:

crc24[bytes_] := (
  (* create a BitVector *)
  
  crcBV = CreateDataStructure["BitVector", 24];
  
  (* initialize the bit vector with 0xb704ce *)
  
  initial = {1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 
    0, 1, 1, 1, 0};
  k = 23;
  Map[If[# == 1, crcBV["BitSet", k--], k--] &, initial];
  
  (* polynome info *)
  
  poly = {23, 18, 17, 14, 11, 10, 7, 6, 5, 4, 3, 1};
  
  (* process each bit of data *)
  Map[
   (b = #;
     i = 7;
     While[i >= 0,
      If[BitAnd[b, BitShiftLeft[1, i]] != 0, v = 1, v = 0];
      xorResult = BitXor[crcBV["BitGet", 23], v];
      k = 23;
      While[k > 0,
       If[MemberQ[poly, k],
        (* true *)
        
        bx = BitXor[crcBV["BitGet", k - 1], xorResult];
        If[bx != 0, crcBV["BitSet", k], crcBV["BitClear", k]],
        (* false *)
        
        If[crcBV["BitGet", k - 1] != 0, crcBV["BitSet", k], 
          crcBV["BitClear", k]];
        ];
       k--;
       ];
      If[xorResult != 0, crcBV["BitSet", 0], crcBV["BitClear", 0]];
      i--
      ]) &
   , bytes // Normal];
  
  bitList = Map[crcBV["BitGet", #] &, Flatten@Range[23, 0, -1]];
  pList = Partition[bitList, 8];
  BaseEncode@
   ByteArray@
    Map[(#[[8]] + #[[7]]*2 + #[[6]]*4 + #[[5]]*8 + #[[4]]*16 + \
#[[3]]*32 + #[[2]]*64 + #[[1]]*128) &, pList]
  )

toBitList[bytes_ByteArray] := (
  Flatten@Map[IntegerDigits[#, 2, 8] &, bytes // Normal]
  )

toInteger[bytes_ByteArray] := (
  Total[MapIndexed[#1*(2^(#2 - 1)) &, 
     Reverse[toBitList[bytes]]]][[1]]
  )

toByteArray[int_Integer] := (
  digits2 = IntegerDigits[int, 2];
  padded = 
   PadLeft[digits2, 
    Mod[8 - Mod[Length[digits2], 8], 8] + Length[digits2]];
  ByteArray[Map[FromDigits[#, 2] &, Partition[padded, 8]]]
  )

toMultiprecision[bytes_ByteArray] := (
  bl = BitLength[toInteger[bytes]];
  part = Partition[IntegerDigits[bl, 2, 16], 8];
  map = Map[(#[[8]] + #[[7]]*2 + #[[6]]*4 + #[[5]]*8 + #[[4]]*16 + \
#[[3]]*32 + #[[2]]*64 + #[[1]]*128) &, part];
  ByteArray[Join[map, bytes // Normal]]
  )

toMultiprecisionByteArray[int_Integer] := (
  toMultiprecision[toByteArray[int]]
  )

addPacketHeader[tagType_String, body_ByteArray] := (
  bodyLength = Length[body];
  encodedLength = toByteArray[bodyLength];
  If[Length[encodedLength] == 3, 
   encodedLength = Prepend[encodedLength // Normal, 0]];
  blength = BitLength[bodyLength];
  Which[
   tagType == "PublicKey",
   tag = Which[blength <= 8, 2^^10011000, blength <= 16, 2^^10011001, 
     blength > 16, 2^^10011010],
   
   tagType == "UserID",
   tag = Which[blength <= 8, 2^^10110100, blength <= 16, 2^^10110101, 
     blength > 16, 2^^10110110],
   
   tagType == "Signature",
   tag = Which[blength <= 8, 2^^10001000, blength <= 16, 2^^10001001, 
     blength > 16, 2^^10001010],

   tagType == "PrivateKey", 
   tag = Which[blength <= 8, 2^^10010100, blength <= 16, 2^^10010101, 
    blength > 16, 2^^10010110]
   ];
  Join[{tag}, encodedLength, body]
  )

addSubPacketHeader[tagType_String, body_ByteArray] := (
  bodyLength = Length[body] + 1; (* length includes the type byte *)
 
   encodedLength = toByteArray[bodyLength];
  If[Length[encodedLength] == 4, 
   encodedLength = Prepend[encodedLength // Normal, 0]];
  If[Length[encodedLength] == 3, 
   encodedLength = Flatten@Prepend[encodedLength // Normal, {0, 0}]];
  blength = BitLength[bodyLength];
  Which[
   tagType == "SignatureCreationTime",
   type = 2,
   
   tagType == "KeyID",
   type = 16;
   
   ];
  Join[encodedLength, {type}, body]
  )

addHashedOrNoneSubPacketHeader[body_] := (
  bodyLength = Length[body];
  encodedLength = toByteArray[bodyLength];
  If[Length[encodedLength] == 1, 
   encodedLength = Prepend[encodedLength // Normal, 0]];
  Join[encodedLength, body]
  )

getUserIDPacketForSignature[uID_ByteArray] := (
  length = Length[uID];
  length32 = 
   Map[(#[[8]] + #[[7]]*2 + #[[6]]*4 + #[[5]]*8 + #[[4]]*16 + \
#[[3]]*32 + #[[2]]*64 + #[[1]]*128) &, 
    Partition[IntegerDigits[length, 2, 32], 8]];
  ByteArray[Join[{16^^b4}, length32, userID]]
  )

getTrailerData[sigData_] := (
  length = Length[sigData];
  length32 = 
   Map[(#[[8]] + #[[7]]*2 + #[[6]]*4 + #[[5]]*8 + #[[4]]*16 + \
#[[3]]*32 + #[[2]]*64 + #[[1]]*128) &, 
    Partition[IntegerDigits[length, 2, 32], 8]];
  ByteArray[Join[{4}, {16^^FF}, length32]]
  )

Key creation:

keyPair = GenerateAsymmetricKeyPair[]

The code:

(* =============== Public Key Packet ============= *)
rsa = 1;
version = 4;
timeOfCreation = 
  toByteArray[
   QuantityMagnitude[
    DateDifference[{1970, 1, 1}, {2021, 4, 9}, "Second"]]];
publicKeyAlgorithm = rsa; 
mpPublicByteArray = 
  toMultiprecision[keyPair["PublicKey"]["PublicByteArray"]];
mpPublicExponent = 
  toMultiprecisionByteArray[keyPair["PublicKey"]["PublicExponent"]];
bodyKeyPacket = 
  ByteArray[
   Join[{version}, timeOfCreation, {publicKeyAlgorithm}, 
    mpPublicByteArray, mpPublicExponent]];
keyPacket = addPacketHeader["PublicKey", bodyKeyPacket];

(* ============= Fingerprint and Key ID =========\[Equal]*)

fingerprint = Hash[ByteArray@keyPacket, "SHA", "ByteArray"];
keyID = Take[fingerprint, -8];

(* ============\[Equal] User ID Packet ============= *)

userID = StringToByteArray["John <test@ygtjispt.com>"];
userIDPacket = addPacketHeader["UserID", userID];
userIDDataForSignature = getUserIDPacketForSignature[userID];
hashedUserID = Hash[userIDDataForSignature, "SHA", "ByteArray"];

(* ============= Signature Packet ============ *)
sha1 = 2;
signatureVersion = 4;
signatureType = 16; (* certification User ID and Public Key packet *)
\
signatureAlgorithm = rsa;
hashAlgorithm = sha1;
signatureCreationTime = 
  toByteArray[
   QuantityMagnitude[
    DateDifference[{1970, 1, 1}, {2021, 4, 10}, "Second"]]];
hashedSubPacket1 = 
  addSubPacketHeader["SignatureCreationTime", signatureCreationTime];
hashedSubpackets = addHashedOrNoneSubPacketHeader[hashedSubPacket1];
nonHashedSubPacket1 = addSubPacketHeader["KeyID", keyID];
nonHashedSubPackets = 
  addHashedOrNoneSubPacketHeader[nonHashedSubPacket1];
signatureData = 
  Join[{signatureVersion}, {signatureType}, {signatureAlgorithm}, \
{hashAlgorithm}, hashedSubpackets];
hashedSignatureData = 
  Hash[ByteArray[signatureData], "SHA", "ByteArray"];
highBits16 = Take[hashedSignatureData // Normal, 2];
trailerData = getTrailerData[signatureData];
hashedTrailer = Hash[trailerData, "SHA", "ByteArray"];

(* Encrypted data *)

hashField = 
  Join[fingerprint, hashedUserID, hashedSignatureData, 
   hashedTrailer];
encryptedData = 
  toMultiprecision[Encrypt[keyPair["PrivateKey"], hashField]["Data"]];

signaturePacketBody = 
  ByteArray@
   Join[signatureData, nonHashedSubPackets, highBits16, encryptedData];
signaturePacket = addPacketHeader["Signature", signaturePacketBody];

(* ============= Final block =================== *)

finalArray = 
  ByteArray@Join[keyPacket, userIDPacket, signaturePacket];
pKey64 = BaseEncode[finalArray];
crc24Base64 = crc24[finalArray];
Print["-----BEGIN PGP PUBLIC KEY BLOCK-----\n", "Version: OpenPGP\n", \
"Comment: Created with Mathematica 12.2\n", "\n", 
 InsertLinebreaks[pKey64, 
  64], "\n", "=", crc24Base64, "\n", "-----END PGP PUBLIC KEY \
BLOCK-----"]

The result:

enter image description here

Importation into PGPTool:

enter image description here

Producing a Private Key block, which includes the data for both the public and private key, is a more complicated undertaking, dealing with passphrase, private key encryption, deprecated versions and all. One thing required is the value of p and q, the original prime factors. The following function may assist:

(* extracts the prime numbers p and q from the data generated by \
GenerateAsymetricKeypair[] - required for a Private Key packet*)

getpq[n_, d_, e_] := (
  k = d*e - 1;
  t = 0;
  r = NestWhile[t++; #/2 &, k, EvenQ, 1];
  i = 1;
  While[i < 100,
   g = RandomInteger[{0, n - 1}];
   y = PowerMod[g, r, n];
   If[y == 1 || y == n - 1, Goto[loop]];
   j = 1;
   While[j < t - 1,
    x = PowerMod[y, 2, n];
    If[x == 1, Goto[end]];
    If[x == n - 1, Goto[loop]];
    y = x;
    j++;
    ];
   x = PowerMod[y, 2, n];
   If[x == 1, Goto[end]];
   Label[loop];
   i++;
   ];
  Label[end];
  p = GCD[y - 1, n];
  q = n/p;
  {p, q}
  )

(* usage of the above function and related data required
 to build a Private Key packet *)
{p, q} = getpq[
   keyPair["PublicKey"]["PublicModulus"],
   keyPair["PrivateKey"]["PrivateExponent"],
   keyPair["PublicKey"]["PublicExponent"]
   ];
   u = ModularInverse[p, q];
mpD = toMultiprecision[keyPair["PrivateKey"]["PrivateByteArray"]];
mpP = toMultiprecisionByteArray[p];
mpQ = toMultiprecisionByteArray[q];
mpU = toMultiprecisionByteArray[u];
(* ..... and more *)

ORIGINAL ANSWER (NO LONGER RELEVANT): The example below uses the PublicByteArray representing the key to demonstrate formatting a block. This array is just the PublicModulus converted to a byte array. This byte array is not formatted according to the OpenPGP standard. It is missing several pieces of information that would have to be added for the resulting block to be valid.

Here is an example of producing a block for the public key. First we need a function to produce the crc24 hash. I wanted to play with BitVector (introduced in 12.1) so I created the following (non-optimized) function, which gives the four base64 characters hash. I guess this would not work with 12.0.

crc24[bytes_] := (
  (* create a BitVector *)
  
  crcBV = CreateDataStructure["BitVector", 24];
  
  (* initialize the bit vector with 0xb704ce *)
  
  initial = {1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 
    0, 1, 1, 1, 0};
  k = 23;
  Map[If[# == 1, crcBV["BitSet", k--], k--] &, initial];
  
  (* polynome info *)
  
  poly = {23, 18, 17, 14, 11, 10, 7, 6, 5, 4, 3, 1};
  
  (* process each bit of data *)
  Map[
   (b = #;
     i = 7;
     While[i >= 0,
      If[BitAnd[b, BitShiftLeft[1, i]] != 0, v = 1, v = 0];
      xorResult = BitXor[crcBV["BitGet", 23], v];
      k = 23;
      While[k > 0,
       If[MemberQ[poly, k],
        (* true *)
        
        bx = BitXor[crcBV["BitGet", k - 1], xorResult];
        If[bx != 0, crcBV["BitSet", k], crcBV["BitClear", k]],
        (* false *)
        
        If[crcBV["BitGet", k - 1] != 0, crcBV["BitSet", k], 
          crcBV["BitClear", k]];
        ];
       k--;
       ];
      If[xorResult != 0, crcBV["BitSet", 0], crcBV["BitClear", 0]];
      i--
      ]) &
   , bytes // Normal];
  
  bitList = Map[crcBV["BitGet", #] &, Flatten@Range[23, 0, -1]];
  pList = Partition[bitList, 8];
  BaseEncode@
   ByteArray@
    Map[(#[[8]] + #[[7]]*2 + #[[6]]*4 + #[[5]]*8 + #[[4]]*16 + \
#[[3]]*32 + #[[2]]*64 + #[[1]]*128) &, pList]
  )

The following generates the keys and prints a public key block (with a bogus Version and Comment).

keyPair = GenerateAsymmetricKeyPair[];
pKeyArray = keyPair["PublicKey"]["PublicByteArray"];
pKey64 = BaseEncode[pKeyArray];
crc24Base64 = crc24[pKeyArray];
Print["-----BEGIN PGP PUBLIC KEY BLOCK-----\n",
 "Version: OpenPGP\n",
 "Comment: Created with Mathematica 12.2\n",
 "\n",
 InsertLinebreaks[pKey64, 64], "\n",
 "=", crc24Base64, "\n",
 "-----END PGP PUBLIC KEY BLOCK-----"
 ]

enter image description here

$\endgroup$
13
  • $\begingroup$ Many thanks for this! Unfortunately I can't test it, because I only have 12.0. Have you tested the output with say PGPTool to see whether it is recognised as a key block? Also how can one get a private key block? That needs to include both keys in the strict sense but I don't know in what order. If you can post the output as text, I will see whether PGPTool accepts it. $\endgroup$ – ool Apr 6 at 16:09
  • $\begingroup$ See my update in the answer. $\endgroup$ – Jean-Pierre Apr 7 at 0:24
  • $\begingroup$ I'm not sure about "several" other pieces of information being mandatory to include. See this answer. This is so tantalising. $\endgroup$ – ool Apr 7 at 18:13
  • $\begingroup$ Once you have the key in the proper format, then yes it is easy to produce the block and send your key, but the format includes fields in a specific order with proper headers identifying each type of data and its size. These fields include the modulus and the exponent for the public key itself, as well as UserID (email or other) and a signature packet requiring the computation of hash for specific fields followed by encryption with the private key for authentication. This is much more than simply providing a byte array of the modulus. $\endgroup$ – Jean-Pierre Apr 7 at 19:46
  • $\begingroup$ Brilliant, @Jean-Pierre - many thanks for all this work. If the key block is recognised by PGPTool then I count that as a complete success where the public key is concerned. I strongly doubt any other PGP program would fail to recognise it if it's fine for PGPTool. Unfortunately I only have Mathematica 12.0 so I am getting "=BaseEncode[lengthy function here]" after the 780 Base64 chars. This may well be the time for me to upgrade. $\endgroup$ – ool Apr 10 at 18:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.