Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ license = "MIT"
backend-avx = ["dep:poulpy-cpu-avx"]

[dependencies]
poulpy-core = { git = "https://github.com/phantomzone-org/poulpy", branch = "main" }
poulpy-schemes = { git = "https://github.com/phantomzone-org/poulpy", branch = "main" }
poulpy-hal = { git = "https://github.com/phantomzone-org/poulpy", branch = "main" }
poulpy-cpu-ref = { git = "https://github.com/phantomzone-org/poulpy", branch = "main" }
poulpy-cpu-avx = { git = "https://github.com/phantomzone-org/poulpy", branch = "main", optional = true }
poulpy-core = { git = "https://github.com/cedoor/poulpy.git", branch = "feat/serialization-key-types" }
poulpy-schemes = { git = "https://github.com/cedoor/poulpy.git", branch = "feat/serialization-key-types" }
poulpy-hal = { git = "https://github.com/cedoor/poulpy.git", branch = "feat/serialization-key-types" }
poulpy-cpu-ref = { git = "https://github.com/cedoor/poulpy.git", branch = "feat/serialization-key-types" }
poulpy-cpu-avx = { git = "https://github.com/cedoor/poulpy.git", branch = "feat/serialization-key-types", optional = true }
Comment thread
cedoor marked this conversation as resolved.
Outdated
getrandom = "0.3"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ The public API is identical regardless of which backend is selected.
- [ ] Sub-word operations: [#10](https://github.com/cedoor/squid/issues/10)
- [ ] Identity / noise refresh: [#11](https://github.com/cedoor/squid/issues/11)
- [ ] NTT backend: [#12](https://github.com/cedoor/squid/issues/12)
- [ ] Key serialization: [#13](https://github.com/cedoor/squid/issues/13)
- [x] Key serialization: [#13](https://github.com/cedoor/squid/issues/13)

### Milestone 3 — Developer Experience & Optimization: [#3](https://github.com/cedoor/squid/milestone/3)

Expand Down
109 changes: 108 additions & 1 deletion src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,18 @@
//! let result: u32 = ctx.decrypt(&c, &sk);
//! ```

use std::io::{self, Cursor};

use poulpy_core::layouts::{
Base2K, Degree, Dnum, Dsize, GGLWEToGGSWKeyLayout, GGSWLayout, GLWEAutomorphismKeyLayout,
GLWELayout, GLWESecret, GLWESecretPrepared, GLWESwitchingKeyLayout, GLWEToLWEKeyLayout,
LWESecret, Rank, TorusPrecision,
};
use poulpy_hal::{api::ModuleNew, layouts::Module, source::Source};
use poulpy_hal::{
api::ModuleNew,
layouts::{Module, ReaderFrom, WriterTo},
source::Source,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};
use poulpy_schemes::bin_fhe::{
bdd_arithmetic::{
Add, And, BDDKey, BDDKeyEncryptSk, BDDKeyLayout, BDDKeyPrepared, BDDKeyPreparedFactory,
Expand All @@ -47,6 +53,9 @@ use crate::{
/// No generic backend parameter surfaces in squid's public API.
type Mod = Module<crate::backend::BE>;

/// Binary blob format for [`Context::serialize_secret_key`] / [`Context::serialize_evaluation_key`].
const KEY_BLOB_VERSION: u8 = 1;

#[inline]
fn assert_eval_threads(n: usize) {
assert!(n >= 1, "eval_threads must be >= 1, got {n}");
Expand Down Expand Up @@ -347,6 +356,104 @@ impl Context {
(sk, ek)
}

/// Serializes the **standard-form** GLWE + LWE secrets (little-endian, versioned).
///
/// The prepared GLWE secret is not stored; reload with [`Context::deserialize_secret_key`],
/// which re-runs DFT preparation for this context's backend.
pub fn serialize_secret_key(&self, sk: &SecretKey) -> io::Result<Vec<u8>> {
let mut out = Vec::new();
out.push(KEY_BLOB_VERSION);
sk.sk_glwe.write_to(&mut out)?;
sk.sk_lwe.write_to(&mut out)?;
Ok(out)
}

/// Restores a [`SecretKey`] from [`Context::serialize_secret_key`] output for the same [`Params`].
///
/// # Errors
///
/// Returns [`std::io::Error`] with kind [`InvalidData`](io::ErrorKind::InvalidData) if the
/// blob is truncated, has a wrong version, or does not match this context's [`Params`].
pub fn deserialize_secret_key(&mut self, bytes: &[u8]) -> io::Result<SecretKey> {
let Some((&ver, rest)) = bytes.split_first() else {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"empty secret key blob",
));
Comment thread
cedoor marked this conversation as resolved.
Outdated
};
if ver != KEY_BLOB_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unsupported secret key blob version {ver}"),
));
}
let mut r = Cursor::new(rest);
let mut sk_glwe = GLWESecret::alloc_from_infos(&self.params.glwe_layout);
sk_glwe.read_from(&mut r)?;
let mut sk_lwe = LWESecret::alloc(self.params.bdd_layout.cbt_layout.brk_layout.n_lwe);
sk_lwe.read_from(&mut r)?;
if (r.position() as usize) != rest.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"trailing bytes in secret key blob",
));
}
let mut sk_glwe_prepared =
GLWESecretPrepared::alloc_from_infos(&self.module, &self.params.glwe_layout);
sk_glwe_prepared.prepare(&self.module, &sk_glwe);
Ok(SecretKey {
sk_glwe,
sk_glwe_prepared,
sk_lwe,
})
}

/// Serializes the standard-form BDD evaluation key (little-endian, versioned).
/// The prepared key is not stored; use [`Context::deserialize_evaluation_key`].
pub fn serialize_evaluation_key(&self, ek: &EvaluationKey) -> io::Result<Vec<u8>> {
let mut out = Vec::new();
out.push(KEY_BLOB_VERSION);
ek.bdd_key.write_to(&mut out)?;
Ok(out)
}

/// Restores an [`EvaluationKey`] from [`Context::serialize_evaluation_key`] for the same [`Params`].
///
/// # Errors
///
/// Returns [`std::io::Error`] with kind [`InvalidData`](io::ErrorKind::InvalidData) if the
/// blob does not match this context's [`Params`] layouts.
pub fn deserialize_evaluation_key(&mut self, bytes: &[u8]) -> io::Result<EvaluationKey> {
let Some((&ver, rest)) = bytes.split_first() else {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"empty evaluation key blob",
));
};
if ver != KEY_BLOB_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unsupported evaluation key blob version {ver}"),
));
}
let mut r = Cursor::new(rest);
let mut bdd_key = BDDKey::alloc_from_infos(&self.params.bdd_layout);
bdd_key.read_from(&mut r)?;
if (r.position() as usize) != rest.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"trailing bytes in evaluation key blob",
));
}
let mut bdd_key_prepared =
BDDKeyPrepared::alloc_from_infos(&self.module, &self.params.bdd_layout);
bdd_key_prepared.prepare(&self.module, &bdd_key, scratch::borrow(&mut self.arena));
Ok(EvaluationKey {
bdd_key,
bdd_key_prepared,
})
}
Comment thread
cedoor marked this conversation as resolved.
Outdated

// ── Encrypt / Decrypt ────────────────────────────────────────────────────

/// Encrypt a plaintext value under the given secret key.
Expand Down
12 changes: 4 additions & 8 deletions src/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
//!
//! - [`SecretKey`] — the private GLWE + LWE secret pair plus the prepared
//! (DFT-domain) GLWE secret needed for encryption and decryption.
//! Keep this secret; never serialize it in production.
//! Keep this secret; treat serialized blobs like raw key material.
//! - [`EvaluationKey`] — the public evaluation key bundle needed for all
//! homomorphic operations. Wraps Poulpy's `BDDKey` in standard and prepared
//! forms.
//!
//! Both types are created by [`crate::context::Context::keygen`] and are
//! opaque: no fields are public.
//! opaque: no fields are public. Standard-form serialization is exposed via
//! [`crate::Context::serialize_secret_key`], [`crate::Context::serialize_evaluation_key`],
//! and the matching `deserialize_*` methods.

use poulpy_core::layouts::{GLWESecret, GLWESecretPrepared, LWESecret};
use poulpy_schemes::bin_fhe::{
Expand All @@ -25,12 +27,8 @@ use poulpy_schemes::bin_fhe::{
///
/// Created by [`crate::Context::keygen`].
pub struct SecretKey {
// TODO: read when adding load/serialize, key rotation, or re-prepare-from-standard APIs.
#[allow(dead_code)]
pub(crate) sk_glwe: GLWESecret<Vec<u8>>,
pub(crate) sk_glwe_prepared: GLWESecretPrepared<Vec<u8>, crate::backend::BE>,
// TODO: same as `sk_glwe` — needed for any path that reconstructs or exports the full secret.
#[allow(dead_code)]
pub(crate) sk_lwe: LWESecret<Vec<u8>>,
}

Expand All @@ -43,8 +41,6 @@ pub struct SecretKey {
/// Created by [`crate::Context::keygen`].
pub struct EvaluationKey {
/// Standard-form BDD key (circuit bootstrapping + switching keys).
// TODO: read when adding serialization, portable storage, or re-prepare after load (hot path uses `bdd_key_prepared` only).
#[allow(dead_code)]
pub(crate) bdd_key: BDDKey<Vec<u8>, CGGI>,
/// Prepared (DFT-domain) copy used on the hot path.
pub(crate) bdd_key_prepared: BDDKeyPrepared<Vec<u8>, CGGI, crate::backend::BE>,
Expand Down
23 changes: 23 additions & 0 deletions tests/key_serialization.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use squid::{Context, Params};

#[test]
fn secret_and_evaluation_keys_roundtrip_through_blob_format() {
let params = Params::unsecure();
let mut ctx = Context::new(params);
let (sk, ek) = ctx.keygen();

let sk_blob = ctx.serialize_secret_key(&sk).expect("serialize sk");
let ek_blob = ctx.serialize_evaluation_key(&ek).expect("serialize ek");

let sk2 = ctx
.deserialize_secret_key(&sk_blob)
.expect("deserialize sk");
let ek2 = ctx
.deserialize_evaluation_key(&ek_blob)
.expect("deserialize ek");

let a = ctx.encrypt::<u32>(11, &sk2);
let b = ctx.encrypt::<u32>(22, &sk2);
let c = ctx.add(&a, &b, &ek2);
assert_eq!(ctx.decrypt(&c, &sk2), 33);
}
Loading