Skip to Content
We're improving our docs. Share your experience and help shape what comes next.
DocumentationDevice AppReferencesCryptography API examples

Cryptography library and syscalls examples

This page shows how to call functions from the Ledger OS cryptography API. It covers digital signatures (ECDSA with Secp256k1, EdDSA with Ed25519/Ed448, Schnorr/BIP-0340) and symmetric encryption (AES-CBC).

This page focuses on calling the API correctly. For conceptual background on asymmetric and symmetric cryptography, elliptic curves, and block ciphers, see the device app architecture overview.

Digital signature algorithms

ECDSA (Secp256k1)

ECDSA is a standard digital signature scheme relying on elliptic curves. A signature scheme usually consists of three algorithms:

  • a key generation algorithm generates a pair of private key and public key
  • a signature algorithm signs the digest of a message with the private key
  • a verification algorithm verifies a signature with the public key

ECDSA uses Weierstrass curves, i.e. curves expressed with a (simple) Weierstrass equation: y^2 = x^3 + a * x + b. a and b are parameters of the curve but they don’t need to be detailed in this document (we only write this equation to highlight the difference between a Weierstrass curve and a twisted Edwards curve introduced later in this document).

One of the most common (Weierstrass) curve is Secp256k1.

Key generation

The function generates a random integer as the private key while the public key is a point on the chosen curve. A point is represented by its coordinates (x,y) which are each represented by an integer.

There are two ways to get the pair of keys:

  1. Generate both randomly with cx_ecfp_generate_pair_no_throw. In this case, the function initializes the structures cx_ecfp_private_key_t and cx_ecfp_public_key_t which hold the private key and the public key respectively.
  2. Initialize the private key with cx_ecfp_init_private_key_no_throw given a bytes array and generate the public key with cx_ecfp_generate_pair_no_throw. cx_ecfp_init_private_key_no_throw initializes a structure cx_ecfp_private_key_t which holds the private key used to sign a message and cx_ecfp_generate_pair_no_throw calculates the public key and initializes a structure cx_ecfp_public_key_t which holds the public key used to verify a signature.

For example, a pair of private/public key to be used with the curve Secp256k1 is generated as follows:

Case 1:

cx_ecfp_private_key_t privateKey; cx_ecfp_public_key_t publicKey; cx_ecfp_generate_pair_no_throw(CX_CURVE_SECP256K1, &publicKey, &privateKey, 0); /* the '0' indicates that a random private key should be generated */

Case 2:

uint8_t rawKey[32] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20}; /* The key in a bytes array */ cx_ecfp_private_key_t privateKey; cx_ecfp_public_key_t publicKey; cx_ecfp_init_private_key_no_throw(CX_CURVE_SECP256K1, rawKey, sizeof(rawKey), &privateKey); cx_ecfp_generate_pair_no_throw(CX_CURVE_SECP256K1, &publicKey, &privateKey, 1); /* the '1' indicates that a private key has already been generated and initialized */

If one needs only to verify a given signature, there is no need to generated a pair of keys. Thus, only the public key has to be initialized with cx_ecfp_init_public_key_no_throw given a bytes array representing the point with the prefix 04 (This convention must be respected).

In the following example, rawPKey is a bytes array filled with the prefix 04 followed by the 32-byte x-coordinate and the 32-byte y-coordinate of the point representing the public key.

uint8_t rawPKey[65] = {0x04, 0x84, 0xbf, 0x75, 0x62, 0x26, 0x2b, 0xbd, 0x69, 0x40, 0x08, 0x57, 0x48, 0xf3, 0xbe, 0x6a, 0xfa, 0x52, 0xae, 0x31, 0x71, 0x55, 0x18, 0x1e, 0xce, 0x31, 0xb6, 0x63, 0x51, 0xcc, 0xff, 0xa4, 0xb0, 0x8c, 0xc4, 0x3d, 0x63, 0xb2, 0x85, 0x9d, 0x46, 0x9f, 0xee, 0x15, 0xf3, 0x1c, 0x9e, 0xdb, 0x53, 0x24, 0x26, 0x6e, 0x6f, 0xd0, 0x40, 0x7e, 0x87, 0x38, 0x2d, 0x60, 0xfc, 0x45, 0x11, 0xac, 0xd8}; cx_ecfp_public_key_t publicKey; cx_ecfp_init_public_key_no_throw(CX_CURVE_SECP256K1, rawPKey, sizeof(rawPKey), &publicKey);

Signing

The signature function takes as input a private key, previously initialized, the digest of a message and outputs an encoded signature (DER encoded). The signature algorithm may be:

  1. deterministic, i.e. the same signature is calculated each time, given the same inputs. In this case the mode CX_RND_RFC6979 must be specified.

  2. random, i.e. the signature values change each time, even if the inputs are identical. In this case the mode CX_RND_TRNG must be specified.

The message shall be hashed to get the correct input length, depending on the chosen curve. Following the previous example with the curve Secp256k1, a message msg is first hashed with the hash function SHA-256 as follows:

uint8_t msg[] = "sample"; uint8_t digest[CX_SHA256_SIZE]; /* CX_SHA256_SIZE is 32 bytes */ cx_hash_sha256(msg, sizeof(msg), digest, CX_SHA256_SIZE);

Assuming that privateKey has been initialized with the curve Secp256k1 and the digest of the message has been computed as in the previous example, an example of a random signature with the curve Secp256k1 is as follows:

uint8_t signature[256]; size_t signatureLen = sizeof(signature); cx_ecdsa_sign_no_throw(&privateKey, CX_RND_TRNG, CX_SHA256, digest, CX_SHA256_SIZE, signature, &signatureLen, NULL)

Verification

The verification function takes as input the public key, the digest of the message and the corresponding (encoded) signature and returns 1 if the signature is verified and 0 otherwise.

Assuming that publicKey has been initialized with the curve Secp256k1 and the signature signature has been calculated as in the previous example, an example of a signature verification with the curve Secp256k1 is as follows:

cx_ecdsa_verify_no_throw(&publicKey, digest, CX_SHA256_SIZE, signature, signatureLen);

EdDSA (Ed25519 and Ed448)

EdDSA is a digital signature scheme relying on specific curves, called Edwards curves. Especially, in this section, we consider twisted Edwards curves. The equation of these curves is different from the Weierstrass equation. They are defined by: a * x^2 + y^2 = 1 + d * x^2 * y^2, where a and d are parameters of the curve.

We supported two twisted Edwards curves: Ed25519 and Ed448. As before, the signature scheme consists of three algortihms: key generation, signature and verification.

Key generation

As for ECDSA, a pair of private key and public keys is generated with cx_ecfp_generate_pair_no_throw or the private key is first initialized with cx_ecfp_init_private_key_no_throw and the corresponding public key is generated and initialized with cx_ecfp_generate_pair_no_throw.

The following code describes the initialization of the private key and the generation of a public key for Ed25519:

uint8_t rawKey[32]; /* All zero key */ cx_ecfp_private_key_t privateKey; cx_ecfp_public_key_t publicKey; cx_ecfp_init_private_key_no_throw(CX_CURVE_Ed25519, rawKey, 32, &private_key); cx_ecfp_generate_pair_no_throw(CX_CURVE_Ed25519, &publicKey, &privateKey, 1);

In the case of Ed448:

uint8_t rawKey[57]; /* All zero key */ cx_ecfp_512_private_key_t privateKey; cx_ecfp_512_public_key_t publicKey; cx_ecfp_init_private_key_no_throw(CX_CURVE_Ed448, rawKey, 57, (cx_ecfp_private_key_t *)&private_key); cx_ecfp_generate_pair_no_throw(CX_CURVE_Ed448, (cx_ecfp_public_key_t *)&publicKey, &privateKey, 1);

The generation of a random pair for Ed25519 is as follows:

cx_ecfp_private_key_t privateKey; cx_ecfp_public_key_t publicKey; cx_ecfp_generate_pair_no_throw(CX_CURVE_Ed25519, &publicKey, &privateKey, 0);

In the case of Ed448:

cx_ecfp_512_private_key_t privateKey; cx_ecfp_512_public_key_t publicKey; cx_ecfp_generate_pair_no_throw(CX_CURVE_Ed448, (cx_ecfp_public_key_t *)&publicKey, &privateKey, 0);

Depending on the length of the keys, one should use the correct structures to hold the keys. For Ed25519, cx_ecfp_private_key_t (cx_ecfp_public_key_t for the public key) is used since the private key is 32-byte long (the public key is 64-byte long), while the 57-byte (456 bits) private key used with Ed448 must be stored within cx_ecfp_512_private_key_t and the 114-byte public key must be stored within cx_ecfp_512_public_key_t.

Signing

The signature is performed by the function cx_eddsa_sign_no_throw. In contrary to ECDSA, the digest of the message is calculated inside the function so it mustn’t be calculated before. However, the identifier of the algorithm used to compute the digest (hash function) must be specified. Usually, SHA-512 is used for Ed25519 and SHAKE-128 for Ed448. Here are some examples:

Assuming that the keys have been properly generated and initialized as in the previous examples, the signature of a message with the curve Ed25519 is obtained as follows:

uint8_t msg[] = "sample"; uint8_t signature[64]; /* The length of the signature is 64 bytes */ cx_eddsa_sign_no_throw(&privateKey, CX_SHA512, msg, sizeof(msg), signature, 64);

Similarly, the signature of a message with the curve Ed448 is obtained as follows:

uint8_t msg[] = "sample"; uint8_t signature[114]; /* The length of the signature is 114 bytes */ cx_eddsa_sign_no_throw((cx_ecfp_private_key_t *)&privateKey, CX_SHAKE128, msg, sizeof(msg), signature, 114);

Verification

The signature verification is performed by the function cx_eddsa_verify_no_throw. As for the signature, the identifier of the hash function is specified.

The verification of the signature is done as follows for Ed25519:

cx_eddsa_verify_no_throw(&publicKey, CX_SHA512, msg, sizeof(msg), signature, 64);

And for Ed448:

cx_eddsa_verify_no_throw((cx_ecfp_public_key_t *)&publicKey, CX_SHAKE128, msg, sizeof(msg), signature, 114);

Schnorr signatures (BIP-0340)

Schnorr signatures are variants of ECDSA. In this document, we give examples for the variant specified in BIP0340 for the Taproot upgrade of Bitcoin. The curve used is Secp256k1.

Key generation

The key generation is the same as for ECDSA.

Signing

The signature is performed by the function cx_ecschnorr_sign_no_throw which takes as input the private key, the message (not any digest), and an auxiliary data which is usually some random data. The auxiliary data is given to the function through the sig parameter and the value CX_RND_PROVIDED is set to the mode parameter to specify this. The parameter sig contains the calculated signature if the function returns properly.

In the example below, the value CX_ECSCHNORR_BIP0340 is also set to the mode parameter to specify that the BIP-0340 implementation is used.

Thus, assuming a properly generated and initialized private key privateKey, a message msg and an auxiliary data aux, the signature is done as follows:

uint8_t msg[32]; /* All zero message */ uint8_t aux[32]; /* All zero auxiliary data */ uint8_t signature[64]; size_t signatureLen = sizeof(signature); memcpy(signature, aux, sizeof(aux)); /* First copy the auxiliary data to the signature array */ cx_ecschnorr_sign_no_throw(privateKey, CX_ECSCHNORR_BIP0340 | CX_RND_PROVIDED, CX_SHA256, msg, sizeof(msg), signature, &signatureLen);

In this example, the hash function used is SHA-256.

Symmetric encryption (AES-CBC)

An encryption is the process of transforming an intelligible message to an unintelligible one. This transformation must be reversible, i.e. the intelligible message can be recovered by the recipient. This transformation is the decryption. Both encryption and decryption are parametrized by a secret key.

Key initialization

The secret key needs to be initialized before one calls the encryption or decryption function. Different sizes of key can be used so it must be specified first. According to the algorithm used, the structure cx_aes_key_t or cx_des_key_t is initialized given a bytes array with the function cx_aes_init_key_no_throw or cx_des_init_key_no_throw.

For example, to perform an AES encryption with a 128-bit key, the initialization step is done as follows:

uint8_t rawKey[16]; /* All zero key */ cx_aes_key_t aesKey; cx_aes_init_key_no_throw(rawKey, sizeof(rawKey), &aesKey);

If a 192-bit key is used:

uint8_t rawKey[24]; cx_aes_key_t aesKey; cx_aes_init_key_no_throw(rawKey, sizeof(rawKey), &aesKey);

If a 256-bit key is used:

uint8_t rawKey[32]; cx_aes_key_t aesKey; cx_aes_init_key_no_throw(rawKey, sizeof(rawKey), &aesKey);

Encryption

The encryption function takes as input a secret key, an initialization vector (which is needed to perform a chained encryption), and the plaintext and outputs the ciphertext. Since the block cipher works on blocks, if the length of the plaintext is not a multiple of the block size, the plaintext must be padded, i.e. additional data is added at the end of the message to fulfill this requirement. This is done by specifying a type of padding method:

  1. CX_PAD_ISO9797M1 to complete the plaintext with only 0.

  2. CX_PAD_ISO9797M2 to complete the plaintext with one 1, then all 0.

If the padding is not required, CX_PAD_NONE is used.

The encryption is performed by cx_aes_iv_no_throw or cx_des_iv_no_throw depending on the algorithm. This function can perform both encryption and decryption so the value CX_ENCRYPT must be passed to the mode parameter for encryption and CX_DECRYPT for decryption.

The encryption of a 32-byte plaintext using AES in CBC mode and with a 128-bit key is done as follows:

uint8_t plaintext[32]; /* All zero plaintext */ uint8_t iv[16]; /* The length of the initialization vector is always 16 */ uint8_t ciphertext[32]; size_t ciphertextLen = sizeof(ciphertext); cx_aes_iv_no_throw(&aes_key, CX_ENCRYPT | CX_PAD_NONE | CX_CHAIN_CBC, iv, CX_AES_BLOCK_SIZE, plaintext, sizeof(plaintext), ciphertext, &ciphertextLen); /* CX_AES_BLOCK_SIZE is 16 bytes */

The encryption of a 21-byte plaintext using the padding method CX_PAD_ISO9797M1 (with AES in CBC mode):

uint8_t plaintext[21]; uint8_t iv[16]; /* The length of the initialization vector is always 16 */ uint8_t ciphertext[32]; size_t ciphertextLen = sizeof(ciphertext); cx_aes_iv_no_throw(&aes_key, CX_ENCRYPT | CX_PAD_ISO9797M1 | CX_CHAIN_CBC, iv, CX_AES_BLOCK_SIZE, plaintext, sizeof(plaintext), ciphertext, &ciphertextLen);

Decryption

The decryption function takes as input a secret key, an initialization vector, and the ciphertext and outputs the plaintext. If the length of the plaintext that has been encrypted is not a multiple of the block size, the padding method used must be specified when calling the decryption function.

The decryption is performed by cx_aes_iv_no_throw or cx_des_iv_no_throw depending on the algorithm and with the mode value CX_DECRYPT.

Assuming that the plaintext has been encrypted as in the previous example, the decryption of the obtained ciphertext using AES-CBC is as follows:

uint8_t plaintext[21]; uint8_t iv[16]; /* The length of the initialization vector is always 16 */ size_t plaintextLen = sizeof(plaintext); cx_aes_iv_no_throw(&aes_key, CX_DECRYPT | CX_PAD_ISO9797M1 | CX_CHAIN_CBC, iv, CX_AES_BLOCK_SIZE, ciphertext, ciphertextLen, plaintext, &plaintextLen);

See also

Last updated on
Ledger
Copyright © Ledger SAS. All rights reserved. Ledger, Ledger Stax, Ledger Flex, Ledger Nano, Ledger Nano S, Ledger OS, Ledger Wallet, [LEDGER] (logo), [L] (logo) are trademarks owned by Ledger SAS.