/// <summary> /// Load all manifest entries from a given manifest in a JAR /// </summary> /// <param name="source">source JAR</param> /// <param name="manifest">manifest to load</param> /// <returns>manifest data</returns> public ManifestData Load(IJar source, string path) { if (source == null) { throw new ArgumentNullException(nameof(source)); } if (path.IsNullOrEmpty()) { throw new ArgumentNullException(nameof(path)); } // If this file does not exist, obviously we cannot load it if (!source.Contains(path)) { throw new ManifestException($"Manifest {path} does not exist"); } ManifestData manifest = new ManifestData { ManifestDigest = String.Empty, Entries = new List <ManifestEntry>() }; // The manifest digest is supposed to refer to the digest that THIS manifest // expects the main manifest to have. Until set otherwise, assume this digest // is = to our own hash (which means loading the main manifest populates this // value with the main manifest hash for future comparison) using (Hasher h = new Hasher()) { manifest.ManifestDigest = source.SHA256(h, path).ToBase64(); } try { using (StreamReader reader = new StreamReader(source.Open(path))) { string[] lines = Unwrap70(reader.ReadToEnd().Split( new char[] { (char)10, (char)13 }, StringSplitOptions.RemoveEmptyEntries)); Populate(manifest, lines); } } catch (Exception ex) { throw new ManifestException($"Failed to open or parse manifest {path}", ex); } return(manifest); }
/// <summary> /// Produce a SHA256 of a file in the JAR. If the file does not exist, an exception is thrown. /// </summary> /// <param name="this"></param> /// <param name="hasher">hasher implementation</param> /// <param name="path">filename to generate a hash of</param> /// <returns>the byte data of the SHA-256 hash</returns> public static byte[] SHA256(this IJar @this, Hasher hasher, string path) { if (hasher == null) { throw new ArgumentNullException(nameof(hasher)); } if (path.IsNullOrEmpty()) { throw new ArgumentNullException(nameof(path)); } if ([email protected](path)) { throw new JarException($"File to hash {path} does not exist in JAR"); } using (Stream file = @this.Open(path)) { return(hasher.SHA256(file)); } }
/// <summary> /// Perform JAR digital signature verification against a JAR filename on disk /// </summary> /// <param name="jar">JAR container. The caller is expected to dispose this type themselves - it will not be disposed /// by this method</param> /// <param name="certificates">certificate to verify / accept against</param> /// <param name="nonStandardCountCheck">whether to perform the additional file count verification check against /// MANIFEST.MF (recommended if the file is actually an arbitrary ZIP)</param> /// <returns>digital signature verification state of the JAR</returns> public static VerificationResult Jar(IJar jar, IVerificationCertificates certificates, bool nonStandardCountCheck = true) { // Unsigned ZIP and probably not even a JAR if (!jar.Contains(@"META-INF\MANIFEST.MF")) { return(new VerificationResult { Status = SigningStatus.NotSigned, Valid = false }); } IManifestLoader manifestLoader = new ManifestLoader(); ManifestData centralManifest = manifestLoader.Load(jar, @"META-INF\MANIFEST.MF"); if (nonStandardCountCheck) { // Non-standard check: Ensure that no unsigned files have been ADDED // to the JAR (file qty. [except signature itself] must match manifest entries) // int nonManifestFiles = jar.NonSignatureFiles().Count(); if (centralManifest.Entries.Count != nonManifestFiles) { Log.Message($"Expected {centralManifest.Entries.Count} file(s) found {nonManifestFiles}"); return(new VerificationResult { Status = SigningStatus.FundamentalHashMismatch, Valid = false }); } } // Verify the hashes of every file in the JAR // using (var h = new Hasher()) { Log.Message($"Central manifest contains {centralManifest.Entries.Count} entries"); foreach (ManifestEntry e in centralManifest.Entries) { Log.Message($"Digest check {e.Path} ({e.Digest})"); // Check each file matches the hash in the manifest if (jar.SHA256(h, e.Path).ToBase64() != e.Digest) { Log.Message($"{e.Path} has an incorrect digest"); return(new VerificationResult { Status = SigningStatus.FundamentalHashMismatch, Valid = false }); } } } // Detect signatures // // ISignatureFinder finder = new SignatureFinder(); List <Signature> signatures = finder.Find(jar); if (!signatures.Any()) { Log.Message("No signatures detected"); return(new VerificationResult { Status = SigningStatus.NotSigned, Valid = false }); } Log.Message($"{signatures.Count} signature(s) detected"); // Verify signatures // // SignatureVerifier ver = new SignatureVerifier(); if (ver.Verify(jar, centralManifest, signatures, certificates)) { return(new VerificationResult { Status = SigningStatus.SignedValid, Valid = true }); } else { return(new VerificationResult { Status = SigningStatus.SignedInvalid, Valid = false }); } }