mod common;
use common::{
TempDir, create_with_security, generate_identity, import_identity_with_security, kc, kc_ok,
security, security_available, security_ok,
};
use kc::crypto::KeyBlob;
use kc::der;
use kc::{KeychainFile, RecordType, Value};
fn attributes(file: &KeychainFile, record_type: RecordType, number: u32) -> Vec<(String, Value)> {
let relation = file
.schema()
.relation(record_type)
.expect("relation is in the schema");
let record = file
.records_of_type(record_type)
.into_iter()
.find(|record| record.number == number)
.expect("record exists");
relation
.attributes
.iter()
.enumerate()
.filter_map(|(position, attribute)| {
record
.attribute(position)
.map(|value| (attribute.name.clone(), value.clone()))
})
.collect()
}
fn only_record(file: &KeychainFile, record_type: RecordType) -> u32 {
let records = file.records_of_type(record_type);
assert_eq!(
records.len(),
1,
"expected one {} record",
record_type.name()
);
records[0].number
}
#[test]
fn kc_writes_the_records_security_import_writes() {
if !security_available() {
eprintln!("skipping: /usr/bin/security is unavailable");
return;
}
let dir = TempDir::new("identity-compare");
let (certificate, key) = generate_identity(&dir, "id", "kc identity compare");
let theirs = dir.join("theirs.keychain");
create_with_security(&theirs, "pw");
import_identity_with_security(
&dir,
&theirs,
"id",
&certificate,
&key,
"kc identity compare",
);
let ours = dir.join("ours.keychain");
kc_ok(&["create", ours.to_str().expect("utf-8 path")], Some("pw"));
kc_ok(
&[
"add",
"identity",
"--cert",
certificate.to_str().expect("utf-8 path"),
"--key",
key.to_str().expect("utf-8 path"),
ours.to_str().expect("utf-8 path"),
],
Some("pw"),
);
let theirs = KeychainFile::open(&theirs).expect("open the keychain macOS wrote");
let ours = KeychainFile::open(&ours).expect("open the keychain kc wrote");
let their_cert = only_record(&theirs, RecordType::X509_CERTIFICATE);
let our_cert = only_record(&ours, RecordType::X509_CERTIFICATE);
assert_eq!(
attributes(&theirs, RecordType::X509_CERTIFICATE, their_cert),
attributes(&ours, RecordType::X509_CERTIFICATE, our_cert),
);
let certificate_der = std::fs::read(&certificate).expect("read the certificate");
let certificate_der = der::pem_or_der(&certificate_der).expect("decode the certificate");
for file in [&theirs, &ours] {
let record = file.records_of_type(RecordType::X509_CERTIFICATE)[0];
assert_eq!(
record.key_data, certificate_der,
"certificate is stored as-is"
);
assert_eq!(record.unknown3, 0);
}
let their_key = only_record(&theirs, RecordType::PRIVATE_KEY);
let our_key = only_record(&ours, RecordType::PRIVATE_KEY);
assert_eq!(
attributes(&theirs, RecordType::PRIVATE_KEY, their_key),
attributes(&ours, RecordType::PRIVATE_KEY, our_key),
);
let their_record = theirs.records_of_type(RecordType::PRIVATE_KEY)[0];
let our_record = ours.records_of_type(RecordType::PRIVATE_KEY)[0];
assert_eq!(our_record.unknown3, their_record.unknown3);
assert_eq!(our_record.version, their_record.version);
let their_blob = KeyBlob::parse(&their_record.key_data).expect("parse their key blob");
let our_blob = KeyBlob::parse(&our_record.key_data).expect("parse our key blob");
assert_eq!(our_blob.version, their_blob.version);
assert_eq!(our_blob.header, their_blob.header);
assert_eq!(our_blob.wrapped, their_blob.wrapped);
assert_eq!(
our_blob.crypto_blob.len(),
their_blob.crypto_blob.len(),
"the wrapped key is the same size"
);
assert_eq!(
theirs
.keychain()
.tables
.iter()
.map(|table| table.record_type.0)
.collect::<Vec<_>>(),
ours.keychain()
.tables
.iter()
.map(|table| table.record_type.0)
.collect::<Vec<_>>(),
);
let their_table = theirs
.keychain()
.table(RecordType::X509_CERTIFICATE)
.expect("their certificate table");
let our_table = ours
.keychain()
.table(RecordType::X509_CERTIFICATE)
.expect("our certificate table");
assert_eq!(
our_table.indexes, their_table.indexes,
"index regions match"
);
assert_eq!(our_table.unknown_free_list, their_table.unknown_free_list);
}
#[test]
fn security_finds_an_identity_kc_wrote() {
if !security_available() {
eprintln!("skipping: /usr/bin/security is unavailable");
return;
}
let dir = TempDir::new("identity-read");
let (certificate, key) = generate_identity(&dir, "id", "kc identity read");
let path = dir.join("k.keychain");
let as_str = path.to_str().expect("utf-8 path");
kc_ok(&["create", as_str], Some("pw"));
kc_ok(
&[
"add",
"identity",
"--cert",
certificate.to_str().expect("utf-8 path"),
"--key",
key.to_str().expect("utf-8 path"),
as_str,
],
Some("pw"),
);
security_ok(&["unlock-keychain", "-p", "pw", as_str]);
let found = security_ok(&["find-certificate", "-a", "-c", "kc identity read", as_str]);
assert!(found.contains("kc identity read"), "unexpected: {found}");
assert!(found.contains("0x80001000"), "unexpected: {found}");
let identities = security_ok(&["find-identity", as_str]);
assert!(
identities.contains("1 identities found"),
"unexpected: {identities}"
);
assert!(
identities.contains("kc identity read"),
"unexpected: {identities}"
);
let listed = kc_ok(&["find", "identity", as_str], None);
assert!(listed.contains("kc identity read"), "unexpected: {listed}");
}
#[test]
fn the_stored_private_key_is_the_key_that_went_in() {
let dir = TempDir::new("identity-unwrap");
let (certificate, key) = generate_identity(&dir, "id", "kc identity unwrap");
let path = dir.join("k.keychain");
let as_str = path.to_str().expect("utf-8 path");
kc_ok(&["create", as_str], Some("pw"));
kc_ok(
&[
"add",
"identity",
"--cert",
certificate.to_str().expect("utf-8 path"),
"--key",
key.to_str().expect("utf-8 path"),
as_str,
],
Some("pw"),
);
let mut file = KeychainFile::open(&path).expect("open");
file.unlock(b"pw").expect("unlock");
let record = file.records_of_type(RecordType::PRIVATE_KEY)[0];
let blob = KeyBlob::parse(&record.key_data).expect("parse the key blob");
let keys = file
.db_blob()
.expect("the metadata record")
.unlock(b"pw")
.expect("derive the database keys");
let unwrapped =
kc::crypto::unwrap_blob(keys.encryption_key.as_slice(), &blob.iv, &blob.crypto_blob)
.expect("unwrap the private key");
let expected = std::fs::read(&key).expect("read the key");
let expected = der::pem_or_der(&expected).expect("decode the key");
assert_eq!(unwrapped.as_slice(), expected.as_slice());
let certificate_der = std::fs::read(&certificate).expect("read the certificate");
let certificate_der = der::pem_or_der(&certificate_der).expect("decode the certificate");
let parsed = der::Certificate::parse(&certificate_der).expect("parse the certificate");
let label = file
.schema()
.attribute(RecordType::PRIVATE_KEY, record, "Label")
.and_then(Value::as_bytes)
.expect("the key record has a Label");
assert_eq!(label, parsed.public_key_hash());
}
#[test]
fn an_ec_key_is_refused_rather_than_stored_wrongly() {
let dir = TempDir::new("identity-ec");
let path = dir.join("k.keychain");
let as_str = path.to_str().expect("utf-8 path");
kc_ok(&["create", as_str], Some("pw"));
let certificate = dir.join("ec-cert.pem");
let key = dir.join("ec-key.pem");
let generated = std::process::Command::new("/usr/bin/openssl")
.args([
"req",
"-x509",
"-newkey",
"ec",
"-pkeyopt",
"ec_paramgen_curve:prime256v1",
"-nodes",
"-days",
"30",
"-subj",
"/CN=kc identity ec",
])
.arg("-keyout")
.arg(&key)
.arg("-out")
.arg(&certificate)
.output()
.expect("run openssl");
if !generated.status.success() {
eprintln!("skipping: openssl could not generate an EC key");
return;
}
let output = kc(
&[
"add",
"identity",
"--cert",
certificate.to_str().expect("utf-8 path"),
"--key",
key.to_str().expect("utf-8 path"),
as_str,
],
Some("pw"),
);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("only RSA"), "unexpected error: {stderr}");
}
#[test]
fn a_certificate_that_is_not_a_certificate_is_refused() {
let dir = TempDir::new("identity-junk");
let path = dir.join("k.keychain");
let as_str = path.to_str().expect("utf-8 path");
kc_ok(&["create", as_str], Some("pw"));
let junk = dir.join("junk.der");
std::fs::write(&junk, b"not DER at all").expect("write");
let (_, key) = generate_identity(&dir, "id", "kc identity junk");
let output = kc(
&[
"add",
"identity",
"--cert",
junk.to_str().expect("utf-8 path"),
"--key",
key.to_str().expect("utf-8 path"),
as_str,
],
Some("pw"),
);
assert!(!output.status.success());
let file = KeychainFile::open(&path).expect("open");
assert!(
file.keychain()
.table(RecordType::X509_CERTIFICATE)
.is_none()
);
let _ = security(&["delete-keychain", as_str]);
}