mirror of
https://wiilab.wiimart.org/wiimart/WiiMart-Patcher
synced 2025-09-02 19:41:13 +02:00
Add initial certificate patching
This commit is contained in:
commit
a3f2361ad2
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.idea
|
||||
cache/
|
||||
WSC-Patcher
|
||||
output/
|
39
README.md
Normal file
39
README.md
Normal file
@ -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 <base domain>
|
||||
```
|
||||
|
||||
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, `*.<basedomain>` 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.
|
||||
|
101
cert_store.go
Normal file
101
cert_store.go
Normal file
@ -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)
|
||||
}
|
6
docs/README.md
Normal file
6
docs/README.md
Normal file
@ -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.
|
75
docs/opcacrt6.yml
Normal file
75
docs/opcacrt6.yml
Normal file
@ -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
|
9
go.mod
Normal file
9
go.mod
Normal file
@ -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
|
||||
)
|
6
go.sum
Normal file
6
go.sum
Normal file
@ -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=
|
122
main.go
Normal file
122
main.go
Normal file
@ -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 <base domain>\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)
|
||||
}
|
125
modify_arc.go
Normal file
125
modify_arc.go
Normal file
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user