Add initial certificate patching

This commit is contained in:
Spotlight 2021-12-30 02:56:07 -06:00
commit a3f2361ad2
No known key found for this signature in database
GPG Key ID: 874AA355B3209BDC
9 changed files with 487 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.idea
cache/
WSC-Patcher
output/

39
README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}