Constructs a hybrid KEM type from the given parameters.
A hybrid KEM combines a post-quantum KEM with a traditional elliptic curve Diffie-Hellman key exchange. The shared secrets from both components are combined using the C2PRI combiner construction with SHA3-256.
The resulting type provides:
PublicKey: The hybrid encapsulation (public) keySecretKey: The hybrid decapsulation (secret) keyKeyPair: A public/secret key pairEncapsulatedSecret: A shared secret with its ciphertextpub fn HybridKem(comptime params: Params) type
pub fn HybridKem(comptime params: Params) type {
return struct {
const is_nist_curve = params.Group == P256Group or params.Group == P384Group;
fn expandRandomnessSeed(seed: [32]u8) ![params.Group.seed_length]u8 {
if (!is_nist_curve) return seed;
var xof = params.Xof.init(.{});
xof.update(&seed);
var expanded: [params.Group.seed_length]u8 = undefined;
xof.squeeze(&expanded);
return expanded;
}
fn expandDecapsKeyG(seed: [params.Nseed]u8) !struct {
ek_pq: params.PqKem.PublicKey,
ek_t: [params.Group.element_length]u8,
dk_pq: params.PqKem.SecretKey,
dk_t: [params.Group.scalar_length]u8,
} {
var xof = params.Xof.init(.{});
xof.update(&seed);
var seeds: [params.pq_nseed + params.Group.seed_length]u8 = undefined;
xof.squeeze(&seeds);
const kp_pq = try params.PqKem.KeyPair.generateDeterministic(seeds[0..params.pq_nseed].*);
const dk_t = try params.Group.randomScalar(seeds[params.pq_nseed..]);
const ek_t_point = try params.Group.mulBase(dk_t);
return .{
.ek_pq = kp_pq.public_key,
.ek_t = if (is_nist_curve) params.Group.encodePoint(ek_t_point) else ek_t_point,
.dk_pq = kp_pq.secret_key,
.dk_t = dk_t,
};
}
fn c2priCombiner(ss_pq: [32]u8, ss_t: [params.Group.scalar_length]u8, ct_t: []const u8, ek_t: []const u8) [params.Nss]u8 {
var hasher = sha3.Sha3_256.init(.{});
hasher.update(&ss_pq);
hasher.update(&ss_t);
hasher.update(ct_t);
hasher.update(ek_t);
hasher.update(params.label);
var output: [params.Nss]u8 = undefined;
hasher.final(&output);
return output;
}
/// A hybrid KEM public key (encapsulation key).
///
/// The public key is the concatenation of the post-quantum KEM public key
/// and the traditional elliptic curve public key.
pub const PublicKey = struct {
bytes: [params.Nek]u8,
/// Size of a serialized representation of the key, in bytes.
pub const encoded_length = params.Nek;
/// Serializes the key into a byte array.
pub fn toBytes(self: PublicKey) [encoded_length]u8 {
return self.bytes;
}
/// Deserializes the key from a byte array.
pub fn fromBytes(buf: *const [encoded_length]u8) PublicKey {
return .{ .bytes = buf.* };
}
/// Generates a shared secret, encapsulated for the public key,
/// using random bytes.
///
/// This is recommended over `encapsDeterministic`.
pub fn encaps(pk: PublicKey, io: std.Io) !EncapsulatedSecret {
var seed_pq: [32]u8 = undefined;
io.random(&seed_pq);
var seed_t: [32]u8 = undefined;
io.random(&seed_t);
var seed_t_expanded: [params.Group.seed_length]u8 = try expandRandomnessSeed(seed_t);
return encapsInner(pk, &seed_pq, &seed_t_expanded);
}
/// Generates a shared secret, encapsulated for the public key,
/// using the provided seed.
///
/// Calling `encaps` instead is recommended.
pub fn encapsDeterministic(pk: PublicKey, seed: []const u8) !EncapsulatedSecret {
if (seed.len < 32) return error.InsufficientRandomness;
var seed_pq: [32]u8 = seed[0..32].*;
var seed_t_expanded: [params.Group.seed_length]u8 = undefined;
const t_randomness = seed[32..];
if (t_randomness.len < params.Group.seed_length) {
// Provided randomness is shorter than seed_length, use it directly
// (test vectors provide just enough for randomScalar)
@memcpy(seed_t_expanded[0..t_randomness.len], t_randomness);
// Pad the rest with zeros if needed (shouldn't be used by randomScalar)
if (t_randomness.len < params.Group.seed_length) {
@memset(seed_t_expanded[t_randomness.len..], 0);
}
} else {
// Full randomness provided
@memcpy(&seed_t_expanded, t_randomness[0..params.Group.seed_length]);
}
return encapsInner(pk, &seed_pq, &seed_t_expanded);
}
fn encapsInner(
pk: PublicKey,
seed_pq: *[32]u8,
seed_t_expanded: *[params.Group.seed_length]u8,
) !EncapsulatedSecret {
const pq_nek = params.PqKem.PublicKey.encoded_length;
const ek_pq = try params.PqKem.PublicKey.fromBytes(pk.bytes[0..pq_nek]);
const ek_t = pk.bytes[pq_nek..][0..params.Group.element_length];
const pq_encap = ek_pq.encapsDeterministic(seed_pq);
const sk_e = try params.Group.randomScalar(seed_t_expanded);
const ct_t_point = try params.Group.mulBase(sk_e);
const ct_t = if (is_nist_curve) params.Group.encodePoint(ct_t_point) else ct_t_point;
const ek_t_point = if (is_nist_curve) try params.Group.decodePoint(ek_t) else ek_t.*;
const ss_t = params.Group.elementToSharedSecret(try params.Group.mul(ek_t_point, sk_e));
var ct_h: [params.Nct]u8 = undefined;
@memcpy(ct_h[0..pq_encap.ciphertext.len], &pq_encap.ciphertext);
@memcpy(ct_h[pq_encap.ciphertext.len..], &ct_t);
return .{
.shared_secret = c2priCombiner(pq_encap.shared_secret, ss_t, &ct_t, ek_t),
.ciphertext = ct_h,
};
}
};
/// A hybrid KEM secret key (decapsulation key).
///
/// The secret key is stored as a seed from which the actual key material
/// is derived on demand. This is more compact than storing expanded keys.
pub const SecretKey = struct {
seed: [params.Nseed]u8,
/// Size of a serialized representation of the key, in bytes.
pub const encoded_length = params.Ndk;
/// Serializes the key into a byte array.
pub fn toBytes(self: SecretKey) [encoded_length]u8 {
return self.seed;
}
/// Deserializes the key from a byte array.
pub fn fromBytes(buf: *const [encoded_length]u8) SecretKey {
return .{ .seed = buf.* };
}
/// Decapsulates the shared secret from the ciphertext using the secret key.
pub fn decaps(self: SecretKey, ct: *const [params.Nct]u8) ![params.Nss]u8 {
const expanded = try expandDecapsKeyG(self.seed);
const pq_ct_len = params.PqKem.ciphertext_length;
const ct_t = ct[pq_ct_len..][0..params.Group.element_length];
const ss_pq = try expanded.dk_pq.decaps(ct[0..pq_ct_len]);
const ct_t_point = if (is_nist_curve) try params.Group.decodePoint(ct_t) else ct_t.*;
const ss_t = params.Group.elementToSharedSecret(try params.Group.mul(ct_t_point, expanded.dk_t));
return c2priCombiner(ss_pq, ss_t, ct_t, &expanded.ek_t);
}
};
/// A hybrid KEM key pair.
pub const KeyPair = struct {
public_key: PublicKey,
secret_key: SecretKey,
/// Deterministically derives a key pair from a cryptographically secure seed.
///
/// Except in tests, applications should generally call `generate()` instead.
pub fn generateDeterministic(seed: [params.Nseed]u8) !KeyPair {
const expanded = try expandDecapsKeyG(seed);
var ek_bytes: [params.Nek]u8 = undefined;
const pq_ek = expanded.ek_pq.toBytes();
@memcpy(ek_bytes[0..pq_ek.len], &pq_ek);
@memcpy(ek_bytes[pq_ek.len..], &expanded.ek_t);
return .{ .public_key = .{ .bytes = ek_bytes }, .secret_key = .{ .seed = seed } };
}
/// Generates a new random key pair.
pub fn generate(io: std.Io) !KeyPair {
var seed: [params.Nseed]u8 = undefined;
io.random(&seed);
return generateDeterministic(seed);
}
};
/// An encapsulated shared secret with its ciphertext.
pub const EncapsulatedSecret = struct {
/// Length in bytes of the shared secret.
pub const shared_length = params.Nss;
/// Length in bytes of the ciphertext.
pub const ciphertext_length = params.Nct;
/// The shared secret (output of the combiner function).
shared_secret: [shared_length]u8,
/// The ciphertext to be transmitted to the decapsulating party.
ciphertext: [ciphertext_length]u8,
};
};
}