Le’Sec is a personal research project, to explore concepts of a security library with plugin functionality as its core feature.
Background: OpenSSL’s plugin development #
I’ve followed OpenSSL Library’s development for many years, with a particular interest in how crypto plugins were implemented.
Ages ago, there were ENGINEs, which I believe worked well for a library where all structures are publically open, making it relatively safe to pass instance pointers back and forth between libcrypto and each engine.
Much later on (2018-2019), a new architecture for plugins was drafted and started being implemented. This time around, they’re called providers. This is a much more dynamic, and declarative architecture.
Initial ideas forming #
Having watched all this happening over the years, ideas started to form, based on a single thought:
What would happen if, instead of trying to build a plugin system for an existing crypto library, I start fresh and with a plugin interface, and then build everything around that.
This was about two years ago, and got down to a few questions begin answered:
-
Minimize initial churn
Is it possible to have any plugin implementation initially have to declare very little, and yet allowing for dynamism?
The idea that had formed in my head was that I could take inspiration from the Unix
syscall()
. Each plugin’s implementation of, well, anything could simply be a dispatch function that takes an object pointer to work with, a function number, and variadic arguments:uint32_t dispatchfn(T *o, int num, ...);
This function would of course not be called directly by anyone. Instead, a wrapper function corresponding to each function number
num
would be used. -
A common structure for everything
The idea is to have a common structure that can be passed around everywhere, and be the foundation for any type
T
(as shown with the dispatch function above).This picture was an early drawing of this common structure:
-
dispatch would be the dispatch function mentioned above.
-
destroy would be a separate destructor function, whose responsibility was to destroy the data contents.
-
data would be the address to plugin internal data. This is expected to be created, initialized and handled entirely by the dispatch function, and destroyed by the destroy function.
This common structure would not only be passed from application / library to plugins, but also between plugins, or even be passed back from plugins.
-
-
Generalism
Another idea I had was to explore a generic API for calling plugin functionality, as it seemed like quite a few algorithms could be implemented with relatively few common patterns.
For example, quite a lot of crypto functionality in other libraries support one-shot calls, or stream calls (the typical init / update / final triple), or both.
That should keep the amount of dispatch function numbers low, and ensure that there’s only one way to perform certain functions, and the leave it to the caller (application) to use the functionality in sensible combinations.
-
Environment
Avoid the mistake of having any global data at all cost, i.e. have an object of some sort that can be passed around everywhere, to hold arbitrary data that anyone could need. This environment would have to be created by the calling application, passed in calls that need it, and destroyed by the calling application when not needed any more.
Starting to hack, summer of 2023 #
I was on a long vacation in the summer of 2023. For a part of it, I started writing down the ideas I had, and formed the first few Stories.
This is also when I started to write Le’Sec Core, Le’Sec App and
its lesec-tool
, and the ltc plugin, which is based on LibTomCrypt.
They became a proof of concept, and turned out to actually work, for the little they supported at the time (RSA, DSA, DH, and symmetric ciphers in ECB mode).
During this time and in a period in the following fall, further ideas started to take form:
-
Objects, Operations and Associated Operations
Ideas around these things went through a few iterations, but eventually got down to this:
-
An Object is a conceptual data structure, based on the common structure that I talked about above, that primarily exist to carry a payload of some sort.
Among typical Objects, I expected to find cryptographic keys. As a matter of fact, that was the primary application of an Object that I could think of.
The Object’s dispatch function isn’t expected to provide much functionality of its own, not even functionality to manipulate its payload directly.
However…
-
An Operation is a conceptual data structure, also based on the common structure that I talked about above, that primarily exist to operate on some sort of input and to produce some sort of output, possibly based on an Object, and also on arbitrary parameters (which I’m talking about further down).
Among typical Operations, I could see Encryptors, Decryptors, Signers, Verifiers, and more.
However…
-
The one thing that tied everything together was the idea of the Associated Operation, which is an Operation as described above, with just one additional property: it’s intimately associated with an Object, and therefore and by definition, it has access to that Object’s payload and its internals.
By consequence, the dispatch function of a Associated Operation mustn’t be possible to retrieve directly from the plugin.
Instead, it comes down to the Object’s dispatch function having functionality to create appropriate Operation instances for the task to be performed, and pass that back to the calling application.
Among typical Associated Operations, I could see primitive Encryptors, Decryptors, Signers, Verifiers, etc. What exactly constitutes “primitive” depends on the algorithm and its implementation, but the idea is that these “primitives” should be useful for other higher level algorithms.
For example, based on PKCS#1, an “RSA” key Object implementation could provide the primitives RSAEP, RSADP, RSASP1 and RSAVP1.
However, the use of Associated Operations doesn’t stop there, they could also be used to operate on the payload itself. The common Associated Operations defined at the time ended up being:
- the Generator, which generates an Object payload, given suitable parameters.
- the Constructor, which constructs an Object payload, given data passed as parameters.
- the Extractor, which can be used to extract the items from the Object’s payload, in parametrized form.
-
-
Arbitrary parameters
Parameters have been mentioned a few times.
All algorithm specifications make it very clear that they each use a set of parameters that’s unique to that algorithm. They can represent items of an Object’s payload, or diverse parameters that affect the way an Operation operates.
This must be supported somehow.
An idea I had for parameters is that most of the churn to format the parameter data in memory shall remain with the calling application, leaving it to the plugin implementation to simply use the parameter data as is.
To enable the calling application’s function, paraneters must therefore be described by the plugin implementation, with enough description data so the calling application knows exactly how to deal with each parameter value, down to the last bit.
A basic axiom with this is that if a parameter isn’t declared, it simply doesn’t exist.
This also allows for a good level of discoverability, which could be verified at all times with
lesec-tool
’slist
subcommand.
All of this has turned out to be a foundation that still holds today.