[Discussion] Swift Crypto

Swift Crypto

Package Description

Swift Crypto is an open-source implementation of a substantial portion of the API of Apple CryptoKit suitable for use on Linux platforms. It enables cross-platform or server applications with the advantages of CryptoKit.

Package Name swift-crypto
Module Name Crypto
Proposed Maturity Level Sandbox
License Apache 2.0
Dependencies None (Vendored copy of BoringSSL)

Introduction

An extremely wide range of applications rely on accessing some form of cryptographic primitive in order to function safely and correctly. Swift Crypto brings the API of Apple CryptoKit, a framework focused on providing solutions to common cryptographic problems with safe, composable, high-level interfaces, into the wider Swift ecosystem. As server-side Swift is a vital part of the wider Swift ecosystem, Swift Crypto aims to be an important building block for a wide range of server-side applications and frameworks, bringing the same powerful APIs that are available on Apple platforms to server-side use-cases.

Motivation

The number of server-side use-cases for cryptography is enormous, and affects almost all server-side applications in one form or another. Often these use-cases can be quite simple, such as needing to verify message hashes. However, in many cases the work required is substantially more complex, such as performing key agreement operations to establish shared encryption secrets, or performing message signing operations.

Today there are a wide range of cryptographic libraries available to server developers to allow them to perform cryptographic operations. Most of these libraries, however, are quite low-level. This leads to users needing to interact with raw pointers, or to manually manage memory, or to have to make complex choices about exactly what cryptographic technologies to use. In the best case this can lead to decision paralysis, but in the worst case users can inadvertently introduce security critical vulnerabilities into their applications.

We believe that there is an important space in the ecosystem for a library that focuses on providing high-quality interfaces to a limited number of cryptographic primitives. The goal is to provide safe, high-performance APIs to a collection of cryptographic "good choices". In the vast majority of cases, developers will not need anything other than what Swift Crypto provides, and will be able to write safe Swift code to interact with these high-level APIs without needing to be experts in their use. In some cases developers will be developing brand new systems that use cryptography: in these cases, they can safely pick anything from Swift Crypto (that doesn't include the word Insecure in its API!) and be confident that their choice is a good one.

Detailed design

Swift Crypto has an extremely large API surface, so an exhaustive exploration of that API would make this document impractically long. We recommend consulting the Apple CryptoKit documentation for an outline of this API. However, we can provide a high level overview of the functions provided by Swift Crypto, and some examples of the API.

Cryptographic Hash Functions

Cryptographic hash functions are commonly used to provide a "message digest": essentially, it maps an arbitrarily sized chunk of data into a fixed size chunk of data. A cryptographic hash function must be a "one-way function", which means that given a specific digest it must be very hard to produce a message that would generate that digest. Note that cryptographic hash functions are not suitable for password hashing: at this time, Swift Crypto does not contain any hash functions that are suitable for this use-case.

Swift Crypto provides 5 cryptographic hash functions:

  • SHA256
  • SHA384
  • SHA512
  • Insecure.SHA1
  • Insecure.MD5

Each of these hash functions supports two usage modes: one-shot and incremental/streaming. The one shot mode is extremely straightforward to use:

static func hash<D>(data: D) -> Self.Digest where D : DataProtocol

They can also be used incrementally with the following functions:

init()

mutating func update<D>(data: D) where D : DataProtocol

mutating func update(bufferPointer: UnsafeRawBufferPointer)

func finalize() -> Self.Digest

Swift Crypto's cryptographic hash functions and digest types are all value types. This allows interesting use-cases built on top of value semantics, such as rapidly calculating the digest for a number of different values that have the same prefix and different suffixes.

Digest's are simple types: they are sequences of UInt8, and they are able to be compared with both themselves and any arbitrary sequence of bytes.

Message Authentication Codes

A message authentication code (MAC) is a short chunk of information that is used to confirm that a message was sent by a specific sender, and has not been modified in transit. These are heavily used in any circumstance where information is transferred between multiple peers where it is possible to ensure that these peers have a shared secret, or in constructions where data is handed to an untrusted party that will eventually send it back to you (e.g. macaroons).

Swift Crypto supports one such MAC, HMAC. HMAC is parameterised over the hash functions above. Like the hash functions above, HMACs can be calculated using both a one-shot API and a streaming API. The streaming API looks essentially identical to the hash function API, but substitutes HashFunction.Digest for HMAC<HashFunction>.MAC. The one-shot API is:

static func authenticationCode<D>(for data: D, using key: SymmetricKey) -> HMAC<H>.MAC where D : DataProtocol

Once again, this is a generic API that allows a wide range of data types to be used, including SwiftNIO's ByteBufferView and Foundation's Data.

This API also provides an extra helper for users that are already holding a HMAC<HashFunction>.MAC:

static func isValidAuthenticationCode<D>(_ authenticationCode: HMAC<H>.MAC, authenticating authenticatedData: D, using key: SymmetricKey) -> Bool where D : DataProtocol

This allows users to express the high level question ("is this MAC valid for that data") without needing to directly calculate the MAC over the data they want to verify.

Ciphers

Swift Crypto provides two ciphers for use. In both cases these are Authenticated Encryption with Additional Data (AEAD) ciphers. These are modern constructions that are extremely resilient against attacks that can be launched by modifying the ciphertext of encrypted data. As AEAD ciphers cannot operate in a streaming mode, both ciphers offer one-shot APIs. There are four functions provided:

static func seal<Plaintext>(_ message: Plaintext, using key: SymmetricKey, nonce: Cipher.Nonce? = nil) throws -> Cipher.SealedBox where Plaintext : DataProtocol
static func seal<Plaintext, AuthenticatedData>(_ message: Plaintext, using key: SymmetricKey, nonce: Cipher.Nonce? = nil, authenticating authenticatedData: AuthenticatedData) throws -> Cipher.SealedBox where Plaintext : DataProtocol, AuthenticatedData : DataProtocol

static func open(_ sealedBox: Cipher.SealedBox, using key: SymmetricKey) throws -> Data
static func open<AuthenticatedData>(_ sealedBox: Cipher.SealedBox, using key: SymmetricKey, authenticating authenticatedData: AuthenticatedData) throws -> Data where AuthenticatedData : DataProtocol

These APIs provide simple options for the common case where there is no authenticated additional data, while allowing users to provide that data when needed. They also choose sensible random values for nonces when users don't need to provide a specific one.

Both ciphers also offer a SealedBox construction. This construction is an association of plaintext, nonce, and tag, the complete union of things that need to be preserved for decryption of the data. SealedBox supports a default serialization mode as well, so in many cases users can simply use SealedBox.combined to emit the data and SealedBox.init(combined:) to parse it.

Public Key Cryptography

The gold standard of modern public key cryptography is elliptic curve cryptography. Swift Crypto supports the most widely-used elliptic curve constructions. In particular, it supports both key agreement and signing using the three major NIST elliptic curves (P-256, P-384, and P-521) and Curve 25519. These curves cover the vast majority of elliptic curve uses today, and offer a wide range of compatibility with other implementations.

The curves provide extensive APIs for signing and verifying signatures, as well as performing key exchanges. These APIs follow the patterns above: they provide generic interfaces over DataProtocol, they return typed objects (e.g. ECDSASignature) with value semantics, and they have very little configuration. There are also helpers for generating keys from random bytes.

Maturity Justification

While CryptoKit has wide usage on Apple platforms, Swift Crypto has up until it was released on February 3rd 2020 only been used by Apple internally. For this reason it does not meet the criterion for moving directly to Incubating. Due to the importance of this use-case and the value of a shared API between client and server platforms, we expect that Swift Crypto will rapidly acquire sufficient use to move to Incubating and Graduated, but at this time Sandbox is the most appropriate case.

22 Likes

+1

This is fantastic to see happen :) This is unblocking so many uses cases by providing a common implementation.

+1 :)

+1, seems like a no-brainer to me :slight_smile:

I really could have used this at my last job, but I'm glad to see it coming anyway.

+1, no concerns here :+1:

+1.

Just one thought...

This project vendors libcrypto, requiring all users to compile it themselves. The pros/cons of this vendoring approach were rehearsed during the discussion about migrating swift-nio-ssl to use BoringSSL.

It should be noted that, given the community's strong adoption of SwiftNIO, it is very likely that swift-crypto and swift-nio-ssl will both be required by apps and frameworks.

In this case, users are now forced to compile libcrypto twice - once for swift-crypto and again for swift-nio-ssl.

A "not fantastic" situation is now even less fantastic...

7 Likes

I guess one option would be to split our BoringSSL into a separate wrapped package that provides no API guarantees (either keep it at 0.X.X or just increment the major version number when required). This could also help those libraries that still need to vend yet another BoringSSL version for missing features in Crypto, like RSA support or DER support. There's a potential complication in getting end users to take updates when there are security issues which might be more complicated if major versions were being bumped.

Having said that, as we've ported Vapor over to Crypto and integrated a few versions of BoringSSL (JWT, APNS, Crypto and NIO SSL all pull in their own version of BoringSSL, though APNS is in the process of being migrated to sit on top of JWT), the compile times haven't really been that noticeable.

I think there are three options, none appealing.

The first is to do what Tim suggests: make a BoringSSL package to which everyone must pin a specific version. This puts us at enormous risk of dependency hell, because everyone is pinning to one specific version, so even though the other libraries can technically float it’s pretty easy to make the constraints unsatisfiable over major version changes.

The second is to try to version BoringSSL’s API. This is essentially impossible and I’m not inclined to do it, so we could try to redefine the scope and say that we version that API for approved consumers only, where those approved consumers are swift-nio-ssl and swift-crypto. This is more tractable: we can actually build and test across the range of versions to validate that things actually do work. However, this makes the BoringSSL package an attractive nuisance for others: even if we tell non-approved consumers not to use it, they will, and we’ll also get a lot of requests to become approved consumers that widens the space of supported API. I think this one starts out well and goes really bad really fast, ending up either as 1 or as the full-fledged version of 2.

The third option is to take the APIs in BoringSSL that swift crypto and swift-nio-ssl want and wrap them in Swift APIs that we do version. This is a bit like 2, but we don’t need a whitelist of approved consumers because by definition the only versioned APIs are the ones we need. This has the same problems: other users will want to expand that scope, and we rapidly have provided a low-level BoringSSL wrapper API. The more of BoringSSL we wrap, the more likely we’ll have to rapidly bump major versions, and we return to dependency hell very quickly.

If the community is sufficiently troubled by the extra compile I am open to investigating one or all of those solutions. But I want to be clear that I don’t think there are any solutions that will satisfy the desires of the community in this way. Longer term we have opportunities around moving swift-nio-ssl onto swift-crypto, but we have a lot of work between there and here.

3 Likes

Thanks very much everybody. The proposal has been accepted (on the 4th March 2020) to "sandbox" and is now listed in the SSWG Package Index.

1 Like