From 7413ea21287bb55417e2505cb968cba7a8fe2688 Mon Sep 17 00:00:00 2001 From: Kyle Tse Date: Mon, 5 Oct 2026 19:12:29 +0000 Subject: [PATCH] feat(pro): pin the lockdocs-pro issuer public key --- crates/lockdocs/src/pro.rs | 37 +++++++++++++++++++++++-------- crates/lockdocs/tests/cli_gate.rs | 2 +- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/crates/lockdocs/src/pro.rs b/crates/lockdocs/src/pro.rs index 360f81c..4f0f068 100644 --- a/crates/lockdocs/src/pro.rs +++ b/crates/lockdocs/src/pro.rs @@ -6,8 +6,8 @@ use std::time::Duration; use mcp_kit::licence::{self, Licence, LicencePolicy, ProRequired}; -/// Marker for the key list below. No token verifies while it is the only entry. -pub const KEY_PLACEHOLDER: &str = "PLACEHOLDER-lockdocs-pro-issuer-public-key-not-issued-yet"; +/// The lockdocs-pro issuer public key (base64url, raw Ed25519). Public, not a secret. +pub const ISSUER_PUBLIC_KEY: &str = "3WKM0pn_td25hztpKOVCSFZvE8zs06uXrMWU1p5O4so"; /// Base URL of the in-terminal checkout. #[allow(dead_code)] @@ -18,16 +18,12 @@ pub const CHECKOUT_BASE: &str = "https://buy.sylphx.com"; pub const PRICE: &str = "US$120"; /// The lockdocs Pro licence policy. -/// -/// TODO(Services S1): replace `KEY_PLACEHOLDER` with the issued lockdocs-pro -/// Ed25519 public key (base64url). Until then this list contains no valid key, -/// so every token is invalid and nothing Pro unlocks. pub const POLICY: LicencePolicy<'static> = LicencePolicy { product: "lockdocs", tier: "Pro", require_product: true, accepted_plans: &["pro"], - public_keys: &[KEY_PLACEHOLDER], + public_keys: &[ISSUER_PUBLIC_KEY], env_var: "LOCKDOCS_LICENCE_TOKEN", file_name: "licence", // Published pricing page (docs/pro.md). The price itself is `docs/pricing.json`. @@ -54,12 +50,35 @@ pub fn renewal_note(policy: &LicencePolicy, licence: &Licence) -> Option #[cfg(test)] mod tests { use super::*; + use base64::engine::general_purpose::URL_SAFE_NO_PAD; + use base64::Engine as _; + use ed25519_dalek::{Signer, SigningKey}; + + fn token(key: &SigningKey, payload: &str) -> String { + let sig = key.sign(payload.as_bytes()); + format!("{}.{}", URL_SAFE_NO_PAD.encode(payload), URL_SAFE_NO_PAD.encode(sig.to_bytes())) + } #[test] - fn placeholder_key_verifies_nothing() { - assert_eq!(POLICY.public_keys, &[KEY_PLACEHOLDER]); + fn issuer_key_is_pinned_and_decodes_to_32_bytes() { + assert_eq!(POLICY.public_keys, &[ISSUER_PUBLIC_KEY]); + assert_eq!(URL_SAFE_NO_PAD.decode(ISSUER_PUBLIC_KEY).unwrap().len(), 32); for t in ["", "x.y", "e30.AAAA"] { assert!(POLICY.verify(t).is_err()); } } + + #[test] + fn token_from_a_listed_key_verifies_and_another_key_does_not() { + let listed = SigningKey::from_bytes(&[7; 32]); + let other = SigningKey::from_bytes(&[8; 32]); + let public: &'static str = Box::leak(URL_SAFE_NO_PAD.encode(listed.verifying_key().to_bytes()).into_boxed_str()); + let keys: &'static [&'static str] = Box::leak(vec![public].into_boxed_slice()); + let policy = LicencePolicy { public_keys: keys, ..POLICY }; + let payload = r#"{"plan":"pro","issuedAt":1,"product":"lockdocs"}"#; + assert!(policy.verify(&token(&listed, payload)).is_ok()); + assert!(policy.verify(&token(&other, payload)).is_err()); + // The pinned production key refuses a token from a key it does not list. + assert!(POLICY.verify(&token(&listed, payload)).is_err()); + } } diff --git a/crates/lockdocs/tests/cli_gate.rs b/crates/lockdocs/tests/cli_gate.rs index 88f0552..471399b 100644 --- a/crates/lockdocs/tests/cli_gate.rs +++ b/crates/lockdocs/tests/cli_gate.rs @@ -26,7 +26,7 @@ fn pro_commands_exit_3_and_free_commands_do_not() { let o = run(&["licence", "status"], home.path()); assert_eq!(o.status.code(), Some(0)); assert!(String::from_utf8_lossy(&o.stdout).contains("inactive")); - // The placeholder key refuses every token, so activate fails (exit 1), never 3. + // The pinned issuer key refuses an unsigned token, so activate fails (exit 1), never 3. let o = run(&["licence", "activate", "e30.AAAA"], home.path()); assert_eq!(o.status.code(), Some(1)); let o = run(&["version"], home.path());