From edf3af0f7c9d28d15faee86dab082dc4eb2531c3 Mon Sep 17 00:00:00 2001 From: NinjaCheetah <58050615+NinjaCheetah@users.noreply.github.com> Date: Thu, 27 Mar 2025 21:06:59 -0400 Subject: [PATCH] Added signature verification, info command now has WiiPy feature parity rustii CLI info command now displays the signing status of TMDs/Tickets/WADs like WiiPy does, and displays the ASCII TID for a title when applicable. This means that this command now has full feature parity with WiiPy. --- Cargo.lock | 274 +++++++++++++++++++++++++ Cargo.toml | 3 +- src/bin/playground/main.rs | 41 ++-- src/bin/rustii/info.rs | 94 +++++++-- src/bin/rustii/title/wad.rs | 6 +- src/title/cert.rs | 370 ++++++++++++++++++++++++++++++++++ src/title/keys/dev-pub.pem | 14 ++ src/title/keys/retail-pub.pem | 14 ++ src/title/mod.rs | 32 ++- src/title/ticket.rs | 4 + src/title/tmd.rs | 4 + src/title/wad.rs | 8 +- 12 files changed, 817 insertions(+), 47 deletions(-) create mode 100644 src/title/keys/dev-pub.pem create mode 100644 src/title/keys/retail-pub.pem diff --git a/Cargo.lock b/Cargo.lock index 7005340..c0a94f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,6 +72,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "base64ct" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" + [[package]] name = "block-buffer" version = "0.10.4" @@ -167,6 +179,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -186,6 +204,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -193,6 +222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", ] @@ -206,6 +236,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "glob" version = "0.3.2" @@ -240,24 +281,125 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + [[package]] name = "libc" version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + [[package]] name = "once_cell" version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -276,6 +418,35 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "regex" version = "1.11.1" @@ -305,6 +476,27 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustii" version = "0.1.0" @@ -316,6 +508,7 @@ dependencies = [ "glob", "hex", "regex", + "rsa", "sha1", ] @@ -330,12 +523,61 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.100" @@ -371,6 +613,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "windows-sys" version = "0.59.0" @@ -443,3 +691,29 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index c6e1ee5..6b53d8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,9 @@ doc = true byteorder = "1" cbc = "0" aes = "0" +rsa = { version = "0", features = ["sha2"] } hex = "0" -sha1 = "0" +sha1 = { version = "0", features = ["oid"]} glob = "0" regex = "1" clap = { version = "4", features = ["derive"] } diff --git a/src/bin/playground/main.rs b/src/bin/playground/main.rs index cf913b1..73ddc10 100644 --- a/src/bin/playground/main.rs +++ b/src/bin/playground/main.rs @@ -1,43 +1,44 @@ // Sample file for testing rustii library stuff. use std::fs; -use rustii::title::{content, crypto, wad}; +use rustii::title::{wad, cert}; use rustii::title; fn main() { - let data = fs::read("boot2.wad").unwrap(); - let mut title = title::Title::from_bytes(&data).unwrap(); + let data = fs::read("sm.wad").unwrap(); + let title = title::Title::from_bytes(&data).unwrap(); println!("Title ID from WAD via Title object: {}", hex::encode(title.tmd.title_id)); let wad = wad::WAD::from_bytes(&data).unwrap(); println!("size of tmd: {:?}", wad.tmd().len()); println!("num content records: {:?}", title.tmd.content_records.len()); println!("first record data: {:?}", title.tmd.content_records.first().unwrap()); - if !title.tmd.is_fakesigned() { - title.tmd.fakesign().unwrap(); - } println!("TMD is fakesigned: {:?}",title.tmd.is_fakesigned()); println!("title version from ticket is: {:?}", title.ticket.title_version); println!("title key (enc): {:?}", title.ticket.title_key); println!("title key (dec): {:?}", title.ticket.dec_title_key()); - if !title.ticket.is_fakesigned() { - title.ticket.fakesign().unwrap(); - } println!("ticket is fakesigned: {:?}", title.ticket.is_fakesigned()); println!("title is fakesigned: {:?}", title.is_fakesigned()); - let content_region = content::ContentRegion::from_bytes(&wad.content(), title.tmd.content_records).unwrap(); - assert_eq!(wad.content(), content_region.to_bytes().unwrap()); - println!("content OK"); - - let content_dec = content_region.get_content_by_index(0, title.ticket.dec_title_key()).unwrap(); - println!("content dec from index: {:?}", content_dec); - - let content = content_region.get_enc_content_by_index(0).unwrap(); - assert_eq!(content, crypto::encrypt_content(&content_dec, title.ticket.dec_title_key(), 0, content_region.content_records[0].content_size)); - println!("content re-encrypted OK"); - println!("wad header: {:?}", wad.header); + + let cert_chain = &title.cert_chain; + println!("cert chain OK"); + let result = cert::verify_ca_cert(&cert_chain.ca_cert()).unwrap(); + println!("CA cert {} verified successfully: {}", cert_chain.ca_cert().child_cert_identity(), result); + + let result = cert::verify_child_cert(&cert_chain.ca_cert(), &cert_chain.tmd_cert()).unwrap(); + println!("TMD cert {} verified successfully: {}", cert_chain.tmd_cert().child_cert_identity(), result); + let result = cert::verify_tmd(&cert_chain.tmd_cert(), &title.tmd).unwrap(); + println!("TMD verified successfully: {}", result); + + let result = cert::verify_child_cert(&cert_chain.ca_cert(), &cert_chain.ticket_cert()).unwrap(); + println!("Ticket cert {} verified successfully: {}", cert_chain.ticket_cert().child_cert_identity(), result); + let result = cert::verify_ticket(&cert_chain.ticket_cert(), &title.ticket).unwrap(); + println!("Ticket verified successfully: {}", result); + + let result = title.verify().unwrap(); + println!("full title verified successfully: {}", result); } diff --git a/src/bin/rustii/info.rs b/src/bin/rustii/info.rs index 876211d..247687e 100644 --- a/src/bin/rustii/info.rs +++ b/src/bin/rustii/info.rs @@ -5,13 +5,27 @@ use std::{str, fs}; use std::path::Path; -use rustii::{title, title::tmd, title::ticket, title::wad, title::versions}; +use rustii::{title, title::cert, title::tmd, title::ticket, title::wad, title::versions}; use crate::filetypes::{WiiFileType, identify_file_type}; -fn print_tmd_info(tmd: tmd::TMD) { +fn tid_to_ascii(tid: [u8; 8]) -> Option { + let tid = String::from_utf8_lossy(&tid[4..]).trim_end_matches('\0').trim_start_matches('\0').to_owned(); + if tid.len() == 4 { + Some(tid) + } else { + None + } +} + +fn print_tmd_info(tmd: tmd::TMD, cert: Option) { // Print all important keys from the TMD. println!("Title Info"); - println!(" Title ID: {}", hex::encode(tmd.title_id).to_uppercase()); + let ascii_tid = tid_to_ascii(tmd.title_id); + if ascii_tid.is_some() { + println!(" Title ID: {} ({})", hex::encode(tmd.title_id).to_uppercase(), ascii_tid.unwrap()); + } else { + println!(" Title ID: {}", hex::encode(tmd.title_id).to_uppercase()); + } if hex::encode(tmd.title_id)[..8].eq("00000001") { if hex::encode(tmd.title_id).eq("0000000100000001") { println!(" Title Version: {} (boot2v{})", tmd.title_version, tmd.title_version); @@ -67,7 +81,24 @@ fn print_tmd_info(tmd: tmd::TMD) { println!(" vWii Title: {}", tmd.is_vwii != 0); println!(" DVD Video Access: {}", tmd.check_access_right(tmd::AccessRight::DVDVideo)); println!(" AHB Access: {}", tmd.check_access_right(tmd::AccessRight::AHB)); - println!(" Fakesigned: {}", tmd.is_fakesigned()); + if cert.is_some() { + let signing_str = match cert::verify_tmd(&cert.unwrap(), &tmd) { + Ok(result) => match result { + true => "Valid (Unmodified TMD)", + false => { + if tmd.is_fakesigned() { + "Fakesigned" + } else { + "Invalid (Modified TMD)" + } + }, + }, + Err(_) => "Invalid (Modified TMD)" + }; + println!(" Signature: {}", signing_str); + } else { + println!(" Fakesigned: {}", tmd.is_fakesigned()); + } println!("\nContent Info"); println!(" Total Contents: {}", tmd.num_contents); println!(" Boot Content Index: {}", tmd.boot_index); @@ -81,10 +112,15 @@ fn print_tmd_info(tmd: tmd::TMD) { } } -fn print_ticket_info(ticket: ticket::Ticket) { +fn print_ticket_info(ticket: ticket::Ticket, cert: Option) { // Print all important keys from the Ticket. println!("Ticket Info"); - println!(" Title ID: {}", hex::encode(ticket.title_id).to_uppercase()); + let ascii_tid = tid_to_ascii(ticket.title_id); + if ascii_tid.is_some() { + println!(" Title ID: {} ({})", hex::encode(ticket.title_id).to_uppercase(), ascii_tid.unwrap()); + } else { + println!(" Title ID: {}", hex::encode(ticket.title_id).to_uppercase()); + } if hex::encode(ticket.title_id)[..8].eq("00000001") { if hex::encode(ticket.title_id).eq("0000000100000001") { println!(" Title Version: {} (boot2v{})", ticket.title_version, ticket.title_version); @@ -120,7 +156,24 @@ fn print_ticket_info(ticket: ticket::Ticket) { println!(" Decryption Key: {}", key); println!(" Title Key (Encrypted): {}", hex::encode(ticket.title_key)); println!(" Title Key (Decrypted): {}", hex::encode(ticket.dec_title_key())); - println!(" Fakesigned: {}", ticket.is_fakesigned()); + if cert.is_some() { + let signing_str = match cert::verify_ticket(&cert.unwrap(), &ticket) { + Ok(result) => match result { + true => "Valid (Unmodified Ticket)", + false => { + if ticket.is_fakesigned() { + "Fakesigned" + } else { + "Invalid (Modified Ticket)" + } + }, + }, + Err(_) => "Invalid (Modified Ticket)" + }; + println!(" Signature: {}", signing_str); + } else { + println!(" Fakesigned: {}", ticket.is_fakesigned()); + } } fn print_wad_info(wad: wad::WAD) { @@ -147,11 +200,28 @@ fn print_wad_info(wad: wad::WAD) { } println!(" Has Meta/Footer: {}", wad.meta_size() != 0); println!(" Has CRL: {}", wad.crl_size() != 0); - println!(" Fakesigned: {}", title.is_fakesigned()); + let signing_str = match title.verify() { + Ok(result) => match result { + true => "Legitimate (Unmodified TMD + Ticket)", + false => { + if title.is_fakesigned() { + "Fakesigned" + } else if cert::verify_tmd(&title.cert_chain.tmd_cert(), &title.tmd).unwrap() { + "Piratelegit (Unmodified TMD, Modified Ticket)" + } else if cert::verify_ticket(&title.cert_chain.ticket_cert(), &title.ticket).unwrap() { + "Edited (Modified TMD, Unmodified Ticket)" + } else { + "Illegitimate (Modified TMD + Ticket)" + } + }, + }, + Err(_) => "Illegitimate (Modified TMD + Ticket)" + }; + println!(" Signing Status: {}", signing_str); println!(); - print_ticket_info(title.ticket); + print_ticket_info(title.ticket, Some(title.cert_chain.ticket_cert())); println!(); - print_tmd_info(title.tmd); + print_tmd_info(title.tmd, Some(title.cert_chain.tmd_cert())); } pub fn info(input: &str) { @@ -162,11 +232,11 @@ pub fn info(input: &str) { match identify_file_type(input) { Some(WiiFileType::Tmd) => { let tmd = tmd::TMD::from_bytes(fs::read(in_path).unwrap().as_slice()).unwrap(); - print_tmd_info(tmd); + print_tmd_info(tmd, None); }, Some(WiiFileType::Ticket) => { let ticket = ticket::Ticket::from_bytes(fs::read(in_path).unwrap().as_slice()).unwrap(); - print_ticket_info(ticket); + print_ticket_info(ticket, None); }, Some(WiiFileType::Wad) => { let wad = wad::WAD::from_bytes(fs::read(in_path).unwrap().as_slice()).unwrap(); diff --git a/src/bin/rustii/title/wad.rs b/src/bin/rustii/title/wad.rs index bafe27e..bd72819 100644 --- a/src/bin/rustii/title/wad.rs +++ b/src/bin/rustii/title/wad.rs @@ -7,7 +7,7 @@ use std::{str, fs}; use std::path::{Path, PathBuf}; use clap::Subcommand; use glob::glob; -use rustii::title::{tmd, ticket, content, wad}; +use rustii::title::{cert, tmd, ticket, content, wad}; use rustii::title; #[derive(Subcommand)] @@ -59,7 +59,7 @@ pub fn pack_wad(input: &str, output: &str) { } else if cert_files.len() > 1 { panic!("Error: More than one Cert file found in the source directory.") } - let cert_chain = fs::read(&cert_files[0]).expect("could not read cert chain file"); + let cert_chain = cert::CertificateChain::from_bytes(&fs::read(&cert_files[0]).expect("could not read cert chain file")).unwrap(); // Read footer, if one exists (only accept one file). let footer_files: Vec = glob(&format!("{}/*.footer", in_path.display())) .expect("failed to read glob pattern") @@ -106,7 +106,7 @@ pub fn unpack_wad(input: &str, output: &str) { let ticket_file_name = format!("{}.tik", tid); fs::write(Path::join(out_path, ticket_file_name), title.ticket.to_bytes().unwrap()).expect("could not write Ticket file"); let cert_file_name = format!("{}.cert", tid); - fs::write(Path::join(out_path, cert_file_name), title.cert_chain()).expect("could not write Cert file"); + fs::write(Path::join(out_path, cert_file_name), title.cert_chain.to_bytes().unwrap()).expect("could not write Cert file"); let meta_file_name = format!("{}.footer", tid); fs::write(Path::join(out_path, meta_file_name), title.meta()).expect("could not write footer file"); // Iterate over contents, decrypt them, and write them out. diff --git a/src/title/cert.rs b/src/title/cert.rs index 855f070..dc4e14a 100644 --- a/src/title/cert.rs +++ b/src/title/cert.rs @@ -3,4 +3,374 @@ // // Implements the structures and methods required for validating the signatures of Wii titles. +use std::error::Error; +use std::fmt; +use std::io::{Cursor, Read, Write, SeekFrom, Seek}; +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use rsa::pkcs8::DecodePublicKey; +use rsa::pkcs1v15::Pkcs1v15Sign; +use rsa::{RsaPublicKey, BigUint}; +use sha1::{Digest, Sha1}; +use crate::title::{tmd, ticket}; + +#[derive(Debug)] +pub enum CertificateError { + InvalidSignatureKeyType(u32), + InvalidContainedKeyType(u32), + UnknownCertificate, + MissingCertificate(String), + IncorrectCertificate(String), + NonMatchingCertificates, + IOError(std::io::Error), +} + +impl fmt::Display for CertificateError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let description = match *self { + CertificateError::InvalidSignatureKeyType(_) => "The key type this certificate appears to be signed with is not valid.", + CertificateError::InvalidContainedKeyType(_) => "The key type contained in this certificate is not valid.", + CertificateError::UnknownCertificate => "An unknown certificate was found in the certificate chain.", + CertificateError::MissingCertificate(_) => "A required certificate was not found in the certificate chain.", + CertificateError::IncorrectCertificate(_) => "A provided certificate did not match the expected type.", + CertificateError::NonMatchingCertificates => "The provided certificate does not match the data you are attempting to verify with it.", + CertificateError::IOError(_) => "The provided certificate data was invalid.", + }; + f.write_str(description) + } +} + +impl Error for CertificateError {} + +#[derive(Debug, Clone)] +pub enum CertificateKeyType { + Rsa4096, + Rsa2048, + ECC +} + +#[derive(Debug, Clone)] +pub struct Certificate { + signer_key_type: CertificateKeyType, + signature: Vec, + signature_issuer: [u8; 64], + pub_key_type: CertificateKeyType, + child_cert_identity: [u8; 64], + pub_key_id: u32, + pub_key_modulus: Vec, + pub_key_exponent: u32 +} + +impl Certificate { + /// Creates a new Certificate instance from the binary data of a certificate file. + pub fn from_bytes(data: &[u8]) -> Result { + let mut buf = Cursor::new(data); + let signer_key_type_int = buf.read_u32::().map_err(CertificateError::IOError)?; + let signer_key_type = match signer_key_type_int { + 0x00010000 => CertificateKeyType::Rsa4096, + 0x00010001 => CertificateKeyType::Rsa2048, + 0x00010002 => CertificateKeyType::ECC, + _ => return Err(CertificateError::InvalidSignatureKeyType(signer_key_type_int)) + }; + let signature_len = match signer_key_type { + CertificateKeyType::Rsa4096 => 512, + CertificateKeyType::Rsa2048 => 256, + CertificateKeyType::ECC => 60, + }; + let mut signature = vec![0u8; signature_len]; + buf.read_exact(&mut signature).map_err(CertificateError::IOError)?; + // Skip past padding at the end of the signature. + buf.seek(SeekFrom::Start(0x40 + signature_len as u64)).map_err(CertificateError::IOError)?; + let mut signature_issuer = [0u8; 64]; + buf.read_exact(&mut signature_issuer).map_err(CertificateError::IOError)?; + let pub_key_type_int = buf.read_u32::().map_err(CertificateError::IOError)?; + let pub_key_type = match pub_key_type_int { + 0x00000000 => CertificateKeyType::Rsa4096, + 0x00000001 => CertificateKeyType::Rsa2048, + 0x00000002 => CertificateKeyType::ECC, + _ => return Err(CertificateError::InvalidContainedKeyType(pub_key_type_int)) + }; + let mut child_cert_identity = [0u8; 64]; + buf.read_exact(&mut child_cert_identity).map_err(CertificateError::IOError)?; + let pub_key_id = buf.read_u32::().map_err(CertificateError::IOError)?; + let mut pub_key_modulus: Vec; + let mut pub_key_exponent: u32 = 0; + // The key size and exponent are different based on the key type. ECC has no exponent. + match pub_key_type { + CertificateKeyType::Rsa4096 => { + pub_key_modulus = vec![0u8; 512]; + buf.read_exact(&mut pub_key_modulus).map_err(CertificateError::IOError)?; + pub_key_exponent = buf.read_u32::().map_err(CertificateError::IOError)?; + }, + CertificateKeyType::Rsa2048 => { + pub_key_modulus = vec![0u8; 256]; + buf.read_exact(&mut pub_key_modulus).map_err(CertificateError::IOError)?; + pub_key_exponent = buf.read_u32::().map_err(CertificateError::IOError)?; + }, + CertificateKeyType::ECC => { + pub_key_modulus = vec![0u8; 60]; + buf.read_exact(&mut pub_key_modulus).map_err(CertificateError::IOError)?; + } + } + Ok(Certificate { + signer_key_type, + signature, + signature_issuer, + pub_key_type, + child_cert_identity, + pub_key_id, + pub_key_modulus, + pub_key_exponent + }) + } + + /// Dumps the data in a Certificate back into binary data that can be written to a file. + pub fn to_bytes(&self) -> Result, std::io::Error> { + let mut buf: Vec = Vec::new(); + match self.signer_key_type { + CertificateKeyType::Rsa4096 => { buf.write_u32::(0x00010000)? }, + CertificateKeyType::Rsa2048 => { buf.write_u32::(0x00010001)? }, + CertificateKeyType::ECC => { buf.write_u32::(0x00010002)? }, + } + buf.write_all(&self.signature)?; + // Pad to nearest 64 bytes after the signature. + buf.resize(0x40 + self.signature.len(), 0); + buf.write_all(&self.signature_issuer)?; + match self.pub_key_type { + CertificateKeyType::Rsa4096 => { buf.write_u32::(0x0000000)? }, + CertificateKeyType::Rsa2048 => { buf.write_u32::(0x00000001)? }, + CertificateKeyType::ECC => { buf.write_u32::(0x00000002)? }, + } + buf.write_all(&self.child_cert_identity)?; + buf.write_u32::(self.pub_key_id)?; + buf.write_all(&self.pub_key_modulus)?; + // The key exponent is only used for the RSA keys and not ECC keys, so only write it out + // if this is one of those two key types. + if matches!(self.pub_key_type, CertificateKeyType::Rsa4096) || + matches!(self.pub_key_type, CertificateKeyType::Rsa2048) { + buf.write_u32::(self.pub_key_exponent)?; + } + // Pad the certificate data out to the nearest multiple of 64. + buf.resize((buf.len() + 63) & !63, 0); + Ok(buf) + } + + pub fn signature_issuer(&self) -> String { + String::from_utf8_lossy(&self.signature_issuer).trim_end_matches('\0').to_owned() + } + + pub fn child_cert_identity(&self) -> String { + String::from_utf8_lossy(&self.child_cert_identity).trim_end_matches('\0').to_owned() + } + + pub fn pub_key_modulus(&self) -> Vec { + self.pub_key_modulus.clone() + } + + pub fn pub_key_exponent(&self) -> u32 { + self.pub_key_exponent + } +} + +#[derive(Debug)] +pub struct CertificateChain { + ca_cert: Certificate, + tmd_cert: Certificate, + ticket_cert: Certificate, +} + +impl CertificateChain { + pub fn from_bytes(data: &[u8]) -> Result { + let mut buf = Cursor::new(data); + let mut offset: u64 = 0; + let mut ca_cert: Option = None; + let mut tmd_cert: Option = None; + let mut ticket_cert: Option = None; + // Iterate 3 times, because the chain should contain 3 certs. + for _ in 0..3 { + buf.seek(SeekFrom::Start(offset)).map_err(CertificateError::IOError)?; + let signer_key_type = buf.read_u32::().map_err(CertificateError::IOError)?; + let signature_len = match signer_key_type { + 0x00010000 => 512, // 0x200 + 0x00010001 => 256, // 0x100 + 0x00010002 => 60, + _ => return Err(CertificateError::InvalidSignatureKeyType(signer_key_type)) + }; + buf.seek(SeekFrom::Start(offset + 0x80 + signature_len)).map_err(CertificateError::IOError)?; + let pub_key_type = buf.read_u32::().map_err(CertificateError::IOError)?; + let pub_key_len = match pub_key_type { + 0x00000000 => 568, // 0x238 + 0x00000001 => 312, // 0x138 + 0x00000002 => 120, + _ => return Err(CertificateError::InvalidContainedKeyType(pub_key_type)) + }; + // Cert size is the base length (0xC8) + the signature length + the public key length. + // Like a lot of values, it needs to be rounded to the nearest multiple of 64. + let cert_size = (0xC8 + signature_len + pub_key_len + 63) & !63; + buf.seek(SeekFrom::End(0)).map_err(CertificateError::IOError)?; + buf.seek(SeekFrom::Start(offset)).map_err(CertificateError::IOError)?; + let mut cert_buf = vec![0u8; cert_size as usize]; + buf.read_exact(&mut cert_buf).map_err(CertificateError::IOError)?; + let cert = Certificate::from_bytes(&cert_buf)?; + let issuer_name = String::from_utf8_lossy(&cert.signature_issuer).trim_end_matches('\0').to_owned(); + if issuer_name.eq("Root") { + ca_cert = Some(cert.clone()); + } else if issuer_name.contains("Root-CA") { + let child_name = String::from_utf8_lossy(&cert.child_cert_identity).trim_end_matches('\0').to_owned(); + if child_name.contains("CP") { + tmd_cert = Some(cert.clone()); + } else if child_name.contains("XS") { + ticket_cert = Some(cert.clone()); + } else { + return Err(CertificateError::UnknownCertificate); + } + } else { + return Err(CertificateError::UnknownCertificate); + } + offset += cert_size; + } + if ca_cert.is_none() { return Err(CertificateError::MissingCertificate("CA".to_owned())) } + if tmd_cert.is_none() { return Err(CertificateError::MissingCertificate("TMD".to_owned())) } + if ticket_cert.is_none() { return Err(CertificateError::MissingCertificate("Ticket".to_owned())) } + Ok(CertificateChain { + ca_cert: ca_cert.unwrap(), + tmd_cert: tmd_cert.unwrap(), + ticket_cert: ticket_cert.unwrap(), + }) + } + + pub fn from_certs(ca_cert: Certificate, tmd_cert: Certificate, ticket_cert: Certificate) -> Result { + if String::from_utf8_lossy(&ca_cert.signature_issuer).trim_end_matches('\0').ne("Root") { + return Err(CertificateError::IncorrectCertificate("CA".to_owned())); + } + if !String::from_utf8_lossy(&tmd_cert.child_cert_identity).trim_end_matches('\0').contains("CP") { + return Err(CertificateError::IncorrectCertificate("TMD".to_owned())); + } + if !String::from_utf8_lossy(&ticket_cert.child_cert_identity).contains("XS") { + return Err(CertificateError::IncorrectCertificate("Ticket".to_owned())); + } + Ok(CertificateChain { + ca_cert, + tmd_cert, + ticket_cert, + }) + } + + pub fn to_bytes(&self) -> Result, std::io::Error> { + let mut buf: Vec = Vec::new(); + buf.write_all(&self.ca_cert().to_bytes()?)?; + buf.write_all(&self.tmd_cert().to_bytes()?)?; + buf.write_all(&self.ticket_cert().to_bytes()?)?; + Ok(buf) + } + + pub fn ca_cert(&self) -> Certificate { + self.ca_cert.clone() + } + + pub fn tmd_cert(&self) -> Certificate { + self.tmd_cert.clone() + } + + pub fn ticket_cert(&self) -> Certificate { + self.ticket_cert.clone() + } +} + +/// Verifies a Wii CA certificate (either CA00000001 for retail or CA00000002 for development) using +/// the root keys. +pub fn verify_ca_cert(ca_cert: &Certificate) -> Result { + // Reject if the issuer isn't "Root" and this isn't one of the CA certs. + if String::from_utf8_lossy(&ca_cert.signature_issuer).trim_end_matches('\0').ne("Root") || + !String::from_utf8_lossy(&ca_cert.child_cert_identity).contains("CA") { + return Err(CertificateError::IncorrectCertificate("CA".to_owned())); + } + let root_key = if String::from_utf8_lossy(&ca_cert.child_cert_identity).trim_end_matches('\0').eq("CA00000001") { + // Include key str from local file. + let retail_pem = include_str!("keys/retail-pub.pem"); + RsaPublicKey::from_public_key_pem(retail_pem).unwrap() + } else if String::from_utf8_lossy(&ca_cert.child_cert_identity).trim_end_matches('\0').eq("CA00000002") { + // Include key str from local file. + let dev_pem = include_str!("keys/dev-pub.pem"); + RsaPublicKey::from_public_key_pem(dev_pem).unwrap() + } else { + return Err(CertificateError::UnknownCertificate); + }; + let mut hasher = Sha1::new(); + let cert_body = ca_cert.to_bytes().unwrap(); + hasher.update(&cert_body[576..]); + let cert_hash = hasher.finalize().as_slice().to_owned(); + match root_key.verify(Pkcs1v15Sign::new::(), &cert_hash, ca_cert.signature.as_slice()) { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } +} + +/// Verifies a TMD or Ticket signing certificate using a CA certificate. The CA certificate and +/// child certificate being verified must match, or this function will return an error without +/// attempting signature verification. +pub fn verify_child_cert(ca_cert: &Certificate, child_cert: &Certificate) -> Result { + if ca_cert.signature_issuer().ne("Root") || !ca_cert.child_cert_identity().contains("CA") { + return Err(CertificateError::IncorrectCertificate("CA".to_owned())); + } + if format!("Root-{}", ca_cert.child_cert_identity()).ne(&child_cert.signature_issuer()) { + return Err(CertificateError::NonMatchingCertificates) + } + let mut hasher = Sha1::new(); + let cert_body = child_cert.to_bytes().unwrap(); + hasher.update(&cert_body[320..]); + let cert_hash = hasher.finalize().as_slice().to_owned(); + let public_key_modulus = BigUint::from_bytes_be(&ca_cert.pub_key_modulus()); + let public_key_exponent = BigUint::from(ca_cert.pub_key_exponent()); + let root_key = RsaPublicKey::new(public_key_modulus, public_key_exponent).unwrap(); + match root_key.verify(Pkcs1v15Sign::new::(), &cert_hash, child_cert.signature.as_slice()) { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } +} + +/// Verifies the signature of a TMD using a TMD signing certificate. The TMD certificate must match +/// the certificate used to sign the TMD, or this function will return an error without attempting +/// signature verification. +pub fn verify_tmd(tmd_cert: &Certificate, tmd: &tmd::TMD) -> Result { + if !tmd_cert.signature_issuer().contains("Root-CA") || !tmd_cert.child_cert_identity().contains("CP") { + return Err(CertificateError::IncorrectCertificate("TMD".to_owned())); + } + if format!("{}-{}", tmd_cert.signature_issuer(), tmd_cert.child_cert_identity()).ne(&tmd.signature_issuer()) { + return Err(CertificateError::NonMatchingCertificates) + } + let mut hasher = Sha1::new(); + let tmd_body = tmd.to_bytes().unwrap(); + hasher.update(&tmd_body[320..]); + let tmd_hash = hasher.finalize().as_slice().to_owned(); + let public_key_modulus = BigUint::from_bytes_be(&tmd_cert.pub_key_modulus()); + let public_key_exponent = BigUint::from(tmd_cert.pub_key_exponent()); + let root_key = RsaPublicKey::new(public_key_modulus, public_key_exponent).unwrap(); + match root_key.verify(Pkcs1v15Sign::new::(), &tmd_hash, tmd.signature.as_slice()) { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } +} + +/// Verifies the signature of a Ticket using a Ticket signing certificate. The Ticket certificate +/// must match the certificate used to sign the Ticket, or this function will return an error +/// without attempting signature verification. +pub fn verify_ticket(ticket_cert: &Certificate, ticket: &ticket::Ticket) -> Result { + if !ticket_cert.signature_issuer().contains("Root-CA") || !ticket_cert.child_cert_identity().contains("XS") { + return Err(CertificateError::IncorrectCertificate("Ticket".to_owned())); + } + if format!("{}-{}", ticket_cert.signature_issuer(), ticket_cert.child_cert_identity()).ne(&ticket.signature_issuer()) { + return Err(CertificateError::NonMatchingCertificates) + } + let mut hasher = Sha1::new(); + let tmd_body = ticket.to_bytes().unwrap(); + hasher.update(&tmd_body[320..]); + let tmd_hash = hasher.finalize().as_slice().to_owned(); + let public_key_modulus = BigUint::from_bytes_be(&ticket_cert.pub_key_modulus()); + let public_key_exponent = BigUint::from(ticket_cert.pub_key_exponent()); + let root_key = RsaPublicKey::new(public_key_modulus, public_key_exponent).unwrap(); + match root_key.verify(Pkcs1v15Sign::new::(), &tmd_hash, ticket.signature.as_slice()) { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } +} diff --git a/src/title/keys/dev-pub.pem b/src/title/keys/dev-pub.pem new file mode 100644 index 0000000..0468f95 --- /dev/null +++ b/src/title/keys/dev-pub.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0B/hANQ1VrJLVtrpcbWl +04S5MAO+G78oojBbBgZFRn1bAlHSVhonT56fnOxkYVCrPSrjNmhmrKS66Brj15qm +sEqLy6fm+2SJRevf24W6CR/X0RS1o6eA46Iubs2HtaTG+RDkAyIIgUsM7qGhffc5 +aV9hfvY1KNuUljegVgN/ezJBOJXAqPGYLhVl447twi5ZDuJne4YJ9IwuMD+8QFys +GAQvgiCE5JNoA9p/QTSSSFYrjuEvePgDJGMwvHvn7nJK9FikcuerRqGnwQwvGPoH +w93YmAahHJzBMLJHozyNR95n8p5Vd7EcQ0k9W7p2NKfk5xUxt99Zgf4koRRVTL2P +AFzh2zUIXM/HeAa23iVAaKJstUktRYBDj+Hlqe11xe1FHc54lDnMw7ooojEqG4cZ +7w9ztxOVDAJZGnRipgfzfAqnoY+pQ6NtdSpfQZLwE2EAqpy0G74UvrH5/Gkv36CU +Rt5and4spfaMHAwhQpKHyy2qo9JjdS9z4J+vRHnSgXQp9pgAr95rWS3BmIK99YHM +q/LLkQKe81xM/bv/ScH6Gy/jHeelYOy0frz+MkJblW+BtpkXSH47eJFR2y54sf0u +vn5iaz6hZbT7AMy3Ua9QcynEo5Oept2cUKDnOGsBRXlrQa9h94VVlE87wi3DvQ0A ++HmKQrGqoIMgZZrHOVq08ykCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/src/title/keys/retail-pub.pem b/src/title/keys/retail-pub.pem new file mode 100644 index 0000000..efb4fbd --- /dev/null +++ b/src/title/keys/retail-pub.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+CRsWLrnUAMB+7fC6+AB +BXHakiN48FFOwAMd0NIe09B+/IUgabXem7lRqLyQokSSbTeSla6UNqqmowJRDHsd +7dX7IIadfzAW9r5l04OhbbMyG5U1GJCxcAKTfuGT9X6ZokdOnTgkx67jhUH1Z+dR +jHoOOOfrr0EZG8/xe0KmtO3mzo3nMY9/UgSzmQ4iZ0Wv1IWyRJMAiwjH9rflawKz +6P4MnYWcuLaCI7irJ+5fZTgHiy25HioVPoWBgHKiO23ZMoEFT2+w9vWtKD7KC3rz +VFXgPae2gybz7INK8xQEisbfINKFCGc8q2Kix7wTGlM+C2aAaxwwZks3IzG9xLDK +2NEe57vZKFVIquwfZughs8igR2kAxeaI6AzOPGHWnLuhN8ZgT3py3Yx7Pj1RKQ2q +all7CB+dNjOjRno1YQmsp919Li+ywa644g9Ikti5+LRvTjwR9PR9i3V9/v6jiZwz +WVxe/evLq+hBPjqagDxpNW6ysq1cxMhYRV7197MGRLR8ZAaM34CfdgJaLbRG4D18 +9i805wJFewKkz12d1TylOnymKXiMZ8oIv+zKQ6lXrRbJThzYdcoQfc5+ARjw32v+ +5R3b2ZHCbmDNSFiqWSyCAHXyn1JskXxv5UA+p9SlDOw7c4TeiG6C0utNTkK18rFJ +qB6nznFE3CmUz8ROH5HL1JUCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/src/title/mod.rs b/src/title/mod.rs index 7c84315..c3c32c0 100644 --- a/src/title/mod.rs +++ b/src/title/mod.rs @@ -17,10 +17,12 @@ use std::fmt; #[derive(Debug)] pub enum TitleError { + BadCertChain, BadTicket, BadTMD, BadContent, InvalidWAD, + CertificateError(cert::CertificateError), TMDError(tmd::TMDError), TicketError(ticket::TicketError), WADError(wad::WADError), @@ -30,10 +32,12 @@ pub enum TitleError { impl fmt::Display for TitleError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let description = match *self { + TitleError::BadCertChain => "The provided certificate chain data was invalid.", TitleError::BadTicket => "The provided Ticket data was invalid.", TitleError::BadTMD => "The provided TMD data was invalid.", TitleError::BadContent => "The provided content data was invalid.", TitleError::InvalidWAD => "The provided WAD data was invalid.", + TitleError::CertificateError(_) => "An error occurred while processing certificate data.", TitleError::TMDError(_) => "An error occurred while processing TMD data.", TitleError::TicketError(_) => "An error occurred while processing ticket data.", TitleError::WADError(_) => "A WAD could not be built from the provided data.", @@ -47,7 +51,7 @@ impl Error for TitleError {} #[derive(Debug)] pub struct Title { - cert_chain: Vec, + pub cert_chain: cert::CertificateChain, crl: Vec, pub ticket: ticket::Ticket, pub tmd: tmd::TMD, @@ -57,11 +61,12 @@ pub struct Title { impl Title { pub fn from_wad(wad: &wad::WAD) -> Result { + let cert_chain = cert::CertificateChain::from_bytes(&wad.cert_chain()).map_err(|_| TitleError::BadCertChain)?; let ticket = ticket::Ticket::from_bytes(&wad.ticket()).map_err(|_| TitleError::BadTicket)?; let tmd = tmd::TMD::from_bytes(&wad.tmd()).map_err(|_| TitleError::BadTMD)?; let content = content::ContentRegion::from_bytes(&wad.content(), tmd.content_records.clone()).map_err(|_| TitleError::BadContent)?; let title = Title { - cert_chain: wad.cert_chain(), + cert_chain, crl: wad.crl(), ticket, tmd, @@ -138,13 +143,26 @@ impl Title { let title_size_bytes = self.title_size(absolute)?; Ok((title_size_bytes as f64 / 131072.0).ceil() as usize) } - - pub fn cert_chain(&self) -> Vec { - self.cert_chain.clone() + + /// Verifies entire certificate chain, and then the TMD and Ticket. Returns true if the title + /// is entirely valid, or false if any component of the verification fails. + pub fn verify(&self) -> Result { + if !cert::verify_ca_cert(&self.cert_chain.ca_cert()).map_err(TitleError::CertificateError)? { + return Ok(false) + } + if !cert::verify_child_cert(&self.cert_chain.ca_cert(), &self.cert_chain.tmd_cert()).map_err(TitleError::CertificateError)? || + !cert::verify_child_cert(&self.cert_chain.ca_cert(), &self.cert_chain.ticket_cert()).map_err(TitleError::CertificateError)? { + return Ok(false) + } + if !cert::verify_tmd(&self.cert_chain.tmd_cert(), &self.tmd).map_err(TitleError::CertificateError)? || + !cert::verify_ticket(&self.cert_chain.ticket_cert(), &self.ticket).map_err(TitleError::CertificateError)? { + return Ok(false) + } + Ok(true) } - pub fn set_cert_chain(&mut self, cert_chain: &[u8]) { - self.cert_chain = cert_chain.to_vec(); + pub fn set_cert_chain(&mut self, cert_chain: cert::CertificateChain) { + self.cert_chain = cert_chain; } pub fn crl(&self) -> Vec { diff --git a/src/title/ticket.rs b/src/title/ticket.rs index e191e37..345801c 100644 --- a/src/title/ticket.rs +++ b/src/title/ticket.rs @@ -220,4 +220,8 @@ impl Ticket { } Ok(()) } + + pub fn signature_issuer(&self) -> String { + String::from_utf8_lossy(&self.signature_issuer).trim_end_matches('\0').to_owned() + } } diff --git a/src/title/tmd.rs b/src/title/tmd.rs index 20c0378..4b0e45e 100644 --- a/src/title/tmd.rs +++ b/src/title/tmd.rs @@ -337,4 +337,8 @@ impl TMD { AccessRight::DVDVideo => (self.access_rights & (1 << 1)) != 0, } } + + pub fn signature_issuer(&self) -> String { + String::from_utf8_lossy(&self.signature_issuer).trim_end_matches('\0').to_owned() + } } diff --git a/src/title/wad.rs b/src/title/wad.rs index f2f4dea..d65e986 100644 --- a/src/title/wad.rs +++ b/src/title/wad.rs @@ -8,7 +8,7 @@ use std::fmt; use std::str; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; -use crate::title::{tmd, ticket, content}; +use crate::title::{cert, tmd, ticket, content}; use crate::title::ticket::TicketError; use crate::title::tmd::TMDError; @@ -103,10 +103,10 @@ impl WADHeader { } impl WADBody { - pub fn from_parts(cert_chain: &[u8], crl: &[u8], ticket: &ticket::Ticket, tmd: &tmd::TMD, + pub fn from_parts(cert_chain: &cert::CertificateChain, crl: &[u8], ticket: &ticket::Ticket, tmd: &tmd::TMD, content: &content::ContentRegion, meta: &[u8]) -> Result { let body = WADBody { - cert_chain: cert_chain.to_vec(), + cert_chain: cert_chain.to_bytes().map_err(WADError::IOError)?, crl: crl.to_vec(), ticket: ticket.to_bytes().map_err(WADError::IOError)?, tmd: tmd.to_bytes().map_err(WADError::IOError)?, @@ -196,7 +196,7 @@ impl WAD { Ok(wad) } - pub fn from_parts(cert_chain: &[u8], crl: &[u8], ticket: &ticket::Ticket, tmd: &tmd::TMD, + pub fn from_parts(cert_chain: &cert::CertificateChain, crl: &[u8], ticket: &ticket::Ticket, tmd: &tmd::TMD, content: &content::ContentRegion, meta: &[u8]) -> Result { let body = WADBody::from_parts(cert_chain, crl, ticket, tmd, content, meta)?; let header = WADHeader::from_body(&body)?;