Escape user IDs.

- This is attacker controlled data which must be sanitizied.
This commit is contained in:
Justus Winter 2024-12-16 13:04:02 +01:00
parent 676ed78656
commit 367e71722f
No known key found for this signature in database
GPG Key ID: 686F55B4AB2B3386
14 changed files with 103 additions and 108 deletions

View File

@ -32,6 +32,7 @@ use crate::{
cli::cert::lint::Command,
cli::types::cert_designator::CertDesignator,
commands::FileOrStdout,
common::ui,
};
@ -514,9 +515,9 @@ pub fn lint(sq: Sq, mut args: Command) -> Result<()> {
let sig = ua.binding_signature();
if sig.hash_algo() == HashAlgorithm::SHA1 {
diag!("Certificate {} contains a \
User ID ({:?}) protected by SHA-1",
User ID ({}) protected by SHA-1",
cert.keyid().to_hex(),
String::from_utf8_lossy(ua.value()));
ui::Safe(ua.userid()));
if !sha1_protected_userid {
sha1_protected_userid = true;
@ -535,8 +536,7 @@ pub fn lint(sq: Sq, mut args: Command) -> Result<()> {
Failed to update \
binding signature: {}",
cert.keyid().to_hex(),
String::from_utf8_lossy(
ua.value()),
ui::Safe(ua.userid()),
err);
}
}

View File

@ -13,7 +13,7 @@ use sequoia_wot as wot;
use crate::Sq;
use crate::cli::key::approvals;
use crate::cli;
use crate::common::ca_creation_time;
use crate::common::{ca_creation_time, ui};
pub fn dispatch(sq: Sq, command: approvals::Command)
-> Result<()>
@ -47,7 +47,7 @@ fn list(sq: Sq, cmd: approvals::ListCommand) -> Result<()> {
wwriteln!(stream=o,
initial_indent = " - ", "{}",
String::from_utf8_lossy(uid.value()));
ui::Safe(uid.userid()));
let approved =
uid.attested_certifications().collect::<BTreeSet<_>>();
@ -63,7 +63,7 @@ fn list(sq: Sq, cmd: approvals::ListCommand) -> Result<()> {
.next()
.map(|kh| kh.to_string())
.unwrap_or_else(|| "unknown certificate".to_string()),
String::from_utf8_lossy(uid.value())));
ui::Safe(uid.userid())));
continue;
}
@ -99,14 +99,14 @@ fn list(sq: Sq, cmd: approvals::ListCommand) -> Result<()> {
sq.info(format_args!(
"Ignoring certification from non-exportable \
certificate {} on {}.",
i.fingerprint(), String::from_utf8_lossy(uid.value())));
i.fingerprint(), ui::Safe(uid.userid())));
continue;
}
if i.primary_key().creation_time() == ca_creation_time() {
sq.info(format_args!(
"Ignoring certification from local shadow CA \
{} on {}.",
i.fingerprint(), String::from_utf8_lossy(uid.value())));
i.fingerprint(), ui::Safe(uid.userid())));
continue;
}
}
@ -199,7 +199,7 @@ fn update(
}
weprintln!(initial_indent = " - ", "{}",
String::from_utf8_lossy(uid.value()));
ui::Safe(uid.userid()));
let previously_approved =
uid.attested_certifications().collect::<BTreeSet<_>>();
@ -231,7 +231,7 @@ fn update(
.next()
.map(|kh| kh.to_string())
.unwrap_or_else(|| "unknown certificate".to_string()),
String::from_utf8_lossy(uid.value())));
ui::Safe(uid.userid())));
continue;
}
@ -279,14 +279,14 @@ fn update(
sq.info(format_args!(
"Ignoring certification from non-exportable \
certificate {} on {}.",
i.fingerprint(), String::from_utf8_lossy(uid.value())));
i.fingerprint(), ui::Safe(uid.userid())));
continue;
}
if i.primary_key().creation_time() == ca_creation_time() {
sq.info(format_args!(
"Ignoring certification from local shadow CA \
{} on {}.",
i.fingerprint(), String::from_utf8_lossy(uid.value())));
i.fingerprint(), ui::Safe(uid.userid())));
continue;
}
}

View File

@ -28,6 +28,7 @@ use crate::cli::types::userid_designator::ResolvedUserID;
use crate::cli;
use crate::common::RevocationOutput;
use crate::common::get_secret_signer;
use crate::common::ui;
use crate::common::userid::{
lint_emails,
lint_names,
@ -39,7 +40,6 @@ struct UserIDRevocation {
cert: Cert,
revoker: Cert,
revocation_packet: Packet,
userid: String,
uid: UserID,
}
@ -57,8 +57,6 @@ impl UserIDRevocation {
let (revoker, mut signer)
= get_secret_signer(sq, &cert, revoker.as_ref())?;
let userid = String::from_utf8_lossy(uid.userid().value()).to_string();
let revocation_packet = {
// Create a revocation for a User ID.
let mut rev = UserIDRevocationBuilder::new()
@ -85,7 +83,6 @@ impl UserIDRevocation {
cert,
revoker,
revocation_packet,
userid,
uid: uid.userid().clone(),
})
}
@ -106,7 +103,7 @@ impl RevocationOutput for UserIDRevocation
fn comment(&self) -> String {
format!("This is a revocation certificate for \
the User ID {} of cert {}.",
self.userid,
ui::Safe(&self.uid),
self.cert.fingerprint())
}
@ -164,7 +161,8 @@ fn userid_add(
if !exists.is_empty() {
return Err(anyhow::anyhow!(
"The certificate already contains the User ID(s) {}.",
exists.iter().map(|s| format!("{:?}", String::from_utf8_lossy(s.value()))).collect::<Vec<_>>()
exists.iter().map(|s| ui::Safe(*s).to_string())
.collect::<Vec<_>>()
.join(", "),
));
}

View File

@ -254,10 +254,10 @@ fn certify(sq: &Sq,
if let Some(_) = active_certification {
if emit_provenance_messages {
sq.info(format_args!(
"Provenance information for {}, {:?} \
"Provenance information for {}, {} \
exists and is current, not updating it",
cert.fingerprint(),
String::from_utf8_lossy(userid.value())));
ui::Safe(userid)));
}
return vec![];
}
@ -267,27 +267,27 @@ fn certify(sq: &Sq,
cert.primary_key().key(),
&userid)
.with_context(|| {
format!("Creating certification for {} {:?}",
format!("Creating certification for {}, {}",
cert.fingerprint(),
String::from_utf8_lossy(userid.value()))
ui::Safe(userid))
})
{
Ok(sig) => {
if emit_provenance_messages {
sq.info(format_args!(
"Recorded provenance information \
for {}, {:?}",
for {}, {}",
cert.fingerprint(),
String::from_utf8_lossy(userid.value())));
ui::Safe(userid)));
}
vec![ Packet::from(userid.clone()), Packet::from(sig) ]
}
Err(err) => {
let err = err.context(format!(
"Warning: recording provenance information \
for {}, {:?}",
for {}, {}",
cert.fingerprint(),
String::from_utf8_lossy(userid.value())));
ui::Safe(userid)));
print_error_chain(&err);
vec![]
}

View File

@ -37,6 +37,7 @@ use crate::cli::packet::{
use crate::cli::types::FileOrStdout;
use crate::cli::types::StdinWarning;
use crate::commands;
use crate::common::ui;
use crate::load_keys;
pub mod armor;
@ -234,8 +235,7 @@ pub fn split(sq: Sq, c: SplitCommand) -> Result<()>
}
},
Packet::UserID(u) => headers.push(
("Comment", format!("UserID: {}",
String::from_utf8_lossy(u.value())))),
("Comment", format!("UserID: {}", ui::Safe(u)))),
_ => (),
}

View File

@ -371,7 +371,7 @@ impl<'a, 'b, 'c> PacketDumper<'a, 'b, 'c> {
UserID(ref u) => {
writeln!(output, "{} Value: {}", i,
String::from_utf8_lossy(u.value()))?;
ui::Safe(u.value()))?;
},
UserAttribute(ref u) => {
@ -907,7 +907,7 @@ impl<'a, 'b, 'c> PacketDumper<'a, 'b, 'c> {
write!(output, "{} Key flags: {:?}", i, k)?,
SignersUserID(ref u) =>
write!(output, "{} Signer's User ID: {}", i,
String::from_utf8_lossy(u))?,
ui::Safe(u))?,
ReasonForRevocation{code, ref reason} => {
write!(output, "{} Reason for revocation: {}{}{}", i, code,
if reason.len() > 0 { ", " } else { "" },

View File

@ -21,6 +21,7 @@ use crate::cli::pki::link;
use crate::cli::types::Expiration;
use crate::cli::types::TrustAmount;
use crate::cli::types::cert_designator;
use crate::common::ui;
pub fn link(sq: Sq, c: link::Command) -> Result<()> {
use link::Subcommands::*;
@ -261,12 +262,7 @@ pub fn list(sq: Sq, mut c: link::ListCommand)
}
dirty = true;
wwriteln!(stream=o,
initial_indent=" - ┌ ", subsequent_indent="",
"{}", cert.fingerprint());
wwriteln!(stream=o,
initial_indent="",
"{:?}", String::from_utf8_lossy(userid.value()));
ui::emit_cert_userid(o, &cert, userid)?;
const INDENT: &'static str = " - ";
@ -275,7 +271,7 @@ pub fn list(sq: Sq, mut c: link::ListCommand)
"link was retracted");
} else {
let mut regex: Vec<_> = certification.regular_expressions()
.map(|re| String::from_utf8_lossy(re))
.map(|re| ui::Safe(re).to_string())
.collect();
regex.sort();
regex.dedup();

View File

@ -27,6 +27,7 @@ use crate::Sq;
use crate::Result;
use crate::cli;
use crate::commands::inspect::Kind;
use crate::common::{PreferredUserID, ui};
pub fn dispatch(sq: Sq, command: cli::verify::Command)
-> Result<()>
@ -243,9 +244,7 @@ impl<'c, 'store, 'rstore> VHelper<'c, 'store, 'rstore> {
let cert = ka.cert();
let cert_fpr = cert.fingerprint();
let issuer = ka.key().keyid();
let mut signer_userid = ka.cert().primary_userid()
.map(|ua| String::from_utf8_lossy(ua.value()).to_string())
.unwrap_or_else(|_| "<unknown>".to_string());
let mut signer_userid = self.sq.best_userid(ka.cert(), false);
// Direct trust.
let mut authenticated = self.trusted.contains(&issuer);
@ -277,9 +276,6 @@ impl<'c, 'store, 'rstore> VHelper<'c, 'store, 'rstore> {
let authenticated_userids
= userids.into_iter().filter(|userid| {
let userid_str =
String::from_utf8_lossy(userid.value());
let paths = n.authenticate(
userid, cert.fingerprint(),
// XXX: Make this user squrable.
@ -293,23 +289,23 @@ impl<'c, 'store, 'rstore> VHelper<'c, 'store, 'rstore> {
amount,
sequoia_wot::FULLY_TRUSTED,
cert_fpr,
userid_str);
ui::Safe(userid));
true
} else if amount > 0 {
weprintln!(indent=prefix,
"Partially authenticated \
({} of {}) {}, {:?} ",
({} of {}) {}, {} ",
amount,
sequoia_wot::FULLY_TRUSTED,
cert_fpr,
userid_str);
ui::Safe(userid));
false
} else {
weprintln!(indent=prefix,
"{}: {:?} is unauthenticated \
"{}: {} is unauthenticated \
and may be an impersonation!",
cert_fpr,
userid_str);
ui::Safe(userid));
false
};
@ -350,13 +346,15 @@ impl<'c, 'store, 'rstore> VHelper<'c, 'store, 'rstore> {
.then_some(u)
})
{
signer_userid = String::from_utf8_lossy(u)
.to_string();
signer_userid = PreferredUserID::from_string(
String::from_utf8_lossy(u),
sequoia_wot::FULLY_TRUSTED);
} else {
// Else just pick the first one.
signer_userid = String::from_utf8_lossy(
authenticated_userids[0].value())
.to_string();
signer_userid = PreferredUserID::from_string(
String::from_utf8_lossy(
authenticated_userids[0].value()),
sequoia_wot::FULLY_TRUSTED);
}
}
}

View File

@ -104,14 +104,14 @@ pub fn diff_certification(unless_quiet: &mut dyn std::io::Write,
"current certification");
for (i, r) in a_regex.into_iter().enumerate() {
wwriteln!(stream = unless_quiet, initial_indent = " - ",
"{}. {}", i + 1, String::from_utf8_lossy(r));
"{}. {}", i + 1, ui::Safe(r));
}
wwriteln!(stream = unless_quiet, initial_indent = " - ",
"new certification");
for (i, r) in b_regex.into_iter().enumerate() {
wwriteln!(stream = unless_quiet, initial_indent = " - ",
"{}. {}", i + 1, String::from_utf8_lossy(r));
"{}. {}", i + 1, ui::Safe(r));
}
}
@ -301,8 +301,6 @@ The certifier is the same as the certificate to certify."));
certifier.primary_key().key().role_as_unspecified())
.into_iter()
.map(|(userid, active_certification)| {
let userid_str = || String::from_utf8_lossy(userid.userid().value());
if let Some(ua) = cert.userids().find(|ua| ua.userid() == userid.userid()) {
if retract {
// Check if we certified it.
@ -318,9 +316,9 @@ The certifier is the same as the certificate to certify."));
if user_supplied_userids {
return Err(anyhow::anyhow!(
"You never certified {:?} for {}, \
"You never certified {} for {}, \
there is nothing to retract.",
userid_str(), cert.fingerprint()));
ui::Safe(userid.userid()), cert.fingerprint()));
} else {
return Ok(vec![ Packet::from(userid.userid().clone()) ]);
}
@ -334,8 +332,8 @@ The certifier is the same as the certificate to certify."));
// It was explicitly mentioned. Return an
// error.
return Err(anyhow::anyhow!(
"Can't certify {:?} for {}, it's revoked",
userid_str(), cert.fingerprint()));
"Can't certify {} for {}, it's revoked",
ui::Safe(userid.userid()), cert.fingerprint()));
} else {
// We're just considering valid, self-signed
// user IDs. Silently, skip it.
@ -351,9 +349,9 @@ The certifier is the same as the certificate to certify."));
if user_supplied_userids {
return Err(anyhow::anyhow!(
"You never certified {:?} for {}, \
"You never certified {} for {}, \
there is nothing to retract.",
userid_str(), cert.fingerprint()));
ui::Safe(userid.userid()), cert.fingerprint()));
} else {
// The user passed --all. Don't error out if some
// user IDs were not linked. Instead, return a
@ -420,8 +418,8 @@ The certifier is the same as the certificate to certify."));
cert.primary_key().key(),
userid.userid())
.with_context(|| {
format!("Creating certification for {:?}",
userid_str())
format!("Creating certification for {}",
ui::Safe(userid.userid()))
})
.map(Into::into)
})

View File

@ -56,7 +56,7 @@ pub fn print_path_header(
" "
},
target_kh,
String::from_utf8_lossy(target_userid.value()),
ui::Safe(target_userid),
if amount >= 2 * FULLY_TRUSTED {
"doubly"
} else if amount >= FULLY_TRUSTED {
@ -85,9 +85,9 @@ pub fn print_path(output: &mut dyn std::io::Write,
subsequent_indent=format!("{}", prefix),
"{}",
if certification_count == 0 {
format!("{:?}", String::from_utf8_lossy(target_userid.value()))
format!("{}", ui::Safe(target_userid))
} else if let Some(userid) = path.root().primary_userid() {
format!("({:?})", String::from_utf8_lossy(userid.value()))
format!("({})", ui::Safe(&userid))
} else {
format!("")
});
@ -197,11 +197,11 @@ pub fn print_path(output: &mut dyn std::io::Write,
if last { " " } else { "" }),
"{}",
if last {
format!("{:?}", String::from_utf8_lossy(target_userid.value()))
format!("{}", ui::Safe(target_userid))
} else if let Some(userid) =
certification.target_cert().and_then(|c| c.primary_userid())
{
format!("({:?})", String::from_utf8_lossy(userid.value()))
format!("({})", ui::Safe(userid.userid()))
} else {
"".into()
});
@ -414,7 +414,7 @@ impl OutputType for ConciseHumanReadableOutputNetwork<'_, '_, '_> {
} else {
format!("{:3}/120", aggregated_amount)
},
String::from_utf8_lossy(userid.value()));
ui::Safe(userid));
if self.paths {
wwriteln!(stream=self.output);

View File

@ -17,6 +17,7 @@ use crate::cli::types::userid_designator::PlainIsAdd;
use crate::cli::types::userid_designator::ResolvedUserID;
use crate::cli::types::userid_designator::UserIDDesignator;
use crate::cli::types::userid_designator::UserIDDesignatorSemantics;
use crate::common::ui;
use crate::common::userid::lint_email;
use crate::common::userid::lint_name;
use crate::common::userid::lint_userid;
@ -123,8 +124,8 @@ where
}
userids.push(designator.resolve_to(userid));
} else {
weprintln!("{:?} is not a self-signed user ID.",
String::from_utf8_lossy(userid.value()));
weprintln!("{} is not a self-signed user ID.",
ui::Safe(&userid));
missing = true;
}
}
@ -274,11 +275,11 @@ where
weprintln!("{}'s self-signed user IDs:", vc.fingerprint());
let mut have_valid = false;
for ua in vc.userids() {
if let Ok(u) = std::str::from_utf8(ua.userid().value()) {
if std::str::from_utf8(ua.userid().value()).is_ok() {
have_valid = true;
weprintln!(initial_indent=" - ",
subsequent_indent=" ",
"{:?}", u);
"{}", ui::Safe(ua.userid()));
}
}
if ! have_valid {
@ -295,11 +296,13 @@ where
continue;
}
if let Ok(u) = std::str::from_utf8(ua.userid().value()) {
if std::str::from_utf8(ua.userid().value()).is_ok() {
let userid = ui::Safe(ua.userid());
if let Err(err) = ua.with_policy(vc.policy(), vc.time()) {
weprintln!(initial_indent=" - ",
subsequent_indent=" ",
"{:?}: {}", u, err);
"{}: {}",
userid, err);
}
}
}
@ -349,7 +352,7 @@ where
t!(" => {:?}",
userids.iter()
.map(|u| {
String::from_utf8_lossy(u.userid().value()).to_string()
ui::Safe(u.userid()).to_string()
})
.collect::<Vec<String>>()
.join(", "));

View File

@ -67,6 +67,7 @@ mod keyring {
cert::Cert,
};
use crate::Sq;
use crate::common::ui;
use super::{Version, Write};
use serde::Serialize;
@ -190,7 +191,7 @@ mod keyring {
}
fn userid(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).into()
ui::Safe(bytes).to_string()
}
}
}

View File

@ -58,6 +58,7 @@ use crate::cli::types::cert_designator;
use crate::cli::types::key_designator;
use crate::cli::types::paths::StateDirectory;
use crate::common::password;
use crate::common::ui;
use crate::output::hint::Hint;
use crate::output::import::{ImportStats, ImportStatus};
use crate::PreferredUserID;
@ -719,8 +720,8 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
Ok(cert) => cert,
Err(err) => {
let err = err.context(format!(
"Error fetching {} ({:?})",
fpr, String::from_utf8_lossy(userid.value())));
"Error fetching {} ({})",
fpr, ui::Safe(&userid)));
return Entry { fpr, userid, cert: Err(err), };
}
};
@ -730,8 +731,8 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
Ok(cert) => cert.clone(),
Err(err) => {
let err = err.context(format!(
"Error parsing {} ({:?})",
fpr, String::from_utf8_lossy(userid.value())));
"Error parsing {} ({})",
fpr, ui::Safe(&userid)));
return Entry { fpr, userid, cert: Err(err), };
}
};
@ -741,23 +742,23 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
Ok(vc) => vc,
Err(err) => {
let err = err.context(format!(
"Certificate {} ({:?}) is invalid",
fpr, String::from_utf8_lossy(userid.value())));
"Certificate {} ({}) is invalid",
fpr, ui::Safe(&userid)));
return Entry { fpr, userid, cert: Err(err) };
}
};
if let Err(err) = vc.alive() {
let err = err.context(format!(
"Certificate {} ({:?}) is invalid",
fpr, String::from_utf8_lossy(userid.value())));
"Certificate {} ({}) is invalid",
fpr, ui::Safe(&userid)));
return Entry { fpr, userid, cert: Err(err), };
}
if let RevocationStatus::Revoked(_) = vc.revocation_status() {
let err = anyhow::anyhow!(
"Certificate {} ({:?}) is revoked",
fpr, String::from_utf8_lossy(userid.value()));
"Certificate {} ({}) is revoked",
fpr, ui::Safe(&userid));
return Entry { fpr, userid, cert: Err(err), };
}
@ -767,8 +768,8 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
{
if let RevocationStatus::Revoked(_) = ua.revocation_status() {
let err = anyhow::anyhow!(
"User ID {:?} on certificate {} is revoked",
String::from_utf8_lossy(userid.value()), fpr);
"User ID {} on certificate {} is revoked",
ui::Safe(&userid), fpr);
return Entry { fpr, userid, cert: Err(err), };
}
}
@ -780,15 +781,15 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
wot::FULLY_TRUSTED);
let r = if paths.amount() < wot::FULLY_TRUSTED {
Err(anyhow::anyhow!(
"{}, {:?} cannot be authenticated at the \
"{}, {} cannot be authenticated at the \
required level ({} of {}). After checking \
that {} really controls {}, you could certify \
their certificate by running \
`sq pki link add --cert {} --userid {:?}`.",
cert.fingerprint(),
String::from_utf8_lossy(userid.value()),
ui::Safe(&userid),
paths.amount(), wot::FULLY_TRUSTED,
String::from_utf8_lossy(userid.value()),
ui::Safe(&userid),
cert.fingerprint(),
cert.fingerprint(),
String::from_utf8_lossy(userid.value())))
@ -811,26 +812,26 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
// We got nothing :/.
if email {
anyhow::anyhow!(
"No known certificates have the email address {:?}",
userid)
"No known certificates have the email address {}",
ui::Safe(userid))
} else {
anyhow::anyhow!(
"No known certificates have the User ID {:?}",
userid)
"No known certificates have the User ID {}",
ui::Safe(userid))
}
} else {
if email {
anyhow::anyhow!(
"None of the certificates with the email \
address {:?} can be authenticated using \
address {} can be authenticated using \
the configured trust model",
userid)
ui::Safe(userid))
} else {
anyhow::anyhow!(
"None of the certificates with the User ID \
{:?} can be authenticated using \
{} can be authenticated using \
the configured trust model",
userid)
ui::Safe(userid))
}
};
@ -843,7 +844,7 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
for (i, Entry { fpr, userid, cert }) in bad.into_iter().enumerate() {
weprintln!("{}. When considering {} ({}):",
i + 1, fpr,
String::from_utf8_lossy(userid.value()));
ui::Safe(&userid));
let err = match cert {
Ok(_) => unreachable!(),
Err(err) => err,
@ -2203,7 +2204,7 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
trust_amount);
if paths.amount() < trust_amount {
hint.push(Err(anyhow::anyhow!(
"{}, {:?} cannot be authenticated \
"{}, {} cannot be authenticated \
at the required level ({} of {}). \
After checking that {} really \
controls {}, you could certify \
@ -2211,9 +2212,9 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
`sq pki link add --cert {} \
--userid {:?}`.",
cert.fingerprint(),
String::from_utf8_lossy(userid.value()),
ui::Safe(&userid),
paths.amount(), trust_amount,
String::from_utf8_lossy(userid.value()),
ui::Safe(&userid),
cert.fingerprint(),
cert.fingerprint(),
String::from_utf8_lossy(userid.value()))));

View File

@ -711,7 +711,7 @@ fn lookup() -> Result<()> {
let human_output = [
(1, format!("- {} {}", HR_OK, &dave_uid)),
(1, format!("{}{} (\"{}\")", HR_PATH, &alice_fpr, &alice_uid)),
(1, format!("{}{} ({})", HR_PATH, &alice_fpr, &alice_uid)),
];
test(
keyring,