Ctrl K
ComunidadGuías…

src/update_metadata.rs

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
use hbb_common::{
    anyhow::{anyhow, Context},
    base64::{engine::general_purpose::STANDARD, Engine as _},
    sodiumoxide::{
        self,
        crypto::sign::{verify_detached, PublicKey, Signature},
    },
    ResultType,
};
use serde_derive::Deserialize;
use std::sync::OnceLock;

mod artifact;

#[cfg(target_os = "macos")]
mod offline;

use artifact::{validate_artifact, validate_artifact_file_name, ArtifactUrlPolicy};

#[cfg(target_os = "macos")]
pub(crate) use offline::{verify_offline_update_metadata, OfflineUpdateMetadataRequirements};

const UPDATE_METADATA_SIGNATURE_CONTEXT: &[u8] = b"RustDesk update metadata v1\n";
const UPDATE_APP: &str = "rustdesk";
pub(crate) const PUBLIC_UPDATE_PACKAGE_ID: &str = "rustdesk";
const METADATA_SCHEMA_VERSION: u32 = 1;
const SIGNATURE_SCHEMA_VERSION: u32 = 1;
const SIGNATURE_ALGORITHM: &str = "ed25519";

const TRUSTED_UPDATE_KEYS: &[TrustedUpdateKey] = &[TrustedUpdateKey {
    key_id: "2026-ed25519-main",
    algorithm: SIGNATURE_ALGORITHM,
    public_key: [
        52, 214, 32, 65, 191, 134, 137, 75, 157, 74, 67, 221, 150, 161, 118, 215, 218, 35, 236,
        156, 127, 120, 133, 156, 197, 141, 37, 22, 47, 106, 181, 177,
    ],
}];

#[derive(Deserialize)]
struct UpdateMetadata {
    schema_version: u32,
    app: String,
    package_id: String,
    version: String,
    release_id: String,
    #[serde(rename = "published_at")]
    _published_at: String,
    signature_key_id: String,
    artifacts: Vec<UpdateArtifact>,
}

#[derive(Deserialize)]
struct UpdateArtifact {
    platform: String,
    arch: String,
    format: String,
    url: String,
    file_name: String,
    size: u64,
    sha256: String,
}

#[derive(Deserialize)]
struct UpdateSignature {
    schema_version: u32,
    algorithm: String,
    key_id: String,
    signature: String,
}

struct TrustedUpdateKey {
    key_id: &'static str,
    algorithm: &'static str,
    public_key: [u8; 32],
}

#[derive(Clone, Copy)]
pub(crate) struct UpdateArtifactQuery<'a> {
    pub platform: &'a str,
    pub arch: &'a str,
    pub format: &'a str,
}

#[derive(Clone, Copy)]
pub(crate) struct UpdateMetadataRequirements<'a> {
    pub expected_version: &'a str,
    pub expected_release_id: &'a str,
    pub expected_artifact_url_prefix: &'a str,
    pub artifact: UpdateArtifactQuery<'a>,
}

#[derive(Clone)]
pub(crate) struct VerifiedUpdateArtifact {
    pub version: String,
    pub url: String,
    #[cfg(target_os = "macos")]
    pub file_name: String,
    pub size: u64,
    pub sha256: String,
}

struct VerificationOptions<'a> {
    requirements: MetadataRequirements<'a>,
    expected_package_id: &'a str,
    artifact_url_policy: ArtifactUrlPolicy<'a>,
    trusted_keys: &'a [TrustedUpdateKey],
}

#[derive(Clone, Copy)]
struct MetadataRequirements<'a> {
    expected_version: &'a str,
    expected_release_id: &'a str,
    artifact: UpdateArtifactQuery<'a>,
}

fn verify_update_metadata_with_options(
    metadata_bytes: &[u8],
    signature_bytes: &[u8],
    options: VerificationOptions<'_>,
) -> ResultType<VerifiedUpdateArtifact> {
    let update_signature: UpdateSignature =
        serde_json::from_slice(signature_bytes).context("invalid update signature JSON")?;
    validate_signature_schema(&update_signature)?;
    let signature = decode_signature(&update_signature.signature)?;
    let trusted_key = options
        .trusted_keys
        .iter()
        .find(|key| {
            key.key_id == update_signature.key_id && key.algorithm == update_signature.algorithm
        })
        .ok_or_else(|| anyhow!("unsupported update signature key"))?;
    verify_metadata_signature(metadata_bytes, &signature, trusted_key)?;
    let metadata: UpdateMetadata =
        serde_json::from_slice(metadata_bytes).context("invalid update metadata JSON")?;
    validate_metadata(&metadata, &update_signature, &options)?;
    let artifact = select_artifact(&metadata, &options.requirements.artifact)?;
    validate_artifact(artifact, options.artifact_url_policy)?;
    Ok(VerifiedUpdateArtifact {
        version: metadata.version.clone(),
        url: artifact.url.clone(),
        #[cfg(target_os = "macos")]
        file_name: artifact.file_name.clone(),
        size: artifact.size,
        sha256: artifact.sha256.clone(),
    })
}

pub(crate) fn verify_update_metadata(
    metadata_bytes: &[u8],
    signature_bytes: &[u8],
    requirements: UpdateMetadataRequirements<'_>,
) -> ResultType<VerifiedUpdateArtifact> {
    let artifact_url_policy =
        ArtifactUrlPolicy::ExactPrefix(requirements.expected_artifact_url_prefix);
    let metadata_requirements = MetadataRequirements {
        expected_version: requirements.expected_version,
        expected_release_id: requirements.expected_release_id,
        artifact: requirements.artifact,
    };
    verify_update_metadata_with_options(
        metadata_bytes,
        signature_bytes,
        VerificationOptions {
            requirements: metadata_requirements,
            expected_package_id: PUBLIC_UPDATE_PACKAGE_ID,
            artifact_url_policy,
            trusted_keys: TRUSTED_UPDATE_KEYS,
        },
    )
}

fn validate_signature_schema(signature: &UpdateSignature) -> ResultType<()> {
    if signature.schema_version != SIGNATURE_SCHEMA_VERSION {
        return Err(anyhow!("unsupported update signature schema version"));
    }
    if signature.algorithm != SIGNATURE_ALGORITHM {
        return Err(anyhow!("unsupported update signature algorithm"));
    }
    Ok(())
}

fn decode_signature(encoded: &str) -> ResultType<Signature> {
    let decoded = STANDARD
        .decode(encoded)
        .context("invalid update signature base64")?;
    if decoded.len() != 64 || STANDARD.encode(&decoded) != encoded {
        return Err(anyhow!("invalid update signature length or encoding"));
    }
    let mut signature = [0u8; 64];
    signature.copy_from_slice(&decoded);
    Signature::from_bytes(&signature).map_err(|_| anyhow!("invalid update signature bytes"))
}

fn verify_metadata_signature(
    metadata_bytes: &[u8],
    signature: &Signature,
    trusted_key: &TrustedUpdateKey,
) -> ResultType<()> {
    init_sodiumoxide()?;
    let mut signed_bytes =
        Vec::with_capacity(UPDATE_METADATA_SIGNATURE_CONTEXT.len() + metadata_bytes.len());
    signed_bytes.extend_from_slice(UPDATE_METADATA_SIGNATURE_CONTEXT);
    signed_bytes.extend_from_slice(metadata_bytes);
    if !verify_detached(signature, &signed_bytes, &PublicKey(trusted_key.public_key)) {
        return Err(anyhow!("invalid update metadata signature"));
    }
    Ok(())
}

fn init_sodiumoxide() -> ResultType<()> {
    static INIT: OnceLock<Result<(), String>> = OnceLock::new();
    let result = INIT.get_or_init(|| {
        sodiumoxide::init().map_err(|_| "failed to initialize sodiumoxide".to_string())
    });
    match result {
        Ok(()) => Ok(()),
        Err(err) => Err(anyhow!(err.clone())),
    }
}

fn validate_metadata(
    metadata: &UpdateMetadata,
    signature: &UpdateSignature,
    options: &VerificationOptions<'_>,
) -> ResultType<()> {
    if metadata.schema_version != METADATA_SCHEMA_VERSION {
        return Err(anyhow!("unsupported update metadata schema version"));
    }
    if metadata.signature_key_id != signature.key_id {
        return Err(anyhow!("update metadata signature key id mismatch"));
    }
    if metadata.app != UPDATE_APP {
        return Err(anyhow!("update metadata app mismatch"));
    }
    if metadata.package_id != options.expected_package_id {
        return Err(anyhow!("update metadata package id is not allowed"));
    }
    if options.requirements.expected_version != metadata.version {
        return Err(anyhow!("update metadata version mismatch"));
    }
    if options.requirements.expected_release_id != metadata.release_id {
        return Err(anyhow!("update metadata release id mismatch"));
    }
    for artifact in &metadata.artifacts {
        validate_artifact_file_name(&artifact.file_name)?;
    }
    Ok(())
}

fn select_artifact<'a>(
    metadata: &'a UpdateMetadata,
    query: &UpdateArtifactQuery<'_>,
) -> ResultType<&'a UpdateArtifact> {
    let mut matches = metadata.artifacts.iter().filter(|artifact| {
        artifact.platform == query.platform
            && artifact.arch == query.arch
            && artifact.format == query.format
    });
    let artifact = matches
        .next()
        .ok_or_else(|| anyhow!("matching update artifact not found"))?;
    if matches.next().is_some() {
        return Err(anyhow!("multiple matching update artifacts found"));
    }
    Ok(artifact)
}

#[cfg(target_os = "macos")]
#[cfg(test)]
#[path = "update_metadata_tests.rs"]
mod tests;