Updates referencesource to .NET 4.7
[mono.git] / mcs / class / referencesource / System.Core / System / Security / Cryptography / Rfc4050KeyFormatter.cs
1 // ==++==
2 // 
3 //   Copyright (c) Microsoft Corporation.  All rights reserved.
4 // 
5 // ==--==
6
7 using System;
8 using System.Diagnostics;
9 using System.Diagnostics.CodeAnalysis;
10 using System.Globalization;
11 using System.IO;
12 using System.Numerics;
13 using System.Xml;
14 using System.Xml.XPath;
15 using System.Text;
16 using System.Diagnostics.Contracts;
17 using Microsoft.Win32.SafeHandles;
18
19 namespace System.Security.Cryptography {
20     /// <summary>
21     ///     Utility class to convert ECC keys into XML and back using a format similar to the one described
22     ///     in RFC 4050 (http://www.ietf.org/rfc/rfc4050.txt).
23     /// 
24     ///     #RFC4050ECKeyFormat
25     /// 
26     ///     The format looks similar to the following:
27     /// 
28     ///         <ECDSAKeyValue xmlns="http://www.w3.org/2001/04/xmldsig-more#">
29     ///             <DomainParameters>
30     ///                 <NamedCurve URN="urn:oid:1.3.132.0.35" />
31     ///             </DomainParameters>
32     ///             <PublicKey>
33     ///                 <X Value="0123456789..." xsi:type="PrimeFieldElemType" />
34     ///                 <Y Value="0123456789..." xsi:type="PrimeFieldElemType" />
35     ///             </PublicKey>
36     ///         </ECDSAKeyValue>
37     /// </summary>
38     internal static class Rfc4050KeyFormatter {
39         private const string DomainParametersRoot = "DomainParameters";
40         private const string ECDHRoot = "ECDHKeyValue";
41         private const string ECDsaRoot = "ECDSAKeyValue";
42         private const string NamedCurveElement = "NamedCurve";
43         private const string Namespace = "http://www.w3.org/2001/04/xmldsig-more#";
44         private const string OidUrnPrefix = "urn:oid:";
45         private const string PublicKeyRoot = "PublicKey";
46         private const string UrnAttribute = "URN";
47         private const string ValueAttribute = "Value";
48         private const string XElement = "X";
49         private const string YElement = "Y";
50
51         private const string XsiTypeAttribute = "type";
52         private const string XsiTypeAttributeValue = "PrimeFieldElemType";
53         private const string XsiNamespace = "http://www.w3.org/2001/XMLSchema-instance";
54         private const string XsiNamespacePrefix = "xsi";
55
56         private const string ECDSA_P256_OID_VALUE = "1.2.840.10045.3.1.7"; // nistP256 or secP256r1
57         private const string ECDSA_P384_OID_VALUE = "1.3.132.0.34"; // nistP384 or secP384r1
58         private const string ECDSA_P521_OID_VALUE = "1.3.132.0.35"; // nistP521 or secP521r1
59
60         /// <summary>
61         ///     Restore a key from XML
62         /// </summary>
63         internal static ECParameters FromXml(string xml, out bool isEcdh) {
64             Contract.Requires(xml != null);
65             Contract.Ensures(Contract.Result<CngKey>() != null);
66
67             ECParameters parameters = new ECParameters();
68
69             // Load the XML into an XPathNavigator to access sub elements
70             using (TextReader textReader = new StringReader(xml))
71             using (XmlTextReader xmlReader = new XmlTextReader(textReader)) {
72                 XPathDocument document = new XPathDocument(xmlReader);
73                 XPathNavigator navigator = document.CreateNavigator();
74                 
75                 // Move into the root element - we don't do a specific namespace check here for compatibility
76                 // with XML that Windows generates.
77                 if (!navigator.MoveToFirstChild()) {
78                     throw new ArgumentException(SR.GetString(SR.Cryptography_MissingDomainParameters));
79                 }
80
81                 // First figure out which algorithm this key belongs to
82                 parameters.Curve = ReadCurve(navigator, out isEcdh);
83
84                 // Then read out the public key value
85                 if (!navigator.MoveToNext(XPathNodeType.Element)) {
86                     throw new ArgumentException(SR.GetString(SR.Cryptography_MissingPublicKey));
87                 }
88
89                 ReadPublicKey(navigator, ref parameters);
90                 return parameters;
91             }
92         }
93
94         /// <summary>
95         ///     Determine which ECC curve the key refers to
96         /// </summary>
97         private static ECCurve ReadCurve(XPathNavigator navigator, out bool isEcdh) {
98             Contract.Requires(navigator != null);
99             Contract.Ensures(Contract.Result<CngAlgorithm>() != null);
100
101             if (navigator.NamespaceURI != Namespace) {
102                 throw new ArgumentException(SR.GetString(SR.Cryptography_UnexpectedXmlNamespace,
103                                                          navigator.NamespaceURI,
104                                                          Namespace));
105             }
106
107             //
108             // The name of the root element determines which algorithm to use, while the DomainParameters
109             // element specifies which curve we should be using.
110             //
111
112             bool isDHKey = navigator.Name == ECDHRoot;
113             bool isDsaKey = navigator.Name == ECDsaRoot;
114
115             if (!isDHKey && !isDsaKey) {
116                 throw new ArgumentException(SR.GetString(SR.Cryptography_UnknownEllipticCurveAlgorithm));
117             }
118
119             // Move into the DomainParameters element
120             if (!navigator.MoveToFirstChild() || navigator.Name != DomainParametersRoot) {
121                 throw new ArgumentException(SR.GetString(SR.Cryptography_MissingDomainParameters));
122             }
123
124             // Now move into the NamedCurve element
125             if (!navigator.MoveToFirstChild() || navigator.Name != NamedCurveElement) {
126                 throw new ArgumentException(SR.GetString(SR.Cryptography_MissingDomainParameters));
127             }
128
129             // And read its URN value
130             if (!navigator.MoveToFirstAttribute() || navigator.Name != UrnAttribute || String.IsNullOrEmpty(navigator.Value)) {
131                 throw new ArgumentException(SR.GetString(SR.Cryptography_MissingDomainParameters));
132             }
133
134             string oidUrn = navigator.Value;
135
136             if (!oidUrn.StartsWith(OidUrnPrefix, StringComparison.OrdinalIgnoreCase)) {
137                 throw new ArgumentException(SR.GetString(SR.Cryptography_UnknownEllipticCurve));
138             }
139             
140             // position the navigator at the end of the domain parameters
141             navigator.MoveToParent();   // NamedCurve
142             navigator.MoveToParent();   // DomainParameters
143
144             // The out-bool only works because we have either/or.  If a third type of data is handled
145             // then a more complex signal is required.
146             Debug.Assert(isDHKey || isDsaKey);
147             isEcdh = isDHKey;
148             return ECCurve.CreateFromValue(oidUrn.Substring(OidUrnPrefix.Length));
149         }
150
151         /// <summary>
152         ///     Read the x and y components of the public key
153         /// </summary>
154         private static void ReadPublicKey(XPathNavigator navigator, ref ECParameters parameters) {
155             Contract.Requires(navigator != null);
156
157             if (navigator.NamespaceURI != Namespace) {
158                 throw new ArgumentException(SR.GetString(SR.Cryptography_UnexpectedXmlNamespace,
159                                                          navigator.NamespaceURI,
160                                                          Namespace));
161             }
162
163             if (navigator.Name != PublicKeyRoot) {
164                 throw new ArgumentException(SR.GetString(SR.Cryptography_MissingPublicKey));
165             }
166
167             // First get the x parameter
168             if (!navigator.MoveToFirstChild() || navigator.Name != XElement) {
169                 throw new ArgumentException(SR.GetString(SR.Cryptography_MissingPublicKey));
170             }
171             if (!navigator.MoveToFirstAttribute() || navigator.Name != ValueAttribute || String.IsNullOrEmpty(navigator.Value)) {
172                 throw new ArgumentException(SR.GetString(SR.Cryptography_MissingPublicKey));
173             }
174
175             BigInteger x = BigInteger.Parse(navigator.Value, CultureInfo.InvariantCulture);
176             navigator.MoveToParent();
177
178             // Then the y parameter
179             if (!navigator.MoveToNext(XPathNodeType.Element) || navigator.Name != YElement) {
180                 throw new ArgumentException(SR.GetString(SR.Cryptography_MissingPublicKey));
181             }
182             if (!navigator.MoveToFirstAttribute() || navigator.Name != ValueAttribute || String.IsNullOrEmpty(navigator.Value)) {
183                 throw new ArgumentException(SR.GetString(SR.Cryptography_MissingPublicKey));
184             }
185
186             BigInteger y = BigInteger.Parse(navigator.Value, CultureInfo.InvariantCulture);
187
188             byte[] xBytes = x.ToByteArray();
189             byte[] yBytes = y.ToByteArray();
190
191             int xLen = xBytes.Length;
192             int yLen = yBytes.Length;
193
194             // If the last byte of X is 0x00 that's a padding byte by BigInteger to indicate X is
195             // a positive number with the highest bit in the most significant byte set. We can't count
196             // that in the length of the number.
197             if (xLen > 0 && xBytes[xLen - 1] == 0)
198             {
199                 xLen--;
200             }
201
202             // Ditto for Y.
203             if (yLen > 0 && yBytes[yLen - 1] == 0)
204             {
205                 yLen--;
206             }
207
208             // Q.X and Q.Y have to be the same length.  They ultimately have to be the right length for the curve,
209             // but that requires more knowledge than we have. So we'll ask the system. If it doesn't know, just make
210             // them match each other.
211             int requiredLength = Math.Max(xLen, yLen);
212
213             try {
214                 using (ECDsa ecdsa = ECDsa.Create(parameters.Curve)) {
215                     // Convert the bit value of keysize to a byte value.
216                     // EC curves can have non-mod-8 keysizes (e.g. 521), so the +7 is really necessary.
217                     int curveLength = (ecdsa.KeySize + 7) / 8;
218
219                     // We could just use this answer, but if the user has formatted the input to be
220                     // too long, maybe they know something we don't.
221                     requiredLength = Math.Max(requiredLength, curveLength);
222                 }
223             }
224             catch (ArgumentException) { /* Curve had invalid data, like an empty OID */ }
225             catch (CryptographicException) { /* The system failed to generate a key for the curve */ }
226             catch (NotSupportedException) { /* An unknown curve type was requested */ }
227
228             // There is a chance that the curve is known to Windows but not allowed for ECDH
229             // (curve25519 is known to be in this state). Since RFC4050 is officially only
230             // concerned with ECDSA, and the only known example of this problem does not have
231             // an OID, it is not worth trying to generate the curve under ECDH as a fallback.
232
233             // Since BigInteger does Little Endian and Array.Resize maintains indexes when growing,
234             // just Array.Resize, then Array.Reverse. We could optimize this to be 1N instead of 2N,
235             // but this isn't a very hot codepath, so use tried-and-true methods.
236             Array.Resize(ref xBytes, requiredLength);
237             Array.Resize(ref yBytes, requiredLength);
238             Array.Reverse(xBytes);
239             Array.Reverse(yBytes);
240
241             parameters.Q.X = xBytes;
242             parameters.Q.Y = yBytes;
243         }
244
245         /// <summary>
246         ///     Serialize out information about the elliptic curve
247         /// </summary>
248         private static void WriteDomainParameters(XmlWriter writer, ref ECParameters parameters) {
249             Contract.Requires(writer != null);
250
251             Oid curveOid = parameters.Curve.Oid;
252
253             if (!parameters.Curve.IsNamed || curveOid == null)
254                 throw new ArgumentException(SR.GetString(SR.Cryptography_UnknownEllipticCurve));
255
256             string oidValue = curveOid.Value;
257
258             // If the OID didn't specify a value, use the mutable FriendlyName behavior of
259             // resolving the value without throwing an exception.
260             if (string.IsNullOrEmpty(oidValue))
261             {
262                 // The name strings for the 3 NIST curves from Win7 changed in Win10, but the Win10
263                 // names are what we use. This fallback supports Win7-Win8.1 resolution
264                 switch (curveOid.FriendlyName)
265                 {
266                     case BCryptNative.BCRYPT_ECC_CURVE_NISTP256:
267                         oidValue = ECDSA_P256_OID_VALUE;
268                         break;
269
270                     case BCryptNative.BCRYPT_ECC_CURVE_NISTP384:
271                         oidValue = ECDSA_P384_OID_VALUE;
272                         break;
273
274                     case BCryptNative.BCRYPT_ECC_CURVE_NISTP521:
275                         oidValue = ECDSA_P521_OID_VALUE;
276                         break;
277
278                     default:
279                         Oid resolver = new Oid();
280                         resolver.FriendlyName = curveOid.FriendlyName;
281                         oidValue = resolver.Value;
282                         break;
283                 }
284             }
285
286             if (string.IsNullOrEmpty(oidValue))
287                 throw new ArgumentException(SR.GetString(SR.Cryptography_UnknownEllipticCurve));
288
289             writer.WriteStartElement(DomainParametersRoot);
290
291             // We always use OIDs for the named prime curves
292             writer.WriteStartElement(NamedCurveElement);
293             writer.WriteAttributeString(UrnAttribute, OidUrnPrefix + oidValue);
294             writer.WriteEndElement();   // </NamedCurve>
295
296             writer.WriteEndElement();   // </DomainParameters>
297         }
298
299         private static void WritePublicKeyValue(XmlWriter writer, ref ECParameters parameters) {
300             Contract.Requires(writer != null);
301             
302             writer.WriteStartElement(PublicKeyRoot);
303
304             byte[] providedX = parameters.Q.X;
305             byte[] providedY = parameters.Q.Y;
306
307             int xSize = providedX.Length;
308             int ySize = providedY.Length;
309             const byte SignBit = 0x80;
310
311             // BigInteger will interpret a byte[] number as negative if the most significant bit is set.
312             // Since we're still in Big Endian at this point that means checking val[0].
313             // If the high bit is set, we need to extract into a byte[] with a padding zero to keep the
314             // sign bit cleared.
315
316             if ((providedX[0] & SignBit) == SignBit) {
317                 xSize++;
318             }
319
320             if ((providedY[0] & SignBit) == SignBit) {
321                 ySize++;
322             }
323
324             // We can't just use the arrays that are passed in even when the number wasn't negative,
325             // because we need to reverse the bytes to load into BigInteger.
326             byte[] xBytes = new byte[xSize];
327             byte[] yBytes = new byte[ySize];
328
329             // If the size grew then the offset will be 1, otherwise 0.
330             Buffer.BlockCopy(providedX, 0, xBytes, xSize - providedX.Length, providedX.Length);
331             Buffer.BlockCopy(providedY, 0, yBytes, ySize - providedY.Length, providedY.Length);
332
333             Array.Reverse(xBytes);
334             Array.Reverse(yBytes);
335
336             BigInteger x = new BigInteger(xBytes);
337             BigInteger y = new BigInteger(yBytes);
338
339             writer.WriteStartElement(XElement);
340             writer.WriteAttributeString(ValueAttribute, x.ToString("R", CultureInfo.InvariantCulture));
341             writer.WriteAttributeString(XsiNamespacePrefix, XsiTypeAttribute, XsiNamespace, XsiTypeAttributeValue);
342             writer.WriteEndElement();   // </X>
343
344             writer.WriteStartElement(YElement);
345             writer.WriteAttributeString(ValueAttribute, y.ToString("R", CultureInfo.InvariantCulture));
346             writer.WriteAttributeString(XsiNamespacePrefix, XsiTypeAttribute, XsiNamespace, XsiTypeAttributeValue);
347             writer.WriteEndElement();   // </Y>
348
349             writer.WriteEndElement();   // </PublicKey>
350         }
351
352         /// <summary>
353         ///     Convert a key to XML
354         /// </summary>
355         internal static string ToXml(ECParameters parameters, bool isEcdh) {
356             Contract.Ensures(Contract.Result<String>() != null);
357
358             parameters.Validate();
359
360             StringBuilder keyXml = new StringBuilder();
361
362             XmlWriterSettings settings = new XmlWriterSettings();
363             settings.Indent = true;
364             settings.IndentChars = "  ";
365             settings.OmitXmlDeclaration = true;
366
367             using (XmlWriter writer = XmlWriter.Create(keyXml, settings)) {
368                 // The root element depends upon the type of key
369                 string rootElement = isEcdh ? ECDHRoot : ECDsaRoot;
370                 writer.WriteStartElement(rootElement, Namespace);
371
372                 WriteDomainParameters(writer, ref parameters);
373                 WritePublicKeyValue(writer, ref parameters);
374
375                 writer.WriteEndElement();   // root element
376             }
377
378             return keyXml.ToString();
379         }
380     }
381 }