File: Internal\Cryptography\Pal\Windows\HelpersWindows.cs
Web Access
Project: src\src\runtime\src\libraries\System.Security.Cryptography.Pkcs\src\System.Security.Cryptography.Pkcs.csproj (System.Security.Cryptography.Pkcs)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.Win32.SafeHandles;
using static Interop.Crypt32;
using CryptProvParam = Interop.Advapi32.CryptProvParam;
using X509IssuerSerial = System.Security.Cryptography.Xml.X509IssuerSerial;

namespace Internal.Cryptography.Pal.Windows
{
    internal static class HelpersWindows
    {
        public static CryptographicException ToCryptographicException(this ErrorCode errorCode)
        {
            return ((int)errorCode).ToCryptographicException();
        }

        public static string ToStringAnsi(this IntPtr psz)
        {
            return Marshal.PtrToStringAnsi(psz)!;
        }

        // Used for binary blobs without internal pointers.
        public static unsafe byte[] GetMsgParamAsByteArray(this SafeCryptMsgHandle hCryptMsg, CryptMsgParamType paramType, int index = 0)
        {
            int cbData = 0;
            if (!Interop.Crypt32.CryptMsgGetParam(hCryptMsg, paramType, index, IntPtr.Zero, ref cbData))
                throw Marshal.GetLastPInvokeError().ToCryptographicException();

            byte[] data = new byte[cbData];
            fixed (byte* pvData = data)
            {
                if (!Interop.Crypt32.CryptMsgGetParam(hCryptMsg, paramType, index, pvData, ref cbData))
                    throw Marshal.GetLastPInvokeError().ToCryptographicException();
            }

            return data.Resize(cbData);
        }

        // Used for binary blobs with internal pointers.
        public static SafeHandle GetMsgParamAsMemory(this SafeCryptMsgHandle hCryptMsg, CryptMsgParamType paramType, int index = 0)
        {
            int cbData = 0;
            if (!Interop.Crypt32.CryptMsgGetParam(hCryptMsg, paramType, index, IntPtr.Zero, ref cbData))
            {
                throw Marshal.GetLastPInvokeError().ToCryptographicException();
            }

            SafeHandle pvData = SafeHeapAllocHandle.Alloc(cbData);
            if (!Interop.Crypt32.CryptMsgGetParam(hCryptMsg, paramType, index, pvData.DangerousGetHandle(), ref cbData))
            {
                Exception e = Marshal.GetLastPInvokeError().ToCryptographicException();
                pvData.Dispose();
                throw e;
            }

            return pvData;
        }

        public static byte[] ToByteArray(this DATA_BLOB blob)
        {
            if (blob.cbData == 0)
                return Array.Empty<byte>();

            int length = (int)(blob.cbData);
            byte[] data = new byte[length];
            Marshal.Copy(blob.pbData, data, 0, length);
            return data;
        }

        public static CryptMsgType GetMessageType(this SafeCryptMsgHandle hCryptMsg)
        {
            int cbData = sizeof(CryptMsgType);
            CryptMsgType cryptMsgType;
            if (!Interop.Crypt32.CryptMsgGetParam(hCryptMsg, CryptMsgParamType.CMSG_TYPE_PARAM, 0, out cryptMsgType, ref cbData))
                throw Marshal.GetLastPInvokeError().ToCryptographicException();
            return cryptMsgType;
        }

        public static int GetVersion(this SafeCryptMsgHandle hCryptMsg)
        {
            int cbData = sizeof(int);
            int version;
            if (!Interop.Crypt32.CryptMsgGetParam(hCryptMsg, CryptMsgParamType.CMSG_VERSION_PARAM, 0, out version, ref cbData))
                throw Marshal.GetLastPInvokeError().ToCryptographicException();
            return version;
        }

        /// <summary>
        /// Returns the inner content of the CMS.
        ///
        /// Special case: If the CMS is an enveloped CMS that has been decrypted and the inner content type is Oids.Pkcs7Data, the returned
        /// content bytes are the decoded octet bytes, rather than the encoding of those bytes. This is a documented convenience behavior of
        /// CryptMsgGetParam(CMSG_CONTENT_PARAM) that apparently got baked into the behavior of the managed EnvelopedCms class.
        /// </summary>
        public static ContentInfo GetContentInfo(this SafeCryptMsgHandle hCryptMsg)
        {
            byte[] oidBytes = hCryptMsg.GetMsgParamAsByteArray(CryptMsgParamType.CMSG_INNER_CONTENT_TYPE_PARAM);

            // .NET Framework compat: If we get a null or non-terminated string back from Crypt32, throwing an exception seems more apropros but
            // for the .NET Framework compat, we throw the result at the ASCII Encoder and let the chips fall where they may.
            int length = oidBytes.Length;
            if (length > 0 && oidBytes[length - 1] == 0)
            {
                length--;
            }

            string oidValue = Encoding.ASCII.GetString(oidBytes, 0, length);
            Oid contentType = new Oid(oidValue);
            byte[] content = hCryptMsg.GetMsgParamAsByteArray(CryptMsgParamType.CMSG_CONTENT_PARAM);

            return new ContentInfo(contentType, content);
        }

        public static X509Certificate2Collection GetOriginatorCerts(this SafeCryptMsgHandle hCryptMsg)
        {
            int numCertificates;
            int cbNumCertificates = sizeof(int);
            if (!Interop.Crypt32.CryptMsgGetParam(hCryptMsg, CryptMsgParamType.CMSG_CERT_COUNT_PARAM, 0, out numCertificates, ref cbNumCertificates))
                throw Marshal.GetLastPInvokeError().ToCryptographicException();
            X509Certificate2Collection certs = new X509Certificate2Collection();
            for (int index = 0; index < numCertificates; index++)
            {
                byte[] encodedCertificate = hCryptMsg.GetMsgParamAsByteArray(CryptMsgParamType.CMSG_CERT_PARAM, index);
                X509Certificate2 cert = X509CertificateLoader.LoadCertificate(encodedCertificate);
                certs.Add(cert);
            }
            return certs;
        }

        /// <summary>
        /// Returns (AlgId)(-1) if oid is unknown.
        /// </summary>
        public static AlgId ToAlgId(this string oidValue)
        {
            CRYPT_OID_INFO info = Interop.Crypt32.FindOidInfo(CryptOidInfoKeyType.CRYPT_OID_INFO_OID_KEY, oidValue, OidGroup.All, false);
            return (AlgId)(info.AlgId);
        }

        public static SafeCertContextHandle CreateCertContextHandle(this X509Certificate2 cert)
        {
            IntPtr pCertContext = cert.Handle;
            pCertContext = Interop.Crypt32.CertDuplicateCertificateContext(pCertContext);
            SafeCertContextHandle hCertContext = new SafeCertContextHandle(pCertContext);
            GC.KeepAlive(cert);
            return hCertContext;
        }

        public static byte[] GetSubjectKeyIdentifier(this SafeCertContextHandle hCertContext)
        {
            int cbData = 0;
            if (!Interop.Crypt32.CertGetCertificateContextProperty(hCertContext, CertContextPropId.CERT_KEY_IDENTIFIER_PROP_ID, null, ref cbData))
                throw Marshal.GetLastPInvokeError().ToCryptographicException();

            byte[] ski = new byte[cbData];
            if (!Interop.Crypt32.CertGetCertificateContextProperty(hCertContext, CertContextPropId.CERT_KEY_IDENTIFIER_PROP_ID, ski, ref cbData))
                throw Marshal.GetLastPInvokeError().ToCryptographicException();

            return ski.Resize(cbData);
        }

        public static SubjectIdentifier ToSubjectIdentifier(this CERT_ID certId)
        {
            switch (certId.dwIdChoice)
            {
                case CertIdChoice.CERT_ID_ISSUER_SERIAL_NUMBER:
                    {
                        const int dwStrType = (int)(CertNameStrTypeAndFlags.CERT_X500_NAME_STR | CertNameStrTypeAndFlags.CERT_NAME_STR_REVERSE_FLAG);

                        string issuer;
                        unsafe
                        {
                            DATA_BLOB* dataBlobPtr = &certId.u.IssuerSerialNumber.Issuer;

                            int nc = Interop.Crypt32.CertNameToStr((int)MsgEncodingType.All, dataBlobPtr, dwStrType, null, 0);
                            if (nc <= 1) // The API actually return 1 when it fails; which is not what the documentation says.
                            {
                                throw Marshal.GetLastPInvokeError().ToCryptographicException();
                            }

                            Span<char> name = nc <= 128 ? stackalloc char[128] : new char[nc];
                            fixed (char* namePtr = name)
                            {
                                nc = Interop.Crypt32.CertNameToStr((int)MsgEncodingType.All, dataBlobPtr, dwStrType, namePtr, nc);
                                if (nc <= 1) // The API actually return 1 when it fails; which is not what the documentation says.
                                {
                                    throw Marshal.GetLastPInvokeError().ToCryptographicException();
                                }

                                issuer = new string(namePtr);
                            }
                        }

                        byte[] serial = certId.u.IssuerSerialNumber.SerialNumber.ToByteArray();
                        X509IssuerSerial issuerSerial = new X509IssuerSerial(issuer, serial.ToSerialString());
                        return new SubjectIdentifier(SubjectIdentifierType.IssuerAndSerialNumber, issuerSerial);
                    }

                case CertIdChoice.CERT_ID_KEY_IDENTIFIER:
                    {
                        byte[] ski = certId.u.KeyId.ToByteArray();
                        return new SubjectIdentifier(SubjectIdentifierType.SubjectKeyIdentifier, ski.ToSkiString());
                    }

                default:
                    throw new CryptographicException(SR.Format(SR.Cryptography_Cms_Invalid_Subject_Identifier_Type, certId.dwIdChoice));
            }
        }

        public static SubjectIdentifierOrKey ToSubjectIdentifierOrKey(this CERT_ID certId)
        {
            //
            // SubjectIdentifierOrKey is just a SubjectIdentifier with an (irrelevant here) "key" option thumbtacked onto it so
            // the easiest way is to subcontract the job to SubjectIdentifier.
            //
            SubjectIdentifier subjectIdentifier = certId.ToSubjectIdentifier();
            SubjectIdentifierType subjectIdentifierType = subjectIdentifier.Type;
            switch (subjectIdentifierType)
            {
                case SubjectIdentifierType.IssuerAndSerialNumber:
                    return new SubjectIdentifierOrKey(SubjectIdentifierOrKeyType.IssuerAndSerialNumber, subjectIdentifier.Value!);

                case SubjectIdentifierType.SubjectKeyIdentifier:
                    return new SubjectIdentifierOrKey(SubjectIdentifierOrKeyType.SubjectKeyIdentifier, subjectIdentifier.Value!);

                default:
                    Debug.Fail("Only the framework can construct SubjectIdentifier's so if we got a bad value here, that's our fault.");
                    throw new CryptographicException(SR.Format(SR.Cryptography_Cms_Invalid_Subject_Identifier_Type, subjectIdentifierType));
            }
        }

        public static SubjectIdentifierOrKey ToSubjectIdentifierOrKey(this CERT_PUBLIC_KEY_INFO publicKeyInfo)
        {
            int keyLength = Interop.Crypt32.CertGetPublicKeyLength(MsgEncodingType.All, ref publicKeyInfo);
            string oidValue = publicKeyInfo.Algorithm.pszObjId.ToStringAnsi();
            AlgorithmIdentifier algorithmId = new AlgorithmIdentifier(Oid.FromOidValue(oidValue, OidGroup.PublicKeyAlgorithm), keyLength);

            byte[] keyValue = publicKeyInfo.PublicKey.ToByteArray();
            PublicKeyInfo pki = new PublicKeyInfo(algorithmId, keyValue);
            return new SubjectIdentifierOrKey(SubjectIdentifierOrKeyType.PublicKeyInfo, pki);
        }

        public static AlgorithmIdentifier ToAlgorithmIdentifier(this CRYPT_ALGORITHM_IDENTIFIER cryptAlgorithmIdentifier)
        {
            string oidValue = cryptAlgorithmIdentifier.pszObjId.ToStringAnsi();
            AlgId algId = oidValue.ToAlgId();

            int keyLength;
            switch (algId)
            {
                case AlgId.CALG_RC2:
                    {
                        if (cryptAlgorithmIdentifier.Parameters.cbData == 0)
                        {
                            keyLength = 0;
                        }
                        else
                        {
                            CRYPT_RC2_CBC_PARAMETERS rc2Parameters;
                            unsafe
                            {
                                int cbSize = sizeof(CRYPT_RC2_CBC_PARAMETERS);
                                if (!Interop.Crypt32.CryptDecodeObject(CryptDecodeObjectStructType.PKCS_RC2_CBC_PARAMETERS, cryptAlgorithmIdentifier.Parameters.pbData, (int)(cryptAlgorithmIdentifier.Parameters.cbData), &rc2Parameters, ref cbSize))
                                    throw Marshal.GetLastPInvokeError().ToCryptographicException();
                            }

                            keyLength = rc2Parameters.dwVersion switch
                            {
                                CryptRc2Version.CRYPT_RC2_40BIT_VERSION => KeyLengths.Rc2_40Bit,
                                CryptRc2Version.CRYPT_RC2_56BIT_VERSION => KeyLengths.Rc2_56Bit,
                                CryptRc2Version.CRYPT_RC2_64BIT_VERSION => KeyLengths.Rc2_64Bit,
                                CryptRc2Version.CRYPT_RC2_128BIT_VERSION => KeyLengths.Rc2_128Bit,
                                _ => 0,
                            };
                        }
                        break;
                    }

                case AlgId.CALG_RC4:
                    {
                        int saltLength = 0;
                        if (cryptAlgorithmIdentifier.Parameters.cbData != 0)
                        {
                            using (SafeHandle sh = Interop.Crypt32.CryptDecodeObjectToMemory(CryptDecodeObjectStructType.X509_OCTET_STRING, cryptAlgorithmIdentifier.Parameters.pbData, (int)cryptAlgorithmIdentifier.Parameters.cbData))
                            {
                                unsafe
                                {
                                    DATA_BLOB* pDataBlob = (DATA_BLOB*)(sh.DangerousGetHandle());
                                    saltLength = (int)(pDataBlob->cbData);
                                }
                            }
                        }

                        // For RC4, keyLength = 128 - (salt length * 8).
                        keyLength = KeyLengths.Rc4Max_128Bit - saltLength * 8;
                        break;
                    }

                case AlgId.CALG_DES:
                    // DES key length is fixed at 64 (or 56 without the parity bits).
                    keyLength = KeyLengths.Des_64Bit;
                    break;

                case AlgId.CALG_3DES:
                    // 3DES key length is fixed at 192 (or 168 without the parity bits).
                    keyLength = KeyLengths.TripleDes_192Bit;
                    break;

                default:
                    // We've exhausted all the algorithm types that the .NET Framework used to set the KeyLength for. Key lengths are not a viable way of
                    // identifying algorithms in the long run so we will not extend this list any further.
                    keyLength = 0;
                    break;
            }

            AlgorithmIdentifier algorithmIdentifier = new AlgorithmIdentifier(Oid.FromOidValue(oidValue, OidGroup.All), keyLength);
            switch (oidValue)
            {
                case Oids.RsaOaep:
                    algorithmIdentifier.Parameters = cryptAlgorithmIdentifier.Parameters.ToByteArray();
                    break;
            }
            return algorithmIdentifier;
        }

        public static CryptographicAttributeObjectCollection GetUnprotectedAttributes(this SafeCryptMsgHandle hCryptMsg)
        {
            // For some reason, you can't ask how many attributes there are - you have to ask for the attributes and
            // get a CRYPT_E_ATTRIBUTES_MISSING failure if the count is 0.
            int cbUnprotectedAttr = 0;
            if (!Interop.Crypt32.CryptMsgGetParam(hCryptMsg, CryptMsgParamType.CMSG_UNPROTECTED_ATTR_PARAM, 0, IntPtr.Zero, ref cbUnprotectedAttr))
            {
                int lastError = Marshal.GetLastPInvokeError();
                if (lastError == (int)ErrorCode.CRYPT_E_ATTRIBUTES_MISSING)
                    return new CryptographicAttributeObjectCollection();
                throw lastError.ToCryptographicException();
            }

            using (SafeHandle sh = hCryptMsg.GetMsgParamAsMemory(CryptMsgParamType.CMSG_UNPROTECTED_ATTR_PARAM))
            {
                unsafe
                {
                    CRYPT_ATTRIBUTES* pCryptAttributes = (CRYPT_ATTRIBUTES*)(sh.DangerousGetHandle());
                    return ToCryptographicAttributeObjectCollection(pCryptAttributes);
                }
            }
        }

        public static CspParameters GetProvParameters(this SafeProvOrNCryptKeyHandle handle)
        {
            // A normal key container name is a GUID (~34 bytes ASCII)
            // The longest standard provider name is 64 bytes (including the \0),
            // but we shouldn't have a CAPI call with a software CSP.
            //
            // In debug builds use a buffer which will need to be resized, but is big
            // enough to hold the DWORD "can't fail" values.
            Span<byte> stackSpan = stackalloc byte[
#if DEBUG
                sizeof(int)
#else
                64
#endif
                ];

            stackSpan.Clear();
            int size = stackSpan.Length;

            if (!Interop.Advapi32.CryptGetProvParam(handle, CryptProvParam.PP_PROVTYPE, stackSpan, ref size))
            {
                throw Marshal.GetLastPInvokeError().ToCryptographicException();
            }

            if (size != sizeof(int))
            {
                Debug.Fail("PP_PROVTYPE writes a DWORD - enum misalignment?");
                throw new CryptographicException();
            }

            int provType = BitConverter.ToInt32(stackSpan.Slice(0, size));

            size = stackSpan.Length;
            if (!Interop.Advapi32.CryptGetProvParam(handle, CryptProvParam.PP_KEYSET_TYPE, stackSpan, ref size))
            {
                throw Marshal.GetLastPInvokeError().ToCryptographicException();
            }

            if (size != sizeof(int))
            {
                Debug.Fail("PP_KEYSET_TYPE writes a DWORD - enum misalignment?");
                throw new CryptographicException();
            }

            int keysetType = BitConverter.ToInt32(stackSpan.Slice(0, size));

            // Only CRYPT_MACHINE_KEYSET is described as coming back, but be defensive.
            CspProviderFlags provFlags =
                ((CspProviderFlags)keysetType & CspProviderFlags.UseMachineKeyStore) |
                CspProviderFlags.UseExistingKey;

            byte[]? rented = null;
            Span<byte> asciiStringBuf = stackSpan;

            string provName = GetStringProvParam(handle, CryptProvParam.PP_NAME, ref asciiStringBuf, ref rented, 0);
            int maxClear = provName.Length;
            string keyName = GetStringProvParam(handle, CryptProvParam.PP_CONTAINER, ref asciiStringBuf, ref rented, maxClear);
            maxClear = Math.Max(maxClear, keyName.Length);

            if (rented != null)
            {
                CryptoPool.Return(rented, maxClear);
            }

            Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
            return new CspParameters(provType)
            {
                Flags = provFlags,
                KeyContainerName = keyName,
                ProviderName = provName,
            };
        }

        private static string GetStringProvParam(
            SafeProvOrNCryptKeyHandle handle,
            CryptProvParam dwParam,
            ref Span<byte> buf,
            ref byte[]? rented,
            int clearLen)
        {
            int len = buf.Length;

            if (!Interop.Advapi32.CryptGetProvParam(handle, dwParam, buf, ref len))
            {
                if (len > buf.Length)
                {
                    if (rented != null)
                    {
                        CryptoPool.Return(rented, clearLen);
                    }

                    rented = CryptoPool.Rent(len);
                    buf = rented;
                    len = rented.Length;
                }
                else
                {
                    throw Marshal.GetLastPInvokeError().ToCryptographicException();
                }

                if (!Interop.Advapi32.CryptGetProvParam(handle, dwParam, buf, ref len))
                {
                    throw Marshal.GetLastPInvokeError().ToCryptographicException();
                }
            }

            unsafe
            {
                fixed (byte* asciiPtr = &MemoryMarshal.GetReference(buf))
                {
                    return Marshal.PtrToStringAnsi((IntPtr)asciiPtr, len);
                }
            }
        }

        private static unsafe CryptographicAttributeObjectCollection ToCryptographicAttributeObjectCollection(CRYPT_ATTRIBUTES* pCryptAttributes)
        {
            CryptographicAttributeObjectCollection collection = new CryptographicAttributeObjectCollection();
            for (int i = 0; i < pCryptAttributes->cAttr; i++)
            {
                CRYPT_ATTRIBUTE* pCryptAttribute = &(pCryptAttributes->rgAttr[i]);
                AddCryptAttribute(collection, pCryptAttribute);
            }
            return collection;
        }

        private static unsafe void AddCryptAttribute(CryptographicAttributeObjectCollection collection, CRYPT_ATTRIBUTE* pCryptAttribute)
        {
            string oidValue = pCryptAttribute->pszObjId.ToStringAnsi();
            Oid oid = new Oid(oidValue);
            AsnEncodedDataCollection attributeCollection = new AsnEncodedDataCollection();

            for (int i = 0; i < pCryptAttribute->cValue; i++)
            {
                // CreateBestPkcs9AttributeObjectAvailable is expected to create a copy of the data so that it has ownership
                // of the underlying data.
                ReadOnlySpan<byte> encodedAttribute = pCryptAttribute->rgValue[i].DangerousAsSpan();
                AsnEncodedData attributeObject = PkcsHelpers.CreateBestPkcs9AttributeObjectAvailable(oid, encodedAttribute);
                attributeCollection.Add(attributeObject);
            }

            collection.Add(new CryptographicAttributeObject(oid, attributeCollection));
        }
    }
}