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)
|
||
|
}
|