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