doodle/pkg/license/levelsigning/level_signing.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)
}