///|
/// Serialize to binary packets: SecretKey || [DirectKeySig] ||
/// [UserID || cert] || [SecretSubkey || binding ]. A v6 key without a
/// User ID writes only the Direct Key signature.
pub fn PrivateKey::to_bytes(self : PrivateKey) -> Bytes {
let w = @packet.Writer::new()
w.bytes(
@packet.serialize_packet(
@packet.SecretKey,
self.primary.to_body().exact_view(),
).exact_view(),
)
match self.direct_signature {
Some(b) =>
w.bytes(
@packet.serialize_packet(@packet.Signature, b.to_body().exact_view()).exact_view(),
)
None => ()
}
// Emit every certified User ID with its own certification (M5). A
// bare v6 key emits none here; its Direct Key signature was already
// written above.
for binding in self.user_ids {
let uid = @primitives.utf8_bytes(binding.user_id)
w.bytes(
@packet.serialize_packet(@packet.UserId, uid.exact_view()).exact_view(),
)
w.bytes(
@packet.serialize_packet(
@packet.Signature,
binding.self_signature.to_body().exact_view(),
).exact_view(),
)
}
for c in self.subkeys {
w.bytes(
@packet.serialize_packet(
@packet.SecretSubkey,
c.secret.to_body().exact_view(),
).exact_view(),
)
w.bytes(
@packet.serialize_packet(
@packet.Signature,
c.binding.to_body().exact_view(),
).exact_view(),
)
match c.revocation {
Some(r) =>
w.bytes(
@packet.serialize_packet(@packet.Signature, r.to_body().exact_view()).exact_view(),
)
None => ()
}
}
w.to_bytes()
}
///|
/// Export as ASCII-armored private key block.
pub fn PrivateKey::to_armored(self : PrivateKey) -> String {
@armor.PrivateKey.encode(self.to_bytes().exact_view())
}
///|
/// The Preferred AEAD Ciphersuites advertised by a key as (cipher, AEAD)
/// id pairs, most preferred first (RFC 9580 §5.2.3.15). Reads the
/// self-signature first, then the Direct Key signature of a v6 key, and
/// returns an empty list when the key advertises none.
pub fn PublicKey::preferred_aead(self : PublicKey) -> Array[(Int32, Int32)] {
match self.self_signature().preferred_aead() {
Some(list) => if !list.is_empty() { return list }
None => ()
}
match self.direct_signature {
Some(sig) =>
match sig.preferred_aead() {
Some(list) => return list
None => ()
}
None => ()
}
[]
}
///|
/// True when the key advertises v2 SEIPD support in its Features
/// subpacket (RFC 9580 §5.2.3.32).
pub fn PublicKey::supports_seipd_v2(self : PublicKey) -> Bool {
if self.self_signature().supports_seipd_v2() {
return true
}
match self.direct_signature {
Some(sig) => sig.supports_seipd_v2()
None => false
}
}
///|
/// Public half to binary packets.
pub fn PublicKey::to_bytes(self : PublicKey) -> Bytes {
let w = @packet.Writer::new()
w.bytes(
@packet.serialize_packet(
@packet.PublicKey,
self.primary.to_body().exact_view(),
).exact_view(),
)
match self.direct_signature {
Some(b) =>
w.bytes(
@packet.serialize_packet(@packet.Signature, b.to_body().exact_view()).exact_view(),
)
None => ()
}
// Emit every certified User ID with its own certification (M5). A
// bare v6 key emits none here; its Direct Key signature was already
// written above.
for binding in self.user_ids {
let uid = @primitives.utf8_bytes(binding.user_id)
w.bytes(
@packet.serialize_packet(@packet.UserId, uid.exact_view()).exact_view(),
)
w.bytes(
@packet.serialize_packet(
@packet.Signature,
binding.self_signature.to_body().exact_view(),
).exact_view(),
)
}
for c in self.subkeys {
w.bytes(
@packet.serialize_packet(
@packet.PublicSubkey,
c.key.to_body().exact_view(),
).exact_view(),
)
w.bytes(
@packet.serialize_packet(
@packet.Signature,
c.binding.to_body().exact_view(),
).exact_view(),
)
match c.revocation {
Some(r) =>
w.bytes(
@packet.serialize_packet(@packet.Signature, r.to_body().exact_view()).exact_view(),
)
None => ()
}
}
w.to_bytes()
}
///|
/// Export as ASCII-armored public key block.
pub fn PublicKey::to_armored(self : PublicKey) -> String {
@armor.PublicKey.encode(self.to_bytes().exact_view())
}
///|
fn packets_of(data : BytesView) -> Array[@packet.RawPacket] raise {
@packet.parse_packets(data)
}
///|
/// One subkey as it appeared on the wire during a transferable-key walk:
/// the public half, the secret packet when the wire carried one, and the
/// binding/revocation signatures that followed it.
priv struct ParsedSubkey {
public : @packet.PublicKeyPacket
secret : @packet.SecretKeyPacket?
mut binding : @packet.SignaturePacket?
mut revocation : @packet.SignaturePacket?
}
///|
/// The result of walking a transferable key's packets once, shared by the
/// public and private import paths so the two cannot disagree about what
/// the wire said.
priv struct ParsedKey {
primary_pub : @packet.PublicKeyPacket
primary_secret : @packet.SecretKeyPacket?
user_ids : Array[UserIdBinding]
direct : @packet.SignaturePacket?
subkeys : Array[ParsedSubkey]
revocations : Array[@packet.SignaturePacket]
}
///|
/// Walk the packet stream of a transferable key once: primary packet,
/// User IDs with their certifications, the Direct Key signature, every
/// subkey with its own binding (and revocation), and primary-key
/// revocations. Bindings and revocations attach to the most recent
/// subkey, so a normal `Subkey || Binding [|| Revocation]` sequence pairs
/// correctly no matter how many subkeys the certificate carries (N2).
fn parse_transferable(packets : Array[@packet.RawPacket]) -> ParsedKey raise {
let mut primary_pub : @packet.PublicKeyPacket? = None
let mut primary_secret : @packet.SecretKeyPacket? = None
// User IDs pair with the certification that follows them (wire order;
// a later certification of the same User ID replaces the earlier one).
let bindings : Array[UserIdBinding] = []
let mut pending_uid : String? = None
let mut direct : @packet.SignaturePacket? = None
let subkeys : Array[ParsedSubkey] = []
let revocations : Array[@packet.SignaturePacket] = []
for p in packets {
match p.tag() {
@packet.PublicKey =>
primary_pub = Some(
@packet.PublicKeyPacket::parse(p.body().exact_view()),
)
@packet.SecretKey => {
let sk = @packet.SecretKeyPacket::parse(p.body().exact_view())
primary_pub = Some(sk.public)
primary_secret = Some(sk)
}
@packet.UserId =>
// Lossy on purpose: a key with an undecodable User ID stays usable
// for its subkeys (gopenpgp keeps raw bytes; we keep a best-effort
// string instead of failing the whole key).
pending_uid = Some(@utf8.decode_lossy(p.body().exact_view()))
@packet.Signature => {
let s = @packet.SignaturePacket::parse(p.body().exact_view())
match s.sig_type() {
// All four self-certification types 0x10-0x13 bind the User ID
// (N3): gpg keys certified as casual/persona self-sigs are just
// as much transferable keys as positive ones.
@packet.PositiveCertification
| @packet.GenericCertification
| @packet.PersonaCertification
| @packet.CasualCertification => {
let uid = match pending_uid {
Some(u) => u
// A certification before any User ID cannot be bound to
// anything; keys are not required to interleave, so treat
// it as an unbound stray and reject the import (strict).
None =>
@error.fail("api: certification without a preceding user id")
}
let cert = UserIdBinding::{ user_id: uid, self_signature: s, }
// Last certification of the same User ID wins.
let mut replaced = false
for i in 0.. direct = Some(s)
@packet.SubkeyBinding =>
// A binding after the last subkey re-binds it (last wins); a
// binding with no subkey before it certifies nothing.
if subkeys.length() > 0 {
let last = subkeys[subkeys.length() - 1]
last.binding = Some(s)
}
@packet.KeyRevocation => revocations.push(s)
@packet.SubkeyRevocation =>
if subkeys.length() > 0 {
let last = subkeys[subkeys.length() - 1]
last.revocation = Some(s)
}
// CertificationRevocation withdrawals are not modeled.
_ => ()
}
}
@packet.PublicSubkey =>
subkeys.push(ParsedSubkey::{
public: @packet.PublicKeyPacket::parse(p.body().exact_view()),
secret: None,
binding: None,
revocation: None,
})
@packet.SecretSubkey => {
let sk = @packet.SecretKeyPacket::parse(p.body().exact_view())
subkeys.push(ParsedSubkey::{
public: sk.public,
secret: Some(sk),
binding: None,
revocation: None,
})
}
_ => ()
}
}
match primary_pub {
Some(p) =>
ParsedKey::{
primary_pub: p,
primary_secret,
user_ids: bindings,
direct,
subkeys,
revocations,
}
None => @error.fail("api: no key packet found")
}
}
///|
/// Structural requirements plus cryptographic validation of everything
/// the key model relies on — shared by the public and private import
/// paths so neither can accept what the other rejects.
fn check_transferable(parsed : ParsedKey) -> Unit raise {
// RFC 9580 §5.2.3.10: a v6 key must carry a valid Direct Key
// signature before it may be used. A v6 key without a User ID uses
// that signature as its key-level self-signature.
if parsed.primary_pub.version() >= 6 && parsed.direct is None {
@error.fail("api: v6 key has no direct-key signature")
}
// Only a bare key (no User ID) may rely on its Direct signature
// alone; every certified User ID must carry a certification.
if parsed.user_ids.is_empty() && parsed.direct is None {
@error.fail("api: key has no self-signature")
}
validate_key_signatures(
parsed.primary_pub,
parsed.user_ids,
parsed.direct,
parsed.subkeys,
)
}
///|
/// Import-time structural and cryptographic validation, then assembly
/// into a `PublicKey`.
fn build_public(packets : Array[@packet.RawPacket]) -> PublicKey raise {
let parsed = parse_transferable(packets)
check_transferable(parsed)
let subkeys : Array[SubkeyCert] = []
for s in parsed.subkeys {
// N1: validation above guarantees the binding exists and verifies.
subkeys.push(SubkeyCert::{
key: s.public,
binding: s.binding.unwrap(),
revocation: s.revocation,
})
}
PublicKey::{
primary: parsed.primary_pub,
user_ids: parsed.user_ids,
direct_signature: parsed.direct,
subkeys,
revocations: parsed.revocations,
}
}
///|
/// Import-time cryptographic validation (M1): every signature the key
/// model relies on must verify over the material it certifies. Revocation
/// signatures are excluded — their validity is time-dependent and they
/// are verified lazily by `is_revoked`.
fn validate_key_signatures(
primary : @packet.PublicKeyPacket,
bindings : Array[UserIdBinding],
direct : @packet.SignaturePacket?,
subkeys : Array[ParsedSubkey],
) -> Unit raise {
for b in bindings {
let ok = verify_key_signature(
b.self_signature,
primary,
primary,
user_id=b.user_id,
) catch {
_ => false
}
if !ok {
@error.fail("api: user id self-signature does not verify over the key")
}
}
match direct {
Some(d) => {
let ok = verify_key_signature(d, primary, primary, direct=true) catch {
_ => false
}
if !ok {
@error.fail("api: direct-key signature does not verify over the key")
}
}
None => ()
}
for s in subkeys {
// N1 (RFC 9580 §10.1): a transferable subkey MUST carry a binding
// signature. Accepting an unbound subkey let a subkey injected into a
// forwarded certificate be treated as encryption-capable without any
// authentication by the primary key.
let b = match s.binding {
Some(b) => b
None =>
@error.fail("api: subkey has no binding signature (RFC 9580 §10.1)")
}
let ok = verify_key_signature(b, primary, s.public) catch { _ => false }
if !ok {
@error.fail("api: subkey binding signature does not verify over the key")
}
}
}
///|
/// Parse a transferable public key from binary packets.
pub fn PublicKey::from_bytes(data : BytesView) -> PublicKey raise {
build_public(packets_of(data))
}
///|
/// Parse a transferable private key from binary packets. The secret
/// material stays in whatever state the wire had it (locked or unlocked);
/// use `PrivateKey::unlock` for locked keys.
pub fn PrivateKey::from_bytes(data : BytesView) -> PrivateKey raise {
let parsed = parse_transferable(packets_of(data))
check_transferable(parsed)
let primary = match parsed.primary_secret {
Some(p) => p
None => @error.fail("api: no secret key packet found")
}
// A private certificate may carry public-only subkeys (an export that
// skipped some secret material); they are validated like any other
// subkey but contribute no usable secret here.
let subkeys : Array[SecretSubkeyCert] = []
for s in parsed.subkeys {
let secret = match s.secret {
Some(sk) => sk
None => continue
}
subkeys.push(SecretSubkeyCert::{
secret,
binding: s.binding.unwrap(),
revocation: s.revocation,
})
}
PrivateKey::{
primary,
user_ids: parsed.user_ids,
direct_signature: parsed.direct,
subkeys,
revocations: parsed.revocations,
}
}
///|
/// Parse an armored key block (public or private) into a PrivateKey when
/// it carries secret material.
pub fn PrivateKey::from_armored(text : String) -> PrivateKey raise {
let block = @armor.decode(text)
match block.armor_type() {
@armor.PrivateKey => PrivateKey::from_bytes(block.data().exact_view())
_ => @error.fail("api: not a private key armor block")
}
}
///|
/// Parse an armored key block into a public key; a private-key block is
/// accepted and stripped to its public half.
pub fn PublicKey::from_armored(text : String) -> PublicKey raise {
let block = @armor.decode(text)
match block.armor_type() {
@armor.PublicKey => PublicKey::from_bytes(block.data().exact_view())
@armor.PrivateKey =>
PrivateKey::from_bytes(block.data().exact_view()).to_public()
_ => @error.fail("api: not a key armor block")
}
}