Bifroest Mr. Tines MacCTC CTCjava Manual Pages
Bifroest Mr. Tines CTC Home CTClib MacCTC CTCjava Manual


Public Key Ciphers

The handling of PKE keys and their associated algorithms has been considerably rationalised from previous CTC releases. The aim is to provide as far as possible, cipher independent key management requiring only cipher specific code for the specific ciphers themselves. This has been acheived with a few minor exceptions provided that the keys (both public and secret) and the ciphertexts (both key-exchange and signature) all consist only of a fixed (for the cipher) number integers (of 64k bits or less). All PKE ciphers implemented so far have been based on Number Theory and are in terms of such large numbers. Hence all naturally fall into this form. The implementation of ciphers not in this form would require considerably more coding.

Code Structure

The following files are important in the manipulation of PKE ciphers:-

  1. bignums.c: Multiple precision arithmetic(MPA).
  2. pkbignum.c: Some commonly useful operations using MPA, notably modular-inverse and finding random primes.
  3. pkcipher.c: A range of operation for formatting, examining and copying keys
  4. pkautils.c: Encrypting and decrypting secret-keys and generating key finger-prints.
  5. pkops.c: Actual PKE operations (some coded in line; some referencing functions in separate files).
  6. keyhash.c: Maintenance and control of key-ring hashtable.
  7. keyio.c: I/O operations on keys.
  8. keyutils.c: Miscellaneous key operations.

The division of functions between files is not as systematic as would be ideal. This requires further work. However under the current regime bignums.c, pkbignum.c, keyhash.c, keyio.c, keyutils.c and pkautils.c are all wholly cipher independent (albeit they include additions to simplify some PKE implementions). You are strongly advised to leave them that way. If you make any modifications here, you are probably have serious problem upgrading to future versions of CTC. There is a lot of code there (about 110k). However it is necessary to make some changes to pkcipher.c and pkops.c.

Adding PKE cipher

The following operations need to be done.

Add byte values

You need to define the byte values to be used for your cipher. We recommend that you follow the value define in specification (section 9.1) for OpenPGP. This will normally be in the range "100 to 110 - Private/Experimental algorithm".

Modify master control functions

There are three very important functions that they are used by large quantities of the code to determine how to handle any PKE data (key or ciphertext). With these modified to respond correctly to your cipher byte value, the bulk of the PKE code will work for your cipher without further modification. The key functions are the first three functions in pkcipher.c:-

boolean valid_PKE_algor(byte algor)
This function must be modified to return TRUE for the new algorithm byte value. Insert a suitable case label before the return TRUE statement.
boolean recognised_PKE_algor(byte algor)
Strictly this function needs no modification as it will return TRUE for any value that recognised_PKE_algor() returns TRUE on.
int PKEsize(byte algor, PKEsizeParam purpose)
This routine returns five vital statistics for the algorithm, which should be placed in a constant of type PKEparams. This defines the number of multiple precision numbers in public key, secret key, (key-exchange)ciphertext and signature respectively. It is also necessary to specify which MPN in the public key is the 'characteristic number', i.e. the number which defines the 'number of bits' of the key. (e.g. An RSA key consists of a modulus and an exponent. The modulus is the characteristic number.)
Once the appropriate constant array is available the appropriate value needs to be returned if the function is called with the new algorithm byte.

Check sizing

You need to check that your new PKEparams values have not broken the existing maximum values for each. (Note if they have the first time PKEsize() executes for the broken value, the assert statement at the end of the routine will fail.) If you have exceed a maximum then it is necessary to increase the size of the relevant maximum size constant. N.B. It is very important to increase the size of the #define constants to ensure enough space is allocated. Do not attempt to circumvent the checks.

(Optionally) Supply Naming Information

In file keywords.c, the constant array PKEalgors provides algorithm byte of printable text translation. Most application will handle the algorithm more elegantly if the additional name conversion is available.

Other algorithm dependencies

There are a couple of other places in pkcipher.c where some algorithms diverge from the default. RSA & eliptic-curves alway direct access to the key Id. (without calculation of key fingerprint) in read_mpn_summary().Pegwit codes perform some operations in slightly non-standard ways for compatibility with those programs. Unless there are very good reasons for doing so, like compatibility with other implementations, you are recommended to stick strictly to default behaviour.

Actually code the algorithm

There are essentially five separate operations that need coding. They are:-

  1. Encrypting to a public key
  2. Verifying a signature with a public key
  3. Decrypting with a secret key
  4. Signing with a secret key.
  5. Generate a new key pair

The first two are useful on their own as they allow users to communicate using other people's public keys even though they cannot use secret keys for the algorithm. All five are necessary for full use of the algorithm.

Encrypting to a Public Key

This is the action of sending (key-exchange) a session key to a public key. The modifications may be restricted to function packConvKey() in file pkops.c. This function includes two switch() statements that must have appropriate code added for you algorithm.

The first merely needs to prevent the default: case being hit and set the local variable length to the maximum length (in bytes) of ciphertext that the key can handle. Assuming that this maximum size is the length of the 'characteristic integer' the value should equal (pubkeysize(pub_key)+7)/8.

The second needs to actually code the message. The plaintext (of the session key) is available in variable plaintextkey. The public key available in variable pub_key, of which the actually numbers of the key are in pub_key->pkdata.nums[] array. The ciphertext must be placed in the details->cypherKey[] array. Note that all of these value are bignums, and normally require only multiple precision arithmetic. See the existing ciphers as examples.

Verify a signature

This is carried out by getSignature() and verifySignature() both in file pkops.c. Any new Public Key algorithm implementation should leave getSignature()[1] unchanged, and implement signature verification (i.e. the decrypt-with-public-key operation) in the routine verifySignature(), at which time the appropriate message digest for the message to be checked is to hand.

This is a little more complex than encrypting as the handling of the hash digest is not uniform across all algorithms. Again an extra case must be added to the switch() statement that forms near all of verifySignature(). The inputs are:-

digest
The message digest being validated. To obtain its length used hashDigest(sig->md_algor).
sig->signature
The signature notable the nums field of this structure is the array of MPNs that is the signature itself.
sig->pubkey
The public key; as for encrypting the vital number are in pub_key->pkdata.nums[]

The only output is the function return value. TRUE for a valid signature, FALSE is not. Assuming that default formatting is being used then the digest should be formatted with formatSignature() and then converted to a bignum with get_mpn(). Non-default formatting is far more complex and beyond the scope of this document.

Decrypting with a secret key

This is done in function unpackConvKey() in file pkops.c. An extra case must be added to the main switch statement for the new algorithm. The cyphertext is available in the bignum array cyphertextkey[]. The secret key is available in sec_key with the significant number of the key in sec_key->pkdata.nums[]. If the secret key is not self contained but data from the associated public key is also required, then this is accessible by getting the public key publicKey(sec_key) and using the values in pub_key->pkdata.nums[]in the normal way. Assuming that default behaviour has been used for the formatting on encryption, the resulting plaintext number should be converted to external (file) format with put_mpn() and placed in keybuffer as output (see RSA case as an example). If other formatting is used it is necessary build a suitable buffer for extractConvKey(), or even by-pass extractConvKey() entirely. However implementing this way is far more complex and beyond the scope of this document.

Signing with a secret key

This is done within function putSignature() in file pkops.c. This function includes two switch() statements that must have appropriate code added for you algorithm.

The first merely needs to prevent the default: case being hit and set the local variable length to the maximum length (in bytes) of ciphertext that the key can handle. Assuming that this maximum size is the length of the 'characteristic integer' the value should equal (pubkeysize(pub_key)+7)/8.

The second actually does all of the work. The plaintext signature (the value to be signed as a bignum) is available in variable plaintextsig. The secret key available in sec_key. The key data is extracted as described above for decrypting. The results must be placed as bignums in the sig->signature.nums[] array. This (as with the verification) operation assumes default cyphertext packet formatting. Using any other formatting makes the implementation far more complex and beyond the scope of this document.

Generating a new key-pair

This is done within function newPKAkey(). All implementations to date have implemented the actual generation in a separate function and merely added a new case to the switch() statement calling this function. It is recommended the new implementations do the same. The function should complete the numeric values in each key, sec_key->pkdata.nums[] & pub_key->pkdata.nums[].

Whereas it is not strictly necessary, it is advisable to implement verifySecKey() to take a validate that a secret key really is valid (really is will interoperate with the associated public key). verifySecKey() is used on decrypting a key to ensure that it really has been decrypted correctly. The decryption process includes a checksum, but it is only 16-bit so random false positives are possible, albeit rare. A proper implementation of verifySecKey() should eliminate them.

Notes


[1]: Functionally, getSignature() is truly vestigial; and has a role for RSA entirely for historical reasons. With RSA the whole packet can be recovered, its format inspected and the bytes corresponding to the signature can be exactly recovered; the later verification is a simply byte-by-byte comparison. It made sense when RSA was the only supported algorithm to perform that part of the processing as soon as the secret key encrypted packet was extracted.

webmaster@bifroest.demon.co.uk