Noah Petherbridge
82884c79ae
Add the ability for the free version of the game to allow loading levels that use embedded custom doodads if those levels are signed. * Uses the same signing keys as the JWT token for license registrations. * Levels and Levelpacks can both be signed. So individual levels with embedded doodads can work in free versions of the game. * Levelpacks now support embedded doodads properly: the individual levels in the pack don't need to embed a custom doodad, but if the doodad exists in the levelpack's doodads/ folder it will load from there instead - for full versions of the game OR when the levelpack is signed. Signatures are computed by getting a listing of embedded assets inside the zipfile (the assets/ folder in levels, and the doodads/ + levels/ folders in levelpacks). Thus for individual signed levels, the level geometry and metadata may be changed without breaking the signature but if custom doodads are changed the signature will break. The doodle-admin command adds subcommands to `sign-level` and `verify-level` to manage signatures on levels and levelpacks. When using the `doodad levelpack create` command, any custom doodads the levels mention that are found in your profile directory get embedded into the zipfile by default (with --doodads custom).
194 lines
5.0 KiB
Go
194 lines
5.0 KiB
Go
package levelsigning
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/levelpack"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
|
)
|
|
|
|
// IsLevelSigned returns a quick answer.
|
|
func IsLevelSigned(lvl *level.Level) bool {
|
|
return VerifyLevel(license.Signer, lvl)
|
|
}
|
|
|
|
// IsLevelPackSigned returns a quick answer.
|
|
func IsLevelPackSigned(lp *levelpack.LevelPack) bool {
|
|
return VerifyLevelPack(license.Signer, lp)
|
|
}
|
|
|
|
/*
|
|
SignLevel creates a signature on a level file which allows it to load its
|
|
embedded doodads even for free versions of the game.
|
|
|
|
Free versions will verify a level's signature before bailing out with the
|
|
"can't play levels w/ embedded doodads" response.
|
|
|
|
NOTE: this only supported Zipfile levels and will assume the level you
|
|
pass has a Zipfile to access embedded assets.
|
|
*/
|
|
func SignLevel(key *ecdsa.PrivateKey, lvl *level.Level) ([]byte, error) {
|
|
// Encode the attached files data to deterministic JSON.
|
|
certificate, err := StringifyAssets(lvl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Info("Sign file tree: %s", certificate)
|
|
digest := shasum(certificate)
|
|
|
|
signature, err := ecdsa.SignASN1(rand.Reader, key, digest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log.Info("Digest: %x Signature: %x", digest, signature)
|
|
|
|
return signature, nil
|
|
}
|
|
|
|
// VerifyLevel verifies a level's signature and returns if it is OK.
|
|
func VerifyLevel(publicKey *ecdsa.PublicKey, lvl *level.Level) bool {
|
|
// No signature = not verified.
|
|
if lvl.Signature == nil || len(lvl.Signature) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Encode the attached files data to deterministic JSON.
|
|
certificate, err := StringifyAssets(lvl)
|
|
if err != nil {
|
|
log.Error("VerifyLevel: couldn't stringify assets: %s", err)
|
|
return false
|
|
}
|
|
|
|
digest := shasum(certificate)
|
|
|
|
// Verify the signature against our public key.
|
|
return ecdsa.VerifyASN1(publicKey, digest, lvl.Signature)
|
|
}
|
|
|
|
/*
|
|
SignLevelpack applies a signature to a levelpack as a whole, to allow its
|
|
shared custom doodads to be loaded by its levels in free games.
|
|
*/
|
|
func SignLevelPack(key *ecdsa.PrivateKey, lp *levelpack.LevelPack) ([]byte, error) {
|
|
// Encode the attached files data to deterministic JSON.
|
|
certificate, err := StringifyLevelpackAssets(lp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Info("Sign file tree: %s", certificate)
|
|
digest := shasum(certificate)
|
|
|
|
signature, err := ecdsa.SignASN1(rand.Reader, key, digest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log.Info("Digest: %x Signature: %x", digest, signature)
|
|
|
|
return signature, nil
|
|
}
|
|
|
|
// VerifyLevelPack verifies a levelpack's signature and returns if it is OK.
|
|
func VerifyLevelPack(publicKey *ecdsa.PublicKey, lp *levelpack.LevelPack) bool {
|
|
// No signature = not verified.
|
|
if lp.Signature == nil || len(lp.Signature) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Encode the attached files data to deterministic JSON.
|
|
certificate, err := StringifyLevelpackAssets(lp)
|
|
if err != nil {
|
|
log.Error("VerifyLevelPack: couldn't stringify assets: %s", err)
|
|
return false
|
|
}
|
|
|
|
digest := shasum(certificate)
|
|
|
|
// Verify the signature against our public key.
|
|
return ecdsa.VerifyASN1(publicKey, digest, lp.Signature)
|
|
}
|
|
|
|
// StringifyAssets creates the signing checksum of a level's attached assets.
|
|
func StringifyAssets(lvl *level.Level) ([]byte, error) {
|
|
// Get a listing of all embedded files. Note: gives us a conveniently
|
|
// sorted array of files too.
|
|
files := lvl.Files.List()
|
|
|
|
// Pair each filename with its SHA256 sum.
|
|
var checksum = map[string]string{}
|
|
for _, filename := range files {
|
|
if sum, err := lvl.Files.Checksum(filename); err != nil {
|
|
return nil, fmt.Errorf("when checksum %s got error: %s", filename, err)
|
|
} else {
|
|
checksum[filename] = sum
|
|
}
|
|
}
|
|
|
|
// Encode the payload to deterministic JSON.
|
|
certificate, err := json.Marshal(checksum)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return certificate, nil
|
|
}
|
|
|
|
// StringifyLevelpackAssets creates the signing checksum of a level's attached assets.
|
|
func StringifyLevelpackAssets(lp *levelpack.LevelPack) ([]byte, error) {
|
|
var (
|
|
files = []string{}
|
|
seen = map[string]struct{}{}
|
|
)
|
|
|
|
// Enumerate the files in the zipfile assets/ folder.
|
|
for _, file := range lp.Zipfile.File {
|
|
if file.Name == "index.json" {
|
|
continue
|
|
}
|
|
|
|
if _, ok := seen[file.Name]; !ok {
|
|
files = append(files, file.Name)
|
|
seen[file.Name] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// Pair each filename with its SHA256 sum.
|
|
var checksum = map[string]string{}
|
|
for _, filename := range files {
|
|
file, err := lp.Zipfile.Open(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bin, err := ioutil.ReadAll(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
checksum[filename] = fmt.Sprintf("%x", shasum(bin))
|
|
}
|
|
|
|
// Encode the payload to deterministic JSON.
|
|
certificate, err := json.Marshal(checksum)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return certificate, nil
|
|
}
|
|
|
|
// Common function to SHA-256 checksum a thing.
|
|
func shasum(data []byte) []byte {
|
|
h := sha256.New()
|
|
h.Write(data)
|
|
return h.Sum(nil)
|
|
}
|