This document describes the strategy taken by the haskell-cryptography organisation on the subject of implementing and structuring bindings to cryptography libraries.
This is a living document.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (RFC2119 + RFC8174) when, and only when, they appear in all capitals, as shown here.
This is the 'lower' level of the bindings. It is designed to serve as a thin
layer over C, consisting only of foreign import
bindings, any necessary setup
to ensure that users do not have to deal with C linkage or bundling issues, and
documentation of use.
Specifically, the following principles apply:
- Users of these libraries SHOULD never have to even think about how the libraries are bundled or linked to. Any and all handling of this, for any platform, MUST be done by the library.
- These libraries MUST NOT have any dependencies other than
base
. Test and benchmark suites are exempt from this rule - The documentation for the modules MUST be strong enough to stand alone: specifically, at least 80% of all questions regarding the use of bindings SHOULD be answerable entirely from the documentation of the module, not the library it wraps or binds to.
- CI MUST check that the wrapping or bundling works correctly. This MUST include checks on Windows: this is especially important, as this is the most problematic platform.
This is the 'higher' level of the bindings, broadly divided into two categories:
- The 'algorithmic' category, where the focus is on providing a familiar interface to the libraries providing this functionality; and
- The 'functional' category, where the focus is on performing some task, regardless of what algorithms get used there.
An example of a package in the 'algorithmic' category, consider the package
cryptography-argon2
, which would provide a set of wrappers around familiar
Haskell functionality, allowing it to be used in a Haskell project which
requires Argon2 (for whatever reason) without having to deal with C, the FFI, or
anything similar. An example of a package in the 'functional' category, consider
the package cryptography-passwords
, which embodies good practices for
password hashing, allowing Haskell projects to handle this task easily, without
necessarily needing to understand precisely how this gets done.
Both of the examples would use the C-esque cryptography-libsodium-bindings
for
implementations: the differences are their focus and the APIs they expose. For
'algorithmic' category packages, the focus is on providing easy and secure use
of an algorithm in Haskell, without considering use case; for 'functional'
category packages, the focus is on providing an easy and secure way to do some
task, without focus on the algorithm or algorithms chosen to do so.
These packages, unlike the C-esque layer, are expected to provide more abstraction, both to avoid the complexities of C and the FFI, but also to address various expectations that Haskellers have, regarding things such as:
- No unnecessary use of
IO
(even if this is by way of 'safe'unsafePerformIO
use); - Implementation of standard, law-abiding type classes, such as
Eq
, in a correct and secure way; - Wrappers around raw
ForeignPtr
s for data that is meant to be passed around, or generated in a very specific way.
Specifically, the following principles generally apply:
- These libraries SHOULD NOT depend on non-boot packages;
- Test and Benchmark suites are exempt from this rule
- Abstraction SHOULD be kept minimal.
newtype
-wrapping aForeignPtr
is fine, type class hierarchies without laws are not. - Whenever possible, if there are multiple secure ways to do something, without a definite 'this one is always better' option, users of these libraries should be given all options, as well as a detailed explanation of why they should prefer any given one.
- Partiality SHOULD NOT exist unless specifically marked as unsafe, through a
dedicated
Unsafe
module or prefixing the name withunsafe
. This extends to instances of known partial type classes (such asRead
).
These principles apply to 'algorithmic' category packages specifically:
- The packages should document as much as possible that is known about the algorithm, providing relevant links for detail where it would be too hard to include.
- The typical secure uses should be enumerated.
- Any caveats to performance should be documented clearly.
- 'The algorithm is the focus': abstractions should only be done to hide the use of C FFI, or where the abstraction is both obvious and unarguable. Use cases should not be pre-supposed.
These principles apply to 'functional' category packages specifically:
- 'The task is the focus': specifics regarding algorithms or implementations should only factor into the abstractions if there really is no other way.
- There should be one, and only one, correct way to perform the task the package is designed for. If there are legitimately multiple right ways to do something, their differences must be clearly enumerated and marked, with a focus on non-security-familiar users.
- Doing the right thing should be easy; doing the wrong thing should be close to impossible.
Given infinite time, there are quite a few possible bindings and wrappers we could define: however, given finite time and labour, some standards for inclusion and effort must be put down. In our case, given any possible thing we could work on, it must fit the following criteria to be considered, in the following order:
- Is this a good practice in the current year regarding security?
- Is there genuine need from some person or organization in the ecosystem and community for this?
We prioritize security over demand, as one of the core foci of this organization and project is the improvement of security-related practices in the Haskell ecosystem. By implementing functionality that is known to be insecure or a bad practice, we are implicitly stating that this is OK, which works directly counter to a stated goal.