3 // Copyright (c) Microsoft Corporation. All rights reserved.
8 using System.Diagnostics;
9 using System.Diagnostics.CodeAnalysis;
10 using System.Globalization;
12 using System.Numerics;
14 using System.Xml.XPath;
16 using System.Diagnostics.Contracts;
17 using Microsoft.Win32.SafeHandles;
19 namespace System.Security.Cryptography {
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).
24 /// #RFC4050ECKeyFormat
26 /// The format looks similar to the following:
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>
33 /// <X Value="0123456789..." xsi:type="PrimeFieldElemType" />
34 /// <Y Value="0123456789..." xsi:type="PrimeFieldElemType" />
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";
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";
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
61 /// Restore a key from XML
63 internal static ECParameters FromXml(string xml, out bool isEcdh) {
64 Contract.Requires(xml != null);
65 Contract.Ensures(Contract.Result<CngKey>() != null);
67 ECParameters parameters = new ECParameters();
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();
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));
81 // First figure out which algorithm this key belongs to
82 parameters.Curve = ReadCurve(navigator, out isEcdh);
84 // Then read out the public key value
85 if (!navigator.MoveToNext(XPathNodeType.Element)) {
86 throw new ArgumentException(SR.GetString(SR.Cryptography_MissingPublicKey));
89 ReadPublicKey(navigator, ref parameters);
95 /// Determine which ECC curve the key refers to
97 private static ECCurve ReadCurve(XPathNavigator navigator, out bool isEcdh) {
98 Contract.Requires(navigator != null);
99 Contract.Ensures(Contract.Result<CngAlgorithm>() != null);
101 if (navigator.NamespaceURI != Namespace) {
102 throw new ArgumentException(SR.GetString(SR.Cryptography_UnexpectedXmlNamespace,
103 navigator.NamespaceURI,
108 // The name of the root element determines which algorithm to use, while the DomainParameters
109 // element specifies which curve we should be using.
112 bool isDHKey = navigator.Name == ECDHRoot;
113 bool isDsaKey = navigator.Name == ECDsaRoot;
115 if (!isDHKey && !isDsaKey) {
116 throw new ArgumentException(SR.GetString(SR.Cryptography_UnknownEllipticCurveAlgorithm));
119 // Move into the DomainParameters element
120 if (!navigator.MoveToFirstChild() || navigator.Name != DomainParametersRoot) {
121 throw new ArgumentException(SR.GetString(SR.Cryptography_MissingDomainParameters));
124 // Now move into the NamedCurve element
125 if (!navigator.MoveToFirstChild() || navigator.Name != NamedCurveElement) {
126 throw new ArgumentException(SR.GetString(SR.Cryptography_MissingDomainParameters));
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));
134 string oidUrn = navigator.Value;
136 if (!oidUrn.StartsWith(OidUrnPrefix, StringComparison.OrdinalIgnoreCase)) {
137 throw new ArgumentException(SR.GetString(SR.Cryptography_UnknownEllipticCurve));
140 // position the navigator at the end of the domain parameters
141 navigator.MoveToParent(); // NamedCurve
142 navigator.MoveToParent(); // DomainParameters
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);
148 return ECCurve.CreateFromValue(oidUrn.Substring(OidUrnPrefix.Length));
152 /// Read the x and y components of the public key
154 private static void ReadPublicKey(XPathNavigator navigator, ref ECParameters parameters) {
155 Contract.Requires(navigator != null);
157 if (navigator.NamespaceURI != Namespace) {
158 throw new ArgumentException(SR.GetString(SR.Cryptography_UnexpectedXmlNamespace,
159 navigator.NamespaceURI,
163 if (navigator.Name != PublicKeyRoot) {
164 throw new ArgumentException(SR.GetString(SR.Cryptography_MissingPublicKey));
167 // First get the x parameter
168 if (!navigator.MoveToFirstChild() || navigator.Name != XElement) {
169 throw new ArgumentException(SR.GetString(SR.Cryptography_MissingPublicKey));
171 if (!navigator.MoveToFirstAttribute() || navigator.Name != ValueAttribute || String.IsNullOrEmpty(navigator.Value)) {
172 throw new ArgumentException(SR.GetString(SR.Cryptography_MissingPublicKey));
175 BigInteger x = BigInteger.Parse(navigator.Value, CultureInfo.InvariantCulture);
176 navigator.MoveToParent();
178 // Then the y parameter
179 if (!navigator.MoveToNext(XPathNodeType.Element) || navigator.Name != YElement) {
180 throw new ArgumentException(SR.GetString(SR.Cryptography_MissingPublicKey));
182 if (!navigator.MoveToFirstAttribute() || navigator.Name != ValueAttribute || String.IsNullOrEmpty(navigator.Value)) {
183 throw new ArgumentException(SR.GetString(SR.Cryptography_MissingPublicKey));
186 BigInteger y = BigInteger.Parse(navigator.Value, CultureInfo.InvariantCulture);
188 byte[] xBytes = x.ToByteArray();
189 byte[] yBytes = y.ToByteArray();
191 int xLen = xBytes.Length;
192 int yLen = yBytes.Length;
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)
203 if (yLen > 0 && yBytes[yLen - 1] == 0)
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);
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;
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);
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 */ }
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.
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);
241 parameters.Q.X = xBytes;
242 parameters.Q.Y = yBytes;
246 /// Serialize out information about the elliptic curve
248 private static void WriteDomainParameters(XmlWriter writer, ref ECParameters parameters) {
249 Contract.Requires(writer != null);
251 Oid curveOid = parameters.Curve.Oid;
253 if (!parameters.Curve.IsNamed || curveOid == null)
254 throw new ArgumentException(SR.GetString(SR.Cryptography_UnknownEllipticCurve));
256 string oidValue = curveOid.Value;
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))
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)
266 case BCryptNative.BCRYPT_ECC_CURVE_NISTP256:
267 oidValue = ECDSA_P256_OID_VALUE;
270 case BCryptNative.BCRYPT_ECC_CURVE_NISTP384:
271 oidValue = ECDSA_P384_OID_VALUE;
274 case BCryptNative.BCRYPT_ECC_CURVE_NISTP521:
275 oidValue = ECDSA_P521_OID_VALUE;
279 Oid resolver = new Oid();
280 resolver.FriendlyName = curveOid.FriendlyName;
281 oidValue = resolver.Value;
286 if (string.IsNullOrEmpty(oidValue))
287 throw new ArgumentException(SR.GetString(SR.Cryptography_UnknownEllipticCurve));
289 writer.WriteStartElement(DomainParametersRoot);
291 // We always use OIDs for the named prime curves
292 writer.WriteStartElement(NamedCurveElement);
293 writer.WriteAttributeString(UrnAttribute, OidUrnPrefix + oidValue);
294 writer.WriteEndElement(); // </NamedCurve>
296 writer.WriteEndElement(); // </DomainParameters>
299 private static void WritePublicKeyValue(XmlWriter writer, ref ECParameters parameters) {
300 Contract.Requires(writer != null);
302 writer.WriteStartElement(PublicKeyRoot);
304 byte[] providedX = parameters.Q.X;
305 byte[] providedY = parameters.Q.Y;
307 int xSize = providedX.Length;
308 int ySize = providedY.Length;
309 const byte SignBit = 0x80;
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
316 if ((providedX[0] & SignBit) == SignBit) {
320 if ((providedY[0] & SignBit) == SignBit) {
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];
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);
333 Array.Reverse(xBytes);
334 Array.Reverse(yBytes);
336 BigInteger x = new BigInteger(xBytes);
337 BigInteger y = new BigInteger(yBytes);
339 writer.WriteStartElement(XElement);
340 writer.WriteAttributeString(ValueAttribute, x.ToString("R", CultureInfo.InvariantCulture));
341 writer.WriteAttributeString(XsiNamespacePrefix, XsiTypeAttribute, XsiNamespace, XsiTypeAttributeValue);
342 writer.WriteEndElement(); // </X>
344 writer.WriteStartElement(YElement);
345 writer.WriteAttributeString(ValueAttribute, y.ToString("R", CultureInfo.InvariantCulture));
346 writer.WriteAttributeString(XsiNamespacePrefix, XsiTypeAttribute, XsiNamespace, XsiTypeAttributeValue);
347 writer.WriteEndElement(); // </Y>
349 writer.WriteEndElement(); // </PublicKey>
353 /// Convert a key to XML
355 internal static string ToXml(ECParameters parameters, bool isEcdh) {
356 Contract.Ensures(Contract.Result<String>() != null);
358 parameters.Validate();
360 StringBuilder keyXml = new StringBuilder();
362 XmlWriterSettings settings = new XmlWriterSettings();
363 settings.Indent = true;
364 settings.IndentChars = " ";
365 settings.OmitXmlDeclaration = true;
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);
372 WriteDomainParameters(writer, ref parameters);
373 WritePublicKeyValue(writer, ref parameters);
375 writer.WriteEndElement(); // root element
378 return keyXml.ToString();