///|
fn build_eddsa_secret_key_packet(
seed : Bytes,
creation : Int32,
) -> @packet.SecretKeyPacket raise {
let ed = @pki.ed25519_public_of_seed(seed.exact_view())
// Public params: length-prefixed OID, MPI of the point (0x40 || key).
let point = Bytes::from_array([0x40]).add(ed.public)
let params : Array[Bytes] = [@pki.ed25519_oid(), point]
let pub_packet : @packet.PublicKeyPacket = {
version: 4,
creation_time: creation,
algo: @packet.EdDsaLegacy,
params,
}
// Secret material: MPI(seed) + 2-octet checksum.
let sw = @packet.Writer::new()
sw.mpi(seed.exact_view())
let mpi_bytes = sw.to_bytes()
let w2 = @packet.Writer::new()
w2.bytes(mpi_bytes.exact_view())
w2.u16(@packet.session_key_checksum(mpi_bytes.exact_view()))
@packet.SecretKeyPacket::{
public: pub_packet,
s2k_usage: 0,
cipher_algo: 0,
aead_algo: 0,
s2k_spec: Bytes::new(0),
iv: Bytes::new(0),
secret_material: w2.to_bytes(),
dummy: false,
}
}
///|
/// Build a v6 Ed25519 secret key packet (RFC 9580 §5.5.2.3/§5.5.5.9):
/// native 32-octet public point, 32 raw octets of secret seed and no
/// trailing checksum.
fn build_ed25519_v6_secret_key_packet(
seed : Bytes,
creation : Int32,
) -> @packet.SecretKeyPacket raise {
guard seed.length() == 32 else {
@error.fail("api: Ed25519 seed must be 32 octets")
}
let ed = @pki.ed25519_public_of_seed(seed.exact_view())
let params : Array[Bytes] = [ed.public]
let pub_packet : @packet.PublicKeyPacket = {
version: 6,
creation_time: creation,
algo: @packet.Ed25519,
params,
}
@packet.SecretKeyPacket::{
public: pub_packet,
s2k_usage: 0,
cipher_algo: 0,
aead_algo: 0,
s2k_spec: Bytes::new(0),
iv: Bytes::new(0),
secret_material: seed,
dummy: false,
}
}
///|
fn build_rsa_secret_key_packet(
key : @pki.RsaPrivateKey,
creation : Int32,
) -> @packet.SecretKeyPacket {
// Public part: MPI(n), MPI(e).
let params : Array[Bytes] = [
@pki.i2osp(key.public.n, (key.public.n.bit_length() + 7) / 8),
@pki.i2osp(key.public.e, (key.public.e.bit_length() + 7) / 8),
]
let pub_packet : @packet.PublicKeyPacket = {
version: 4,
creation_time: creation,
algo: @packet.RsaEncryptOrSign,
params,
}
// Secret material: MPI(d) MPI(p) MPI(q) MPI(u) + 2-octet checksum.
let sw = @packet.Writer::new()
sw.mpi(@pki.i2osp(key.d, (key.d.bit_length() + 7) / 8).exact_view())
sw.mpi(@pki.i2osp(key.p, (key.p.bit_length() + 7) / 8).exact_view())
sw.mpi(@pki.i2osp(key.q, (key.q.bit_length() + 7) / 8).exact_view())
sw.mpi(@pki.i2osp(key.u, (key.u.bit_length() + 7) / 8).exact_view())
let mpi_bytes = sw.to_bytes()
let w2 = @packet.Writer::new()
w2.bytes(mpi_bytes.exact_view())
w2.u16(@packet.session_key_checksum(mpi_bytes.exact_view()))
@packet.SecretKeyPacket::{
public: pub_packet,
s2k_usage: 0,
cipher_algo: 0,
aead_algo: 0,
s2k_spec: Bytes::new(0),
iv: Bytes::new(0),
secret_material: w2.to_bytes(),
dummy: false,
}
}
///|
/// Generate a fresh transferable key: RSA primary (certify+sign) with a
/// User ID self-certification plus an RSA encryption subkey. `expiry`
/// sets a Key Expiration Time (seconds from the key's creation; 0, the
/// default, never expires) on both self-certification and binding.
pub fn generate_key(
user_id : String,
bits~ : Int32 = 3072,
expiry~ : Int64 = 0L,
) -> PrivateKey raise {
// Int truncation of the Int64 wall clock is wire-safe (Writer::u32 emits
// the two's-complement low 32 bits, which is the correct u32 epoch past
// 2038) but stays negative in memory; only date FORMATTING from this
// value would be wrong (F09, documented limitation).
let now = @primitives.unix_now().to_int()
let sub_bits = if bits >= 4096 { 3072 } else { bits }
let primary_rsa = @pki.generate_rsa_key(bits~)
let primary = build_rsa_secret_key_packet(primary_rsa, now)
let self_sig = make_self_signature(
primary,
primary.public,
user_id,
now,
key_expiry=expiry,
)
let sub_rsa = @pki.generate_rsa_key(bits=sub_bits)
let subkey = build_rsa_secret_key_packet(sub_rsa, now)
let binding = make_binding_signature(
primary,
primary.public,
subkey.public,
now,
key_expiry=expiry,
)
PrivateKey::{
primary,
user_ids: [
UserIdBinding::{ user_id, self_signature: self_sig, revocation: None, },
],
direct_signature: None,
subkeys: [SecretSubkeyCert::{ secret: subkey, binding, revocation: None, }],
revocations: [],
}
}
///|
/// Generate a transferable key with an Ed25519 primary (EdDSA legacy,
/// signing/certification) and a Curve25519Legacy ECDH encryption
/// subkey — the gopenpgp "Default" profile shape. EdDSA signatures
/// require the digest itself as the Ed25519 message (RFC 9580 §5.2.3).
///
/// # Example
///
/// ```mbt check
/// test "generate an ed25519 key" {
/// let key = @api.generate_ed25519_key("A ")
/// assert_eq(key.user_id(), "A ")
/// inspect(key.fingerprint().length(), content="20")
/// }
/// ```
pub fn generate_ed25519_key(user_id : String) -> PrivateKey raise {
let now = @primitives.unix_now().to_int()
let seed = @primitives.random_bytes(32)
let primary = build_eddsa_secret_key_packet(seed, now)
let self_sig = make_self_signature(primary, primary.public, user_id, now)
let subkey = build_ecdh_secret_key_packet(now)
let binding = make_binding_signature(
primary,
primary.public,
subkey.public,
now,
)
PrivateKey::{
primary,
user_ids: [
UserIdBinding::{ user_id, self_signature: self_sig, revocation: None, },
],
direct_signature: None,
subkeys: [SecretSubkeyCert::{ secret: subkey, binding, revocation: None, }],
revocations: [],
}
}
///|
/// Build a v6 X25519 encryption secret subkey packet (RFC 9580 §5.5.5.7):
/// the public parameter is the 32-octet native public key and the secret
/// material the 32-octet native secret key, both raw (v6 native
/// algorithms carry no MPI prefix and no checksum).
fn build_v6_x25519_secret_key_packet(
creation : Int32,
) -> @packet.SecretKeyPacket raise {
let x = @pki.x25519_generate()
let pub_packet : @packet.PublicKeyPacket = {
version: 6,
creation_time: creation,
algo: @packet.X25519,
params: [x.public],
}
@packet.SecretKeyPacket::{
public: pub_packet,
s2k_usage: 0,
cipher_algo: 0,
aead_algo: 0,
s2k_spec: Bytes::new(0),
iv: Bytes::new(0),
secret_material: x.scalar,
dummy: false,
}
}
///|
/// Generate a v6 Ed25519 signing key with an X25519 encryption subkey
/// (RFC 9580 §5.5.5.7/§5.5.5.9) — the shape of the Appendix A.3/A.4
/// sample certificate: a v6 primary key with a User ID self-certification,
/// the Direct Key signature the RFC requires before a v6 key may be used,
/// and a v6 X25519 subkey with a v6 binding signature.
pub fn generate_ed25519_v6_key(user_id : String) -> PrivateKey raise {
let now = @primitives.unix_now().to_int()
let seed = @primitives.random_bytes(32)
let primary = build_ed25519_v6_secret_key_packet(seed, now)
let self_sig = make_self_signature(primary, primary.public, user_id, now)
let direct = make_direct_key_signature(primary, primary.public, now)
let subkey = build_v6_x25519_secret_key_packet(now)
let binding = make_binding_signature(
primary,
primary.public,
subkey.public,
now,
)
PrivateKey::{
primary,
user_ids: [
UserIdBinding::{ user_id, self_signature: self_sig, revocation: None, },
],
direct_signature: Some(direct),
subkeys: [SecretSubkeyCert::{ secret: subkey, binding, revocation: None, }],
revocations: [],
}
}
///|
/// Generate a v6 certificate in the gpg dual-subkey shape: a CERT-ONLY
/// Ed25519 primary (key flags 0x01 — the self-signature withholds the
/// sign flag), an X25519 encryption subkey and an Ed25519 signing subkey
/// whose v6 binding embeds a v6 Primary Key Binding (cross-certification,
/// RFC 9580 §5.2.3.26 — a v6 key's self-signatures MUST be v6, so the
/// embedded signature is v6-shaped). Document signatures then flow
/// through the signing subkey (`signing_packet`), exactly like the gpg
/// dual-subkey interop fixtures, exercising the v6 cross-certification
/// path end to end.
pub fn generate_ed25519_v6_key_with_signing_subkey(
user_id : String,
) -> PrivateKey raise {
let now = @primitives.unix_now().to_int()
let primary = build_ed25519_v6_secret_key_packet(
@primitives.random_bytes(32),
now,
)
// Cert-only: certify flag only, so every document signature must come
// from the signing subkey.
let self_sig = make_self_signature(
primary,
primary.public,
user_id,
now,
key_flags=0x01,
)
let direct = make_direct_key_signature(primary, primary.public, now)
let enc_subkey = build_v6_x25519_secret_key_packet(now)
let enc_binding = make_binding_signature(
primary,
primary.public,
enc_subkey.public,
now,
)
let sign_subkey = build_ed25519_v6_secret_key_packet(
@primitives.random_bytes(32),
now,
)
// Cross-certification is mandatory for a sign-capable subkey: the
// binding embeds the subkey's own v6 PrimaryKeyBinding over the pair.
let sign_binding = make_binding_signature(
primary,
primary.public,
sign_subkey.public,
now,
key_flags=0x02,
subkey_secret=sign_subkey,
)
PrivateKey::{
primary,
user_ids: [
UserIdBinding::{ user_id, self_signature: self_sig, revocation: None, },
],
direct_signature: Some(direct),
subkeys: [
SecretSubkeyCert::{
secret: enc_subkey,
binding: enc_binding,
revocation: None,
},
SecretSubkeyCert::{
secret: sign_subkey,
binding: sign_binding,
revocation: None,
},
],
revocations: [],
}
}