361 lines
9.3 KiB
Go
361 lines
9.3 KiB
Go
// Package mospkg provides functionality for Mosis app packages (.mosis files)
|
|
package mospkg
|
|
|
|
import (
|
|
"archive/zip"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// KeyPair represents an Ed25519 signing keypair
|
|
type KeyPair struct {
|
|
PrivateKey ed25519.PrivateKey
|
|
PublicKey ed25519.PublicKey
|
|
}
|
|
|
|
// GenerateKeyPair generates a new Ed25519 keypair
|
|
func GenerateKeyPair() (*KeyPair, error) {
|
|
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("generate key: %w", err)
|
|
}
|
|
return &KeyPair{
|
|
PrivateKey: privateKey,
|
|
PublicKey: publicKey,
|
|
}, nil
|
|
}
|
|
|
|
// Fingerprint returns the SHA256 fingerprint of the public key
|
|
func (kp *KeyPair) Fingerprint() string {
|
|
hash := sha256.Sum256(kp.PublicKey)
|
|
return fmt.Sprintf("SHA256:%s", base64.StdEncoding.EncodeToString(hash[:]))
|
|
}
|
|
|
|
// PrivateKeyPEM returns the private key in PEM format
|
|
func (kp *KeyPair) PrivateKeyPEM() ([]byte, error) {
|
|
pkcs8, err := x509.MarshalPKCS8PrivateKey(kp.PrivateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal private key: %w", err)
|
|
}
|
|
return pem.EncodeToMemory(&pem.Block{
|
|
Type: "PRIVATE KEY",
|
|
Bytes: pkcs8,
|
|
}), nil
|
|
}
|
|
|
|
// PublicKeyPEM returns the public key in PEM format
|
|
func (kp *KeyPair) PublicKeyPEM() ([]byte, error) {
|
|
pkix, err := x509.MarshalPKIXPublicKey(kp.PublicKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal public key: %w", err)
|
|
}
|
|
return pem.EncodeToMemory(&pem.Block{
|
|
Type: "PUBLIC KEY",
|
|
Bytes: pkix,
|
|
}), nil
|
|
}
|
|
|
|
// LoadPrivateKey loads an Ed25519 private key from PEM data
|
|
func LoadPrivateKey(pemData []byte) (ed25519.PrivateKey, error) {
|
|
block, _ := pem.Decode(pemData)
|
|
if block == nil {
|
|
return nil, fmt.Errorf("failed to decode PEM block")
|
|
}
|
|
|
|
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse private key: %w", err)
|
|
}
|
|
|
|
ed25519Key, ok := key.(ed25519.PrivateKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("key is not Ed25519")
|
|
}
|
|
|
|
return ed25519Key, nil
|
|
}
|
|
|
|
// LoadPublicKey loads an Ed25519 public key from PEM data
|
|
func LoadPublicKey(pemData []byte) (ed25519.PublicKey, error) {
|
|
block, _ := pem.Decode(pemData)
|
|
if block == nil {
|
|
return nil, fmt.Errorf("failed to decode PEM block")
|
|
}
|
|
|
|
key, err := x509.ParsePKIXPublicKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse public key: %w", err)
|
|
}
|
|
|
|
ed25519Key, ok := key.(ed25519.PublicKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("key is not Ed25519")
|
|
}
|
|
|
|
return ed25519Key, nil
|
|
}
|
|
|
|
// PublicKeyFingerprint returns the SHA256 fingerprint of a public key
|
|
func PublicKeyFingerprint(publicKey ed25519.PublicKey) string {
|
|
hash := sha256.Sum256(publicKey)
|
|
return fmt.Sprintf("SHA256:%s", base64.StdEncoding.EncodeToString(hash[:]))
|
|
}
|
|
|
|
// GenerateManifestMF generates MANIFEST.MF content for a package
|
|
func GenerateManifestMF(packagePath string) ([]byte, error) {
|
|
reader, err := zip.OpenReader(packagePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open package: %w", err)
|
|
}
|
|
defer reader.Close()
|
|
|
|
var lines []string
|
|
lines = append(lines, "Manifest-Version: 1.0")
|
|
lines = append(lines, "Created-By: mosis-portal")
|
|
lines = append(lines, "")
|
|
|
|
// Sort files for consistent ordering
|
|
var fileNames []string
|
|
for _, file := range reader.File {
|
|
if file.FileInfo().IsDir() {
|
|
continue
|
|
}
|
|
// Skip META-INF files
|
|
if strings.HasPrefix(file.Name, "META-INF/") {
|
|
continue
|
|
}
|
|
fileNames = append(fileNames, file.Name)
|
|
}
|
|
sort.Strings(fileNames)
|
|
|
|
// Generate hash for each file
|
|
for _, name := range fileNames {
|
|
for _, file := range reader.File {
|
|
if file.Name != name {
|
|
continue
|
|
}
|
|
|
|
rc, err := file.Open()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open file %s: %w", name, err)
|
|
}
|
|
|
|
hasher := sha256.New()
|
|
if _, err := io.Copy(hasher, rc); err != nil {
|
|
rc.Close()
|
|
return nil, fmt.Errorf("hash file %s: %w", name, err)
|
|
}
|
|
rc.Close()
|
|
|
|
digest := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
|
|
lines = append(lines, fmt.Sprintf("Name: %s", name))
|
|
lines = append(lines, fmt.Sprintf("SHA-256-Digest: %s", digest))
|
|
lines = append(lines, "")
|
|
}
|
|
}
|
|
|
|
return []byte(strings.Join(lines, "\n")), nil
|
|
}
|
|
|
|
// SignManifest signs MANIFEST.MF content with an Ed25519 private key
|
|
func SignManifest(manifestMF []byte, privateKey ed25519.PrivateKey) []byte {
|
|
return ed25519.Sign(privateKey, manifestMF)
|
|
}
|
|
|
|
// VerifySignature verifies a signature against MANIFEST.MF using a public key
|
|
func VerifySignature(manifestMF, signature []byte, publicKey ed25519.PublicKey) bool {
|
|
return ed25519.Verify(publicKey, manifestMF, signature)
|
|
}
|
|
|
|
// SignPackage signs a .mosis package by adding META-INF/MANIFEST.MF and META-INF/CERT.SIG
|
|
func SignPackage(packagePath, outputPath string, privateKey ed25519.PrivateKey) error {
|
|
// Generate MANIFEST.MF
|
|
manifestMF, err := GenerateManifestMF(packagePath)
|
|
if err != nil {
|
|
return fmt.Errorf("generate manifest: %w", err)
|
|
}
|
|
|
|
// Sign manifest
|
|
signature := SignManifest(manifestMF, privateKey)
|
|
|
|
// Open source package
|
|
srcReader, err := zip.OpenReader(packagePath)
|
|
if err != nil {
|
|
return fmt.Errorf("open source package: %w", err)
|
|
}
|
|
defer srcReader.Close()
|
|
|
|
// Create output package
|
|
outFile, err := os.Create(outputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("create output: %w", err)
|
|
}
|
|
defer outFile.Close()
|
|
|
|
writer := zip.NewWriter(outFile)
|
|
defer writer.Close()
|
|
|
|
// Copy existing files (except META-INF)
|
|
for _, file := range srcReader.File {
|
|
if strings.HasPrefix(file.Name, "META-INF/") {
|
|
continue
|
|
}
|
|
|
|
// Copy file to new archive
|
|
destFile, err := writer.CreateHeader(&file.FileHeader)
|
|
if err != nil {
|
|
return fmt.Errorf("create header: %w", err)
|
|
}
|
|
|
|
srcFile, err := file.Open()
|
|
if err != nil {
|
|
return fmt.Errorf("open source file: %w", err)
|
|
}
|
|
|
|
if _, err := io.Copy(destFile, srcFile); err != nil {
|
|
srcFile.Close()
|
|
return fmt.Errorf("copy file: %w", err)
|
|
}
|
|
srcFile.Close()
|
|
}
|
|
|
|
// Add META-INF/MANIFEST.MF
|
|
manifestWriter, err := writer.Create("META-INF/MANIFEST.MF")
|
|
if err != nil {
|
|
return fmt.Errorf("create MANIFEST.MF: %w", err)
|
|
}
|
|
if _, err := manifestWriter.Write(manifestMF); err != nil {
|
|
return fmt.Errorf("write MANIFEST.MF: %w", err)
|
|
}
|
|
|
|
// Add META-INF/CERT.SIG (base64 encoded signature)
|
|
sigWriter, err := writer.Create("META-INF/CERT.SIG")
|
|
if err != nil {
|
|
return fmt.Errorf("create CERT.SIG: %w", err)
|
|
}
|
|
if _, err := sigWriter.Write([]byte(base64.StdEncoding.EncodeToString(signature))); err != nil {
|
|
return fmt.Errorf("write CERT.SIG: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerifyPackageSignature verifies the signature of a signed .mosis package
|
|
func VerifyPackageSignature(packagePath string, publicKey ed25519.PublicKey) (bool, error) {
|
|
reader, err := zip.OpenReader(packagePath)
|
|
if err != nil {
|
|
return false, fmt.Errorf("open package: %w", err)
|
|
}
|
|
defer reader.Close()
|
|
|
|
// Find MANIFEST.MF and CERT.SIG
|
|
var manifestMF []byte
|
|
var signature []byte
|
|
|
|
for _, file := range reader.File {
|
|
switch file.Name {
|
|
case "META-INF/MANIFEST.MF":
|
|
rc, err := file.Open()
|
|
if err != nil {
|
|
return false, fmt.Errorf("open MANIFEST.MF: %w", err)
|
|
}
|
|
manifestMF, err = io.ReadAll(rc)
|
|
rc.Close()
|
|
if err != nil {
|
|
return false, fmt.Errorf("read MANIFEST.MF: %w", err)
|
|
}
|
|
|
|
case "META-INF/CERT.SIG":
|
|
rc, err := file.Open()
|
|
if err != nil {
|
|
return false, fmt.Errorf("open CERT.SIG: %w", err)
|
|
}
|
|
sigB64, err := io.ReadAll(rc)
|
|
rc.Close()
|
|
if err != nil {
|
|
return false, fmt.Errorf("read CERT.SIG: %w", err)
|
|
}
|
|
signature, err = base64.StdEncoding.DecodeString(string(sigB64))
|
|
if err != nil {
|
|
return false, fmt.Errorf("decode signature: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if manifestMF == nil {
|
|
return false, fmt.Errorf("MANIFEST.MF not found")
|
|
}
|
|
if signature == nil {
|
|
return false, fmt.Errorf("CERT.SIG not found")
|
|
}
|
|
|
|
// Verify signature
|
|
if !VerifySignature(manifestMF, signature, publicKey) {
|
|
return false, nil
|
|
}
|
|
|
|
// Verify file hashes
|
|
hashes, err := parseManifestMF(manifestMF)
|
|
if err != nil {
|
|
return false, fmt.Errorf("parse MANIFEST.MF: %w", err)
|
|
}
|
|
|
|
for _, file := range reader.File {
|
|
if file.FileInfo().IsDir() || strings.HasPrefix(file.Name, "META-INF/") {
|
|
continue
|
|
}
|
|
|
|
expectedHash, ok := hashes[file.Name]
|
|
if !ok {
|
|
return false, fmt.Errorf("file not in manifest: %s", file.Name)
|
|
}
|
|
|
|
rc, err := file.Open()
|
|
if err != nil {
|
|
return false, fmt.Errorf("open file %s: %w", file.Name, err)
|
|
}
|
|
|
|
hasher := sha256.New()
|
|
if _, err := io.Copy(hasher, rc); err != nil {
|
|
rc.Close()
|
|
return false, fmt.Errorf("hash file %s: %w", file.Name, err)
|
|
}
|
|
rc.Close()
|
|
|
|
actualHash := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
|
|
if actualHash != expectedHash {
|
|
return false, fmt.Errorf("hash mismatch for %s", file.Name)
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// parseManifestMF parses MANIFEST.MF content and returns a map of filename -> SHA256 hash
|
|
func parseManifestMF(data []byte) (map[string]string, error) {
|
|
hashes := make(map[string]string)
|
|
lines := strings.Split(string(data), "\n")
|
|
|
|
var currentFile string
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "Name: ") {
|
|
currentFile = strings.TrimPrefix(line, "Name: ")
|
|
} else if strings.HasPrefix(line, "SHA-256-Digest: ") && currentFile != "" {
|
|
hashes[currentFile] = strings.TrimPrefix(line, "SHA-256-Digest: ")
|
|
currentFile = ""
|
|
}
|
|
}
|
|
|
|
return hashes, nil
|
|
}
|