mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2025-06-05 23:11:02 -04:00
Added signature verification, info command now has WiiPy feature parity
Some checks are pending
Build rustii / build-linux-x86_64 (push) Waiting to run
Build rustii / build-macos-arm64 (push) Waiting to run
Build rustii / build-macos-x86_64 (push) Waiting to run
Build rustii / build-windows-x86_64 (push) Waiting to run
Some checks are pending
Build rustii / build-linux-x86_64 (push) Waiting to run
Build rustii / build-macos-arm64 (push) Waiting to run
Build rustii / build-macos-x86_64 (push) Waiting to run
Build rustii / build-windows-x86_64 (push) Waiting to run
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.
This commit is contained in:
parent
444c3def54
commit
edf3af0f7c
274
Cargo.lock
generated
274
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"] }
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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<String> {
|
||||
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<cert::Certificate>) {
|
||||
// Print all important keys from the TMD.
|
||||
println!("Title Info");
|
||||
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));
|
||||
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<cert::Certificate>) {
|
||||
// Print all important keys from the Ticket.
|
||||
println!("Ticket Info");
|
||||
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()));
|
||||
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();
|
||||
|
@ -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<PathBuf> = 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.
|
||||
|
@ -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<u8>,
|
||||
signature_issuer: [u8; 64],
|
||||
pub_key_type: CertificateKeyType,
|
||||
child_cert_identity: [u8; 64],
|
||||
pub_key_id: u32,
|
||||
pub_key_modulus: Vec<u8>,
|
||||
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<Self, CertificateError> {
|
||||
let mut buf = Cursor::new(data);
|
||||
let signer_key_type_int = buf.read_u32::<BigEndian>().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::<BigEndian>().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::<BigEndian>().map_err(CertificateError::IOError)?;
|
||||
let mut pub_key_modulus: Vec<u8>;
|
||||
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::<BigEndian>().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::<BigEndian>().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<Vec<u8>, std::io::Error> {
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
match self.signer_key_type {
|
||||
CertificateKeyType::Rsa4096 => { buf.write_u32::<BigEndian>(0x00010000)? },
|
||||
CertificateKeyType::Rsa2048 => { buf.write_u32::<BigEndian>(0x00010001)? },
|
||||
CertificateKeyType::ECC => { buf.write_u32::<BigEndian>(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::<BigEndian>(0x0000000)? },
|
||||
CertificateKeyType::Rsa2048 => { buf.write_u32::<BigEndian>(0x00000001)? },
|
||||
CertificateKeyType::ECC => { buf.write_u32::<BigEndian>(0x00000002)? },
|
||||
}
|
||||
buf.write_all(&self.child_cert_identity)?;
|
||||
buf.write_u32::<BigEndian>(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::<BigEndian>(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<u8> {
|
||||
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<CertificateChain, CertificateError> {
|
||||
let mut buf = Cursor::new(data);
|
||||
let mut offset: u64 = 0;
|
||||
let mut ca_cert: Option<Certificate> = None;
|
||||
let mut tmd_cert: Option<Certificate> = None;
|
||||
let mut ticket_cert: Option<Certificate> = 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::<BigEndian>().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::<BigEndian>().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<Self, CertificateError> {
|
||||
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<Vec<u8>, std::io::Error> {
|
||||
let mut buf: Vec<u8> = 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<bool, CertificateError> {
|
||||
// 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::<Sha1>(), &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<bool, CertificateError> {
|
||||
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::<Sha1>(), &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<bool, CertificateError> {
|
||||
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::<Sha1>(), &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<bool, CertificateError> {
|
||||
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::<Sha1>(), &tmd_hash, ticket.signature.as_slice()) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
|
14
src/title/keys/dev-pub.pem
Normal file
14
src/title/keys/dev-pub.pem
Normal file
@ -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-----
|
14
src/title/keys/retail-pub.pem
Normal file
14
src/title/keys/retail-pub.pem
Normal file
@ -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-----
|
@ -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<u8>,
|
||||
pub cert_chain: cert::CertificateChain,
|
||||
crl: Vec<u8>,
|
||||
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<Title, TitleError> {
|
||||
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,
|
||||
@ -139,12 +144,25 @@ impl Title {
|
||||
Ok((title_size_bytes as f64 / 131072.0).ceil() as usize)
|
||||
}
|
||||
|
||||
pub fn cert_chain(&self) -> Vec<u8> {
|
||||
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<bool, TitleError> {
|
||||
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<u8> {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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<WADBody, WADError> {
|
||||
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<WAD, WADError> {
|
||||
let body = WADBody::from_parts(cert_chain, crl, ticket, tmd, content, meta)?;
|
||||
let header = WADHeader::from_body(&body)?;
|
||||
|
Loading…
x
Reference in New Issue
Block a user