use crate::{
certs::{app_manifest_signer, AppLicenseType, Expiring, SignedAppLicense},
crypto::PublicKey,
};
use crate::api::{
licensing::Licensing,
rejections::{ApiError, UnauthorizedReason},
};
use ax_types::AppManifest;
use chrono::Utc;
pub fn validate_signed_manifest(
manifest: &AppManifest,
ax_public_key: &PublicKey,
licensing: &Licensing,
) -> Result<(), ApiError> {
app_manifest_signer::validate(manifest, ax_public_key)
.map_err(|x| ApiError::InvalidManifest { msg: x.to_string() })?;
if licensing.is_node_licensed(ax_public_key)? {
let app_id = manifest.app_id();
let license = licensing
.app_id_license(&app_id)
.ok_or_else(|| ApiError::AppUnauthorized {
app_id: app_id.clone(),
reason: UnauthorizedReason::NoLicense,
})
.and_then(|license_str| {
license_str
.parse::<SignedAppLicense>()
.map_err(|_| ApiError::AppUnauthorized {
app_id: app_id.clone(),
reason: UnauthorizedReason::MalformedLicense,
})
})?;
license.validate(ax_public_key).map_err(|_| ApiError::AppUnauthorized {
app_id,
reason: UnauthorizedReason::InvalidSignature,
})?;
match license.license.license_type {
AppLicenseType::Expiring(Expiring { expires_at, app_id }) => {
if app_id != manifest.app_id() {
Err(ApiError::AppUnauthorized {
app_id,
reason: UnauthorizedReason::WrongSubject,
})
} else if expires_at < Utc::now() {
Err(ApiError::AppUnauthorized {
app_id,
reason: UnauthorizedReason::Expired,
})
} else {
Ok(())
}
}
}
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use crate::api::{licensing::Licensing, rejections::ApiError};
use super::*;
use crate::crypto::{PrivateKey, PublicKey};
use ax_types::{app_id, AppId};
struct TestFixture {
ax_public_key: PublicKey,
signed_manifest: AppManifest,
node_license: String,
expired_node_license: String,
app_license: String,
falsified_app_license: String,
expired_app_license: String,
app_id: AppId,
}
fn setup() -> TestFixture {
let ax_private_key: PrivateKey = "0WBFFicIHbivRZXAlO7tPs7rCX6s7u2OIMJ2mx9nwg0w=".parse().unwrap();
let app_id = app_id!("com.actyx.auth-test");
let serialized_manifest = serde_json::json!({
"appId": app_id,
"displayName": "auth test app",
"version": "v0.0.1",
"signature": "v2tzaWdfdmVyc2lvbgBtZGV2X3NpZ25hdHVyZXhYZ0JGTTgyZVpMWTdJQzhRbmFuVzFYZ0xrZFRQaDN5aCtGeDJlZlVqYm9qWGtUTWhUdFZNRU9BZFJaMVdTSGZyUjZUOHl1NEFKdFN5azhMbkRvTVhlQnc9PWlkZXZQdWJrZXl4LTBuejFZZEh1L0pEbVM2Q0ltY1pnT2o5WTk2MHNKT1ByYlpIQUpPMTA3cVcwPWphcHBEb21haW5zgmtjb20uYWN0eXguKm1jb20uZXhhbXBsZS4qa2F4U2lnbmF0dXJleFg4QmwzekNObm81R2JwS1VvYXRpN0NpRmdyMEtHd05IQjFrVHdCVkt6TzlwelcwN2hGa2tRK0dYdnljOVFhV2hIVDVhWHp6TyttVnJ4M2VpQzdUUkVBUT09/w=="
});
TestFixture {
ax_public_key: ax_private_key.into(),
signed_manifest: serde_json::from_value(serialized_manifest).unwrap(),
node_license: "v25saWNlbnNlVmVyc2lvbgBrbGljZW5zZVR5cGWhaGV4cGlyaW5nomVhcHBJZG5jb20uYWN0eXgubm9kZWlleHBpcmVzQXR0MjA1MC0wMS0wMVQwMDowMDowMFppY3JlYXRlZEF0eB4yMDIyLTAyLTAzVDA3OjE0OjE1LjQ0ODMzMTI4MVppc2lnbmF0dXJleFgvTHgyK1JPVzJaTk1zc2dCK1k4WjFxeVNRbnRFSDRkUm9GRi8zdkVHRFo3Q1pHeXlkdG8zUlBJbStreGd2TkdrM0FMNzM4TSs0UU5oazlvUG5LZjRDZz09aXJlcXVlc3RlcqFlZW1haWxuaW5mb0BhY3R5eC5jb23/".into(),
expired_node_license: "v25saWNlbnNlVmVyc2lvbgBrbGljZW5zZVR5cGWhaGV4cGlyaW5nomVhcHBJZG5jb20uYWN0eXgubm9kZWlleHBpcmVzQXR0MjAyMC0wMS0wMVQwMDowMDowMFppY3JlYXRlZEF0eB4yMDIyLTAyLTAzVDA3OjE4OjUwLjYwMjYxNDY5MFppc2lnbmF0dXJleFh2Zjh0L3RRQkZxcy9OTDN1TEFjWE5senRlVDFueldZazdBN044a3JpOVBQUmtJb0NZOVVpR0JGNGVPenY0cERSREloZXRUZ1gwM2U5UnZ4MWhiR0hEQT09aXJlcXVlc3RlcqFlZW1haWxuaW5mb0BhY3R5eC5jb23/".into(),
app_license: "v25saWNlbnNlVmVyc2lvbgBrbGljZW5zZVR5cGWhaGV4cGlyaW5nomVhcHBJZHNjb20uYWN0eXguYXV0aC10ZXN0aWV4cGlyZXNBdHQyMDUwLTAxLTAxVDAwOjAwOjAwWmljcmVhdGVkQXR4HjIwMjItMDItMDNUMDc6MTY6MzkuMjA4MDQ1NjI0WmlzaWduYXR1cmV4WGphWWlENHQxdmF1ZXNldUtMTDhnRU5BZFpPVlNkcXozTmdGVndqYW96M0x2NzcxZnZZQVk3NitoYW5nN2pCaTV1UXhBQmFsMm91azYxTUZXZ2gxMEJnPT1pcmVxdWVzdGVyoWVlbWFpbG5pbmZvQGFjdHl4LmNvbf8=".into(),
falsified_app_license: "v25saWNlbnNlVmVyc2lvbgBrbGljZW5zZVR5cGWhaGV4cGlyaW5nomVhcHBJZHNjb20uYWN0eXguYXV0aC10ZXN0aWV4cGlyZXNBdHQxOTcxLTAxLTAxVDAwOjAxOjAxWmljcmVhdGVkQXR0MTk3MC0wMS0wMVQwMDowMTowMVppc2lnbmF0dXJleFg1dmEvQ3NYWlk3TUV6VVJ0SUEwVm9mL3R1T3FlejZCN3FYby9JNTl4T0NkUDNwUFVabGZEekZPbExIK09oZXJjWGkwRTJ1RXFnZ2x1cUdyaGFDVVhDZz09aXJlcXVlc3RlcqFlZW1haWx0Y3VzdG9tZXJAZXhhbXBsZS5jb23/".into(),
expired_app_license: "v25saWNlbnNlVmVyc2lvbgBrbGljZW5zZVR5cGWhaGV4cGlyaW5nomVhcHBJZHNjb20uYWN0eXguYXV0aC10ZXN0aWV4cGlyZXNBdHQyMDIwLTAxLTAxVDAwOjAwOjAwWmljcmVhdGVkQXR4HjIwMjItMDItMDNUMDc6MTc6NDkuNzQ0NDM5NTQ2WmlzaWduYXR1cmV4WFU4S0VjYWxOOTliVlpSOU1nL0hwVCsyT3VjR2dNd2NOa2pkV0Q4cmVBOVJnWmRtTWVjaUlXYysybHlnYTJqMG9tS2RpN3RpRDVvTGV2QXBKcXR6dkJRPT1pcmVxdWVzdGVyoWVlbWFpbG5pbmZvQGFjdHl4LmNvbf8=".into(),
app_id,
}
}
#[test]
fn should_succeed_when_node_in_dev_mode() {
let x = setup();
validate_signed_manifest(&x.signed_manifest, &x.ax_public_key, &Licensing::default()).unwrap();
}
#[test]
fn should_succeed_when_node_in_prod_mode() {
let x = setup();
let mut apps = BTreeMap::new();
apps.insert(x.app_id, x.app_license);
validate_signed_manifest(
&x.signed_manifest,
&x.ax_public_key,
&Licensing::new(x.node_license, apps),
)
.unwrap();
}
#[test]
fn should_fail_when_node_in_prod_mode_without_app_license() {
let x = setup();
let result = validate_signed_manifest(
&x.signed_manifest,
&x.ax_public_key,
&Licensing::new(x.node_license, BTreeMap::default()),
)
.unwrap_err();
assert_eq!(
result,
ApiError::AppUnauthorized {
app_id: x.app_id,
reason: UnauthorizedReason::NoLicense
}
);
}
#[test]
fn should_fail_when_node_in_prod_mode_with_falsified_app_license() {
let x = setup();
let mut apps = BTreeMap::new();
apps.insert(x.app_id.clone(), x.falsified_app_license);
let result = validate_signed_manifest(
&x.signed_manifest,
&x.ax_public_key,
&Licensing::new(x.node_license, apps),
)
.unwrap_err();
assert_eq!(
result,
ApiError::AppUnauthorized {
app_id: x.app_id,
reason: UnauthorizedReason::InvalidSignature
}
);
}
#[test]
fn should_fail_when_node_in_prod_mode_with_malformed_app_license() {
let x = setup();
let mut apps = BTreeMap::new();
apps.insert(x.app_id.clone(), "malformed license".to_owned());
let result = validate_signed_manifest(
&x.signed_manifest,
&x.ax_public_key,
&Licensing::new(x.node_license, apps),
)
.unwrap_err();
assert_eq!(
result,
ApiError::AppUnauthorized {
app_id: x.app_id,
reason: UnauthorizedReason::MalformedLicense
}
);
}
#[test]
fn should_fail_when_node_in_prod_mode_with_expired_app_license() {
let x = setup();
let mut apps = BTreeMap::new();
apps.insert(x.app_id.clone(), x.expired_app_license);
let result = validate_signed_manifest(
&x.signed_manifest,
&x.ax_public_key,
&Licensing::new(x.node_license, apps),
)
.unwrap_err();
assert_eq!(
result,
ApiError::AppUnauthorized {
app_id: x.app_id,
reason: UnauthorizedReason::Expired
}
);
}
#[test]
fn should_fail_when_node_in_prod_mode_with_falsified_node_license() {
let x = setup();
let mut apps = BTreeMap::new();
apps.insert(x.app_id.clone(), x.app_license);
let mut node_license = base64::decode(&x.node_license).unwrap();
for i in 0..node_license.len() - 3 {
if &node_license[i..i + 4] == b"2050" {
node_license[i..i + 4].copy_from_slice(b"2049");
}
}
let node_license = base64::encode(node_license);
assert_ne!(x.node_license, node_license);
let result = validate_signed_manifest(
&x.signed_manifest,
&x.ax_public_key,
&Licensing::new(node_license, apps),
)
.unwrap_err();
assert_eq!(
result,
ApiError::NodeUnauthorized {
reason: UnauthorizedReason::InvalidSignature
}
);
}
#[test]
fn should_fail_when_node_in_prod_mode_with_malformed_node_license() {
let x = setup();
let mut apps = BTreeMap::new();
apps.insert(x.app_id.clone(), x.app_license);
let result = validate_signed_manifest(
&x.signed_manifest,
&x.ax_public_key,
&Licensing::new("malformed".into(), apps),
)
.unwrap_err();
assert_eq!(
result,
ApiError::NodeUnauthorized {
reason: UnauthorizedReason::MalformedLicense
}
);
}
#[test]
fn should_fail_when_node_in_prod_mode_with_expired_node_license() {
let x = setup();
let mut apps = BTreeMap::new();
apps.insert(x.app_id.clone(), x.app_license);
let result = validate_signed_manifest(
&x.signed_manifest,
&x.ax_public_key,
&Licensing::new(x.expired_node_license, apps),
)
.unwrap_err();
assert_eq!(
result,
ApiError::NodeUnauthorized {
reason: UnauthorizedReason::Expired
}
);
}
#[test]
fn should_fail_when_ax_public_key_is_wrong() {
let x = setup();
let result = validate_signed_manifest(
&x.signed_manifest,
&PrivateKey::generate().into(),
&Licensing::default(),
)
.unwrap_err();
assert!(
matches!(result, ApiError::InvalidManifest { msg} if msg == "Failed to validate developer certificate. Invalid signature for provided input.")
);
}
}