use ed25519_dalek::{Signer, SigningKey};
use crate::{
signature_domain_key, signed_payload_acl_field, signed_payload_bundle_record,
signed_payload_extension, signed_payload_optional_repo_key_field, signed_payload_ref_entry,
signed_payload_ref_list_field, signed_payload_string_field, AclState, CommitHash,
ExtensionEntry, ObjectBundle, ObjectBundleRecord, RefEntry, RefName, RepoKey, RepoParams,
Signature, SignedField,
};
pub fn sign_string_field(
params: &RepoParams,
key: &SigningKey,
field_name: &str,
value: String,
update_seq: u64,
) -> SignedField<String> {
let repo_key = signature_domain_key(params, &key.verifying_key().to_bytes());
let payload = signed_payload_string_field(&repo_key, field_name, &value, update_seq);
let sig = sign_to_array(key, &payload);
SignedField {
value,
update_seq,
signature: sig,
}
}
pub fn sign_ref_list_field(
params: &RepoParams,
key: &SigningKey,
field_name: &str,
value: Vec<RefName>,
update_seq: u64,
) -> SignedField<Vec<RefName>> {
let repo_key = signature_domain_key(params, &key.verifying_key().to_bytes());
let payload = signed_payload_ref_list_field(&repo_key, field_name, &value, update_seq);
let sig = sign_to_array(key, &payload);
SignedField {
value,
update_seq,
signature: sig,
}
}
pub fn sign_acl_field(
params: &RepoParams,
key: &SigningKey,
field_name: &str,
value: AclState,
update_seq: u64,
) -> SignedField<AclState> {
let repo_key = signature_domain_key(params, &key.verifying_key().to_bytes());
let payload = signed_payload_acl_field(&repo_key, field_name, &value, update_seq);
let sig = sign_to_array(key, &payload);
SignedField {
value,
update_seq,
signature: sig,
}
}
pub fn sign_optional_repo_key_field(
params: &RepoParams,
key: &SigningKey,
field_name: &str,
value: Option<RepoKey>,
update_seq: u64,
) -> SignedField<Option<RepoKey>> {
let repo_key = signature_domain_key(params, &key.verifying_key().to_bytes());
let payload =
signed_payload_optional_repo_key_field(&repo_key, field_name, value.as_ref(), update_seq);
let sig = sign_to_array(key, &payload);
SignedField {
value,
update_seq,
signature: sig,
}
}
pub fn sign_ref_entry(
params: &RepoParams,
key: &SigningKey,
ref_name: &str,
target: CommitHash,
update_seq: u64,
auth_epoch: u64,
) -> RefEntry {
let repo_key = signature_domain_key(params, &key.verifying_key().to_bytes());
let payload = signed_payload_ref_entry(&repo_key, ref_name, &target, update_seq, auth_epoch);
let sig = sign_to_array(key, &payload);
RefEntry {
target,
update_seq,
updater: key.verifying_key().to_bytes(),
auth_epoch,
signature: sig,
}
}
pub fn sign_bundle_record(
params: &RepoParams,
key: &SigningKey,
bundle: ObjectBundle,
auth_epoch: u64,
) -> ObjectBundleRecord {
let repo_key = signature_domain_key(params, &key.verifying_key().to_bytes());
let payload = signed_payload_bundle_record(&repo_key, &bundle, auth_epoch);
let sig = sign_to_array(key, &payload);
ObjectBundleRecord {
bundle,
added_by: key.verifying_key().to_bytes(),
auth_epoch,
signature: sig,
}
}
pub const BUNDLE_TIP_EXTENSION_PREFIX: &str = "bundle-tip:";
pub fn bundle_tip_extension_key(bundle_id: &crate::ObjectBundleId) -> String {
let mut s = String::with_capacity(BUNDLE_TIP_EXTENSION_PREFIX.len() + 64);
s.push_str(BUNDLE_TIP_EXTENSION_PREFIX);
for b in bundle_id.iter() {
s.push_str(&format!("{b:02x}"));
}
s
}
pub fn sign_bundle_tip_extension(
params: &RepoParams,
key: &SigningKey,
bundle_id: &crate::ObjectBundleId,
tip: &CommitHash,
update_seq: u64,
) -> (String, ExtensionEntry) {
let ext_key = bundle_tip_extension_key(bundle_id);
let entry = sign_extension(params, key, &ext_key, tip.to_vec(), update_seq);
(ext_key, entry)
}
pub fn parse_bundle_tip_extension_key(ext_key: &str) -> Option<crate::ObjectBundleId> {
let hex = ext_key.strip_prefix(BUNDLE_TIP_EXTENSION_PREFIX)?;
if hex.len() != 64 {
return None;
}
let mut out = [0u8; 32];
for (i, byte) in out.iter_mut().enumerate() {
let s = hex.get(i * 2..i * 2 + 2)?;
*byte = u8::from_str_radix(s, 16).ok()?;
}
Some(out)
}
pub fn sign_extension(
params: &RepoParams,
key: &SigningKey,
ext_key: &str,
value: Vec<u8>,
update_seq: u64,
) -> ExtensionEntry {
let repo_key = signature_domain_key(params, &key.verifying_key().to_bytes());
let payload = signed_payload_extension(&repo_key, ext_key, &value, update_seq);
let sig = sign_to_array(key, &payload);
ExtensionEntry {
value,
update_seq,
signature: sig,
}
}
fn sign_to_array(key: &SigningKey, payload: &[u8]) -> Signature {
key.sign(payload).to_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::SigningKey;
fn fixed_signing_key() -> SigningKey {
SigningKey::from_bytes(&[0x42; 32])
}
#[test]
fn bundle_tip_key_format_is_prefix_plus_hex() {
let bundle_id: crate::ObjectBundleId = [
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
0x1c, 0x1d, 0x1e, 0x1f,
];
let key = bundle_tip_extension_key(&bundle_id);
assert_eq!(
key,
"bundle-tip:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
);
}
#[test]
fn parse_bundle_tip_round_trip() {
let bundle_id: crate::ObjectBundleId = [0xab; 32];
let key = bundle_tip_extension_key(&bundle_id);
let parsed = parse_bundle_tip_extension_key(&key)
.expect("key produced by bundle_tip_extension_key must round-trip");
assert_eq!(parsed, bundle_id);
}
#[test]
fn parse_bundle_tip_rejects_unknown_prefix() {
assert!(parse_bundle_tip_extension_key(
"name:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
)
.is_none());
assert!(parse_bundle_tip_extension_key("plain-string").is_none());
assert!(parse_bundle_tip_extension_key("").is_none());
}
#[test]
fn parse_bundle_tip_rejects_wrong_length_hex() {
let key = "bundle-tip:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1";
assert!(parse_bundle_tip_extension_key(key).is_none());
let key = "bundle-tip:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1faa";
assert!(parse_bundle_tip_extension_key(key).is_none());
}
#[test]
fn parse_bundle_tip_rejects_non_hex() {
let key = "bundle-tip:zz0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
assert!(parse_bundle_tip_extension_key(key).is_none());
}
#[test]
fn sign_bundle_tip_extension_produces_state_that_validates() {
use crate::{validate_state, RepoParams, RepoState};
let key = fixed_signing_key();
let owner_pk = key.verifying_key().to_bytes();
let prefix = bs58::encode(&owner_pk).into_string()[..12].to_string();
let params = RepoParams { prefix };
let bundle_id: crate::ObjectBundleId = [0x33; 32];
let tip: CommitHash = [0x77; 20];
let (ext_key, entry) = sign_bundle_tip_extension(¶ms, &key, &bundle_id, &tip, 0);
assert!(ext_key.starts_with(BUNDLE_TIP_EXTENSION_PREFIX));
assert_eq!(entry.value, tip.to_vec());
assert_eq!(entry.update_seq, 0);
let mut state = RepoState {
owner: owner_pk,
..Default::default()
};
state.extensions.insert(ext_key, entry);
validate_state(¶ms, &state).expect("freshly-signed bundle-tip extension must validate");
}
#[test]
fn bundle_tip_key_under_extension_size_limit() {
let bundle_id: crate::ObjectBundleId = [0xff; 32];
let key = bundle_tip_extension_key(&bundle_id);
assert_eq!(key.len(), BUNDLE_TIP_EXTENSION_PREFIX.len() + 64);
assert!(key.len() < crate::limits::MAX_EXTENSION_KEY_BYTES);
}
}