summary refs log tree commit diff
diff options
context:
space:
mode:
authormw <megan@cryptoworkshop.com>2021-09-03 14:35:32 +1000
committermw <megan@cryptoworkshop.com>2021-09-03 14:35:32 +1000
commit33c6a78371a5f6789c36186d7b89576a7c19d942 (patch)
tree98b77aee1360d479ece0de5642b82c8ad67a47d4
parentmoved test (diff)
downloadBouncyCastle.NET-ed25519-33c6a78371a5f6789c36186d7b89576a7c19d942.tar.xz
Updated PemReader to support differing whitespace assumptions.
Added tests.
Issue #2
-rw-r--r--crypto/crypto.csproj1
-rw-r--r--crypto/src/util/io/pem/PemHeader.cs7
-rw-r--r--crypto/src/util/io/pem/PemReader.cs355
-rw-r--r--crypto/test/src/crypto/io/test/PemReaderTest.cs157
-rw-r--r--crypto/test/src/util/io/pem/test/AllTests.cs12
5 files changed, 492 insertions, 40 deletions
diff --git a/crypto/crypto.csproj b/crypto/crypto.csproj
index 9c07654d4..a6ddea38e 100644
--- a/crypto/crypto.csproj
+++ b/crypto/crypto.csproj
@@ -6078,6 +6078,7 @@
     <Compile Include="test\src\crypto\io\test\CipherStreamTest.cs">
       <SubType>Code</SubType>
     </Compile>
+    <Compile Include="test\src\crypto\io\test\PemReaderTest.cs" />
     <Compile Include="test\src\crypto\prng\test\CtrDrbgTest.cs">
       <SubType>Code</SubType>
     </Compile>
diff --git a/crypto/src/util/io/pem/PemHeader.cs b/crypto/src/util/io/pem/PemHeader.cs
index 72da8a4f7..c6236f534 100644
--- a/crypto/src/util/io/pem/PemHeader.cs
+++ b/crypto/src/util/io/pem/PemHeader.cs
@@ -51,5 +51,10 @@ namespace Org.BouncyCastle.Utilities.IO.Pem
 
 			return s.GetHashCode();
 		}
-	}
+
+        public override string ToString()
+        {
+			return name + ":" + val;
+        }
+    }
 }
diff --git a/crypto/src/util/io/pem/PemReader.cs b/crypto/src/util/io/pem/PemReader.cs
index 7e6252b9b..008a03524 100644
--- a/crypto/src/util/io/pem/PemReader.cs
+++ b/crypto/src/util/io/pem/PemReader.cs
@@ -1,24 +1,32 @@
 using System;
 using System.Collections;
 using System.IO;
-using System.Text;
+
 
 using Org.BouncyCastle.Utilities.Encoders;
 
 namespace Org.BouncyCastle.Utilities.IO.Pem
 {
-	public class PemReader
-	{
-		private const string BeginString = "-----BEGIN ";
-		private const string EndString = "-----END ";
 
+	public class PemReader
+	{		
 		private readonly TextReader reader;
+		private readonly MemoryStream buffer;
+		private readonly StreamWriter textBuffer;
+		private readonly IList pushback = Platform.CreateArrayList();
+		int c = 0;
+
+		
 
 		public PemReader(TextReader reader)
 		{
 			if (reader == null)
 				throw new ArgumentNullException("reader");
 
+
+			buffer = new MemoryStream();
+			textBuffer = new StreamWriter(buffer);
+
 			this.reader = reader;
 		}
 
@@ -27,64 +35,337 @@ namespace Org.BouncyCastle.Utilities.IO.Pem
 			get { return reader; }
 		}
 
+
 		/// <returns>
 		/// A <see cref="PemObject"/>
 		/// </returns>
-		/// <exception cref="IOException"></exception>
+		/// <exception cref="IOException"></exception>	
 		public PemObject ReadPemObject()
-		{
-            string line = reader.ReadLine();
+        {
 
-            while (line != null && !Platform.StartsWith(line, BeginString)) 
+			//
+			// Look for BEGIN
+			//
+
+			for (; ; )
+			{
+
+				// Seek a leading dash, ignore anything up to that point.
+				if (!seekDash())
+				{
+					// There are no pem objects here.
+					return null; 
+				}
+
+
+				// consume dash [-----]BEGIN ...
+				if (!consumeDash())
+				{
+					throw new IOException("no data after consuming leading dashes");
+				}
+
+
+				skipWhiteSpace();
+
+
+				if (!expect("BEGIN"))
+				{
+					continue;
+				}
+
+				break;
+
+			}
+
+
+			skipWhiteSpace();
+
+			//
+			// Consume type, accepting whitespace
+			//
+
+			if (!bufferUntilStopChar('-',false) )
             {
-                line = reader.ReadLine();
-            }
+				throw new IOException("ran out of data before consuming type");
+			}
+
+			string type = bufferedString().Trim();
 
-            if (line != null)
+
+			// Consume dashes after type.
+
+			if (!consumeDash())
+            {
+				throw new IOException("ran out of data consuming header");
+			}
+
+			skipWhiteSpace();
+
+
+			//
+			// Read ahead looking for headers.
+			// Look for a colon for up to 64 characters, as an indication there might be a header.
+			//
+
+			IList headers = Platform.CreateArrayList();
+
+			while (seekColon(64))
             {
-                line = line.Substring(BeginString.Length);
-                int index = line.IndexOf('-');
 
-                if (index > 0 && Platform.EndsWith(line, "-----") && (line.Length - index) == 5)
+				if (!bufferUntilStopChar(':',false))
                 {
-                    string type = line.Substring(0, index);
+					throw new IOException("ran out of data reading header key value");
+				}
+
+				string key = bufferedString().Trim();
+
 
-                    return LoadObject(type);
+				c = Read();
+				if (c != ':')
+                {
+					throw new IOException("expected colon");
                 }
-            }
+				
+
+				//
+				// We are going to look for well formed headers, if they do not end with a "LF" we cannot
+				// discern where they end.
+				//
+			
+				if (!bufferUntilStopChar('\n', false)) // Now read to the end of the line.
+                {
+					throw new IOException("ran out of data before consuming header value");
+				}
+
+				skipWhiteSpace();
+
+				string value = bufferedString().Trim();
+				headers.Add(new PemHeader(key,value));
+			}
+
+
+			//
+			// Consume payload, ignoring all white space until we encounter a '-'
+			//
+
+			skipWhiteSpace();
+
+			if (!bufferUntilStopChar('-',true))
+			{
+				throw new IOException("ran out of data before consuming payload");
+			}
+
+			string payload = bufferedString();
+		
+			// Seek the start of the end.
+			if (!seekDash())
+			{
+				throw new IOException("did not find leading '-'");
+			}
+
+			if (!consumeDash())
+			{
+				throw new IOException("no data after consuming trailing dashes");
+			}
+
+			if (!expect("END "+type))
+			{
+				throw new IOException("END "+type+" was not found.");
+			}
+
+
+
+			if (!seekDash())
+			{
+				throw new IOException("did not find ending '-'");
+			}
+
+
+			// consume trailing dashes.
+			consumeDash();
+			
+
+			return new PemObject(type, headers, Base64.Decode(payload));
 
-            return null;
 		}
 
-		private PemObject LoadObject(string type)
+
+	
+		private string bufferedString()
+        {
+			textBuffer.Flush();
+			string value = Strings.FromUtf8ByteArray(buffer.ToArray());
+			buffer.Position = 0;
+			buffer.SetLength(0);
+			return value;
+        }
+
+
+		private bool seekDash()
+        {
+			c = 0;
+			while((c = Read()) >=0)
+            {
+				if (c == '-')
+                {
+					break;
+                }
+            }
+
+			PushBack(c);
+
+			return c == '-';
+        }
+
+
+		/// <summary>
+		/// Seek ':" up to the limit.
+		/// </summary>
+		/// <param name="upTo"></param>
+		/// <returns></returns>
+		private bool seekColon(int upTo)
 		{
-			string endMarker = EndString + type;
-			IList headers = Platform.CreateArrayList();
-			StringBuilder buf = new StringBuilder();
+			c = 0;
+			bool colonFound = false;
+			IList read = Platform.CreateArrayList();
+
+			for (; upTo>=0 && c >=0; upTo--)
+            {
+				c = Read();
+				read.Add(c);
+				if (c == ':')
+                {
+					colonFound = true;
+					break;
+                }
+            }
 
-			string line;
-			while ((line = reader.ReadLine()) != null)
+			while(read.Count>0)
+            {
+				PushBack((int)read[read.Count-1]);
+				read.RemoveAt(read.Count-1);
+            }
+
+			return colonFound;
+		}
+
+
+
+		/// <summary>
+		/// Consume the dashes
+		/// </summary>
+		/// <returns></returns>
+		private bool consumeDash()
+        {
+			c = 0;
+			while ((c = Read()) >= 0)
 			{
-				int colonPos = line.IndexOf(':');
-				if (colonPos >= 0)
+				if (c != '-')
 				{
-                    string hdr = line.Substring(0, colonPos);
-                    string val = line.Substring(colonPos + 1).Trim();
+					break;
+				}
+			}
 
-                    headers.Add(new PemHeader(hdr, val));
-                    continue;
+			PushBack(c);
+
+			return c != -1;
+		}
+
+		/// <summary>
+		/// Skip white space leave char in stream.
+		/// </summary>
+		private void skipWhiteSpace()
+        {
+			while ((c = Read()) >= 0)
+			{
+				if (c > ' ')
+				{
+					break;
 				}
+			}
+			PushBack(c);
+		}
 
-                if (Platform.IndexOf(line, endMarker) >= 0)
-                    break;
+		/// <summary>
+		/// Read forward consuming the expected string.
+		/// </summary>
+		/// <param name="value">expected string</param>
+		/// <returns>false if not consumed</returns>
 
-                buf.Append(line.Trim());
+		private bool expect(String value)
+        {
+			for (int t=0; t<value.Length; t++)
+            {
+				c = Read();
+				if (c == value[t])
+                {
+					continue;
+                } else
+                {
+					return false;
+                }
             }
 
-            if (line == null)
-				throw new IOException(endMarker + " not found");
+			return true;
+        }
+		
+
+		/// <summary>
+		/// Consume until dash.
+		/// </summary>
+		/// <returns>true if stream end not met</returns>
+		private bool bufferUntilStopChar(char stopChar,   bool skipWhiteSpace)
+        {
+			while ((c = Read()) >= 0)
+			{	
+				if (skipWhiteSpace && c <=' ')
+                {
+					continue;
+                }
 
-			return new PemObject(type, headers, Base64.Decode(buf.ToString()));
+				if (c != stopChar)
+				{
+					textBuffer.Write((char)c);
+					textBuffer.Flush();
+					
+				} else
+                {
+					  PushBack(c);
+					break;
+                }
+			}
+			
+			return c > -1;
 		}
+
+
+
+		private void PushBack(int value)
+        {
+			if (pushback.Count == 0)
+            {
+				pushback.Add(value);
+            } else
+            {
+				pushback.Insert(0, value);
+            }
+        }
+
+
+		private int Read()
+        {
+			if (pushback.Count>0)
+            {
+				int i = (int)pushback[0];
+				pushback.RemoveAt(0);
+				return i;
+            }
+
+			return reader.Read();
+        }
+
+
+
+
 	}
 }
diff --git a/crypto/test/src/crypto/io/test/PemReaderTest.cs b/crypto/test/src/crypto/io/test/PemReaderTest.cs
new file mode 100644
index 000000000..27b7a1f69
--- /dev/null
+++ b/crypto/test/src/crypto/io/test/PemReaderTest.cs
@@ -0,0 +1,157 @@
+using NUnit.Framework;
+using Org.BouncyCastle.Asn1;
+using Org.BouncyCastle.Asn1.X509;
+using Org.BouncyCastle.Pkcs;
+using Org.BouncyCastle.Utilities.IO.Pem;
+using System;
+using System.IO;
+
+namespace Org.BouncyCastle.Crypto.IO.Tests
+{
+    [TestFixture]
+    public class PemReaderTest
+    {
+
+        [Test]
+        public void TestMalformedInput()
+        {
+            string raw = "-----BEGIN CERTIFICATE REQUEST----- MIIBkTCB+wIBADAUMRIwEAYDVQQDDAlUZXN0MlNBTnMwgZ8wDQYJKoZIhvcNAQEB BQADgY0AMIGJAoGBAPPPH7W8LqBMCwSu/MsmCeSCfBzMEp4k+aZmeKw8EQD1R3FK WtPy/LcaUyQhyIeNPFAH8JEz0dJRJjleFL8G5pv7c2YXjBmIfbF/W2eETBIohMDP pWOqKYiT1mqzw25rP1VuXGXaSfN22RReomUd9O2GuEkaqz5x5iTRD6aLmDoJAgMB AAGgPjA8BgkqhkiG9w0BCQ4xLzAtMCsGA1UdEQQkMCKCD3NhbjEudGVzdC5sb2Nh bIIPc2FuMi50ZXN0LmxvY2FsMA0GCSqGSIb3DQEBCwUAA4GBAOacp+9s7/jpmSTA ORvx4nsDwBsY4VLeuPUc2gYmHqfVgrCCSHKPQtQge0P5atudbo+q8Fn+/5JnJR6/ JaooICY3M+/QVrvzvV30i5W8aEIERfXsEIcFyVxv24p6SbrGAcSjwpqvgAf0z82F D3f1qdFATb9HAFsuD/J0HexTFDvB -----END CERTIFICATE REQUEST-----";
+
+            PemReader pemReader = new PemReader(new StringReader(raw));
+            var item = pemReader.ReadPemObject();
+
+            var pkcs10 = Pkcs10CertificationRequest.GetInstance(Asn1Sequence.GetInstance(item.Content));
+            var subject = pkcs10.GetCertificationRequestInfo().Subject.ToString();
+
+            Assert.AreEqual("CERTIFICATE REQUEST", item.Type);
+            Assert.AreEqual("CN=Test2SANs", subject);
+        }
+
+
+
+        [Test]
+        public void TestSaneInput()
+        {
+
+            String test = "Certificate:\n" +
+         "    Data:\n" +
+         "        Version: 3 (0x2)\n" +
+         "        Serial Number: 865 (0x361)\n" +
+         "    Signature Algorithm: ecdsa-with-SHA1\n" +
+         "        Issuer: CN=estExampleCA\n" +
+         "        Validity\n" +
+         "            Not Before: Sep 29 12:41:31 2014 GMT\n" +
+         "            Not After : Dec 16 12:41:31 2022 GMT\n" +
+         "        Subject: CN=*.cisco.com\n" +
+         "        Subject Public Key Info:\n" +
+         "            Public Key Algorithm: rsaEncryption\n" +
+         "                Public-Key: (1024 bit)\n" +
+         "                Modulus:\n" +
+         "                    00:b7:08:e6:18:f2:32:d7:07:44:4b:f3:b1:83:01:\n" +
+         "                    59:f8:bc:ec:26:71:92:9a:53:70:f2:c0:be:2a:d6:\n" +
+         "                    26:6f:45:11:86:d7:ee:37:9d:d3:2f:22:b2:8b:9b:\n" +
+         "                    c5:96:00:36:73:97:c3:4c:f2:7a:0b:2c:e0:cc:d9:\n" +
+         "                    f0:ec:ba:1b:75:8c:66:b1:86:10:fd:be:df:6b:67:\n" +
+         "                    9c:0e:6b:2a:0e:d0:80:a8:dc:7a:d4:df:6e:79:28:\n" +
+         "                    a7:60:1a:11:b7:ae:40:94:bb:b4:11:ed:1b:6f:a7:\n" +
+         "                    91:ae:33:ec:bf:9c:30:f3:dc:91:2c:b4:3e:8c:c9:\n" +
+         "                    bd:f1:d1:aa:f6:c2:1d:6a:cd\n" +
+         "                Exponent: 65537 (0x10001)\n" +
+         "        X509v3 extensions:\n" +
+         "            X509v3 Basic Constraints: \n" +
+         "                CA:FALSE\n" +
+         "            X509v3 Key Usage: \n" +
+         "                Digital Signature, Non Repudiation, Key Encipherment\n" +
+         "    Signature Algorithm: ecdsa-with-SHA1\n" +
+         "         30:44:02:20:76:4f:3a:6c:b4:99:cb:1e:37:f4:0d:6e:e1:74:\n" +
+         "         4b:99:bb:f5:c4:b6:3d:c1:61:df:8c:d7:1f:9f:e7:d3:64:d6:\n" +
+         "         02:20:64:38:8f:6f:32:37:2b:7d:cf:28:93:e5:e6:e7:70:c5:\n" +
+         "         a9:12:04:b0:4b:a5:29:7b:23:df:85:f2:18:44:8b:d2\n" +
+         "-----BEGIN CERTIFICATE-----\n" +
+         "MIIBezCCASOgAwIBAgICA2EwCQYHKoZIzj0EATAXMRUwEwYDVQQDEwxlc3RFeGFt\n" +
+         "cGxlQ0EwHhcNMTQwOTI5MTI0MTMxWhcNMjIxMjE2MTI0MTMxWjAWMRQwEgYDVQQD\n" +
+         "DAsqLmNpc2NvLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAtwjmGPIy\n" +
+         "1wdES/OxgwFZ+LzsJnGSmlNw8sC+KtYmb0URhtfuN53TLyKyi5vFlgA2c5fDTPJ6\n" +
+         "CyzgzNnw7LobdYxmsYYQ/b7fa2ecDmsqDtCAqNx61N9ueSinYBoRt65AlLu0Ee0b\n" +
+         "b6eRrjPsv5ww89yRLLQ+jMm98dGq9sIdas0CAwEAAaMaMBgwCQYDVR0TBAIwADAL\n" +
+         "BgNVHQ8EBAMCBeAwCQYHKoZIzj0EAQNHADBEAiB2TzpstJnLHjf0DW7hdEuZu/XE\n" +
+         "tj3BYd+M1x+f59Nk1gIgZDiPbzI3K33PKJPl5udwxakSBLBLpSl7I9+F8hhEi9I=\n" +
+         "-----END CERTIFICATE-----\n";
+
+            PemReader pemReader = new PemReader(new StringReader(test));
+            var item = pemReader.ReadPemObject();
+            Assert.AreEqual("CERTIFICATE", item.Type);
+            X509CertificateStructure cert = X509CertificateStructure.GetInstance(Asn1Sequence.GetInstance(item.Content));
+            Assert.AreEqual("CN=estExampleCA", cert.Issuer.ToString());
+        }
+
+
+        [Test]
+        public void TestWithHeaders()
+        {
+            String hdr = "Proc-Type: 4,CRL\n";
+            String hdr2 = "CRL: CRL Header\n";
+            String hdr3 = "Originator-Certificate: originator certificate\n";
+            String hdr4 = "CRL: crl header\n";
+            String hdr5 = "Originator-Certificate: next originator certificate\n";
+
+            String test = "-----BEGIN CERTIFICATE-----\n" + hdr + hdr2 + "    \t\r\0" + hdr3 + hdr4 + hdr5 +
+         "MIIBezCCASOgAwIBAgICA2EwCQYHKoZIzj0EATAXMRUwEwYDVQQDEwxlc3RFeGFt\n" +
+         "cGxlQ0EwHhcNMTQwOTI5MTI0MTMxWhcNMjIxMjE2MTI0MTMxWjAWMRQwEgYDVQQD\n" +
+         "DAsqLmNpc2NvLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAtwjmGPIy\n" +
+         "1wdES/OxgwFZ+LzsJnGSmlNw8sC+KtYmb0URhtfuN53TLyKyi5vFlgA2c5fDTPJ6\n" +
+         "CyzgzNnw7LobdYxmsYYQ/b7fa2ecDmsqDtCAqNx61N9ueSinYBoRt65AlLu0Ee0b\n" +
+         "b6eRrjPsv5ww89yRLLQ+jMm98dGq9sIdas0CAwEAAaMaMBgwCQYDVR0TBAIwADAL\n" +
+         "BgNVHQ8EBAMCBeAwCQYHKoZIzj0EAQNHADBEAiB2TzpstJnLHjf0DW7hdEuZu/XE\n" +
+         "tj3BYd+M1x+f59Nk1gIgZDiPbzI3K33PKJPl5udwxakSBLBLpSl7I9+F8hhEi9I=\n" +
+         "-----END CERTIFICATE-----\n";
+
+            PemReader pemReader = new PemReader(new StringReader(test));
+            var item = pemReader.ReadPemObject();
+            Assert.AreEqual("CERTIFICATE", item.Type);
+            X509CertificateStructure cert = X509CertificateStructure.GetInstance(Asn1Sequence.GetInstance(item.Content));
+            Assert.AreEqual("CN=estExampleCA", cert.Issuer.ToString());
+
+
+            int t = 0;
+            foreach(string[] items in new String[][] { 
+                new string[] { "Proc-Type", "4,CRL" },
+                new string[] { "CRL", "CRL Header" },
+                new string[] { "Originator-Certificate", "originator certificate" },
+                new string[] { "CRL", "crl header" },
+                new string[] { "Originator-Certificate", "next originator certificate" },
+
+            })
+            {
+                Assert.AreEqual(items[0], ((PemHeader)item.Headers[t]).Name);
+                Assert.AreEqual(items[1], ((PemHeader)item.Headers[t]).Value);
+                t++;
+            }
+
+        }
+
+        [Test]
+        public void TestNoWhiteSpace()
+        {
+           
+
+            String test = "-----BEGIN CERTIFICATE-----"+ 
+         "MIIBezCCASOgAwIBAgICA2EwCQYHKoZIzj0EATAXMRUwEwYDVQQDEwxlc3RFeGFt" +
+         "cGxlQ0EwHhcNMTQwOTI5MTI0MTMxWhcNMjIxMjE2MTI0MTMxWjAWMRQwEgYDVQQD" +
+         "DAsqLmNpc2NvLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAtwjmGPIy" +
+         "1wdES/OxgwFZ+LzsJnGSmlNw8sC+KtYmb0URhtfuN53TLyKyi5vFlgA2c5fDTPJ6" +
+         "CyzgzNnw7LobdYxmsYYQ/b7fa2ecDmsqDtCAqNx61N9ueSinYBoRt65AlLu0Ee0b" +
+         "b6eRrjPsv5ww89yRLLQ+jMm98dGq9sIdas0CAwEAAaMaMBgwCQYDVR0TBAIwADAL" +
+         "BgNVHQ8EBAMCBeAwCQYHKoZIzj0EAQNHADBEAiB2TzpstJnLHjf0DW7hdEuZu/XE" +
+         "tj3BYd+M1x+f59Nk1gIgZDiPbzI3K33PKJPl5udwxakSBLBLpSl7I9+F8hhEi9I=" +
+         "-----END CERTIFICATE-----";
+
+            PemReader pemReader = new PemReader(new StringReader(test));
+            var item = pemReader.ReadPemObject();
+            Assert.AreEqual("CERTIFICATE", item.Type);
+            X509CertificateStructure cert = X509CertificateStructure.GetInstance(Asn1Sequence.GetInstance(item.Content));
+            Assert.AreEqual("CN=estExampleCA", cert.Issuer.ToString());
+        }
+  
+    }
+}
diff --git a/crypto/test/src/util/io/pem/test/AllTests.cs b/crypto/test/src/util/io/pem/test/AllTests.cs
index c0ca667f5..2eea7221c 100644
--- a/crypto/test/src/util/io/pem/test/AllTests.cs
+++ b/crypto/test/src/util/io/pem/test/AllTests.cs
@@ -66,9 +66,17 @@ namespace Org.BouncyCastle.Utilities.IO.Pem.Tests
         [Test]
         public void TestMalformed()
         {
-            PemReader rd = new PemReader(new StringReader("-----BEGIN \n"));
+			try
+			{
+				PemReader rd = new PemReader(new StringReader("-----BEGIN \n"));
+				rd.ReadPemObject();
+				Assert.Fail("must fail on malformed");
+			} catch (IOException ioex)
+            {
+				Assert.AreEqual("ran out of data before consuming type", ioex.Message);
+            }
 
-            Assert.IsNull(rd.ReadPemObject());
+           
         }
 
 		private void lengthTest(string type, IList headers, byte[] data)