Key material lifecycle
Example source: examples/02_key_materials.py
This example creates the major CKKS key types, reports their dense layouts and sizes, and optionally persists selected material. The tutorial distinguishes stored key state and specialization from application-maintained cryptographic relations, ownership, and restoration.
What you will learn
- which keys are needed for encryption, decryption, multiplication, and rotation;
- how key shapes expose components, decomposition digits, RNS limbs, and coefficients;
- why a
RotationKeyis bound to one canonical signed step; - how
ArtifactStorediffers from key generation and key placement; - why secret-key persistence requires an explicit opt-in.
Run the example
python examples/02_key_materials.py \
--preset slots8192-scale40-levels7-int64 \
--rotations=-4,-1,1,2,42
3
To retain selected artifacts in a local store:
python examples/02_key_materials.py \
--preset slots8192-scale40-levels7-int64 \
--rotations=1,2,4 \
--store /tmp/fhelium-key-demo2
3
4
The second command persists public, relinearization, and rotation keys. It does not persist the secret key.
1. Create exact key types
secret_key = engine.secret_key
public_key = engine.public_key
relinearization_key = engine.relinearization_key
for rotation_step in rotation_steps:
engine.rotation_key(rotation_step)2
3
4
5
6
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] |
Calling the engine properties may lazily create missing key material. Code that must forbid secret-key creation can construct the engine with allow_sk_gen=False and install only the keys it owns.
2. Treat rotation step as stored specialization
key = engine.rotation_keys[rotation_step]
assert key.rotation_step == rotation_step2
RotationKeySet validates canonical 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 context_id proves an external ciphertext/key relation.
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. Persist selected public/evaluation material
store = ArtifactStore(path)
store.put("keys/public", public_key, overwrite=True)
relinearization_ref = store.put(
"keys/relinearization",
relinearization_key,
overwrite=True,
)
rotation_keys = store.collection("keys/rotation")
rotation_keys.put("1", engine.rotation_keys[1], overwrite=True)2
3
4
5
6
7
8
9
10
The store uses a transactional SQLite catalog for logical names and immutable safetensors objects for exact key payloads. It adds typed current-generation references, collections, checksums, and local durability policy. Overwriting a name creates a new artifact ID and makes the previous reference stale; it does not retain prior key versions. The store does not decide which user owns a key, where that key should be cached, or when it should move to CUDA.
5. Secret-key persistence is deliberately noisy
store.put(
"keys/secret",
secret_key,
allow_secret=True,
overwrite=True,
)2
3
4
5
6
The explicit allow_secret=True prevents an accidental generic save path from writing a secret key. It is not encryption at rest. Production systems still need an external key-management service (KMS), permissions, encryption, audit policy, and deletion policy appropriate to their threat model. The store's payload checksum detects accidental corruption; it does not authenticate data against an actor who can modify both the catalog and payload.
6. Restore the exact type
restored = store.get(relinearization_ref, device=engine.device)
assert type(restored) is fh.RelinearizationKey
torch.testing.assert_close(restored.data, relinearization_key.data)2
3
The serialized metadata reconstructs the key type, context, modulus basis, polynomial domain, prime IDs, and other exact state. A successful tensor load is not enough if that metadata does not match the current engine or intended operation.
Do not make every key globally resident
A serving layer should provision exact user/model keysets, enforce a memory budget, and lease only the keys required by the current operation. The core engine intentionally does not infer that policy.
Complete runnable source
#!/usr/bin/env python3
"""Inspect and optionally persist dense process-local CKKS key layouts.
Each ``RotationKey`` records its canonical signed ``rotation_step``;
``RotationKeySet`` validates the same identity when constructing or updating the mapping.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import torch
from common import add_engine_args, format_bytes, make_engine, print_table
import fhelium as fh
from fhelium.artifacts import ArtifactStore
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")
parser.add_argument(
"--store",
type=Path,
help="Persist public/relinearization/rotation keys under this store root.",
)
parser.add_argument(
"--persist-secret",
action="store_true",
help="Explicitly opt in to an unencrypted SecretKey artifact.",
)
args = parser.parse_args()
engine = make_engine(args)
rotation_steps = [
int(item) for item in args.rotations.split(",") if item.strip()
]
secret_key = engine.secret_key
public_key = engine.public_key
relinearization_key = engine.relinearization_key
for rotation_step in rotation_steps:
engine.rotation_key(rotation_step)
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 canonical steps: {list(engine.rotation_keys)}")
print(
"RotationKey tensor canonical step: "
f"{engine.rotation_keys[rotation_steps[0]].rotation_step}"
)
if args.store is not None:
store = ArtifactStore(args.store)
store.put("keys/public", public_key, overwrite=True)
relinearization_ref = store.put(
"keys/relinearization", relinearization_key, overwrite=True
)
rotation_keys = store.collection("keys/rotation")
for rotation_step in rotation_steps:
rotation_keys.put(
str(rotation_step),
engine.rotation_keys[rotation_step],
overwrite=True,
)
if args.persist_secret:
store.put(
"keys/secret",
secret_key,
allow_secret=True,
overwrite=True,
)
restored = store.get(relinearization_ref, device=engine.device)
assert type(restored) is fh.RelinearizationKey
torch.testing.assert_close(restored.data, relinearization_key.data)
print(
f"Persisted {len(store.list(prefix='keys'))} key artifacts "
f"under {args.store.resolve()}"
)
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119