Imagine an application running inside a Trusted Execution Environment (TEE) that needs to encrypt its storage. If the app migrates to a different machine or reboots, it must recover the exact same encryption key, without ever having stored it. How can such an app obtain a deterministic, confidential key from a decentralized network, without any single party ever seeing it?

This is the problem that confidential key derivation (CKD) solves. In this post we describe how we designed and implemented CKD as a new feature of NEAR Protocol’s Chain Signatures, an implementation of threshold signature schemes secured by a Multi-Party Computation (MPC) network. Chain Signatures allow smart contracts and accounts on the NEAR blockchain to securely authorize transactions on external, heterogeneous blockchains without relying on centralized custody or wrapping assets.

At its core, CKD turns our MPC network into a decentralized key management service (KMS). It offers authenticated clients the ability to derive cryptographic keys for various purposes, such as data-at-rest encryption. These keys are available to any authenticated client at any time and are protected against corruption of individual nodes. The keys supplied by the system are deterministic and remain confidential by using public key encryption during the generation process. Determinism is a critical property, particularly for TEE-based applications, where consistent key derivation is essential.

Confidential Key Derivation (CKD)

The confidential key derivation feature is an extension of the current MPC system. At its core it uses threshold BLS signatures. On a high level, it provides authenticated applications with deterministic secrets. Deterministic means that the same application may request the same secret at different points in time. Confidential means that the secret itself is never revealed to any entity other than the application itself, not even to individual nodes in the MPC network.

Applications

Our main use-case for CKD is to allow any app running inside a TEE (for example, Intel TDX) to have a deterministically derived key that is unique to the app and not specific to the TEE. The app can derive the exact same key as many times as it desires even when running on a different TEE. To obtain the key, the app can leverage the CKD functionality in the NEAR protocol blockchain. This key can be used, for example, to encrypt its storage, so that it can be decrypted later in another machine while remaining confidential.

More generally, creating a service that can supply confidential deterministic keys in the NEAR blockchain unlocks a massive range of decentralized possibilities. For example, it can be used to enable time-lock encryption. A smart contract could encrypt data under a key that is only derivable once certain on-chain conditions are met, such as a deadline passing or a governance vote concluding. Until then, not even the MPC nodes can decrypt the data. This enables sealed-bid auctions, delayed reveals, and scheduled disclosures without a trusted third party.

Another example is the generation of verifiable randomness (private or public). Because the derived key is deterministic and verifiable, it can serve as a source of unbiased randomness: no single party can influence the output, and anyone can verify it was computed correctly. This is useful for decentralized applications that require fair randomness, such as games, lotteries, and leader election protocols.

Problem

Informally, confidential keys satisfy the following properties:

  • The TEE app should be able to recover the confidential key without having to store it directly, which is very useful in the case of crashes or reboots
  • The key should never leave the TEE app that requested it. This entails that the key can only be determined by the expected receiver. In particular, neither an MPC node nor a blockchain observer should be able to deduce such a key.
  • Each TEE app should obtain a unique confidential key
  • Correctness assurance. The TEE app should be able to check that the received key was computed correctly

Formally, these keys must be:

  • Deterministically extractable, depending only on the TEE app’s authenticated identifier $\texttt{app\_id}$ and MPC Public Key $\texttt{pk}$
  • Confidential
  • Verifiable

Attacker Model

These are the attacker models we consider throughout this blog post:

  • MPC nodes might be malicious, and try to obtain app’s secrets. In our $t$-of-$n$ threshold setting, where $n$ is the total number of nodes and $t$ is the minimum number required to reconstruct a secret, up to $t-1$ nodes might be malicious.

  • App developer might be malicious. It may attempt to obtain secrets belonging to other applications or to extract secrets from the MPC network.

  • App developer colluding with up to $t-1$ malicious MPC nodes. This is the most general attacker, as it subsumes the two previous cases.

Possible Solution with Encrypted Signatures

One approach to solve the problem is to encrypt signatures over some message, and use the signatures to obtain a secret. As we require determinism, not every possible threshold signature scheme is fit for this purpose. Before CKD was implemented, the MPC network supported only threshold ECDSA and EdDSA signatures, none of which are deterministic. Given that a deterministic signature scheme can be used as a PRF to generate random coins, we use BLS for this purpose. Furthermore, these signatures are very efficient to compute in our threshold setting.

System Overview

flowchart TD
    TEEapp[TEE app] -->|"`1\. send (attestation, A)`"| DevC["Developer contract
    (verifies attestation)"]
    subgraph Blockchain
    DevC -->|"`2\. request key (A)`"| MPCC[MPC contract]
    end
    MPCC -->|"`3\. compute key (app_id, A)`"| MPCN[MPC Network]
    MPCN -->|"`4\. return (es)`"| MPCC -->|"`5\. return (es)`"| DevC -->|"`6\. return (es)`"| TEEapp

In this section we explain our solution in detail. While building our solution, we converged to a protocol similar to VetKeys, even though we were not aware of this result. For simplicity we will describe a reduced version of the system, shown in the diagram above. The details of the full system can be found in docs.

The system contains mainly four components: TEE app, Developer contract, MPC contract and MPC network. The TEE app wishes to use the CKD functionality to obtain confidential keys. To obtain a key in encrypted form, the TEE app sends a public key and an attestation proof to the Developer contract, which verifies that the app is running inside a secure environment through remote attestation. Upon successful verification, the Developer contract calls the MPC contract to request the key derivation. The MPC contract identifies the caller by its account id, which serves as a unique, authenticated identifier for the application, denoted by $\texttt{app\_id}$. The MPC contract then signals the MPC network to compute the corresponding key, encrypted by the public key of the TEE app. Once the MPC network is done computing the key in encrypted form, one of its members, which we call coordinator, publishes this value in the blockchain. Next, the encrypted key is returned to the TEE app that requested it. Note that in the full system the Developer contract can also pass an arbitrary key derivation string alongside the request, so that it can derive as many distinct keys as needed.

Next we will dive into the protocol details, but before that we need to define a few elliptic curve concepts and some notation.

Elliptic Curve Notation

Elliptic curves over finite fields are mathematical objects with many applications in cryptography. An elliptic curve is a set of points in two dimensions, where the point coordinates are scalars in a finite field. The curve points satisfy a polynomial equation, such as $y^2 = x^3 + 4$ for the BLS12-381 curve, and form a group with respect to the point addition operation. The group order is a prime $q$.

For the rest of this blog post, we use the following notation:

  • Lower case variables, such as $x$ and $y$, denote scalars modulo group order $q$
  • Upper case variables, such as $G$ and $Y$, denote elliptic curve points. $G$ will be reserved for the generator of the curve
  • $H$ is a hash to curve function defined in RFC 9380, which translates bytes to elliptic curve points for which computing the discrete log is hard
  • $ Y \gets y \cdot G $ is the scalar multiplication of $y$ by the group generator $G$
  • $y \gets^{$} \mathbb{Z}_q $ is scalar $y$ picked uniformly at random from $ \mathbb{Z}_q $

MPC Network Notation

  • $\texttt{msk}$: secret key of the MPC network
  • $\texttt{pk} \gets \texttt{msk} \cdot G$: public key of MPC network
  • $x_i \in \mathbb{Z}_q$: private key share of node $i$, for $i = 1, \ldots, n$
  • $\lambda_i$: Lagrange polynomial coefficients. By definition we have $\sum_{i=1}^{n}{\lambda_i \cdot x_i} = \texttt{msk}$

Protocol Steps

  • The TEE app generates a fresh elliptic curve ElGamal key pair $(a, A)$ and requests a key from the Developer contract. This request includes a remote attestation proof
  • The Developer contract verifies the TEE app is correctly being executed inside a TEE, by verifying the remote attestation proof. Upon successful verification, it forwards the request to the MPC contract
  • The MPC contract sets $\texttt{app\_id}$ equal to the account id of the caller (the Developer contract), which serves as a unique, authenticated identifier for the application, and approves the request to be handled by the MPC Network
  • MPC node $i$ receives a new CKD request $(\texttt{app\_id}, A)$ and computes:

    \[\begin{aligned} & y_i \gets^{\$} \mathbb{Z}_q \\ & Y_i \gets y_i \cdot G_1 \\ & S_i \gets x_i \cdot H(\texttt{pk}, \texttt{app\_id}) \\ & C_i \gets S_i + y_i \cdot A \\ \end{aligned}\]

Notice that this is basically computing a BLS signature of $H(\texttt{pk}, \texttt{app\_id})$ using the node’s private share $x_i$ as private key, and encrypting the signature using ElGamal with the TEE app’s public key $A$.

  • MPC node $i$ sends $(\lambda_i \cdot Y_i, \lambda_i \cdot C_i)$ to coordinator
  • The coordinator adds the received pairs together and computes the encrypted secret $\texttt{es}$:

    \[\begin{aligned} & Y \gets λ_1 \cdot Y_1 + \ldots + λ_n \cdot Y_n \\ & C \gets λ_1 \cdot C_1 + \ldots + λ_n \cdot C_n = \texttt{msk} \cdot H(\texttt{pk}, \texttt{app\_id}) + a \cdot Y \\ & \texttt{es} \gets (Y, C) \end{aligned}\]
  • Coordinator sends $\texttt{es}$ to TEE app on-chain through the MPC contract
  • TEE app obtains $\texttt{es} = (Y, C)$ and computes the secret (which is a BLS signature):

    \[\texttt{key} \gets C + (- a) \cdot Y = \texttt{msk} \cdot H(\texttt{pk}, \texttt{app\_id})\]
  • Then checks its correctness with respect to the MPC network public key $\texttt{pk}$

The choices related to the last step above, in which the TEE app verifies whether the secret obtained is correct, are explained in more depth in the next section.

Verifiability

As mentioned in the Problem section, achieving verifiability is a desirable feature for such a system. In this section we first show that it is actually necessary, as the system above without any verification step is insecure. Then we explain how we currently achieve private verifiability, and how we plan to achieve public verifiability in the near future.

We consider two definitions of verifiability:

  • private verifiability: the TEE app is able to verify that the received confidential key is correct with respect to the MPC public key
  • public verifiability: anyone, for example the MPC contract or any blockchain observer, should be able to verify that the encrypted key is correct, even without being able to decrypt it

To show why verifiability is needed, we first consider the case where no verification of the resulting confidential key (or its encryption) is done by any protocol party.

No Verifiability

If upon reception of the encrypted secret $\texttt{es}$ the TEE app does not execute any verification step, the following attack by the coordinator is possible. Consider the honest coordinator output:

\[\texttt{es} = (Y, C) \gets (y \cdot G, \texttt{msk} \cdot H(\texttt{pk}, \texttt{app\_id}) + a \cdot Y)\]

If instead the coordinator computes:

\[\begin{aligned} & y \gets \text{arbitrary scalar} \\ & Y \gets y \cdot G \\ & C \gets \text{arbitrary curve point} \\ \end{aligned}\]

Upon reception of $(Y, C)$, the TEE app would compute its confidential key as:

\[\texttt{key} \gets C + (-a) \cdot Y = C + (-y) \cdot A\]

This is a value that is already known by the coordinator, breaking the confidentiality of the obtained key.

Private Verifiability

In our current system, upon reception of $\texttt{es}$, the TEE app first decrypts the key:

$\texttt{key} \gets C + (- a) \cdot Y$ and then verifies it is equal to the expected value $\texttt{msk} \cdot H(\texttt{pk}, \texttt{app\_id})$ by checking its validity as a BLS signature of the message $H(\texttt{pk}, \texttt{app\_id})$.

In a nutshell, this verification leverages the pairing function $e$ associated with elliptic curves used for BLS signatures:

\[e(\texttt{key},G_2) = e(H(\texttt{pk}, \texttt{app\_id}), \texttt{pk})\]

This thwarts the attack explained above because the coordinator has no way to ensure the maliciously constructed $\texttt{key}$ is a correct BLS signature for $H(\texttt{pk}, \texttt{app\_id})$.

Public Verifiability

Achieving public verifiability (with minor modifications) is also possible. This is desirable, because it would allow the MPC contract to detect if something has gone wrong during the distributed computation by the MPC network. It would also guarantee that if the TEE app obtains a response, then this response is correct. One possible variant is the following:

  • app public key:
\[(A_1, A_2) \gets (a \cdot G_1, a \cdot G_2)\]
  • coordinator output:
\[(C, Y_1, Y_2) \gets (\texttt{msk} \cdot H(\texttt{pk}, \texttt{app\_id}) + a \cdot Y_1, y \cdot G_1, y \cdot G_2)\]
  • MPC contract verification:
\[\begin{aligned} & e(Y_1, G_2) = e(G_1, Y_2) \\ & e(C, G_2) = e(A_1, Y_2) \cdot e(H(\texttt{pk}, \texttt{app\_id}), \texttt{pk}) \\ \end{aligned}\]

The first check ensures that $Y_1$ and $Y_2$ are consistent, i.e. that the coordinator used the same scalar $y$ in both groups. The second check verifies that $C$ is a valid ElGamal encryption of the correct BLS signature under the app’s public key, without needing to decrypt it. Together, these prevent the coordinator attack described earlier, since the contract can reject malformed responses before they reach the TEE app.

  • TEE app key decryption and verification:
\[\texttt{key} \gets C + (-a) \cdot Y_1\] \[e(\texttt{key},G_2) = e(H(\texttt{pk}, \texttt{app\_id}), \texttt{pk})\]

This is the same private verification as before: the TEE app decrypts the ElGamal ciphertext and confirms the result is a valid BLS signature. With public verifiability, this step serves as a redundant safety check, since the contract has already verified the encrypted form.

Performance Considerations

CKD is lightweight by design. Each key derivation requires a single round of communication between the MPC nodes and the coordinator: every node computes one scalar multiplication and one hash-to-curve operation on its private share, and the coordinator aggregates the results with simple point additions. There is no multi-round interactive protocol or heavy on-chain computation involved. The on-chain footprint is limited to storing and forwarding the encrypted secret $\texttt{es}$, which consists of just two elliptic curve points. Verification on the TEE side requires a single pairing check. As a result, CKD adds minimal overhead on top of the existing MPC infrastructure.

Conclusion

In this post we introduced confidential key derivation (CKD), a new feature of NEAR’s MPC network that turns it into a decentralized key management service. CKD provides authenticated applications with deterministic, confidential keys by combining threshold BLS signatures with ElGamal encryption, ensuring that no single MPC node ever sees the derived key. We showed why verifiability is essential by demonstrating a concrete coordinator attack, and explained how private verifiability thwarts it. Public verifiability, which would allow on-chain verification of encrypted keys without decryption, is planned as a next step.

For the full protocol specification, including details on key resharing and threshold management, see the CKD documentation. If you are interested in building on top of CKD, check out the mpc repository.

Appendix: Implementation

For those of you who usually say: Talk is cheap. Show me the code. Here is a simple implementation that shows how the confidential key is computed using our threshold-signatures crate.

//!```cargo
//! [dependencies]
//! rand_core = { version = "0.6.4", features = ["getrandom"] }
//! threshold-signatures = { git = "https://github.com/near/threshold-signatures", rev = "01315cf9fef7064a0b529582c41ffad5f122d584"}
//!```
use rand_core::OsRng;
use threshold_signatures::blstrs::{pairing, Gt};
use threshold_signatures::confidential_key_derivation::{
    ciphersuite::{hash_to_curve, Field, G1Projective, G2Projective, Group},
    AppId, Scalar,
};

fn e(p1: &G1Projective, p2: &G2Projective) -> Gt {
    let p1 = p1.into();
    let p2 = p2.into();
    pairing(&p1, &p2)
}

fn gen_keypair_G1() -> (Scalar, G1Projective) {
    let x = Scalar::random(&mut OsRng);
    (x, G1Projective::generator() * x)
}

fn gen_keypair_G2() -> (Scalar, G2Projective) {
    let x = Scalar::random(&mut OsRng);
    (x, G2Projective::generator() * x)
}

fn H(pk: &G2Projective, app_id: &AppId) -> G1Projective {
    hash_to_curve(&[pk.to_compressed().as_ref(), app_id.as_bytes()].concat())
}

fn verify(pk: &G2Projective, app_id: &AppId, sig: &G1Projective) -> bool {
    e(sig, &G2Projective::generator()) == e(&H(pk, app_id), pk)
}

fn ckd(app_id: &AppId, A: &G1Projective, msk: &Scalar, pk: &G2Projective) -> (G1Projective, G1Projective) {
    let (y, Y) = gen_keypair_G1();
    (H(pk, app_id) * msk + A * y, Y)
}

fn main() {
    let (msk, pk) = gen_keypair_G2();
    let app_id = AppId::try_new(b"example-app.testnet").unwrap();
    let (a, A) = gen_keypair_G1();
    let (C, Y) = ckd(&app_id, &A, &msk, &pk);
    let sig = C - Y * a;
    assert!(verify(&pk, &app_id, &sig));
    println!("Computation ended correctly!")
}