add new proxmox-subscription crate

taking over slighlty generified helpers and types:
- subscription info and status
- checking subscription status with shop
- reading/writing local subscription-related files

the perl-based code uses base64 with newlines for the data, and base64
without padding for the checksum. accordingly, calculate the checksum
with and without newlines, and compare the decoded checksum instead of
the encoded one.

furthermore, the perl-based code encodes the subscription status using
Capitalized values instead of lowercase, so alias those for the time
being.

PVE also stores the serverid as 'validdirectory', so add that as alias
as well.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
This commit is contained in:
Fabian Grünbichler 2022-06-21 13:49:14 +02:00
parent 6e4a43d683
commit 4ec9a8183d
11 changed files with 764 additions and 0 deletions

View File

@ -14,6 +14,7 @@ members = [
"proxmox-shared-memory",
"proxmox-section-config",
"proxmox-sortable-macro",
"proxmox-subscription",
"proxmox-sys",
"proxmox-tfa",
"proxmox-time",

View File

@ -14,6 +14,7 @@ CRATES = \
proxmox-shared-memory \
proxmox-section-config \
proxmox-sortable-macro \
proxmox-subscription \
proxmox-sys \
proxmox-tfa \
proxmox-time \

View File

@ -0,0 +1,30 @@
[package]
name = "proxmox-subscription"
version = "0.1.0"
authors = ["Proxmox Support Team <support@proxmox.com>"]
edition = "2018"
license = "AGPL-3"
description = "Proxmox subscription utilitites"
exclude = [ "debian" ]
[dependencies]
anyhow = "1.0"
base64 = "0.13"
hex = "0.4"
lazy_static = "1.4"
openssl = "0.10"
regex = "1.5"
serde = "1.0"
serde_json = "1.0"
proxmox-http = { path = "../proxmox-http", version = "0.6.3", default_features = false, features = ["client-trait", "http-helpers"] }
proxmox-serde = { path = "../proxmox-serde", version = "0.1.1", features = ["serde_json"]}
proxmox-sys = "0.3"
proxmox-time = "1.1"
proxmox-schema = { path = "../proxmox-schema", version = "1.3.3", features = ["api-macro"], optional = true }
[features]
default = []
api-types = ["proxmox-schema"]

View File

@ -0,0 +1,7 @@
rust-proxmox-subscription (0.1.0-1) unstable; urgency=medium
* initial package
* extend subscription code to handle signed SubscriptionInfo
-- Proxmox Support Team <support@proxmox.com> Mon, 20 Jun 2022 16:26:02 +0200

View File

@ -0,0 +1,16 @@
Copyright (C) 2022 Proxmox Server Solutions GmbH
This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@ -0,0 +1,7 @@
overlay = "."
crate_src_path = ".."
maintainer = "Proxmox Support Team <support@proxmox.com>"
[source]
vcs_git = "git://git.proxmox.com/git/proxmox.git"
vcs_browser = "https://git.proxmox.com/?p=proxmox.git"

View File

@ -0,0 +1,178 @@
use anyhow::{bail, format_err, Error};
use lazy_static::lazy_static;
use regex::Regex;
use serde_json::json;
use proxmox_http::{uri::json_object_to_query, HttpClient};
use crate::{
subscription_info::{md5sum, SHARED_KEY_DATA},
SubscriptionInfo, SubscriptionStatus,
};
lazy_static! {
static ref ATTR_RE: Regex = Regex::new(r"<([^>]+)>([^<]+)</[^>]+>").unwrap();
}
const SHOP_URI: &str = "https://shop.proxmox.com/modules/servers/licensing/verify.php";
/// (Re)-register a subscription key with the WHMCS server.
fn register_subscription<C: HttpClient<String>>(
key: &str,
server_id: &str,
checktime: i64,
client: C,
) -> Result<(String, String), Error> {
// WHCMS sample code feeds the key into this, but it's just a challenge, so keep it simple
let rand = hex::encode(&proxmox_sys::linux::random_data(16)?);
let challenge = format!("{}{}", checktime, rand);
let params = json!({
"licensekey": key,
"dir": server_id,
"domain": "www.proxmox.com",
"ip": "localhost",
"check_token": challenge,
});
let query = json_object_to_query(params)?;
let response = client.post(
SHOP_URI,
Some(&query),
Some("application/x-www-form-urlencoded"),
)?;
let body = response.into_body();
Ok((body, challenge))
}
fn parse_status(value: &str) -> SubscriptionStatus {
match value.to_lowercase().as_str() {
"active" => SubscriptionStatus::ACTIVE,
"new" => SubscriptionStatus::NEW,
"notfound" => SubscriptionStatus::NOTFOUND,
"invalid" => SubscriptionStatus::INVALID,
_ => SubscriptionStatus::INVALID,
}
}
fn parse_register_response(
body: &str,
key: String,
server_id: String,
checktime: i64,
challenge: &str,
product_url: String,
) -> Result<SubscriptionInfo, Error> {
let mut info = SubscriptionInfo {
key: Some(key),
status: SubscriptionStatus::NOTFOUND,
checktime: Some(checktime),
url: Some(product_url),
..Default::default()
};
let mut md5hash = String::new();
let is_server_id = |id: &&str| *id == server_id;
for caps in ATTR_RE.captures_iter(body) {
let (key, value) = (&caps[1], &caps[2]);
match key {
"status" => info.status = parse_status(value),
"productname" => info.productname = Some(value.into()),
"regdate" => info.regdate = Some(value.into()),
"nextduedate" => info.nextduedate = Some(value.into()),
"message" if value == "Directory Invalid" => {
info.message = Some("Invalid Server ID".into())
}
"message" => info.message = Some(value.into()),
"validdirectory" => {
if value.split(',').find(is_server_id) == None {
bail!("Server ID does not match");
}
info.serverid = Some(server_id.to_owned());
}
"md5hash" => md5hash = value.to_owned(),
_ => (),
}
}
if let SubscriptionStatus::ACTIVE = info.status {
let response_raw = format!("{}{}", SHARED_KEY_DATA, challenge);
let expected = hex::encode(md5sum(response_raw.as_bytes())?);
if expected != md5hash {
bail!(
"Subscription API challenge failed, expected {} != got {}",
expected,
md5hash
);
}
}
Ok(info)
}
#[test]
fn test_parse_register_response() -> Result<(), Error> {
let response = r#"
<status>Active</status>
<companyname>Proxmox</companyname>
<serviceid>41108</serviceid>
<productid>71</productid>
<productname>Proxmox Backup Server Test Subscription -1 year</productname>
<regdate>2020-09-19 00:00:00</regdate>
<nextduedate>2021-09-19</nextduedate>
<billingcycle>Annually</billingcycle>
<validdomain>proxmox.com,www.proxmox.com</validdomain>
<validdirectory>830000000123456789ABCDEF00000042</validdirectory>
<customfields>Notes=Test Key!</customfields>
<addons></addons>
<md5hash>969f4df84fe157ee4f5a2f71950ad154</md5hash>
"#;
let key = "pbst-123456789a".to_string();
let server_id = "830000000123456789ABCDEF00000042".to_string();
let checktime = 1600000000;
let salt = "cf44486bddb6ad0145732642c45b2957";
let info = parse_register_response(
response,
key.to_owned(),
server_id.to_owned(),
checktime,
salt,
"https://www.proxmox.com/en/proxmox-backup-server/pricing".to_string(),
)?;
assert_eq!(
info,
SubscriptionInfo {
key: Some(key),
serverid: Some(server_id),
status: SubscriptionStatus::ACTIVE,
checktime: Some(checktime),
url: Some("https://www.proxmox.com/en/proxmox-backup-server/pricing".into()),
message: None,
nextduedate: Some("2021-09-19".into()),
regdate: Some("2020-09-19 00:00:00".into()),
productname: Some("Proxmox Backup Server Test Subscription -1 year".into()),
signature: None,
}
);
Ok(())
}
/// Queries the WHMCS server to register/update the subscription key information, parsing the response into a [SubscriptionInfo].
pub fn check_subscription<C: HttpClient<String>>(
key: String,
server_id: String,
product_url: String,
http_client: C,
) -> Result<SubscriptionInfo, Error> {
let now = proxmox_time::epoch_i64();
let (response, challenge) = register_subscription(&key, &server_id, now, http_client)
.map_err(|err| format_err!("Error checking subscription: {}", err))?;
parse_register_response(&response, key, server_id, now, &challenge, product_url)
.map_err(|err| format_err!("Error parsing subscription check response: {}", err))
}

View File

@ -0,0 +1,206 @@
use std::path::Path;
use anyhow::{format_err, Error};
use proxmox_sys::fs::{replace_file, CreateOptions};
use crate::{
subscription_info::{md5sum, SHARED_KEY_DATA},
SubscriptionInfo, SubscriptionStatus,
};
fn parse_subscription_file(raw: &str) -> Result<Option<SubscriptionInfo>, Error> {
let mut cfg = raw.lines();
// first line is key in plain
let key = if let Some(key) = cfg.next() {
key
} else {
return Ok(None);
};
// second line is checksum of encoded data
let checksum = if let Some(csum) = cfg.next() {
base64::decode(csum)?
} else {
return Ok(None);
};
// TODO convert to simple collect with PVE 8.0
let encoded_with_newlines = cfg.fold(String::new(), |mut s, line| {
s.push_str(line);
s.push('\n');
s
});
let mut encoded = encoded_with_newlines.clone();
encoded.retain(|c| c != '\n');
let decoded = base64::decode(&encoded)?;
let decoded = std::str::from_utf8(&decoded)?;
let info: SubscriptionInfo = serde_json::from_str(decoded)?;
let calc_csum = |encoded: &str| {
let csum = format!(
"{}{}{}",
info.checktime.unwrap_or(0),
encoded,
SHARED_KEY_DATA,
);
md5sum(csum.as_bytes())
};
// TODO drop PVE compat csum with PVE 8.0
let pve_csum = calc_csum(&encoded_with_newlines)?;
let pbs_csum = calc_csum(&encoded)?;
if checksum != pbs_csum.as_ref() && checksum != pve_csum.as_ref() {
return Ok(Some(SubscriptionInfo {
status: SubscriptionStatus::INVALID,
message: Some("checksum mismatch".to_string()),
..info
}));
}
match info.key {
Some(ref info_key) if info_key != key => {
return Ok(Some(SubscriptionInfo {
status: SubscriptionStatus::INVALID,
message: Some("subscription key mismatch".to_string()),
..info
}))
}
_ => {}
}
Ok(Some(info))
}
/// Reads in subscription information and does a basic integrity verification.
///
/// The expected format consists of three lines:
/// - subscription key
/// - checksum of encoded data
/// - encoded data
///
/// Legacy variants of this format as used by older versions of PVE/PMG are supported.
pub fn read_subscription<P: AsRef<Path>>(path: P) -> Result<Option<SubscriptionInfo>, Error> {
match proxmox_sys::fs::file_read_optional_string(path)? {
Some(raw) => {
let mut info = parse_subscription_file(&raw)?;
info.as_mut().map(|info| info.check_age(false));
Ok(info)
}
None => Ok(None),
}
}
/// Writes out subscription status in the format parsed by [`read_subscription`].
pub fn write_subscription<P: AsRef<Path>>(
path: P,
file_opts: CreateOptions,
info: &SubscriptionInfo,
) -> Result<(), Error> {
let raw = if info.key == None || info.checktime == None {
String::new()
} else if let SubscriptionStatus::NEW = info.status {
format!("{}\n", info.key.as_ref().unwrap())
} else {
let encoded = base64::encode(serde_json::to_string(&info)?);
let csum = format!(
"{}{}{}",
info.checktime.unwrap_or(0),
encoded,
SHARED_KEY_DATA
);
let csum = base64::encode(md5sum(csum.as_bytes())?);
format!("{}\n{}\n{}\n", info.key.as_ref().unwrap(), csum, encoded)
};
replace_file(path, raw.as_bytes(), file_opts, true)?;
Ok(())
}
/// Deletes the subscription info file.
pub fn delete_subscription<P: AsRef<Path>>(path: P) -> Result<(), Error> {
std::fs::remove_file(path)?;
Ok(())
}
/// Updates apt authentication config for repo access.
pub fn update_apt_auth<P: AsRef<Path>>(
path: P,
file_opts: CreateOptions,
url: &str,
key: Option<String>,
password: Option<String>,
) -> Result<(), Error> {
match (key, password) {
(Some(key), Some(password)) => {
let conf = format!("machine {url}\n login {}\n password {}\n", key, password,);
// we use a namespaced .conf file, so just overwrite..
replace_file(path, conf.as_bytes(), file_opts, true)
.map_err(|e| format_err!("Error saving apt auth config - {}", e))?;
}
_ => match std::fs::remove_file(path) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), // ignore not existing
Err(err) => Err(err),
}
.map_err(|e| format_err!("Error clearing apt auth config - {}", e))?,
}
Ok(())
}
#[test]
fn test_pve_compat() {
// generated with PVE::Subscription::write_subscription() based on a real test subscription
let content = "pve4t-123456789a\nNx5qaBSAwkhF/o39/zPAeA\neyJrZXkiOiJwdmU0dC0xMjM0NTY3ODlhIiwibmV4dGR1ZWRhdGUiOiIwMDAwLTAwLTAwIiwic3Rh\ndHVzIjoiQWN0aXZlIiwidmFsaWRkaXJlY3RvcnkiOiI4MzAwMDAwMDAxMjM0NTY3ODlBQkNERUYw\nMDAwMDA0MiIsImNoZWNrdGltZSI6MTYwMDAwMDAwMCwicHJvZHVjdG5hbWUiOiJQcm94bW94IFZF\nIEZyZWUgVHJpYWwgU3Vic2NyaXB0aW9uIDEyIE1vbnRocyAoNCBDUFVzKSIsInJlZ2RhdGUiOiIy\nMDIyLTA0LTA3IDAwOjAwOjAwIn0=";
let expected = SubscriptionInfo {
status: SubscriptionStatus::ACTIVE,
serverid: Some("830000000123456789ABCDEF00000042".to_string()),
checktime: Some(1600000000),
key: Some("pve4t-123456789a".to_string()),
message: None,
productname: Some("Proxmox VE Free Trial Subscription 12 Months (4 CPUs)".to_string()),
regdate: Some("2022-04-07 00:00:00".to_string()),
nextduedate: Some("0000-00-00".to_string()),
url: None,
signature: None,
};
let parsed = parse_subscription_file(content);
assert!(parsed.is_ok());
let parsed = parsed.unwrap();
assert!(parsed.is_some());
let parsed = parsed.unwrap();
assert_eq!(parsed, expected);
}
#[test]
fn test_pbs_compat() {
let content = "pbst-123456789a\n//6dnM9V6nNmSh2GbQfZDA==\neyJzdGF0dXMiOiJhY3RpdmUiLCJzZXJ2ZXJpZCI6IjgzMDAwMDAwMDEyMzQ1Njc4OUFCQ0RFRjAwMDAwMDQyIiwiY2hlY2t0aW1lIjoxNjAwMDAwMDAwLCJrZXkiOiJwYnN0LTEyMzQ1Njc4OWEiLCJwcm9kdWN0bmFtZSI6IlByb3htb3ggQmFja3VwIFNlcnZlciBUZXN0IFN1YnNjcmlwdGlvbiAtMSB5ZWFyIiwicmVnZGF0ZSI6IjIwMjAtMDktMTkgMDA6MDA6MDAiLCJuZXh0ZHVlZGF0ZSI6IjIwMjEtMDktMTkiLCJ1cmwiOiJodHRwczovL3d3dy5wcm94bW94LmNvbS9lbi9wcm94bW94LWJhY2t1cC1zZXJ2ZXIvcHJpY2luZyJ9\n";
let expected = SubscriptionInfo {
key: Some("pbst-123456789a".to_string()),
serverid: Some("830000000123456789ABCDEF00000042".to_string()),
status: SubscriptionStatus::ACTIVE,
checktime: Some(1600000000),
url: Some("https://www.proxmox.com/en/proxmox-backup-server/pricing".into()),
message: None,
nextduedate: Some("2021-09-19".into()),
regdate: Some("2020-09-19 00:00:00".into()),
productname: Some("Proxmox Backup Server Test Subscription -1 year".into()),
signature: None,
};
let parsed = parse_subscription_file(content);
assert!(parsed.is_ok());
let parsed = parsed.unwrap();
assert!(parsed.is_some());
let parsed = parsed.unwrap();
assert_eq!(parsed, expected);
}

View File

@ -0,0 +1,6 @@
mod subscription_info;
pub use subscription_info::{get_hardware_address, SubscriptionInfo, SubscriptionStatus};
pub mod check;
pub mod files;
pub mod sign;

View File

@ -0,0 +1,97 @@
use anyhow::{bail, Error};
use openssl::{hash::MessageDigest, pkey::Public};
use serde::{Deserialize, Serialize};
use crate::SubscriptionInfo;
#[derive(PartialEq, Eq, Hash, Serialize, Deserialize)]
/// Input for offline key signing requests
pub struct ServerBlob {
/// Server ID generated with [[crate::get_hardware_address()]].
pub serverid: String,
/// Subscription key
pub key: String,
}
/// Common abstraction for signing operations returning [String]-representation of signature.
pub trait Signer {
fn sign(&self, data: &[u8]) -> Result<String, Error>;
}
impl Signer for openssl::pkey::PKey<openssl::pkey::Private> {
fn sign(&self, data: &[u8]) -> Result<String, Error> {
use openssl::pkey;
// see Signer docs - different algorithms require different constructors
let mut signer = match self.id() {
pkey::Id::ED25519 | pkey::Id::ED448 => {
openssl::sign::Signer::new_without_digest(self.as_ref())?
}
pkey::Id::RSA | pkey::Id::EC => {
openssl::sign::Signer::new(MessageDigest::sha512(), self.as_ref())?
}
id => bail!("Unsupported key type '{id:?}'"),
};
Ok(hex::encode(signer.sign_oneshot_to_vec(data)?))
}
}
/// Common verifier abstraction for signatures created by [Signer]s.
pub trait Verifier {
fn verify(&self, data: &[u8], signature: &str) -> Result<(), Error>;
}
impl Verifier for openssl::pkey::PKey<Public> {
fn verify(&self, data: &[u8], signature: &str) -> Result<(), Error> {
use openssl::pkey;
// see Verifier docs - different algorithms require different constructors
let mut verifier = match self.id() {
pkey::Id::ED25519 | pkey::Id::ED448 => {
openssl::sign::Verifier::new_without_digest(self.as_ref())?
}
pkey::Id::RSA | pkey::Id::EC => {
openssl::sign::Verifier::new(MessageDigest::sha512(), self.as_ref())?
}
id => bail!("Unsupported key type '{id:?}'"),
};
if !verifier.verify_oneshot(&hex::decode(signature)?, data)? {
bail!("Signature mismatch.");
}
Ok(())
}
}
#[derive(Serialize, Deserialize)]
/// A signed response to a signature request for offline keys.
pub struct SignedResponse {
/// Signature of response
pub signature: String,
/// Payload (signed [SubscriptionInfo]s)
pub blobs: Vec<SubscriptionInfo>,
}
impl SignedResponse {
/// Verify outer signature (of server response)
pub fn verify(self, key: &openssl::pkey::PKey<Public>) -> Result<Vec<SubscriptionInfo>, Error> {
let canonical =
proxmox_serde::json::to_canonical_json(&serde_json::to_value(&self.blobs)?)?;
match key.verify(&canonical, &self.signature) {
Ok(()) => Ok(self.blobs),
Err(_) => todo!(),
}
}
}
#[derive(Serialize, Deserialize)]
/// A sign request for offline keys
pub struct SignRequest {
/// Subscription key of `proxmox-offline-mirror` instance issuing this request (must be [crate::SubscriptionStatus::ACTIVE]).
pub mirror_key: ServerBlob,
/// Offline keys that should be signed by server.
pub blobs: Vec<ServerBlob>,
}

View File

@ -0,0 +1,215 @@
use anyhow::{bail, format_err, Error};
use openssl::{
hash::{hash, DigestBytes, MessageDigest},
pkey::Public,
};
use serde::{Deserialize, Serialize};
#[cfg(feature = "api-types")]
use proxmox_schema::{api, Updater};
use crate::sign::Verifier;
pub(crate) const SHARED_KEY_DATA: &str = "kjfdlskfhiuewhfk947368";
/// How long the local key is valid for in between remote checks
pub(crate) const SUBSCRIPTION_MAX_LOCAL_KEY_AGE: i64 = 15 * 24 * 3600;
pub(crate) const SUBSCRIPTION_MAX_KEY_CHECK_FAILURE_AGE: i64 = 5 * 24 * 3600;
// Aliases are needed for PVE compat!
#[cfg_attr(feature = "api-types", api())]
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
/// Subscription status
pub enum SubscriptionStatus {
// FIXME: remove?
/// newly set subscription, not yet checked
#[serde(alias = "New")]
NEW,
/// no subscription set
#[serde(alias = "Notfound")]
NOTFOUND,
/// subscription set and active
#[serde(alias = "Active")]
ACTIVE,
/// subscription set but invalid for this server
#[serde(alias = "Invalid")]
INVALID,
}
impl Default for SubscriptionStatus {
fn default() -> Self {
SubscriptionStatus::NOTFOUND
}
}
impl std::fmt::Display for SubscriptionStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SubscriptionStatus::NEW => write!(f, "New"),
SubscriptionStatus::NOTFOUND => write!(f, "NotFound"),
SubscriptionStatus::ACTIVE => write!(f, "Active"),
SubscriptionStatus::INVALID => write!(f, "Invalid"),
}
}
}
#[cfg_attr(feature = "api-types", api(
properties: {
status: {
type: SubscriptionStatus,
},
},
))]
#[cfg_attr(feature = "api-types", derive(Updater))]
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
/// Proxmox subscription information
pub struct SubscriptionInfo {
/// Subscription status from the last check
pub status: SubscriptionStatus,
/// the server ID, if permitted to access
#[serde(skip_serializing_if = "Option::is_none", alias = "validdirectory")]
pub serverid: Option<String>,
/// timestamp of the last check done
#[serde(skip_serializing_if = "Option::is_none")]
pub checktime: Option<i64>,
/// the subscription key, if set and permitted to access
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
/// a more human readable status message
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
/// human readable productname of the set subscription
#[serde(skip_serializing_if = "Option::is_none")]
pub productname: Option<String>,
/// register date of the set subscription
#[serde(skip_serializing_if = "Option::is_none")]
pub regdate: Option<String>,
/// next due date of the set subscription
#[serde(skip_serializing_if = "Option::is_none")]
pub nextduedate: Option<String>,
/// URL to the web shop
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
/// Signature for offline keys
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
}
impl SubscriptionInfo {
/// Returns the canonicalized signed data and, if available, signature contained in `self`.
pub fn signed_data(&self) -> Result<(Vec<u8>, Option<String>), Error> {
let mut data = serde_json::to_value(&self)?;
let signature = data
.as_object_mut()
.ok_or_else(|| format_err!("subscription info not a JSON object"))?
.remove("signature")
.map(|v| v.as_str().map(|v| v.to_owned()))
.flatten();
if self.is_signed() && signature.is_none() {
bail!("Failed to extract signature value!");
}
let data = proxmox_serde::json::to_canonical_json(&data)?;
Ok((data, signature))
}
/// Whether a signature exists - *this does not check the signature's validity!*
///
/// Use [SubscriptionInfo::verify()] to verify the signature.
pub fn is_signed(&self) -> bool {
self.signature.is_some()
}
/// Verify signature, if `self` is signed. Returns `false` if `self` is unsigned, `true` if signature is valid for `key`.
pub fn verify(&self, key: &openssl::pkey::PKey<Public>) -> Result<bool, Error> {
if self.signature.is_none() {
return Ok(false);
}
let (signed, signature) = self.signed_data()?;
let signature = match signature {
None => bail!("Failed to extract signature value."),
Some(sig) => sig,
};
match key.verify(&signed, &signature) {
Ok(()) => Ok(true),
Err(err) => Err(format_err!("Signature verification failed - {err}")),
}
}
/// Checks whether a [SubscriptionInfo]'s `checktime` matches the age criteria:
///
/// - Instances generated (more than 1.5h) in the future are invalid
/// - Signed instances are valid for up to a year, clamped by the next due date
/// - Unsigned instances are valid for 30+5 days
/// - If `recheck` is set to `true`, unsigned instances are only treated as valid for 5 days
/// (this mode is used to decide whether to refresh the subscription information)
///
/// If the criteria are not met, `status` is set to [SubscriptionStatus::INVALID] and `message`
/// to a human-readable error message.
pub fn check_age(&mut self, recheck: bool) {
let now = proxmox_time::epoch_i64();
let age = now - self.checktime.unwrap_or(0);
let cutoff = if recheck {
SUBSCRIPTION_MAX_KEY_CHECK_FAILURE_AGE
} else {
SUBSCRIPTION_MAX_LOCAL_KEY_AGE + SUBSCRIPTION_MAX_KEY_CHECK_FAILURE_AGE
};
// allow some delta for DST changes or time syncs, 1.5h
if age < -5400 {
self.status = SubscriptionStatus::INVALID;
self.message = Some("last check date too far in the future".to_string());
self.signature = None;
} else if age > cutoff {
if let SubscriptionStatus::ACTIVE = self.status {
self.status = SubscriptionStatus::INVALID;
self.message = Some("subscription information too old".to_string());
self.signature = None;
}
}
}
/// Check that server ID contained in [SubscriptionInfo] matches that of current system.
///
/// `status` is set to [SubscriptionStatus::INVALID] and `message` to a human-readable
/// message in case it does not.
pub fn check_server_id(&mut self) {
match (self.serverid.as_ref(), get_hardware_address()) {
(_, Err(err)) => {
self.status = SubscriptionStatus::INVALID;
self.message = Some(format!("Failed to obtain server ID - {err}."));
self.signature = None;
}
(None, _) => {
self.status = SubscriptionStatus::INVALID;
self.message = Some(format!("Missing server ID."));
self.signature = None;
}
(Some(contained), Ok(expected)) if &expected != contained => {
self.status = SubscriptionStatus::INVALID;
self.message = Some("Server ID mismatch.".to_string());
self.signature = None;
}
(Some(_), Ok(_)) => {}
}
}
}
/// Shortcut for md5 sums.
pub(crate) fn md5sum(data: &[u8]) -> Result<DigestBytes, Error> {
hash(MessageDigest::md5(), data).map_err(Error::from)
}
/// Generate the current system's "server ID".
pub fn get_hardware_address() -> Result<String, Error> {
static FILENAME: &str = "/etc/ssh/ssh_host_rsa_key.pub";
let contents = proxmox_sys::fs::file_get_contents(FILENAME)
.map_err(|e| format_err!("Error getting host key - {}", e))?;
let digest = md5sum(&contents).map_err(|e| format_err!("Error digesting host key - {}", e))?;
Ok(hex::encode(&digest).to_uppercase())
}