Key material lifecycle
Example source: examples/02_eager_key_materials.py
Example 02 creates keys from one secret, installs selected evaluator capabilities, and inspects their layouts. File persistence and artifact generations are covered by 09 and 10.
python examples/02_eager_key_materials.py --preset slots8192-scale40-depth7-int64 --rotations=-4,-1,1,2,41. Create typed keys
secret_key = engine.create_secret_key()
public_key = engine.create_public_key(secret_key)
relinearization_key = engine.create_relinearization_key(secret_key)
rotation_key = engine.create_rotation_key(1, secret_key)2
3
4
The roles are distinct:
| Key | Primary use | Typical dense axes |
|---|---|---|
SecretKey | decryption and generation of derived keys | [limb, coefficient] |
PublicKey | public-key encryption | [key component, limb, coefficient] |
RelinearizationKey | three-component to two-component conversion | [digit, key component, limb, coefficient] |
RotationKey | one slot automorphism/key switch | [digit, key component, limb, coefficient] |
The example uses factory calls so key creation is visible. Code that must forbid implicit key creation can construct the engine with allow_automatic_key_generation=False and install only the keys it owns.
Factory methods select placement directly:
secret_key = engine.create_secret_key(device="cuda:0")
public_key = engine.create_public_key(secret_key)
rotation_key = engine.create_rotation_key(1, secret_key)2
3
Derived-key factories infer placement from their secret-key input. Supplying a different device authorizes a copy of that same secret relation; the Engine does not generate an unrelated secret key on the destination.
Operation use does not imply permission to copy a key. By default, a public, secret, or evaluation key must already be on the Tensor operation's device. Create a copy with key.to(device) and pass or install it when placement is caller-managed. Engine(..., allow_automatic_key_replication=True) instead permits the Engine to create and cache device replicas when an installed key is first needed there. The source copy remains allocated, and releasing Python references does not guarantee device-memory erasure.
2. Treat rotation step as stored specialization
key = engine.rotation_keys[rotation_step]
assert key.rotation_step == rotation_step2
RotationKeySet validates normalized signed steps when constructing or updating the mapping. A key for step +1 must not be silently reused as a key for another step, even if both tensors happen to have the same shape.
This distinction matters in distributed and multi-user systems: the tensor layout alone is not sufficient stored key state, and neither the layout nor the runtime key object proves an external ciphertext/key relation. Preserve that relation in the application that provisions the key.
3. Inspect key memory
shape = tuple(relinearization_key.data.shape)
byte_count = relinearization_key.data.nbytes2
Evaluation keys are usually much larger than ciphertexts because they contain multiple decomposition digits and both Q and P basis rows. Capacity planning should use the actual nbytes for the active parameter set instead of a count of Python objects.
4. Install selected evaluator capabilities
engine.set_secret_key(secret_key)
engine.set_public_key(public_key)
engine.set_relinearization_key(relinearization_key)
engine.set_rotation_key(rotation_key)2
3
4
Creation returns a value; installation adds that value to the Engine's inventory. Key placement remains separate. A stored rotation step describes specialization, not the participant or secret that produced the key. Cryptographic correspondence remains the application's responsibility.
Source
#!/usr/bin/env python3
"""Create, inspect, and install process-local CKKS keys.
Each ``RotationKey`` records its normalized signed ``rotation_step``;
``RotationKeySet`` validates the same identity when constructing or updating the mapping.
"""
from __future__ import annotations
import argparse
from common import add_engine_args, format_bytes, make_engine, print_table
def _size(value) -> int:
return value.data.nbytes
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
add_engine_args(parser)
parser.add_argument("--rotations", default="1,2,4")
args = parser.parse_args()
engine = make_engine(args)
rotation_steps = [
int(item) for item in args.rotations.split(",") if item.strip()
]
secret_key = engine.create_secret_key()
public_key = engine.create_public_key(secret_key)
relinearization_key = engine.create_relinearization_key(secret_key)
engine.set_secret_key(secret_key)
engine.set_public_key(public_key)
engine.set_relinearization_key(relinearization_key)
for rotation_step in rotation_steps:
engine.set_rotation_key(
engine.create_rotation_key(rotation_step, secret_key)
)
rows = [
[
"secret",
"[limb, coeff]",
tuple(secret_key.data.shape),
format_bytes(_size(secret_key)),
],
[
"public",
"[key_component, limb, coeff]",
tuple(public_key.data.shape),
format_bytes(_size(public_key)),
],
[
"relinearization",
"[digit, key_component, limb, coeff]",
tuple(relinearization_key.data.shape),
format_bytes(_size(relinearization_key)),
],
]
for rotation_step in rotation_steps:
key = engine.rotation_keys[rotation_step]
rows.append(
[
f"rotation[{rotation_step}]",
"[digit, key_component, limb, coeff]",
tuple(key.data.shape),
format_bytes(_size(key)),
]
)
print_table(["material", "axes", "local shape", "local bytes"], rows)
print(f"RotationKeySet normalized steps: {list(engine.rotation_keys)}")
if __name__ == "__main__":
main()2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76