/// <summary> /// Get only the files in the JAR that have no connection to the signing process /// </summary> /// <param name="this"></param> /// <returns>set of files which are not metadata/signing related</returns> public static IEnumerable <string> NonSignatureFiles(this IJar @this) { string[] manifestExtensions = new string[] { ".RSA", ".DSA", ".MF", ".SF" }; return(@this.Files() .Where(f => { if (f.ToForwardSlashes().StartsWith(@"META-INF/")) { // Is it a manifest or signature? if (manifestExtensions.Any(ext => ext.Equals(Path.GetExtension(f), StringComparison.InvariantCultureIgnoreCase))) { return false; } return true; } else { // Not metadata return true; } })); }
/// <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> /// Verify all signatures in the JAR /// </summary> /// <param name="jar">JAR to verify</param> /// <param name="centralManifest">the main MANIFEST.MF</param> /// <param name="signatures">the set of signatures to verify</param> /// <param name="certificates">the set of permitted certificates we verify against</param> /// <returns>true if all signatures verify as valid - otherwise false</returns> public bool Verify( IJar jar, ManifestData centralManifest, List <Signature> signatures, IVerificationCertificates certificates) { if (jar == null) { throw new ArgumentNullException(nameof(jar)); } if (centralManifest == null) { throw new ArgumentNullException(nameof(centralManifest)); } if (certificates == null) { throw new ArgumentNullException(nameof(certificates)); } if (!signatures.Any()) { return(false); } foreach (Signature sig in signatures) { ManifestData signFile = _loader.Load(jar, sig.ManifestPath); Log.Message($"Signature {sig.BaseName} @ {sig.ManifestPath} with block {sig.Block.Path} type {sig.Block.Type}"); // Sign file hash mismatch if (!VerifyManifestHashes(centralManifest, signFile)) { return(false); } // Ensure we actually have a certificate to verify against if (!certificates.Contains(sig.BaseName)) { throw new MissingCertificateException($"Signature with base name {sig.BaseName} must have a matching certificate " + $"supplied in order to verify"); } } return(signatures.All(s => VerifyPKCS7(jar, s, certificates.Get(s.BaseName)))); }
public List <Signature> Find(IJar jar) { if (jar == null) { throw new ArgumentNullException(nameof(jar)); } // Signature from base name -> data Dictionary <string, Signature> found = new Dictionary <string, Signature>(StringComparer.InvariantCultureIgnoreCase); foreach (string candidate in jar.Files()) { string[] pathParts = candidate.Split('/'); // Must be in META-INF if (pathParts.Length == 2 && pathParts[0] == "META-INF") { string filenameOnly = pathParts[1]; if (filenameOnly.Equals("MANIFEST.MF", StringComparison.InvariantCultureIgnoreCase)) { // We don't care about the non-signature manifest continue; } // Base name being the actual overall signature name string baseName = Path.GetFileNameWithoutExtension(filenameOnly); Populate(found.AddOrGet(baseName, () => new Signature { BaseName = baseName }), candidate, filenameOnly); } } return(found.Values .Where(s => !string.IsNullOrEmpty(s.ManifestPath) || s.Block != null) .ToList()); }
/// <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> /// Verify a PKCS7 signature block /// </summary> /// <param name="jar">JAR from which to read and verify the signature</param> /// <param name="sig">the signature being verified</param> /// <param name="certificate">the raw certificate bytes against which to verify (i.e. public key)</param> /// <returns>whether the PKCS signature is valid</returns> private bool VerifyPKCS7(IJar jar, Signature sig, byte[] certificate) { try { // Detached content to verify - in this case, the .SF file // (against which the signature block validates its hash) CmsProcessableByteArray detachedContent; // We cannot easily reuse a reader against the SF file // So instead, copy to memory in entirety and build from byte array using (Stream sigFile = jar.Open(sig.ManifestPath)) using (MemoryStream sigFileMemory = new MemoryStream()) { sigFile.CopyTo(sigFileMemory); detachedContent = new CmsProcessableByteArray(sigFileMemory.ToArray()); } // Open the signature block (e.g. .RSA or .DSA) using (Stream block = jar.Open(sig.Block.Path)) { X509CertificateParser certParser = new X509CertificateParser(); // Read the caller's certificate (assumed to have a matching public key) X509Certificate cert = certParser.ReadCertificate(certificate); CmsSignedData signedData = new CmsSignedData(detachedContent, block); SignerInformationStore signers = signedData.GetSignerInfos(); int verified = 0; foreach (SignerInformation signer in signers.GetSigners()) { Log.Message($"Verifying against {cert.SubjectDN.ToString()}"); if (signer.Verify(cert)) { verified++; Log.Message($"Signature valid for {cert.SubjectDN.ToString()}"); } else { Log.Message($"Signature INVALID for {cert.SubjectDN.ToString()}"); } } // Every signer must verify OK return(verified == signers.GetSigners().Count); } } catch (Exception ex) { // Cert verification can trigger a number of different possible errors // (Ranging from cert bytes invalid -> key type mismatch) Log.Error(ex, "Failed to verify certifiate: assuming invalid"); return(false); } }
/// <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 }); } }