<?php 
namespace Airship\Barge\Commands; 
 
use \Airship\Barge as Base; 
use ParagonIE\ConstantTime\Base64UrlSafe; 
use \ParagonIE\Halite\{ 
    File, 
    KeyFactory, 
    Asymmetric\SignatureSecretKey 
}; 
 
/** 
 * Class Sign 
 * 
 * This command allows you to sign a cabin, gadget, or motif 
 * with one of your signing keys. 
 * 
 * @package Airship\Barge\Commands 
 */ 
class Sign extends Base\Command 
{ 
    protected $signWithMasterKeys = false; 
    public $essential = true; 
    public $name = 'Sign'; 
    public $description = 'Digitally sign the current Cabin/Gadget/Motif.'; 
    public $display = 3; 
     
    /** 
     * Execute the sign command 
     * 
     * @param array $args - CLI arguments 
     * @echo 
     * @return null 
     */ 
    public function fire(array $args = []) 
    { 
        $path = \count($args) > 0 
            ? $args[0] 
            : \getcwd(); 
 
        if (\count($args) > 1) { 
            // Not enabled by default. 
            if (\in_array('--sign-with-master', \array_slice($args, 1))) { 
                $this->signWithMasterKeys = true; 
            } 
        } 
 
        // Cabins: 
        if (\is_readable($path.'/cabin.json')) { 
            $manifest = \json_decode( 
                \file_get_contents($path.'/cabin.json'), 
                true 
            ); 
            return $this->signCabin($manifest, $path); 
        } 
 
        // Gadgets: 
        if (\is_readable($path.'/gadget.json')) { 
            $manifest = \json_decode( 
                \file_get_contents($path.'/gadget.json'), 
                true 
            ); 
            return $this->signGadget($manifest, $path); 
        } 
 
        // Motifs: 
        if (\is_readable($path.'/src/motif.json')) { 
            $manifest = \json_decode( 
                \file_get_contents($path.'/src/motif.json'), 
                true 
            ); 
            return $this->signMotif($manifest, $path); 
        } 
 
        echo 'Could not find manifest file.', "\n"; 
        exit(255); 
    } 
 
    /** 
     * Common signing process. User selects key, provides password. 
     * 
     * @param array $manifest 
     * @return SignatureSecretKey 
     * @throws \Exception 
     */ 
    protected function signPreamble(array $manifest): SignatureSecretKey 
    { 
        $HTAB = \str_repeat(' ', \intdiv(self::TAB_SIZE, 2)); 
 
        $supplier_name = $manifest['supplier']; 
 
        // Sanity checks: 
        if (!\array_key_exists('suppliers', $this->config)) { 
            echo 'You are not authenticated for any suppliers.', "\n"; 
            exit(255); 
        } 
        if (!\array_key_exists($supplier_name, $this->config['suppliers'])) { 
            echo 'Check the supplier in the JSON file (', $supplier_name, ') for correctness.', 
                'Otherwise, you might need to log in.', "\n"; 
            exit(255); 
        } 
 
        $supplier = $this->config['suppliers'][$supplier_name]; 
        $numKeys = 0; 
 
        if ($this->signWithMasterKeys) { 
            $good_keys = []; 
            // This should really not be used: 
            $numKeys = \count($supplier['signing_keys']); 
            foreach ($supplier['signing_keys'] as $k) { 
                if (!empty($k['salt'])) { 
                    $good_keys[] = $k; 
                    ++$numKeys; 
                } 
            } 
        } else { 
            // This should be used instead: 
            $good_keys = []; 
            foreach ($supplier['signing_keys'] as $k) { 
                if ($k['type'] === 'signing' && !empty($k['salt'])) { 
                    $good_keys[] = $k; 
                    ++$numKeys; 
                } 
            } 
        } 
        if ($numKeys > 1) { 
            echo 'You have more than one signing key available.', "\n"; 
 
            $n = 1; 
            $size = (int) \floor( 
                \log($numKeys, 10) 
            ); 
            $key_associations = $HTAB."ID\t Public Key " . \str_repeat(' ', 33) . "\t Type\n"; 
            foreach ($supplier['signing_keys'] as $sign_key) { 
                if (!$this->signWithMasterKeys && $sign_key['type'] === 'master') { 
                    continue; 
                } 
                $_n = \str_pad($n, $size, ' ', STR_PAD_LEFT); 
 
                // Short format: 
                $pk = Base64UrlSafe::encode( 
                    \Sodium\hex2bin($sign_key['public_key']) 
                ); 
                $key_associations .= $HTAB . $_n . $HTAB . $pk . $HTAB . $sign_key['type'] . "\n"; 
                ++$n; 
            } 
            // Let's ascertain the user's key selection 
            do { 
                echo $key_associations; 
                $choice = (int) $this->prompt('Enter the ID for the key you wish to use: '); 
                if ($choice < 1 || $choice > $numKeys) { 
                    $choice = null; 
                } 
            } while (empty($choice)); 
            $supplierKey = $good_keys[$choice - 1]; 
            echo "\n"; 
        } else { 
            $supplierKey = $good_keys[0]; 
        } 
 
        // The above !empty($k['salt']) check should have rendered this check redundant: 
        if (empty($supplierKey['salt'])) { 
            echo 'Salt not found for this key. It is not possible to reproduce it.', "\n"; 
            exit(255); 
        } 
 
        // Short format: 
        $pk = Base64UrlSafe::encode( 
            \Sodium\hex2bin($supplierKey['public_key']) 
        ); 
 
        // Color coded: Master keys are red, since they take longer. 
        // We don't support signing packages with a master key, but 
        // this decision could be undone in the future. 
        $c = $supplierKey['type'] === 'master' 
            ? $this->c['red'] 
            : $this->c['yellow']; 
 
        echo 'Selected ', $supplierKey['type'], ' key: ', $c, $pk, $this->c[''], "\n"; 
 
        $password = $this->silentPrompt('Enter Password for Signing Key:'); 
 
        // Derive and split the SignatureKeyPair from your password and salt 
        $salt = \Sodium\hex2bin($supplierKey['salt']); 
        switch ($supplierKey['type']) { 
            case 'signing': 
                $type = KeyFactory::MODERATE; 
                echo 'Verifying (this may take a second or two)...'; 
                break; 
            case 'master': 
                $type = KeyFactory::SENSITIVE; 
                echo 'Verifying (this may take a few seconds)...'; 
                break; 
            default: 
                $type = KeyFactory::INTERACTIVE; 
                echo 'Verifying...'; 
        } 
        $keyPair = KeyFactory::deriveSignatureKeyPair( 
            $password, 
            $salt, 
            false, 
            $type 
        ); 
        $sign_secret = $keyPair->getSecretKey(); 
        $sign_public = $keyPair->getPublicKey(); 
        echo ' Done.', "\n"; 
 
        // We don't need this anymore. 
        \Sodium\memzero($password); 
 
        // Check that the public key we derived from the password matches the one on file 
        $pubKey = \Sodium\bin2hex($sign_public->getRawKeyMaterial()); 
        if (!\hash_equals($supplierKey['public_key'], $pubKey)) { 
            // Zero the memory ASAP 
            unset($sign_secret); 
            unset($sign_public); 
            echo 'Invalid password for selected key', "\n"; 
            exit(255); 
        } 
        // Zero the memory ASAP 
        unset($sign_public); 
 
        return $sign_secret; 
    } 
 
    /** 
     * Sign a cabin 
     * 
     * @param array $manifest 
     * @param string $path 
     */ 
    protected function signCabin(array $manifest, string $path) 
    { 
        $pharName = $manifest['supplier'].'.'.$manifest['name'].'.phar'; 
        $sign_secret = $this->signPreamble($manifest); 
 
        // This is the actual signing part. 
        $signature = File::sign( 
            $path.'/dist/'.$pharName, 
            $sign_secret 
        ); 
        // We no longer need this, so unset it. Halite will zero the buffer for us. 
        unset($sign_secret); 
 
        $res = \file_put_contents( 
            $path.'/dist/'.$pharName.'.ed25519.sig', 
            $signature 
        ); 
        if ($res !== false) { 
            echo 'Signed: ', $path, '/dist/', $pharName, '.ed25519.sig', "\n"; 
            exit(0); 
        } 
    } 
     
    /** 
     * Sign a gadget 
     *  
     * @param array $manifest 
     * @param string $path 
     */ 
    protected function signGadget(array $manifest, string $path) 
    { 
        $pharName = $manifest['supplier'].'.'.$manifest['name'].'.phar'; 
        $sign_secret = $this->signPreamble($manifest); 
 
        // This is the actual signing part. 
        $signature = File::sign( 
            $path.'/dist/'.$pharName, 
            $sign_secret 
        ); 
        // We no longer need this, so unset it. Halite will zero the buffer for us. 
        unset($sign_secret); 
         
        $res = \file_put_contents( 
            $path.'/dist/'.$pharName.'.ed25519.sig', 
            $signature 
        ); 
        if ($res !== false) { 
            echo 'Signed: ', $path, '/dist/', $pharName, '.ed25519.sig', "\n"; 
            exit(0); 
        } 
    } 
 
    /** 
     * Sign a motif 
     * 
     * @param array $manifest 
     * @param string $path 
     */ 
    protected function signMotif(array $manifest, string $path) 
    { 
        $zipName = $manifest['supplier'].'.'.$manifest['name'].'.zip'; 
        $sign_secret = $this->signPreamble($manifest); 
 
        // This is the actual signing part. 
        $signature = File::sign( 
            $path.'/dist/'.$zipName, 
            $sign_secret 
        ); 
        // We no longer need this, so unset it. Halite will zero the buffer for us. 
        unset($sign_secret); 
 
        $res = \file_put_contents( 
            $path.'/dist/'.$zipName.'.ed25519.sig', 
            $signature 
        ); 
        if ($res !== false) { 
            echo 'Signed: ', $path, '/dist/', $zipName, '.ed25519.sig', "\n"; 
            exit(0); 
        } 
    } 
} 
 
 |