Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 917a4e6

Browse files
committed
Add support for reading files from disk to X509Certificate2 and X509Certificate2Collection on Unix
With the file support it was easy to add a test that verifies that the Windows and Unix implementations of X509Certificate2Collection.Import on a PFX result in enumerating the files in the same order. The change uses OpenSSL's BIO structure to read the files, avoiding a File.ReadAllBytes approach to avoid cases wherein someone reads a very large file as a certificate.
1 parent 870e5a2 commit 917a4e6

File tree

9 files changed

+223
-25
lines changed

9 files changed

+223
-25
lines changed

src/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.NativeCrypto.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,21 @@ internal static partial class NativeCrypto
1212
{
1313
private delegate int NegativeSizeReadMethod<in THandle>(THandle handle, byte[] buf, int cBuf);
1414

15+
[DllImport(Libraries.CryptoInterop)]
16+
internal static extern int BioTell(SafeBioHandle bio);
17+
18+
[DllImport(Libraries.CryptoInterop)]
19+
internal static extern int BioSeek(SafeBioHandle bio, int pos);
20+
1521
[DllImport(Libraries.CryptoInterop)]
1622
private static extern int GetX509Thumbprint(SafeX509Handle x509, byte[] buf, int cBuf);
1723

1824
[DllImport(Libraries.CryptoInterop)]
1925
private static extern int GetX509NameRawBytes(IntPtr x509Name, byte[] buf, int cBuf);
2026

27+
[DllImport(Libraries.CryptoInterop)]
28+
internal static extern SafeX509Handle ReadX509AsDerFromBio(SafeBioHandle bio);
29+
2130
[DllImport(Libraries.CryptoInterop)]
2231
internal static extern IntPtr GetX509NotBefore(SafeX509Handle x509);
2332

src/Common/src/Interop/Unix/libcrypto/Interop.BIO.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ internal static partial class libcrypto
1818
[DllImport(Libraries.LibCrypto)]
1919
internal static extern SafeBioHandle BIO_new(IntPtr type);
2020

21+
[DllImport(Libraries.LibCrypto)]
22+
internal static extern SafeBioHandle BIO_new_file(string filename, string mode);
23+
2124
[DllImport(Libraries.LibCrypto)]
2225
internal static extern IntPtr BIO_s_mem();
2326

src/Native/System.Security.Cryptography.Native/openssl.c

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,3 +775,76 @@ GetX509RootStorePath()
775775

776776
return dir;
777777
}
778+
779+
/*
780+
Function:
781+
ReadX509AsDerFromBio
782+
783+
Used by System.Security.Cryptography.X509Certificates' OpenSslX509CertificateReader when attempting
784+
to turn the contents of a file into an ICertificatePal object.
785+
786+
Return values:
787+
If bio containns a valid DER-encoded X509 object, a pointer to that X509 structure that was deserialized,
788+
otherwise NULL.
789+
*/
790+
X509*
791+
ReadX509AsDerFromBio(
792+
BIO* bio)
793+
{
794+
return d2i_X509_bio(bio, NULL);
795+
}
796+
797+
/*
798+
Function:
799+
BioTell
800+
801+
Used by System.Security.Cryptography.X509Certificates' OpenSslX509CertificateReader when attempting
802+
to turn the contents of a file into an ICertificatePal object to allow seeking back to the start point
803+
in the event of a deserialization failure.
804+
805+
Return values:
806+
The current seek position of the BIO if it is a file-related BIO, -1 on NULL inputs, and has unspecified
807+
behavior on non-file, non-null BIO objects.
808+
809+
See also:
810+
OpenSSL's BIO_tell
811+
*/
812+
int BioTell(
813+
BIO* bio)
814+
{
815+
if (!bio)
816+
{
817+
return -1;
818+
}
819+
820+
return BIO_tell(bio);
821+
}
822+
823+
/*
824+
Function:
825+
BioTell
826+
827+
Used by System.Security.Cryptography.X509Certificates' OpenSslX509CertificateReader when attempting
828+
to turn the contents of a file into an ICertificatePal object to seek back to the start point
829+
in the event of a deserialization failure.
830+
831+
Return values:
832+
-1 if bio is NULL
833+
-1 if bio is a file-related BIO and seek fails
834+
0 if bio is a file-related BIO and seek succeeds
835+
otherwise unspecified
836+
837+
See also:
838+
OpenSSL's BIO_seek
839+
*/
840+
int BioSeek(
841+
BIO* bio,
842+
int ofs)
843+
{
844+
if (!bio)
845+
{
846+
return -1;
847+
}
848+
849+
return BIO_seek(bio, ofs);
850+
}

src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/CertificatePal.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,13 @@ public static unsafe ICertificatePal FromBlob(byte[] rawData, string password, X
8080

8181
public static ICertificatePal FromFile(string fileName, string password, X509KeyStorageFlags keyStorageFlags)
8282
{
83-
throw new NotImplementedException();
83+
// If we can't open the file, fail right away.
84+
using (SafeBioHandle fileBio = Interop.libcrypto.BIO_new_file(fileName, "rb"))
85+
{
86+
Interop.libcrypto.CheckValidOpenSslHandle(fileBio);
87+
88+
return OpenSslX509CertificateReader.FromBio(fileBio, password);
89+
}
8490
}
8591
}
8692
}

src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509CertificateReader.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,72 @@ internal OpenSslX509CertificateReader(SafeX509Handle handle)
3535

3636
_cert = handle;
3737
}
38+
39+
internal static ICertificatePal FromBio(SafeBioHandle bio, string password)
40+
{
41+
// Try reading the value as: PEM-X509, DER-X509, DER-PKCS12.
42+
int bioPosition = Interop.NativeCrypto.BioTell(bio);
43+
44+
Debug.Assert(bioPosition >= 0);
45+
46+
SafeX509Handle cert = Interop.libcrypto.PEM_read_bio_X509_AUX(bio, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
47+
48+
if (cert != null && !cert.IsInvalid)
49+
{
50+
return new OpenSslX509CertificateReader(cert);
51+
}
52+
53+
// Rewind, try again.
54+
Interop.NativeCrypto.BioSeek(bio, bioPosition);
55+
cert = Interop.NativeCrypto.ReadX509AsDerFromBio(bio);
56+
57+
if (cert != null && !cert.IsInvalid)
58+
{
59+
return new OpenSslX509CertificateReader(cert);
60+
}
61+
62+
// Rewind, try again.
63+
Interop.NativeCrypto.BioSeek(bio, bioPosition);
64+
65+
OpenSslPkcs12Reader pfx;
66+
67+
if (OpenSslPkcs12Reader.TryRead(bio, out pfx))
68+
{
69+
using (pfx)
70+
{
71+
pfx.Decrypt(password);
72+
73+
ICertificatePal first = null;
74+
75+
foreach (OpenSslX509CertificateReader certPal in pfx.ReadCertificates())
76+
{
77+
// When requesting an X509Certificate2 from a PFX only the first entry is
78+
// returned. Other entries should be disposed.
79+
if (first == null)
80+
{
81+
first = certPal;
82+
}
83+
else
84+
{
85+
certPal.Dispose();
86+
}
87+
}
88+
89+
return first;
90+
}
91+
}
92+
93+
// Since we aren't going to finish reading, leaving the buffer where it was when we got
94+
// it seems better than leaving it in some arbitrary other position.
95+
//
96+
// But, before seeking back to start, save the Exception representing the last reported
97+
// OpenSSL error in case the last BioSeek would change it.
98+
Exception openSslException = Interop.libcrypto.CreateOpenSslCryptographicException();
99+
100+
Interop.NativeCrypto.BioSeek(bio, bioPosition);
101+
102+
throw openSslException;
103+
}
38104

39105
public bool HasPrivateKey
40106
{

src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/StorePal.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,22 @@ public static IStorePal FromBlob(byte[] rawData, string password, X509KeyStorage
3434

3535
public static IStorePal FromFile(string fileName, string password, X509KeyStorageFlags keyStorageFlags)
3636
{
37-
throw new NotImplementedException();
37+
using (SafeBioHandle fileBio = Interop.libcrypto.BIO_new_file(fileName, "rb"))
38+
{
39+
Interop.libcrypto.CheckValidOpenSslHandle(fileBio);
40+
41+
OpenSslPkcs12Reader pfx;
42+
43+
if (OpenSslPkcs12Reader.TryRead(fileBio, out pfx))
44+
{
45+
using (pfx)
46+
{
47+
return PfxToCollection(pfx, password);
48+
}
49+
}
50+
}
51+
52+
return null;
3853
}
3954

4055
public static IStorePal FromCertificate(ICertificatePal cert)

src/System.Security.Cryptography.X509Certificates/tests/CertTests.cs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ namespace System.Security.Cryptography.X509Certificates.Tests
99
public static class CertTests
1010
{
1111
[Fact]
12-
[ActiveIssue(1993, PlatformID.AnyUnix)]
12+
[ActiveIssue(1985, PlatformID.AnyUnix)]
1313
public static void X509CertTest()
1414
{
15-
const string CertSubject =
16-
@"CN=Microsoft Corporate Root Authority, OU=ITG, O=Microsoft, L=Redmond, S=WA, C=US, [email protected]";
15+
string certSubject = TestData.NormalizeX500String(
16+
@"CN=Microsoft Corporate Root Authority, OU=ITG, O=Microsoft, L=Redmond, S=WA, C=US, [email protected]");
1717

1818
using (X509Certificate cert = new X509Certificate(Path.Combine("TestData", "microsoft.cer")))
1919
{
20-
Assert.Equal(CertSubject, cert.Subject);
21-
Assert.Equal(CertSubject, cert.Issuer);
20+
Assert.Equal(certSubject, cert.Subject);
21+
Assert.Equal(certSubject, cert.Issuer);
2222

2323
int snlen = cert.GetSerialNumber().Length;
2424
Assert.Equal(16, snlen);
@@ -49,19 +49,19 @@ public static void X509CertTest()
4949
}
5050

5151
[Fact]
52-
[ActiveIssue(1993, PlatformID.AnyUnix)]
52+
[ActiveIssue(1985, PlatformID.AnyUnix)]
5353
public static void X509Cert2Test()
5454
{
55-
const string CertName =
56-
@"[email protected], CN=ABA.ECOM Root CA, O=""ABA.ECOM, INC."", L=Washington, S=DC, C=US";
55+
string certName = TestData.NormalizeX500String(
56+
@"[email protected], CN=ABA.ECOM Root CA, O=""ABA.ECOM, INC."", L=Washington, S=DC, C=US");
5757

5858
DateTime notBefore = new DateTime(1999, 7, 12, 17, 33, 53, DateTimeKind.Utc).ToLocalTime();
5959
DateTime notAfter = new DateTime(2009, 7, 9, 17, 33, 53, DateTimeKind.Utc).ToLocalTime();
6060

6161
using (X509Certificate2 cert2 = new X509Certificate2(Path.Combine("TestData", "test.cer")))
6262
{
63-
Assert.Equal(CertName, cert2.IssuerName.Name);
64-
Assert.Equal(CertName, cert2.SubjectName.Name);
63+
Assert.Equal(certName, cert2.IssuerName.Name);
64+
Assert.Equal(certName, cert2.SubjectName.Name);
6565

6666
Assert.Equal("ABA.ECOM Root CA", cert2.GetNameInfo(X509NameType.DnsName, true));
6767

@@ -147,7 +147,6 @@ public static void X509Cert2ToStringVerbose()
147147
}
148148

149149
[Fact]
150-
[ActiveIssue(1993, PlatformID.AnyUnix)]
151150
public static void X509Cert2CreateFromPfxFile()
152151
{
153152
using (X509Certificate2 cert2 = new X509Certificate2(Path.Combine("TestData", "DummyTcpServer.pfx")))
@@ -158,7 +157,6 @@ public static void X509Cert2CreateFromPfxFile()
158157
}
159158

160159
[Fact]
161-
[ActiveIssue(1993, PlatformID.AnyUnix)]
162160
public static void X509Cert2CreateFromPfxWithPassword()
163161
{
164162
using (X509Certificate2 cert2 = new X509Certificate2(Path.Combine("TestData", "test.pfx"), "test"))

src/System.Security.Cryptography.X509Certificates/tests/CollectionTests.cs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,43 @@ public static void ImportPfx()
174174
}
175175
}
176176

177+
[Fact]
178+
public static void ImportFullChainPfx()
179+
{
180+
X509Certificate2Collection certs = new X509Certificate2Collection();
181+
certs.Import(Path.Combine("TestData", "test.pfx"), "test", X509KeyStorageFlags.DefaultKeySet);
182+
int count = certs.Count;
183+
Assert.Equal(3, count);
184+
185+
// Verify that the read ordering is consistent across the platforms
186+
string[] expectedSubjects =
187+
{
188+
"MS Passport Test Sub CA",
189+
"MS Passport Test Root CA",
190+
"test.local",
191+
};
192+
193+
string[] actualSubjects = certs.OfType<X509Certificate2>().
194+
Select(cert => cert.GetNameInfo(X509NameType.SimpleName, false)).
195+
ToArray();
196+
197+
Assert.Equal(expectedSubjects, actualSubjects);
198+
199+
// And verify that we have private keys when we expect them
200+
bool[] expectedHasPrivateKeys =
201+
{
202+
false,
203+
false,
204+
true,
205+
};
206+
207+
bool[] actualHasPrivateKeys = certs.OfType<X509Certificate2>().
208+
Select(cert => cert.HasPrivateKey).
209+
ToArray();
210+
211+
Assert.Equal(expectedHasPrivateKeys, actualHasPrivateKeys);
212+
}
213+
177214
[Fact]
178215
[ActiveIssue(1993, PlatformID.AnyUnix)]
179216
public static void ImportStoreSavedAsCerData()
@@ -267,7 +304,6 @@ public static void ImportStoreSavedAsPfxData()
267304
}
268305

269306
[Fact]
270-
[ActiveIssue(1993, PlatformID.AnyUnix)]
271307
public static void ImportFromFileTests()
272308
{
273309
using (var pfxCer = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword))

0 commit comments

Comments
 (0)