/// <summary> /// Digitally signs the specified manifest, adds the signature to the manifest, and returns it. /// </summary> /// <param name="manifest">The manifest to sign.</param> /// <param name="privateKey">The PGP private key with which to sign the file.</param> /// <param name="passphrase">The passphrase for the PGP private key.</param> /// <param name="keybaseUsername"> /// The Keybase.io username of the account hosting the PGP public key used for digest verification. /// </param> /// <returns>The signed manifest.</returns> private PackageManifest SignManifest(PackageManifest manifest, string privateKey, string passphrase, string keybaseUsername) { Info("Digitally signing manifest..."); // insert a signature into the manifest. the signer must be included in the hash to prevent tampering. PackageManifestSignature signature = new PackageManifestSignature(); signature.Issuer = PackagingConstants.KeyIssuer; signature.Subject = keybaseUsername; manifest.Signature = signature; Verbose("Creating SHA512 hash of serialized manifest..."); string manifestHash = Utility.ComputeSHA512Hash(manifest.ToJson()); Verbose($"Hash computed successfully: {manifestHash}."); byte[] manifestBytes = Encoding.ASCII.GetBytes(manifest.ToJson()); Verbose("Creating digest..."); byte[] digestBytes = PGPSignature.Sign(manifestBytes, privateKey, passphrase); Verbose("Digest created successfully."); Verbose("Adding signature to manifest..."); manifest.Signature.Digest = Encoding.ASCII.GetString(digestBytes); Success("Manifest signed successfully."); return(manifest); }
/// <summary> /// Serializes the specified manifest to JSON and writes it to a 'manifest.json' file in the specified directory. /// </summary> /// <param name="manifest">The manifest to serialize and write.</param> /// <param name="directory">The directory into which the generated file will be written.</param> private void WriteManifest(PackageManifest manifest, string directory) { string destinationFile = Path.Combine(directory, PackagingConstants.ManifestFilename); string contents = manifest.ToJson(); File.WriteAllText(destinationFile, contents); }
/// <summary> /// Updates the specified Package, replacing the existing Manifest with the specified Manifest. /// </summary> /// <param name="packageFile">The filename of the Package file to update.</param> /// <param name="manifest">The Manifest with which the Package file will be updated.</param> /// <param name="tempDirectory">The path to the temporary directory to use for file operations.</param> private void UpdatePackageManifest(string packageFile, PackageManifest manifest, string tempDirectory) { Verbose($"Updating Manifest in Package '{Path.GetFileName(packageFile)}'..."); string tempPackageDirectory = Path.Combine(tempDirectory, "package"); string tempManifest = Path.Combine(tempPackageDirectory, PackagingConstants.ManifestFilename); string tempPackage = Path.Combine(tempDirectory, Path.GetFileName(packageFile)); Verbose($"Extracting Package to '{tempPackageDirectory}'..."); ZipFile.ExtractToDirectory(packageFile, tempPackageDirectory); Verbose("Package extracted successfully."); Verbose($"Replacing Manifest file..."); File.Delete(tempManifest); File.WriteAllText(tempManifest, manifest.ToJson()); Verbose("Manifest file replaced successfully."); Verbose($"Compressing Package..."); ZipFile.CreateFromDirectory(tempPackageDirectory, tempPackage); Verbose("Package compressed successfully."); Verbose($"Overwriting original Package..."); File.Copy(tempPackage, packageFile, true); Verbose("Package overwritten successfully."); }
/// <summary> /// Verifies the Digest contained in the specified Manifest using the specified PGP Public Key. /// </summary> /// <param name="manifest">The Manifest for which the Digest is to be verified.</param> /// <param name="publicKey">The PGP Public Key with which to verify the Digest.</param> /// <exception cref="InvalidDataException"> /// Thrown when an error is encountered verifying the Digest, or when the Manifest contents do not match the verified Digest. /// </exception> private void VerifyDigest(PackageManifest manifest, string publicKey) { string verifiedDigest = string.Empty; if (!string.IsNullOrEmpty(manifest.Signature.Digest)) { Verbose("Verifying the Manifest Digest..."); byte[] digestBytes = Encoding.ASCII.GetBytes(manifest.Signature.Digest); byte[] verifiedDigestBytes; try { verifiedDigestBytes = PGPSignature.Verify(digestBytes, publicKey); } catch (Exception ex) { throw new InvalidDataException($"an Exception was thrown while verifying the Digest: {ex.GetType().Name}: {ex.Message}", ex); } verifiedDigest = Encoding.ASCII.GetString(verifiedDigestBytes); // deserialize the verified manifest to work around text formatting differences on various platforms PackageManifest verifiedManifest; try { verifiedManifest = JsonConvert.DeserializeObject <PackageManifest>(verifiedDigest); } catch (Exception ex) { throw new InvalidDataException($"an Exception was thrown while deserializing the Digest: {ex.GetType().Name}: {ex.Message}", ex); } // remove the digest and trust from the manifest, then serialize it and compare it to the verified digest. manifest.Signature.Digest = default(string); manifest.Signature.Trust = default(string); // if the scrubbed manifest and verified digest don't match, something was tampered with. if (manifest.ToJson() != verifiedManifest.ToJson()) { throw new InvalidDataException("the Manifest Digest is not valid; the verified Digest does not match the Manifest."); } Verbose("Digest verified successfully."); } else { throw new InvalidDataException("the Manifest Digest is null or empty."); } }
/// <summary> /// Generates and populates <see cref="IPackageManifest"/> objects from the specified directory, including resource /// files and hashing file entries if those options are specified. /// </summary> /// <param name="inputDirectory">The directory from which to generate a list of files.</param> /// <param name="hashFiles">A value indicating whether files added to the manifest are to include a SHA512 hash.</param> /// <param name="manifestFile">The filename of the file to which the manifest is to be saved.</param> /// <returns>The generated manifest.</returns> public IPackageManifest GenerateManifest(string inputDirectory, bool hashFiles = false, string manifestFile = "") { ArgumentValidator.ValidateInputDirectoryArgument(inputDirectory); string[] files = Directory.GetFiles(inputDirectory, "*", SearchOption.AllDirectories); PackageManifestBuilder builder = new PackageManifestBuilder(); Info($"Generating manifest for directory '{inputDirectory}'..."); builder.BuildDefault(); Verbose($"Adding files from '{inputDirectory}'..."); foreach (string file in files) { AddFile(builder, file, inputDirectory, hashFiles); } PackageManifest manifest = builder.Manifest; Success("Manifest generated successfully."); if (!string.IsNullOrEmpty(manifestFile)) { try { Info($"Saving output to file '{manifestFile}'..."); File.WriteAllText(manifestFile, manifest.ToJson()); Success("File saved successfully."); } catch (Exception ex) { throw new Exception($"Unable to write to output file '{manifestFile}': {ex.Message}", ex); } } return(manifest); }
/// <summary> /// Extracts the <see cref="PackageManifest"/> object from the specified Package file. /// </summary> /// <param name="packageFile">The Package from which the manifest is to be extracted.</param> /// <param name="manifestFile">The filename of the file to which the manifest is to be saved.</param> /// <returns>The extracted manifest object.</returns> public PackageManifest ExtractManifest(string packageFile, string manifestFile = "") { ArgumentValidator.ValidatePackageFileArgumentForReading(packageFile); Info($"Extracting manifest '{PackagingConstants.ManifestFilename}' from package '{packageFile}'..."); PackageManifest manifest = new PackageManifest(); ZipArchive archive = default(ZipArchive); StreamReader reader = default(StreamReader); try { Verbose($"Locating manifest inside of package..."); archive = ZipFile.OpenRead(packageFile); ZipArchiveEntry zippedManifestFile = archive.Entries.Where(e => e.Name == PackagingConstants.ManifestFilename).FirstOrDefault(); string manifestString; if (zippedManifestFile != default(ZipArchiveEntry)) { Verbose("Manifest located successfully."); Verbose("Reading manifest from package..."); reader = new StreamReader(zippedManifestFile.Open()); manifestString = reader.ReadToEnd(); Verbose("Manifest read successfully."); } else { throw new FileNotFoundException($"The package '{Path.GetFileName(packageFile)}' does not contain a manifest."); } Verbose("Deserializing manifest..."); manifest = JsonConvert.DeserializeObject <PackageManifest>(manifestString); Verbose("Manifest deserialized successfully."); } catch (JsonException ex) { throw new JsonException($"The manifest within package '{Path.GetFileName(packageFile)}' is malformed: {ex.Message}"); } catch (Exception ex) { throw new IOException($"Error extracting manifest from package '{Path.GetFileName(packageFile)}': {ex.Message}"); } finally { archive?.Dispose(); reader?.Close(); } Success("Manifest extracted successfully."); if (!string.IsNullOrEmpty(manifestFile)) { try { Info($"Saving extracted manifest to file '{manifestFile}'..."); File.WriteAllText(manifestFile, manifest.ToJson()); Info("File saved successfully."); } catch (Exception ex) { throw new Exception($"Unable to write to manifest file '{manifestFile}': {ex.Message}", ex); } } return(manifest); }
/// <summary> /// Processes the desired command with the arguments specified in the command line arguments from Main(). /// </summary> /// <param name="args"> /// The optional command line arguments, used to override the arguments with which the application was started. /// </param> public static void Process(string args = "") { if (args != string.Empty) { Arguments.Populate(args); } string command = string.Empty; try { if (Operands.Count > 1) { command = Operands[1].ToLower(); } if (command == "manifest") { ManifestGenerator generator = new ManifestGenerator(); generator.Updated += Update; PackageManifest manifest = generator.GenerateManifest(Directory, HashFiles, ManifestFile); if (string.IsNullOrEmpty(ManifestFile) && manifest != default(PackageManifest)) { Console.WriteLine(manifest.ToJson()); } } else if (command == "extract-manifest") { ManifestExtractor extractor = new ManifestExtractor(); extractor.Updated += Update; PackageManifest manifest = extractor.ExtractManifest(PackageFile, ManifestFile); if (string.IsNullOrEmpty(ManifestFile) && manifest != default(PackageManifest)) { Console.WriteLine(manifest.ToJson()); } } else if (command == "package") { PackageCreator creator = new PackageCreator(); creator.Updated += Update; string privateKey = string.Empty; if (!string.IsNullOrEmpty(PrivateKeyFile)) { privateKey = File.ReadAllText(PrivateKeyFile); } creator.CreatePackage(Directory, ManifestFile, PackageFile, SignPackage, privateKey, Passphrase, KeybaseUsername); } else if (command == "extract-package") { PackageExtractor extractor = new PackageExtractor(); extractor.Updated += Update; string publicKey = string.Empty; if (!string.IsNullOrEmpty(PublicKeyFile)) { publicKey = File.ReadAllText(PublicKeyFile); } extractor.ExtractPackage(PackageFile, Directory, publicKey, Overwrite, SkipVerification); } else if (command == "trust") { PackageTruster truster = new PackageTruster(); truster.Updated += Update; truster.TrustPackage(PackageFile, File.ReadAllText(PrivateKeyFile), Passphrase); } else if (command == "verify") { PackageVerifier verifier = new PackageVerifier(); verifier.Updated += Update; string publicKey = string.Empty; if (!string.IsNullOrEmpty(PublicKeyFile)) { publicKey = File.ReadAllText(PublicKeyFile); } verifier.VerifyPackage(PackageFile, publicKey); } else { HelpPrinter.PrintHelp(Operands.Count > 2 ? Operands[2] : default(string)); } } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); } }