///|
/// User ID management for an existing certificate (RFC 9580 §10.1):
/// adding identities, revoking one, and naming the primary. All of these
/// re-sign with the primary key, so the key must be unlocked.
///|
/// Refresh the creation-time statement of a hashed subpacket area to
/// `now`, keeping every other statement in place.
fn hashed_with_refreshed_creation(
hashed : Array[@packet.SigSubpacket],
now : Int32,
) -> Array[@packet.SigSubpacket] {
let out : Array[@packet.SigSubpacket] = []
for s in hashed {
if s.sub_type == @packet.SIGSUB_CREATION_TIME {
out.push({ ..s, body: creation_subpacket(now).body, })
} else {
out.push(s)
}
}
out
}
///|
/// Rebuild a hashed area with the Primary User ID flag set (RFC 9580
/// §5.2.3.20, one octet of 0x01), replacing any previous flag statement.
fn hashed_with_primary_flag(
hashed : Array[@packet.SigSubpacket],
now : Int32,
) -> Array[@packet.SigSubpacket] {
let sp : @packet.SigSubpacket = {
critical: false,
sub_type: @packet.SIGSUB_PRIMARY_USER_ID,
body: Bytes::from_array([1]),
}
let out : Array[@packet.SigSubpacket] = []
let mut inserted = false
for s in hashed {
if s.sub_type == @packet.SIGSUB_PRIMARY_USER_ID {
continue
}
if s.sub_type == @packet.SIGSUB_CREATION_TIME {
out.push({ ..s, body: creation_subpacket(now).body, })
} else {
out.push(s)
}
if !inserted && s.sub_type == @packet.SIGSUB_CREATION_TIME {
out.push(sp)
inserted = true
}
}
if !inserted {
out.push(sp)
}
out
}
///|
/// Rebuild a hashed area without the Primary User ID flag — the
/// statement another User ID's certification just claimed.
fn hashed_without_primary_flag(
hashed : Array[@packet.SigSubpacket],
now : Int32,
) -> Array[@packet.SigSubpacket] {
let out : Array[@packet.SigSubpacket] = []
for s in hashed {
if s.sub_type == @packet.SIGSUB_PRIMARY_USER_ID {
continue
}
out.push(s)
}
hashed_with_refreshed_creation(out, now)
}
///|
/// The CertificationRevocation signature (type 0x30) by the primary key
/// over one of its own User IDs, with reason 32 "User ID no longer
/// valid" (RFC 9580 §5.2.3.31) and the optional message.
fn make_uid_revocation_signature(
secret_packet : @packet.SecretKeyPacket,
pub_packet : @packet.PublicKeyPacket,
user_id : String,
message : String?,
now : Int32,
) -> @packet.SignaturePacket raise {
let v6 = pub_packet.version() >= 6
let hashed : Array[@packet.SigSubpacket] = [
{ ..creation_subpacket(now), critical: true, },
{
critical: false,
sub_type: @packet.SIGSUB_REVOCATION_REASON,
body: Bytes::from_array([32]).add(
@primitives.utf8_bytes(message.unwrap_or("")),
),
},
]
if v6 {
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 sig : @packet.SignaturePacket = {
version: if v6 {
6
} else {
4
},
sig_type: @packet.CertificationRevocation,
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: if v6 {
@primitives.random_bytes(
@packet.salt_size_of_hash(@primitives.Sha256.number()),
)
} else {
Bytes::new(0)
},
sig_params: [],
}
let digest = hash_certification(sig, pub_packet, user_id)
finish_signature(sig, secret_packet, @primitives.Sha256, digest.exact_view())
}
///|
/// Return a copy of `key` with a new User ID certified by the primary
/// key — the "I also answer to this address" operation. The new
/// certification inherits the key flags of the key-level self-signature
/// so capability statements stay consistent across User IDs.
/// Re-certifying an existing User ID replaces its binding outright (the
/// same last-certification-wins rule import applies), which is how a
/// formerly revoked identity becomes usable again. The key must be
/// unlocked.
pub fn PrivateKey::add_user_id(
self : PrivateKey,
user_id : String,
created? : Int32,
) -> PrivateKey raise {
guard user_id != "" else { @error.fail("api: user id must not be empty") }
let now = match created {
Some(t) => t
None => @primitives.unix_now().to_int()
}
let flags = governing_flags(Some(self.self_signature())).unwrap_or(0x03)
let sig = make_self_signature(
self.primary,
self.primary.public,
user_id,
now,
key_flags=flags,
)
let user_ids = self.user_ids.copy()
let fresh = UserIdBinding::{ user_id, self_signature: sig, revocation: None, }
let mut replaced = false
for i in 0.. PrivateKey raise {
let now = match created {
Some(t) => t
None => @primitives.unix_now().to_int()
}
let user_ids : Array[UserIdBinding] = []
let mut found = false
for b in self.user_ids {
if !found && b.user_id == user_id {
let sig = make_uid_revocation_signature(
self.primary,
self.primary.public,
user_id,
message,
now,
)
user_ids.push({ ..b, revocation: Some(sig), })
found = true
} else {
user_ids.push(b)
}
}
guard found else { @error.fail("api: no user id \"\{user_id}\" on this key") }
{ ..self, user_ids, }
}
///|
/// Name `user_id` the certificate's primary identity: its certification
/// is re-issued with the Primary User ID flag (RFC 9580 §5.2.3.20) and
/// every other User ID loses the flag. `user_id()` then reports it.
/// The key must be unlocked.
pub fn PrivateKey::set_primary_user_id(
self : PrivateKey,
user_id : String,
created? : Int32,
) -> PrivateKey raise {
let now = match created {
Some(t) => t
None => @primitives.unix_now().to_int()
}
let user_ids : Array[UserIdBinding] = []
let mut found = false
for b in self.user_ids {
let hashed = if b.user_id == user_id {
found = true
hashed_with_primary_flag(b.self_signature.hashed_subpackets, now)
} else {
hashed_without_primary_flag(b.self_signature.hashed_subpackets, now)
}
let re = reissue_sig(b.self_signature, self.primary, hashed, s => {
hash_certification(s, self.primary.public, b.user_id)
})
user_ids.push({ ..b, self_signature: re, })
}
guard found else { @error.fail("api: no user id \"\{user_id}\" on this key") }
{ ..self, user_ids, }
}
///|
/// True when the named User ID carries a cryptographically valid, already
/// effective CertificationRevocation — the lazy-verification model used
/// for key revocations (`is_revoked`): import attaches the signature, and
/// the cryptographic check happens here. `now = 0` disables the
/// effective-date question.
pub fn PublicKey::is_user_id_revoked(
self : PublicKey,
user_id : String,
now : Int64,
) -> Bool {
for b in self.user_ids {
if b.user_id == user_id {
match b.revocation {
Some(s) => {
let verified = verify_key_signature(
s,
self.primary,
self.primary,
user_id~,
) catch {
_ => false
}
if verified && revocation_effective_at(s, now) {
return true
}
}
None => ()
}
}
}
false
}
///|
/// See `PublicKey::is_user_id_revoked`. Delegates to the public half.
pub fn PrivateKey::is_user_id_revoked(
self : PrivateKey,
user_id : String,
now : Int64,
) -> Bool {
self.to_public().is_user_id_revoked(user_id, now)
}