Bifroest | Mr. Tines | CTC Home | CTClib | MacCTC | CTCjava | Manual |
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.
The following files are important in the manipulation of PKE ciphers:-
bignums.c
: Multiple precision
arithmetic(MPA).
pkbignum.c
: Some commonly useful operations using MPA, notably
modular-inverse and finding random primes.
pkcipher.c
: A range of operation
for formatting, examining and copying keys
pkautils.c
: Encrypting and decrypting secret-keys and generating
key finger-prints.
pkops.c
: Actual PKE operations (some coded in line; some referencing
functions in separate files).
keyhash.c
: Maintenance and control of key-ring hashtable.
keyio.c
: I/O operations on keys.
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
.
The following operations need to be done.
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".
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)
case
label before the return
TRUE
statement.
boolean recognised_PKE_algor(byte algor)
recognised_PKE_algor
() returns TRUE on.
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.)
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.
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.
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.
There are essentially five separate operations that need coding. They are:-
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.
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.
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
hashDigest(sig->md_algor)
.
sig->signature
nums
field of this structure is the
array of MPNs that is the signature itself.
sig->pubkey
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.
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.
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.
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.
[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.