Files
SCOPENET-MC/panel/server/src/yggdrasil/keys.rs
T
Claude 99b45141fc Panel: Yggdrasil auth server (authlib-injector), skins and capes
- Yggdrasil API per the authlib-injector spec: metadata with signing key
  and skin domains, authenticate/refresh/validate/invalidate/signout,
  join/hasJoined, profile lookup, texture upload, and the minecraftservices
  endpoints (chat certificates, publickeys, attributes, blocklist)
- 4096-bit signing key generated once into the data volume; textures and
  chat certificates signed SHA1withRSA (verified with Java's own crypto)
- Skins/capes stored content-addressed after validation and re-encoding;
  cape library with public/group/private visibility; head avatars API
- Launcher login returns a game session; launcher sessions recorded for
  launcher-only servers; authlib-injector download mirror
- Schema v2: player UUIDs (offline UUID backfilled), skins, capes, tokens,
  sessions, chat keys, game server tables
- Remove Microsoft sign-in from the engine and panel

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
2026-09-28 06:57:07 +00:00

75 lines
3.0 KiB
Rust

//! The auth server's RSA key. It signs skin/cape data ("textures") and
//! player chat certificates; game clients and servers learn the public half
//! from the Yggdrasil metadata via authlib-injector.
use anyhow::{Context, Result};
use base64::Engine;
use rsa::pkcs1v15::SigningKey;
use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey, EncodePublicKey, LineEnding};
use rsa::signature::{SignatureEncoding, Signer};
use rsa::{RsaPrivateKey, RsaPublicKey};
use sha1::Sha1;
use std::path::Path;
pub const KEY_BITS: usize = 4096;
pub struct Keys {
signer: SigningKey<Sha1>,
/// `-----BEGIN PUBLIC KEY-----` (X.509 SubjectPublicKeyInfo).
pub public_pem: String,
/// DER of the same, base64 — the format of Mojang's `/publickeys`.
pub public_der_b64: String,
}
impl Keys {
pub fn from_private(key: RsaPrivateKey) -> Result<Self> {
let public = RsaPublicKey::from(&key);
let public_pem = public.to_public_key_pem(LineEnding::LF)?;
let public_der_b64 = base64::engine::general_purpose::STANDARD.encode(public.to_public_key_der()?.as_bytes());
Ok(Self { signer: SigningKey::<Sha1>::new(key), public_pem, public_der_b64 })
}
/// Load `path`, or generate and save a new key if it doesn't exist.
pub fn load_or_create(path: &Path) -> Result<Self> {
if let Ok(pem) = std::fs::read_to_string(path) {
let key = RsaPrivateKey::from_pkcs8_pem(&pem).with_context(|| format!("reading {}", path.display()))?;
return Self::from_private(key);
}
tracing::info!("generating the auth server signing key ({KEY_BITS}-bit RSA, one-time)…");
let key = RsaPrivateKey::new(&mut rand::thread_rng(), KEY_BITS)?;
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)?;
}
std::fs::write(path, key.to_pkcs8_pem(LineEnding::LF)?.as_bytes())?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)).ok();
}
Self::from_private(key)
}
/// SHA1withRSA, base64 — what Mojang uses for every Yggdrasil signature.
pub fn sign_b64(&self, data: &[u8]) -> String {
base64::engine::general_purpose::STANDARD.encode(self.signer.sign(data).to_bytes())
}
}
#[cfg(test)]
mod tests {
use super::*;
use rsa::pkcs1v15::{Signature, VerifyingKey};
use rsa::pkcs8::DecodePublicKey;
use rsa::signature::Verifier;
#[test]
fn signs_verifiably() {
let key = RsaPrivateKey::new(&mut rand::thread_rng(), 1024).unwrap();
let keys = Keys::from_private(key).unwrap();
let sig = base64::engine::general_purpose::STANDARD.decode(keys.sign_b64(b"hello")).unwrap();
let public = RsaPublicKey::from_public_key_pem(&keys.public_pem).unwrap();
VerifyingKey::<Sha1>::new(public).verify(b"hello", &Signature::try_from(sig.as_slice()).unwrap()).unwrap();
assert!(keys.public_pem.starts_with("-----BEGIN PUBLIC KEY-----"));
}
}