///|
/// RSA MPI helpers: extract n/e/d/p/q/u as BigInts.
fn rsa_public_parts(p : @packet.PublicKeyPacket) -> (BigInt, BigInt) raise {
let params = p.params()
guard params.length() == 2 else {
@error.fail("api: bad RSA public parameters")
}
(
BigInt::from_octets(params[0].exact_view(), signum=1),
BigInt::from_octets(params[1].exact_view(), signum=1),
)
}
///|
fn rsa_secret_parts(p : @packet.SecretKeyPacket) -> @pki.RsaPrivateKey raise {
guard p.s2k_usage == 0 else {
@error.fail("api: secret key material is locked; unlock it first")
}
let (n, e) = rsa_public_parts(p.public)
let r = @packet.Reader::new(p.secret_material.exact_view())
let d = BigInt::from_octets(r.read_mpi().exact_view(), signum=1)
let pbig = BigInt::from_octets(r.read_mpi().exact_view(), signum=1)
let qbig = BigInt::from_octets(r.read_mpi().exact_view(), signum=1)
let u = BigInt::from_octets(r.read_mpi().exact_view(), signum=1)
@pki.RsaPrivateKey::{ public: { n, e, }, d, p: pbig, q: qbig, u, }
}
///|
///|
/// EdDSA legacy secret parts: one MPI holding the 32-octet seed.
fn eddsa_secret_parts(
p : @packet.SecretKeyPacket,
) -> @pki.Ed25519SecretKey raise {
guard p.s2k_usage == 0 else {
@error.fail("api: secret key material is locked; unlock it first")
}
let r = @packet.Reader::new(p.secret_material.exact_view())
// MPI strips leading zero octets; a seed whose first octet is 0x00
// (probability 1/256) arrives as 31 bytes. Pad back to the fixed
// 32-octet scalar on read.
let seed_mpi = r.read_mpi()
guard seed_mpi.length() <= 32 else {
@error.fail("api: bad EdDSA seed length")
}
let seed = @primitives.pad_left(seed_mpi.exact_view(), 32)
let point = p.public.params()[1]
guard point.length() == 33 else { @error.fail("api: bad EdDSA point") }
@pki.Ed25519SecretKey::{ seed, public: point[1:point.length()].to_owned(), }
}
///|
/// Native Ed25519 secret parts: the secret material is 32 raw octets (no
/// MPI, no framing — RFC 9580 §5.5.5.9) and the public point is the
/// single 32-octet native parameter. A v4 secret key packet appends the
/// 2-octet checksum after the raw octets (v6 does not); strip it.
fn ed25519_native_secret_parts(
p : @packet.SecretKeyPacket,
) -> @pki.Ed25519SecretKey raise {
guard p.s2k_usage == 0 else {
@error.fail("api: secret key material is locked; unlock it first")
}
let want = if p.public.version() >= 6 { 32 } else { 34 }
guard p.secret_material.length() == want else {
@error.fail("api: bad Ed25519 secret key length")
}
let seed = if want == 32 {
p.secret_material
} else {
p.secret_material[0:32].to_owned()
}
let params = p.public.params()
guard params.length() == 1 && params[0].length() == 32 else {
@error.fail("api: bad Ed25519 public key")
}
@pki.Ed25519SecretKey::{ seed, public: params[0], }
}
///|
/// Sign `digest` with the key's algorithm; returns the algorithm-specific
/// signature material in wire order (RSA: DigestInfo-encoded EM; EdDSA
/// legacy: r and s MPIs; Ed25519 v6: one raw 64-octet blob).
fn sign_digest_with_key(
secret_packet : @packet.SecretKeyPacket,
hash : @primitives.HashId,
digest : BytesView,
) -> Array[Bytes] raise {
match secret_packet.public.algo() {
@packet.EdDsaLegacy => {
let ed = eddsa_secret_parts(secret_packet)
let sig64 = ed.sign(digest)
[sig64[0:32].to_owned(), sig64[32:64].to_owned()]
}
@packet.Ed25519 => {
let ed = ed25519_native_secret_parts(secret_packet)
[ed.sign(digest)]
}
_ => {
let rsa = rsa_secret_parts(secret_packet)
[rsa.pkcs1_v15_sign(hash, digest)]
}
}
}
///|
/// Verify algorithm-specific signature material against a public key packet.
fn verify_sig_material(
pub_packet : @packet.PublicKeyPacket,
hash : @primitives.HashId,
digest : BytesView,
sig_params : Array[Bytes],
) -> Bool raise {
match pub_packet.algo() {
@packet.EdDsaLegacy => {
guard sig_params.length() == 2 else { return false }
let point = pub_packet.params()[1]
guard point.length() == 33 && point[0] == b'\x40' else { return false }
let r = @primitives.pad_left(sig_params[0].exact_view(), 32)
let sv = @primitives.pad_left(sig_params[1].exact_view(), 32)
let pub_bytes = point[1:point.length()]
let pk = @pki.ed25519_public_key(pub_bytes.exact_view())
let sig64 = r.add(sv)
pk.verify(digest, sig64.exact_view())
}
@packet.Ed25519 => {
// v6: one raw 64-octet signature over a 32-octet native point.
guard sig_params.length() == 1 && sig_params[0].length() == 64 else {
return false
}
let params = pub_packet.params()
guard params.length() == 1 && params[0].length() == 32 else {
return false
}
let pk = @pki.ed25519_public_key(params[0].exact_view())
pk.verify(digest, sig_params[0].exact_view())
}
_ => {
guard sig_params.length() == 1 else { return false }
let rsa = rsa_public_key(pub_packet)
rsa.pkcs1_v15_verify(hash, digest, sig_params[0].exact_view())
}
}
}
///|
/// Preferred algorithm subpackets used in our self-signatures.
fn preferred_subpackets(v6 : Bool) -> Array[@packet.SigSubpacket] {
let out : Array[@packet.SigSubpacket] = [
{
critical: false,
sub_type: @packet.SIGSUB_PREFERRED_SYM_ALGOS,
body: Bytes::from_array([9, 8, 7]),
},
{
critical: false,
sub_type: @packet.SIGSUB_PREFERRED_HASH_ALGOS,
body: Bytes::from_array([10, 9, 8]),
},
{
critical: false,
sub_type: @packet.SIGSUB_PREFERRED_COMP_ALGOS,
body: Bytes::from_array([2, 1, 0]),
},
{
critical: false,
sub_type: @packet.SIGSUB_FEATURES,
// §5.2.3.32: v1 SEIPD is always supported; v6 keys additionally
// advertise v2 SEIPD so peers know they may send the AEAD shape.
body: Bytes::from_array([
(if v6 {
@packet.FEATURE_SEIPD_V1 | @packet.FEATURE_SEIPD_V2
} else {
@packet.FEATURE_SEIPD_V1
}).to_byte(),
]),
},
]
if v6 {
// §5.2.3.15: AES-256/OCB, AES-256/GCM, AES-128/OCB (the mandatory
// AES-128/OCB pair is implicit and need not be listed).
out.push({
critical: false,
sub_type: @packet.SIGSUB_PREFERRED_AEAD,
body: Bytes::from_array([9, 2, 9, 3, 7, 2]),
})
}
out
}
///|
fn creation_subpacket(now : Int32) -> @packet.SigSubpacket {
{
critical: false,
sub_type: @packet.SIGSUB_CREATION_TIME,
body: Bytes::makei(4, i => (now >> (24 - i * 8)).to_byte()),
}
}
///|
fn key_flags_subpacket(flags : Int32) -> @packet.SigSubpacket {
{
critical: false,
sub_type: @packet.SIGSUB_KEY_FLAGS,
body: Bytes::from_array([flags.to_byte()]),
}
}
///|
/// Attach the fast-hash (first two octets of the DIGEST, RFC 9580
/// §5.2.4) and the algorithm-specific signature material.
fn finish_signature(
sig : @packet.SignaturePacket,
secret_packet : @packet.SecretKeyPacket,
hash : @primitives.HashId,
digest : BytesView,
) -> @packet.SignaturePacket raise {
let params = sign_digest_with_key(secret_packet, hash, digest)
let fast = (digest[0].to_int() << 8) | digest[1].to_int()
{ ..sig, fast_hash: fast, sig_params: params, }
}
///|
/// Create a self-certification of `user_id` over the primary key. Any of
/// the four certification types 0x10-0x13 binds the User ID (N3); the
/// default is PositiveCertification, what gpg and this library emit.
fn make_self_signature(
secret_packet : @packet.SecretKeyPacket,
pub_packet : @packet.PublicKeyPacket,
user_id : String,
now : Int32,
cert_type~ : @packet.SigType = @packet.PositiveCertification,
) -> @packet.SignaturePacket raise {
let v6 = pub_packet.version() >= 6
let hashed : Array[@packet.SigSubpacket] = [
creation_subpacket(now),
key_flags_subpacket(0x03),
..preferred_subpackets(v6),
]
if v6 {
// §5.2.3.12/33: v6 keys identify the issuer by fingerprint.
hashed.push(issuer_fingerprint_subpacket(pub_packet))
}
let unhashed : Array[@packet.SigSubpacket] = if v6 {
[]
} else {
[
{
critical: false,
sub_type: @packet.SIGSUB_ISSUER_KEY_ID,
body: pub_packet.key_id(),
},
]
}
let salt : Bytes = if v6 {
@primitives.random_bytes(
@packet.salt_size_of_hash(@primitives.Sha256.number()),
)
} else {
Bytes::new(0)
}
let sig : @packet.SignaturePacket = {
version: if v6 {
6
} else {
4
},
sig_type: cert_type,
pub_algo: secret_packet.public.algo(),
hash_algo: @primitives.Sha256.number(),
hashed_subpackets: hashed,
unhashed_subpackets: unhashed,
raw_hashed_area: Bytes::new(0),
fast_hash: 0,
salt,
sig_params: [],
}
let digest = hash_certification(sig, pub_packet, user_id)
finish_signature(sig, secret_packet, @primitives.Sha256, digest.exact_view())
}
///|
/// Create the Direct Key signature (type 0x1F) over the primary key that
/// RFC 9580 §5.2.3.10 requires before a v6 key may be used.
fn make_direct_key_signature(
secret_packet : @packet.SecretKeyPacket,
pub_packet : @packet.PublicKeyPacket,
now : Int32,
) -> @packet.SignaturePacket raise {
guard pub_packet.version() >= 6 else {
@error.fail("api: direct key signatures are a v6 construct")
}
let hashed : Array[@packet.SigSubpacket] = [
{ ..creation_subpacket(now), critical: true, },
key_flags_subpacket(0x03),
..preferred_subpackets(true),
issuer_fingerprint_subpacket(pub_packet),
]
let sig : @packet.SignaturePacket = {
version: 6,
sig_type: @packet.DirectSignature,
pub_algo: secret_packet.public.algo(),
hash_algo: @primitives.Sha256.number(),
hashed_subpackets: hashed,
unhashed_subpackets: [],
raw_hashed_area: Bytes::new(0),
fast_hash: 0,
salt: @primitives.random_bytes(
@packet.salt_size_of_hash(@primitives.Sha256.number()),
),
sig_params: [],
}
let digest = hash_direct_key(sig, pub_packet)
finish_signature(sig, secret_packet, @primitives.Sha256, digest.exact_view())
}
///|
/// Create a subkey binding signature (0x18): version 4 for a v4 primary
/// key, version 6 (with a hash salt and the fingerprint issuer subpacket)
/// for a v6 one. A v6 primary key may only certify v6 subkeys.
/// `key_flags` states the subkey's capabilities (default encrypt-only,
/// 0x0C — what this library generates; N2 tests bind signing subkeys
/// with 0x02).
fn make_binding_signature(
secret_packet : @packet.SecretKeyPacket,
primary_pub : @packet.PublicKeyPacket,
subkey_pub : @packet.PublicKeyPacket,
now : Int32,
key_flags~ : Int32 = 0x0C,
) -> @packet.SignaturePacket raise {
let v6 = primary_pub.version() >= 6
guard !v6 || subkey_pub.version() >= 6 else {
@error.fail("api: a v6 primary key must certify v6 subkeys")
}
let hashed : Array[@packet.SigSubpacket] = [
creation_subpacket(now),
key_flags_subpacket(key_flags),
..preferred_subpackets(v6),
]
if v6 {
hashed.push(issuer_fingerprint_subpacket(primary_pub))
}
let unhashed : Array[@packet.SigSubpacket] = if v6 {
[]
} else {
[
{
critical: false,
sub_type: @packet.SIGSUB_ISSUER_KEY_ID,
body: primary_pub.key_id(),
},
]
}
let sig : @packet.SignaturePacket = {
version: if v6 {
6
} else {
4
},
salt: if v6 {
@primitives.random_bytes(
@packet.salt_size_of_hash(@primitives.Sha256.number()),
)
} else {
Bytes::new(0)
},
sig_type: @packet.SubkeyBinding,
pub_algo: secret_packet.public.algo(),
hash_algo: @primitives.Sha256.number(),
hashed_subpackets: hashed,
unhashed_subpackets: unhashed,
raw_hashed_area: Bytes::new(0),
fast_hash: 0,
sig_params: [],
}
let digest = hash_binding(sig, primary_pub, subkey_pub)
finish_signature(sig, secret_packet, @primitives.Sha256, digest.exact_view())
}
///|
/// Frame a key packet for certification hashing: v4 uses 0x99 + u16
/// length, v6 uses 0x9B + u32 length (RFC 9580 §5.2.4).
fn write_key_framing(
w : @packet.Writer,
pub_packet : @packet.PublicKeyPacket,
) -> Unit raise {
let body = pub_packet.to_body()
if pub_packet.version() >= 6 {
w.u8(0x9B)
w.u32(body.length())
w.bytes(body.exact_view())
} else {
// v4 hash framing encodes the packet length as u16 (F09): a larger key
// packet would silently truncate and produce an unverifiable signature.
guard body.length() <= 0xFFFF else {
@error.fail("api: key packet too large")
}
w.u8(0x99)
w.u16(body.length())
w.bytes(body.exact_view())
}
}
///|
/// Hash data for a User ID certification.
fn hash_certification(
sig : @packet.SignaturePacket,
pub_packet : @packet.PublicKeyPacket,
user_id : String,
) -> Bytes raise {
let uid = @primitives.utf8_bytes(user_id)
let w = @packet.Writer::new()
write_key_framing(w, pub_packet)
w.u8(0xB4)
w.u32(uid.length())
w.bytes(uid.exact_view())
finish_signed_digest(sig, w.to_bytes().exact_view())
}
///|
/// Hash data for a Direct Key signature: the framed key packet alone.
fn hash_direct_key(
sig : @packet.SignaturePacket,
pub_packet : @packet.PublicKeyPacket,
) -> Bytes raise {
let w = @packet.Writer::new()
write_key_framing(w, pub_packet)
finish_signed_digest(sig, w.to_bytes().exact_view())
}
///|
/// Hash data for a subkey binding signature.
fn hash_binding(
sig : @packet.SignaturePacket,
primary_pub : @packet.PublicKeyPacket,
subkey_pub : @packet.PublicKeyPacket,
) -> Bytes raise {
let w = @packet.Writer::new()
write_key_framing(w, primary_pub)
write_key_framing(w, subkey_pub)
finish_signed_digest(sig, w.to_bytes().exact_view())
}
///|
/// Verify a self-certification or binding signature over key material.
/// Deliberately public (F14 review decision): it is the key-management
/// entry point for validating third-party keys, exercised from outside
/// the package by the gopenpgp self-certification cross-validation tests.
pub fn verify_key_signature(
sig : @packet.SignaturePacket,
signer_pub : @packet.PublicKeyPacket,
certified_pub : @packet.PublicKeyPacket,
user_id? : String,
direct~ : Bool = false,
) -> Bool raise {
// Same critical rule as document verification (S01): a certification,
// binding or revocation carrying an unrecognized critical subpacket is
// in error. For revocations this is the safe direction — an invalid
// revocation cannot disable a key.
if sig.has_unknown_critical_subpacket() {
return false
}
let digest : Bytes = if direct {
hash_direct_key(sig, certified_pub)
} else {
match user_id {
Some(uid) => hash_certification(sig, certified_pub, uid)
None => hash_binding(sig, signer_pub, certified_pub)
}
}
let hash_id = match @primitives.hash_id_of_number(sig.hash_algo()) {
Some(h) => h
None => return false
}
verify_sig_material(
signer_pub,
hash_id,
digest.exact_view(),
sig.sig_params(),
) catch {
_ => false
}
}
///|
/// RSA public key view of a public key packet.
fn rsa_public_key(
pub_packet : @packet.PublicKeyPacket,
) -> @pki.RsaPublicKey raise {
let (n, e) = rsa_public_parts(pub_packet)
@pki.RsaPublicKey::{ n, e, }
}