commit a3f2361ad28a3e45841f854858f071c0c62357b4 Author: Spotlight Date: Thu Dec 30 02:56:07 2021 -0600 Add initial certificate patching diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f498b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +cache/ +WSC-Patcher +output/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..2596cc8 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# WSC-Patcher + +WSC-Patcher applies patches to the Wii Shop Channel, such as the ability to use your own servers and certificates. +This is useful for research and development of services utilizing the WSC. + +It is important to read the following so that you will have a usable WAD available upon patch completion. + +## Setup +You will need an externally resolvable domain with four subdomains: + - `oss-auth`, utilized for the Wii Shop's main HTML + - `ecs`, utilized for ticket syncing and title purchases + - `ias`, utilized for user registration + - `ccs`, utilized to download titles + +The domain must be equal to or smaller than `shop.wii.com` in length, so 12 characters. + +If you do not plan to interact with EC, and plan to solely utilize HTML/JS components of the Wii Shop Channel, only configuring `oss-auth` is acceptable. + +If you do not have a domain available, you are welcome to utilize `a.taur.cloud` as the base domain. +This domain resolves to `127.0.0.1`, usable within Dolphin. +It is guaranteed `oss-auth`, `ecs`, `ias`, `cas` (cataloguing, within DLC titles), and `ccs`/`ucs` (cached/uncached content servers) are available. + +You may choose to specify a root certificate you already have configured on a server. If so, please provide the public key of the CA within the file `output/root.crt` in DER form. +If not, one will be generated for you. + +## Operation +Invoke WSC-Patcher similar to the following: +``` +./WSC-Patcher +``` + +Throughout its operation, the patcher will perform the following: + - Version 20 (latest, as of writing) of the Wii Shop Channel will be downloaded to `cache/original.wad`. + - If `output/ca.crt` is not present, a 2048-bit (RSA), SHA-1 CA certificate will be generated. + - At the same time, `*.` will be issued for ease of use. + - Modifications are made to the application's main `.arc` (within content index 2) to permit Opera loading the base domain. + - Patches to the application's main dol are also performed. Please see `patches.go` for more information on what these contain. + - The patched WAD is encrypted and written to disk. + \ No newline at end of file diff --git a/cert_store.go b/cert_store.go new file mode 100644 index 0000000..e88501b --- /dev/null +++ b/cert_store.go @@ -0,0 +1,101 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "log" + "math/big" + "time" +) + +// generateSerial generates a random serial number for our issued certificates. +// It is taken from golang std: src/crypto/tls/generate_cert.go +// Direct permalink on GitHub: https://git.io/JyyDw +func generateSerial() *big.Int { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + log.Fatalf("Failed to generate serial number: %v", err) + } + + return serialNumber +} + +func createCertificates() []byte { + //////////////////////////////////// + // Generate root CA // + //////////////////////////////////// + rootCA := x509.Certificate{ + SignatureAlgorithm: x509.SHA1WithRSA, + SerialNumber: generateSerial(), + Subject: pkix.Name{ + CommonName: "Open Shop Channel CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + KeyUsage: x509.KeyUsageCertSign, + BasicConstraintsValid: true, + IsCA: true, + } + + // Sadly, 2048 bits can cause compatability issues with IOS. We must use 1024. + // TODO(spotlightishere): Is it possible to raise to 2048 anyway? + rootPriv, err := rsa.GenerateKey(rand.Reader, 1024) + check(err) + + rootCertBytes, err := x509.CreateCertificate(rand.Reader, &rootCA, &rootCA, &rootPriv.PublicKey, rootPriv) + check(err) + + //////////////////////////////////// + // Issue server TLS certificate // + //////////////////////////////////// + serverCert := x509.Certificate{ + SignatureAlgorithm: x509.SHA1WithRSA, + SerialNumber: generateSerial(), + // We'll issue with a primary common name for our base domain. + Subject: pkix.Name{ + CommonName: baseDomain, + }, + // The SAN will be a wildcard for our base domain, as it cannot be the CN. + DNSNames: []string{ + "*." + baseDomain, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + // TODO: what's non-repudiation + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + IsCA: false, + MaxPathLenZero: true, + } + + serverPriv, err := rsa.GenerateKey(rand.Reader, 2048) + check(err) + + serverCertBytes, err := x509.CreateCertificate(rand.Reader, &serverCert, &rootCA, &rootPriv.PublicKey, serverPriv) + check(err) + + //////////////////////////// + // Persist certificates // + //////////////////////////// + rootCertPem := pemEncode("CERTIFICATE", rootCertBytes) + rootKeyPem := pemEncode("RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(rootPriv)) + serverCertPem := pemEncode("CERTIFICATE", serverCertBytes) + serverKeyPem := pemEncode("RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(serverPriv)) + + writeOut("root.pem", rootCertPem) + writeOut("root.cer", rootCertBytes) + writeOut("root.key", rootKeyPem) + writeOut("server.pem", serverCertPem) + writeOut("server.key", serverKeyPem) + + return rootCertBytes +} + +func pemEncode(typeName string, bytes []byte) []byte { + block := pem.Block{Type: typeName, Bytes: bytes} + return pem.EncodeToMemory(&block) +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..0ffae14 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,6 @@ +# Docs +This directory contains documentation about given patches applied. + +Contents: + - `opcacrt6.yml`: A [Kaitai](https://kaitai.io) structure describing a very basic `opcacrt6.dat`. +It does not attempt to handle things such as client certificates or user passwords. \ No newline at end of file diff --git a/docs/opcacrt6.yml b/docs/opcacrt6.yml new file mode 100644 index 0000000..1d98b94 --- /dev/null +++ b/docs/opcacrt6.yml @@ -0,0 +1,75 @@ +meta: + id: dat + file-extension: dat + endian: be + # Derived from + # https://web.archive.org/web/20090208111457/http://www.opera.com/docs/fileformats/ +seq: + - id: file_version_number + type: u4 + - id: app_version_number + type: u4 + - id: idtag_length + # Asserted to always have a tag length of 1. + contents: [0x00, 0x01] + - id: length_length + # Asserted to always have a length of 4. + contents: [0x00, 0x04] + - id: tags + type: tag + # repeat: eos + +types: + tag: + seq: + - id: tag_type + type: u1 + enum: tag_types + - id: tag_size + type: u4 + - id: tag_contents + type: + switch-on: tag_type + cases: + 'tag_types::ca_certificate': ca_tag + + # All of this should technically not be sequential, but it always is for our purposes. + # If you wish to adapt this for other Opera usage, please adjust accordingly! + ca_tag: + seq: + - id: cert_type_tag + # 'tag_types::ssl_cert_type' + contents: [0x20] + - id: cert_type_length + type: u4 + - id: cert_type + type: u4 + - id: cert_name_tag + contents: [0x21] + - id: cert_name_length + type: u4 + - id: cert_name + type: str + size: cert_name_length + encoding: UTF-8 + - id: cert_subject_tag + contents: [0x22] + - id: cert_subject_length + type: u4 + - id: cert_subject + size: cert_subject_length + - id: cert_contents_tag + contents: [0x23] + - id: cert_contents_length + type: u4 + - id: cert_contents + size: cert_contents_length + +enums: + tag_types: + 0x02: ca_certificate + 0x03: user_certificate + 0x04: user_password + 0x20: ssl_cert_type + 0x21: ssl_cert_name + 0x22: ssl_cert_subject diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6212ec5 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/OpenShopChannel/WSC-Patcher + +go 1.17 + +require ( + github.com/wii-tools/GoNUSD v0.2.1 + github.com/wii-tools/arclib v1.0.0 + github.com/wii-tools/wadlib v0.3.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a0db38f --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/wii-tools/GoNUSD v0.2.1 h1:HW8vssrEiCdFjiugCx3cTTaPw9nlz1TWxzeCqXKqTnQ= +github.com/wii-tools/GoNUSD v0.2.1/go.mod h1:jMYMU5X81Hp0R+bq6/0AxAOijGHXfexaG6dLRilgS2s= +github.com/wii-tools/arclib v1.0.0 h1:OAmbL3NDUmlR0wa1VpJhxnKiIRFZz1CC40lTVPUm8Ms= +github.com/wii-tools/arclib v1.0.0/go.mod h1:uXFan/NSXoQ2pOVPN4ugZ4nJX7esBnjB1QUgVrEzK/4= +github.com/wii-tools/wadlib v0.3.0 h1:ZPCjuiwn9AG2og5paVeP7eH3nfjWC2iL4u8WW5nOJ4U= +github.com/wii-tools/wadlib v0.3.0/go.mod h1:GK+f2POk+rVu1p4xqLSb4ll1SKKbfOO6ZAB+oPLV3uQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..38aa4a2 --- /dev/null +++ b/main.go @@ -0,0 +1,122 @@ +package main + +import ( + "errors" + "fmt" + "github.com/wii-tools/GoNUSD" + "github.com/wii-tools/arclib" + "github.com/wii-tools/wadlib" + "io/fs" + "log" + "os" +) + +// baseDomain holds our needed base domain. +var baseDomain string + +// mainDol holds the main DOL - our content at index 1. +var mainDol []byte + +// mainArc holds the main ARC - our content at index 2. +var mainArc *arclib.ARC + +// filePresent returns whether the specified path is present on disk. +func filePresent(path string) bool { + _, err := os.Stat(path) + return errors.Is(err, fs.ErrNotExist) == false +} + +// createDir creates a directory at the given path if it is not already present. +func createDir(path string) { + if !filePresent(path) { + os.Mkdir(path, 0755) + } +} + +func main() { + if len(os.Args) != 2 { + fmt.Printf("Usage: %s \n", os.Args[0]) + fmt.Println("For more information, please refer to the README.") + os.Exit(-1) + } + + baseDomain = os.Args[1] + if len(baseDomain) > 12 { + fmt.Println("The given base domain must not exceed 12 characters.") + fmt.Println("For more information, please refer to the README.") + os.Exit(-1) + } + + fmt.Println("===========================") + fmt.Println("= WSC-Patcher =") + fmt.Println("===========================") + + // Create directories we may need later. + createDir("./output") + createDir("./cache") + + var originalWad *wadlib.WAD + var err error + + // Determine whether the Wii Shop Channel is cached. + if !filePresent("./cache/original.wad") { + log.Println("Downloading a copy of the original Wii Shop Channel, please wait...") + originalWad, err = GoNUSD.Download(0x00010002_48414241, 21, true) + check(err) + + // Cache this downloaded WAD to disk. + contents, err := originalWad.GetWAD(wadlib.WADTypeCommon) + check(err) + + os.WriteFile("./cache/original.wad", contents, 0755) + } else { + originalWad, err = wadlib.LoadWADFromFile("./cache/original.wad") + check(err) + } + + // Determine whether a certificate authority was provided, or generated previously. + if !filePresent("./output/root.cer") { + log.Println("Generating root certificates...") + createCertificates() + } + + // Load main DOL + mainDol, err = originalWad.GetContent(1) + check(err) + + // Load main ARC + arcData, err := originalWad.GetContent(2) + check(err) + mainArc, err = arclib.Load(arcData) + check(err) + + // Generate filter list and certificate store + log.Println("Applying Opera patches...") + modifyAllowList() + generateOperaCertStore() + + // Save main ARC + updated, err := mainArc.Save() + check(err) + err = originalWad.UpdateContent(2, updated) + check(err) + + // Generate a patched WAD with our changes + output, err := originalWad.GetWAD(wadlib.WADTypeCommon) + check(err) + + log.Println("Done! Install ./output/patched.wad, sit back, and enjoy.") + writeOut("patched.wad", output) +} + +// check has an anxiety attack if things go awry. +func check(err error) { + if err != nil { + panic(err) + } +} + +// writeOut writes a file with the given name and contents to the output folder. +func writeOut(filename string, contents []byte) { + os.WriteFile("./output/"+filename, contents, 0755) +} diff --git a/modify_arc.go b/modify_arc.go new file mode 100644 index 0000000..9d63fdd --- /dev/null +++ b/modify_arc.go @@ -0,0 +1,125 @@ +package main + +import ( + "crypto/x509" + "encoding/binary" + "fmt" + "io/ioutil" +) + +// modifyAllowList patches the Opera filter to include our custom base domain. +func modifyAllowList() { + file, err := mainArc.OpenFile("arc/opera/myfilter.ini") + check(err) + + // TODO(spotlightishere): Find an INI parser that handles reading an array from a section + // As I could not - and I spent a good while looking - and do not want to implement my own parser, + // no matter how rudimentary - I've copied the original file as a template verbatim. We only make one edit, + // adding the base domain. + filter := fmt.Sprintf(`[prefs] +prioritize excludelist=0 + +[include] +file:/cnt/* +https://*.%s/* +miip:* + +[exclude] +*`, baseDomain) + + file.Write([]byte(filter)) +} + +// Tag represents a single byte representing a tag's ID. +type Tag byte + +const ( + TagSSLCertType = 0x20 + TagSSLCertName = 0x21 + TagSSLCertSubject = 0x22 + TagSSLCertContents = 0x23 + + TagCACertificate = 0x02 + TagUserCertificate = 0x03 + TagUserPassword = 0x04 +) + +// generateTag generates a byte representation of a tag and contents. +func generateTag(tag Tag, tagContents []byte) []byte { + // Tag ID + contents := []byte{ + byte(tag), + } + // Tag length + contents = append(contents, toLength(len(tagContents))...) + // Tag contents + contents = append(contents, tagContents...) + + return contents +} + +// generateOperaCertStore creates our own custom Opera cert store for the given certificate. +func generateOperaCertStore() { + file, err := mainArc.OpenFile("arc/opera/opcacrt6.dat") + check(err) + + // Load our existing root certificate in DER form. + rootCertContents, err := ioutil.ReadFile("./output/root.cer") + check(err) + rootCert, err := x509.ParseCertificate(rootCertContents) + check(err) + + // The following array was done manually after several hours of tinkering. + // Please refer to docs/opcacrt6.yml for more about the structure of this file. + // TODO(spotlightishere): Is it possible to somehow generate a structure for easier access? + header := []byte{ + // File version number + 0x00, 0x00, 0x10, 0x00, + // App version number + 0x05, 0x05, 0x00, 0x23, + // ID tag "length" - always one byte + 0x00, 0x01, + // Length field byte length - always four bytes + 0x00, 0x04, + } + + // It's unclear on what 0x01 is supposed to represent, + // but it must be a CA certificate. + certTypeTag := generateTag(TagSSLCertType, []byte{ + 0x0, 0x0, 0x0, 0x1, + }) + + // We can obtain the name and subject from the root certificate. + certNameTag := generateTag(TagSSLCertName, []byte(rootCert.Subject.CommonName)) + certSubjectTag := generateTag(TagSSLCertSubject, rootCert.RawSubject) + + // Finally, our actual certificate. + certContentsTag := generateTag(TagSSLCertContents, rootCertContents) + + // We must enclose our type, name, subject and contents tag in a CA certificate tag. + bundledContents := append(certTypeTag, certNameTag...) + bundledContents = append(bundledContents, certSubjectTag...) + bundledContents = append(bundledContents, certContentsTag...) + + caCertTag := generateTag(TagCACertificate, bundledContents) + + // Thankfully, that is all. + // In the end, we have a structure similar to the following: + // - header (file/app version, id/length byte length) + // - ca certificate + // - id + // - length + // - value: + // - type tag (id, length, value) + // - type name (id, length, value) + // - type subject (id, length, value) + // - type contents (id, length, value) + file.Write(append(header, caCertTag...)) +} + +// toLength returns 4 bytes, suitable for the given length. +func toLength(value int) []byte { + holder := make([]byte, 4) + binary.BigEndian.PutUint32(holder, uint32(value)) + return holder +}