Supreme KMP Crypto Provider
This Kotlin Multiplatform library provides platform-independent data types and functionality related to crypto and PKI applications:
- Multiplatform ECDSA and RSA Signer and Verifier → Check out the included CMP demo App to see it in action
- Multiplatform AES and ChaCha20-Poly1503
- Multiplatform HMAC
- Biometric Authentication on Android and iOS without Callbacks or Activity Passing** (✨Magic!✨)
- Support Attestation on Android and iOS
- Multiplatform, hardware-backed ECDH key agreement
Tip
Do check out the full API docs here!
Using it in your Projects
This library was built for Kotlin Multiplatform. Currently, it targets the JVM, Android and iOS.
Simply declare the desired dependency to get going:
Key Design Principles
The Supreme KMP crypto provider works differently than the JCA. It uses a Provider
to manage private key material and create Signer
instances,
and a Verifier
, that is instantiated on a SignatureAlgorithm
, taking a CryptoPublicKey
as parameter.
In addition, creating ephemeral keys is a dedicated operation, decoupled from a Provider
.
The actual implementation of cryptographic functionality is delegated to platform-native implementations.
Symmetric encryption follows a similar paradigm, utilising structured representations of ciphertexts and type-safe APIs. This prevents misuse and mishaps much more effectively than the JCA.
Moreover, the Supreme KMP crypto provider heavily relies on a type-safe DSL for configuration. This type-safety goes so far as to expose platform-specific configuration options only in platform-specific sources, even when the actual calls to some DSL-configurable type reads the same as in common code.
Warning
Do not ignore the results returned by any operation!
We heavily rely on KmmResult
to communicate the success or failure of operations. Nothing ever throws!
Provider Initialization
Currently, we provide only one provider, the SigningProvider
, which is used to manage signing keys and create signer
instances. Due to limitations of Kotlin, two discrete implementations of the provider exist: one for mobile targets, and
one for the JVM. Their initialization differs.
iOS and Android
On mobile targets (Android and iOS), simply reference the PlatformSigningProvider
object, and you're good to go!
This provider is backed by the AndroidKeyStore and the KeyChain/Secure Enclave and requires no configuration.
JVM
On the JVM, you need to instantiate the JKSProvider
back it with a JCA KeyStore
.
This can either be an already initialized, loaded one, or you can pass a path to a keystore file:
File-Based | with pre-loaded KeyStore |
---|---|
Usually, passing pre-initialized keystore is enough to cover even custom KeyStore
implementations depending on
a specific SecurityProvider
.
In cases where even more flexibility is needed, it is possible to use withCustomAccessor{}
and pass a custom
KeyStore-accessor, implementing the JKSAccessor
interface.
In addition, the JKSProvider
can be initialized without any backing keystore to create only ephemeral keys, if no
options are passed.
Key Management
The provider enables creating, loading, and deleting signing keys.
In addition, it is possible to create a signing key (and a signer) from a CryptoPrivateKey
.
Key Generation
A key's properties cannot be modified after its creation. Fundamental key-generation options, such as key type, are available on all targets and in common code.
The common options include key type and specifics to the key type. As EC and RSA keys are the only supported ones, this amounts to the following configuration options:
EC | RSA |
---|---|
For EC keys, the digest is optional and if none is set, it defaults to the curve's native digest. For RSA keys, the set of digests defaults to SHA-256 and the padding defaults to PSS. It is also possible to override the public exponent, although not all platform respect this override.
Key Agreement
If you want to use a hardware-backed key for key agreement, you need to specify the corresponding purpose:
Provider.createSigningKey(ALIAS) {
ec {
purposes {
keyAgreement = true //defaults to false
signing = true //defaults to true, no impact on key agreement
}
}
}
Warning
Key generated using Supreme ≤0.6.4 don't have the key agreement purpose set and cannot be used for key agreement. Regenerate such keys, if you want to use them for key agreement!
On Android, key usage purposes are enforced by hardware, on iOS this enforcement is done in software. On the JVM, no strict checks are enforced
(but this may change in the future). Also note that only EC key agreement is currently supported. Hence, the keyAgreement
purpose can only be set for EC keys!
iOS and Android
Both iOS and Android support attestation, hardware-backed key storage and authentication to use a key.
Since all of this is, at least in part, hardware-dependent, the PlatformSigningProvider
supports an additional
hardware
configuration block for key generation.
The following snippet is a comprehensive example showcasing this feature set:
val serverChallenge: ByteArray = TODO("This was unpredictably chosen by your server.")
PlatformSigningProvider.createSigningKey(alias = "Swordfish") {
ec {
// as supported by iOS and Android in hardware
curve = ECCurve.SECP_256_R_1
}
hardware {
// you could use PREFERRED if you want the operation to succeed (without hardware backing) on devices that do not support it
backing = REQUIRED
attestation { challenge = serverChallenge }
protection {
timeout = 5.seconds
factors {
biometry = true
deviceLock = false
}
}
}
}
If multiple protections factors are chosen, any one of them can be used to unlock the key. Biometry could be face unlock or fingerprint unlock, depending on the device and how it is configured. If no timeout is specified, the key requires authentication on every use.
In case an attestation challenge is specified, an attestation proof is generated alongside the key. On iOS, this requires an Internet connection! See also Attestation.
Warning
iOS only supports P-256 keys in hardware! Yes, this means hardware-backed RSA keys are altogether unsupported on iOS!
JVM
The JVM supports no additional configuration options, since it supports none of the above features.
Key Loading
To load a key, simply call provider.getSignerForKey(alias) {…}
.
Depending on how the key was created, it may be necessary or just useful to pass additional options.
Most prominently, you may want to display a custom unlock prompt on mobile targets, if the key
is protected by biometry:
provider.getSignerForKey("Swordfish") {
unlockPrompt {
message = "Authenticate key usage"
subtitle = "We require your authentication to sign data" //Android-only
cancelText = "Cancel"
}
}
This configuration will be used for every sign operation as well.
More often than not, though, you'll want to setup an unlockPrompt
as part of the signing operation
(see Signature Creation).
On the JVM (using the JKSProvider
), another toplevel configuration property is present: privateKeyPassword
,
which is used to unlock the private key, in case it is password-protected
In addition, EC and RSA-specific configuration options are available, to specify a digest and/or padding.
To configure such algorithm-specific options, invoke the ec{}
or rsa{}
block accordingly.
Key Deletion
Simply call provider.deleteSigningKey(alias)
to delete a key.
If the operation succeeds, a key was indeed deleted.
If not, it usually means that a non-existent alias was specified.
Private Key Management
Private key can be loaded from PEM-encoded strings or DER-encoded byte arrays into a CryptoPrivateKey
object:
These keys currently cannot be imported into platform-native key stores (Android KeyStore/ iOS KeyChain). Also, while encrypted keys can be parsed, decryption is currently not natively supported.
Creating a Signer from a CryptoPrivateKey
Note
Signers can only be created for private keys that have a public key and/or a curve attached. This may not be the case when an EC private key was parsed from SEC1 encoding without curve and public key info.
Given a CryptoPrivateKey.WithPublicKey
object and a SignatureAlgorithm
object, a signer can be created as follows:
This only works if key and signature algorithm are compatible. Otherwise, it returns KmmResult.failure
.
If you have an EC private key at hand without a public key attached, simply convert it to a CryptoPrivateKey.EC.WithPublicKey
as follows:
Exporting Private Keys
Note
The exportPrivateKey()
method requires an explicit opt-in for SecretExposure
to prevent accidental export of private keys
Private keys can be exported (typically to be DER or PEM-encoded) from ephemeral signers and ephemeral key objects as follows:
While all signers feature an exportPrivateKey()
method, only some signers allow for actually exporting private key material.
Platform-native signers prevent it (i.e. always return a KmmResult.failure
) when trying to export private keys.
Keys from signers created from a CryptoPrivateKey
(see above), as well as ephemeral signers can be exported.
Signature Creation
Regardless of whether a key was freshly created or a pre-existing key way loaded. The result of either operation
is a Signer
, which can be used as desired.
To sign, simply pass data to sign.
On iOS and Android, it is possible to perform additional optional configuration, such as
setting up an unlockPrompt
:
signer.sign(data) {
unlockPrompt {
message = "Authenticate key usage"
subtitle = "We require your authentication to sign data" //Android-only
cancelText = "Cancel"
}
}
Signature Verification
To verify a signature, obtain a Verifier
instance using verifierFor(k: PublicKey)
, either directly on a
SignatureAlgorithm
, or on one of the specialized algorithms (X509SignatureAlgorithm
, CoseAlgorithm
, ...).
A variety of constants, resembling the well-known JCA names, are also available in SignatureAlgorithm
's companion.
As an example, here's how to verify a basic signature using a public key:
val publicKey: CryptoPublicKey.EC = TODO("You have this and trust it.")
val plaintext = "You want to trust this.".encodeToByteArray()
val signature: CryptoSignature = TODO("This was sent alongside the plaintext.")
val verifier = SignatureAlgorithm.ECDSAwithSHA256.verifierFor(publicKey).getOrThrow()
val isValid = verifier.verify(plaintext, signature).isSuccess
println("Looks good? $isValid")
Tip
Not every platform supports every algorithm parameter. For example, iOS does not support raw ECDSA verification (of pre-hashed data) for curve P-521.
If you use .verifierFor
, and this happens, the library will transparently substitute a pure-Kotlin implementation.
If this is not desired, you can specifically enforce a platform verifier by using .platformVerifierFor
.
That way, the library will only ever act as a proxy to platform APIs (JCA, CryptoKit, etc.), and will not use its own implementations.
You can also further configure the verifier, for example to specify the provider
to use on the JVM.
To do this, pass a DSL configuration lambda to verifierFor
/platformVerifierFor
.
There really is not much more to it. This pattern works the same on all platforms. Details on how to parse cryptographic material can be found in the section on decoding in of the Indispensable module description.
Ephemeral Keys and Ephemeral Signers
Ephemeral keys and ephemeral signers are not backed by a provider, but are still delegated to platform functionality. They are just not persisted and work the same across platforms.
To obtain an ephemeral signer, call Signer.Ephemeral{}
and pass EC or RSA-specific configuration options as you would when creating a key using
the SigningProvider
.
Alternatively, you can create an ephemeral key using EphemeralKey{}
(and again, pass algorithm-specific configuration options).
To obtain a signer from this ephemeral key, call getSigner{}
on it. This, similarly to provider-backed keys, takes
algorithm-specific configuration options, such as a specific hash algorithm or padding, in case more than one was
specified when creating the ephemeral key.
Digest Calculation
The Supreme KMP crypto provider introduces a digest()
extension function on the Digest
class.
For a list of supported algorithms, check out the feature matrix.
HMAC Calculation
The Supreme KMP crypto provider introduces a mac()
extension on the MAC
class. It takes two arguments:
key
denotes the MAC keymsg
represents the payload to compute a MAC for
For a list of supported algorithms, check out the feature matrix.
Symmetric Encryption
Symmetric encryption is implemented in a flexible and type-safe fashion. At the same time, the public interface is also rather lean:
- Reference an algorithm such as
SymmetricEncryptionAlgorithm.ChaCha20Poly1305
. - Invoke
randomKey()
on it to obtain aSymmetricKey
object. - Call
encrypt(data)
on the key and receive aSealedBox
.
Decryption is the same straight-forward affair:
Simply call decrypt(key)
on a SealedBox
to recover the plaintext.
Tip
All data classes (keys, algorithms, ciphertext, MAC, et.) are part of the indispensable module. The actual functionality is implemented as extensions in the Supreme KMP crypto provider.
To minimise the potential for error, everything (algorithms, keys, sealed boxes) makes heavy use of generics. Hence, a sealed box containing an authenticated ciphertext will only ever accept a symmetric key that is usable for AEAD. Additional runtime checks ensure that no mixups can happen.
On Type Safety
The API tries to be as type-safe as possible, e.g., it is impossible to specify a dedicated MAC key (or function) for AES-GCM, and non-authenticated AES-CBC does not even support passing additional authenticated data to the encryption process. The same constraints apply to the resulting ciphertexts, making it much harder to accidentally confuse an authenticated encryption algorithm with a non-authenticated one. Signum uses the term characteristics for these defining properties of the whole symmetric encryption data model.
Characteristics
Cryptographic algorithms have various obvious properties, such as the underlying cipher
(AES and ChaCha branch off SymmetricEncryptionAlgorithm
at the root level), name
, and keySize
.
The broader characteristics also apply to key and ciphertexts (called SealedBox
in Signum.)
These are:
AuthCapability
: indicating whether it is an authenticated cipher, and if so, how:Unauthenticated
: Non-authenticated encryption algorithmAuthenticated
: AEAD algorithmIntegrated
: The cipher construction is inherently authenticatedWithDedicatedMac
: An encrypt-then-MAC cipher construction (e.g. AES-CBC-HMAC)
NonceTrait
indicating whether a nonce is requiredWithout
: No nonce/IV may be fed into the encryption processRequired
: A nonce/IV of a length specific to the cipher is required. By default, a nonce will be auto-generated during encryption.
KeyType
denoting how the encryption key is structuredIntegrated
: The key consists of a single byte array, from which encryption key and (if required by the algorithm) a mac key is derived.WithDedicatedMac
: The key consists of an encryption key and a dedicated MAC key to compute the auth tag.
Warning
NEVER re-use an IV! Let the Supreme KMP crypto provider auto-generate them!
In addition to runtime checks for matching algorithms and parameters, algorithms, keys and sealed boxes need matching characteristics to be used with each other. This approach does come with one caveat: It forces you to know what you are dealing with. Luckily, there is a very effective remedy: contracts.
Contracts
The Supreme KMP crypto provider makes heavy use of contracts, to communicate type information to the compiler.
Every one of the following subsections has their own part on contracts.
All contracts can be combined, meaning it is possible to steadily narrow down the properties of an object.
isAuthenticated()
- if
true
, smart-casts the object's AuthCapability toAuthCapability.Authenticated<*>
- if
false
smart-casts the object's AuthCapability toAuthCapability.Unathenticated
- if
hasDedicatedMac()
- if
true
, smart-casts the object's- KeyType to
KeyType.WithDedicatedMac
- AuthCapability to
AuthCapability.Authenticated.WithDedicatedMac
- KeyType to
- if
false
, smart-casts the object's- AuthCapability to a union type of
SymmetricEncryptionalgorithm<AuthCapability.Authenticated.Integrated
andAuthCapability.Unauthenticated
- KeyType to
KeyType.Integrated
- AuthCapability to a union type of
- if
requiresNonce()
- if
true
smart-casts the object's NonceTrait toNonce.Required
- if
false
smart-casts the object's NonceTrait toNonce.Without
- if
In addition, there's isIntegrated()
, which is only defined for objects having the Authenticated.Integrated
characteristic:
- if
true
, smart-casts the object's- AuthCapability to
SymmetricEncryptionalgorithm<AuthCapability.Authenticated.Integrated>
- KeyType to
KeyType.Integrated
- AuthCapability to
- if
false
, smart-casts the object's- KeyType to
KeyType.WithDedicatedMac
- AuthCapability to
AuthCapability.Authenticated.WithDedicatedMac
- KeyType to
Algorithms
The foundation of symmetric encryption is the class SymmetricEncryptionAlgorithm
. Every operation and all related data classes
need a reference to a specific SymmetricEncryptionAlgorithm
.
Cryptographic algorithms have various obvious properties, such as the underlying cipher
(AES and ChaCha branch off SymmetricEncryptionAlgorithm
at the root level), name
, and keySize
.
Taking all characteristics into account results in the following class definition:
As can be seen, this leaves quite some degrees of freedom, especially for AES-based encryption algorithms, which do exhaust this space. As of 01-2025, the following algorithms are implemented:
SymmetricEncryptionAlgorithm.ChaCha20Poly1305
SymmetricEncryptionAlgorithm.AES_128.GCM
SymmetricEncryptionAlgorithm.AES_192.GCM
SymmetricEncryptionAlgorithm.AES_256.GCM
SymmetricEncryptionAlgorithm.AES_128.CBC.HMAC.SHA_1
SymmetricEncryptionAlgorithm.AES_128.CBC.HMAC.SHA_256
SymmetricEncryptionAlgorithm.AES_128.CBC.HMAC.SHA_384
SymmetricEncryptionAlgorithm.AES_128.CBC.HMAC.SHA_512
SymmetricEncryptionAlgorithm.AES_192.CBC.HMAC.SHA_1
SymmetricEncryptionAlgorithm.AES_192.CBC.HMAC.SHA_256
SymmetricEncryptionAlgorithm.AES_192.CBC.HMAC.SHA_384
SymmetricEncryptionAlgorithm.AES_192.CBC.HMAC.SHA_512
SymmetricEncryptionAlgorithm.AES_256.CBC.HMAC.SHA_1
SymmetricEncryptionAlgorithm.AES_256.CBC.HMAC.SHA_256
SymmetricEncryptionAlgorithm.AES_256.CBC.HMAC.SHA_384
SymmetricEncryptionAlgorithm.AES_256.CBC.HMAC.SHA_512
SymmetricEncryptionAlgorithm.AES_128.WRAP.RFC3394
SymmetricEncryptionAlgorithm.AES_192.WRAP.RFC3394
SymmetricEncryptionAlgorithm.AES_256.WRAP.RFC3394
SymmetricEncryptionAlgorithm.AES_128.CBC.PLAIN
SymmetricEncryptionAlgorithm.AES_192.CBC.PLAIN
SymmetricEncryptionAlgorithm.AES_256.CBC.PLAIN
SymmetricEncryptionAlgorithm.AES_128.ECB
SymmetricEncryptionAlgorithm.AES_192.ECB
SymmetricEncryptionAlgorithm.AES_256.ECB
Baseline Usage
Once you know decided on an encryption algorithm, encryption itself is straight-forward:
val secret = "Top Secret".encodeToByteArray()
val secretKey = SymmetricEncryptionAlgorithm.ChaCha20Poly1305.randomKey()
val encrypted = secretKey.encrypt(secret).getOrThrow(/*handle error*/)
encrypted.decrypt(secretKey).getOrThrow(/*handle error*/) shouldBe secret
Encrypted data is always structured and the individual components are easily accessible:
val nonce = encrypted.nonce
val ciphertext = encrypted.encryptedData
val authTag = encrypted.authTag
val keyBytes = secretKey.secretKey.getOrThrow() /*for algorithms with a dedicated MAC key, there's encryptionKey and macKey*/
Decrypting data received from external sources is also straight-forward:
val box = algo.sealedBox.withNonce(nonce).from(ciphertext, authTag).getOrThrow(/*handle error*/)
box.decrypt(preSharedKey, /*also pass AAD*/ externalAAD).getOrThrow(/*handle error*/) shouldBe secret
//alternatively, pass raw data:
preSharedKey.decrypt(nonce, ciphertext, authTag, externalAAD).getOrThrow(/*handle error*/) shouldBe secret
Custom AES-CBC-HMAC
Supreme supports AES-CBC with customizable HMAC to provide AEAD. This is supported across all Supreme targets and works as follows: In addition, it is possible to customise AES-CBC-HMAC by freely defining which data gets fed into the MAC. There are also no constraints on the MAC key length, except that it must not be empty:
val payload = "More matter, with less art!".encodeToByteArray()
//define algorithm parameters
val algorithm = SymmetricEncryptionAlgorithm.AES_192.CBC.HMAC.SHA_512
//with a custom HMAC input calculation function
.Custom(32.bytes) { ciphertext, iv, aad -> //A shorter version of RFC 7518
aad + iv + ciphertext + aad.size.encodeTo4Bytes()
}
//any size is fine, really. omitting the override generates a mac key
//of the same size as the encryption key
val key = algorithm.randomKey(macKeyLength = 32.bit)
val aad = Clock.System.now().toString().encodeToByteArray()
val sealedBox = key.encrypt(
payload,
authenticatedData = aad,
).getOrThrow(/*handle error*/)
//because everything is structured, decryption is simple
val recovered = sealedBox.decrypt(key, aad).getOrThrow(/*handle error*/)
recovered shouldBe payload //success!
//we can also manually construct the sealed box, if we know the algorithm:
val reconstructed = algorithm.sealedBox.withNonce(sealedBox.nonce).from(
encryptedData = sealedBox.encryptedData, /*Could also access authenticatedCipherText*/
authTag = sealedBox.authTag,
).getOrThrow()
val manuallyRecovered = reconstructed.decrypt(
key,
authenticatedData = aad,
).getOrThrow(/*handle error*/)
manuallyRecovered shouldBe payload //great success!
//if we just know algorithm and key bytes, we can also construct a symmetric key
reconstructed.decrypt(
algorithm.keyFrom(key.encryptionKey.getOrThrow(), key.macKey.getOrThrow()).getOrThrow(/*handle error*/),
aad
).getOrThrow(/*handle error*/) shouldBe payload //greatest success!
Contracts
Encryption algorithms feature one specific pair on contract-powered functions to determine the type of the cipher. While this knowledge of this property is purely informational, it might still come in handy:
isBlockCipher()
smart-casts the algorithm toBlockCipher
if true andStreamCipher
otherwise.isStreamCipher()
smart-casts the algorithm toStreamCipher
if true andBlockCipher
otherwise.
Symmetric Keys
Symmetric keys share the same characteristics as symmetric encryption algorithms, to ensure that keys can only be used with compatible algorithms.
Generating, Importing, and Exporting
The main function for key generation is SymmetricEncryptionAlgorithm.randomKey()
.
This always works, even without type information.
For algorithms with a dedicated MAC key, an overloaded variant is available too:
Note
Parameters and properties of the different key types are deliberately named distinctly and the functions are intentionally only available, if enough
type information about the algorithm is available. hasDedicatedMac
is available on keys too!!
It is, of course, possible to access the raw key bytes to export the,. Depending on the key type, these are:
encryptionKey
andmacKey
for symmetric keys with a dedicated MAC keysecretKey
for symmetric key which only use a single key
Importing keys is also straight-forward. For encryption algorithms with a single key (and only for those),
simply call SymmetricEncryptionalgorithm.keyFrom(secretKey: ByteArray)
.
In case of an AEAD algorithm with a dedicated MAC key, call keyFrom(encryptionKey: ByteArray, macKey: ByteArray)
.
Danger Zone
It is possible to manually generate a nonce/IV for algorithms that require an IV/nonce. However, you typically don't need this
since IVs/nonces are auto-generated when encrypting. If you insist, you can call SymmetricEncryptionAlgorithm.randomKey()
on algorithms that require a nonce. You must, however, explicitly add an opt-in for @HazardousMaterials
!.
If you really want to feed a manually generated nonce/IV into the encryption process, call andPredefinedNonce(nonce: ByteArray)
on a symmetric key object, prior to calling encrypt(data: ByteArray)
.
Sealed Boxes and Decryption
Sealed boxes represent encrypted data. There's more to the ciphertext bytes to encrypted data. Most notably the nonce/IV, for
algorithms which require them. In Signum's data model, the algorithm is also part of a sealed box in order to match characteristics.
Yet, sealed boxes are a bit more relaxed. They don't really care for whether an AEAD algorithm requires a dedicated MAC key
or not. Hence, there is no contract-backed function hasDedicatedMacKey()
.
Tip
If you want to decrypt external data and don't need to pass it around as a SealedBox
,
use SymmetricKey.decrypt
rather than SealedBox.decrypt
!
Decryption is possible in two ways: On the on hand, you can create a SealedBox
by calling SymmetricEncryptionAlgorithm.sealedBox()
and then call .decrypt(key)
on it.
Alternatively, it is possible to directly call SymmetricKey.decrypt()
and pass nonce/IV (if any), ciphertext bytes, auth tag (if any) and additional authenticated data (if any).
The first variant will allow for arbitrary combinations of characteristics for convenience.
The second option, however, will only allow passing a nonce/IV if the algorithm associated with a symmetric key
has the corresponding characteristic.
The same holds for the auth tag and additional authenticated data.
Attestation
The Android KeyStore offers key attestation certificates for hardware-backed keys.
These certificates are exposed by the signer's .attestation
property.
For iOS, Apple does not provide this capability, but rather supports app attestation. We therefore piggy-back onto iOS app attestation to provide a home-brew "key attestation" scheme. The guarantees are different: you are trusting the OS, not the actual secure hardware; and you are trusting that our library properly interfaces with the OS. On a technical level, it works as follows:
Note
This section assumes in-depth knowledge of how an Apple attestation statement is created and validated, as described in the Apple Developer Documentation on AppAttest.
We make use of the fact that verification of clientHashData
is purely up to the back-end.
Hence, we create an attestation key, immediately afterwards create a P-256 key inside the secure enclave, and compute
clientHashData
over both the nonce obtained from the back-end and the public key bytes of the freshly created, SE-protected
EC key.
The iOS attestation type hence includes an attestation statement, the challenge, and the public key, so that the back-end
can easily verify the attestation result based on Apple's AppAttest service and the public key bytes, hence emulating
key attestation. Strictly speaking, this is a violation of the process described by Apple, but cryptographically, it is
perfectly sound!
The JVM also "supports" a custom attestation format. By default, it is rather nonsensical. However, if you plug an HSM that supports attestation to the JCA, you can make use of it.
The feature matrix also contains remarks on attestation, while details on the attestation format can be found in the corresponding API documentation.
Key Agreement
Bug
The Android OS has a bug related to key agreement in hardware. See important remarks on key agreement!
In general, key agreement requires one private and n public values. Key distribution/exchange may happen by any means and is not modelled in Signum. In addition, iOS only supports ECDH key agreement, hence only ECDH key agreement with a single public value is supported. Private key agreement material is usually generated locally (preferably in hardware), as outlined in the key generation subsection on this matter. However, it is also possible to import an EC private key.
On iOS and Android (starting with Android 12), key agreement is possible in hardware and can require biometric authentication for hardware-backed keys. Custom biometric prompt text can be set in the same manner as for signing.
Warning
Key generated using Supreme ≤0.6.4 don't have the key agreement purpose set and cannot be used for key agreement. Regenerate such keys, if you want to use them for key agreement:
Tip
To generate an ephemeral private value for ECDH key agreement, simply invoke KeyAgreementPrivateValue.ECDH.Companion.Ephemeral()
.
Every KeyAgreementPrivateValue
comes with the corresponding public value attached. This may come in handy for testing.
Once a private and a public value have been obtained, simply call theOneValue.keyAgreement(theOtherValue)
.
The keyAgreement()
extension function is present on both KeyAgreementPublicValue
and KeyAgreementPrivateValue
, thus making it irrelevant
whether the function is invoked on the public value or on the private value.
The return value of a key agreement is always a (KmmResult-wrapped) ByteArray
without additional semantics.