File: System\Security\Cryptography\X509Certificates\UnixExportProvider.cs
Web Access
Project: src\src\libraries\System.Security.Cryptography\src\System.Security.Cryptography.csproj (System.Security.Cryptography)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Diagnostics;
using System.Formats.Asn1;
using System.Runtime.InteropServices;
using System.Security.Cryptography.Asn1;
using System.Security.Cryptography.Asn1.Pkcs12;
using System.Security.Cryptography.Pkcs;
using Microsoft.Win32.SafeHandles;
 
namespace System.Security.Cryptography.X509Certificates
{
    internal abstract class UnixExportProvider : IExportPal
    {
        private static readonly Asn1Tag s_contextSpecific0 =
            new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true);
 
        internal static readonly PbeParameters s_windowsPbe =
            new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 2000);
 
        protected ICertificatePalCore? _singleCertPal;
        protected X509Certificate2Collection? _certs;
 
        internal UnixExportProvider(ICertificatePalCore singleCertPal)
        {
            _singleCertPal = singleCertPal;
        }
 
        internal UnixExportProvider(X509Certificate2Collection certs)
        {
            _certs = certs;
        }
 
        public void Dispose()
        {
            // Don't dispose any of the resources, they're still owned by the caller.
            _singleCertPal = null;
            _certs = null;
        }
 
        protected abstract byte[] ExportPkcs7();
 
        protected abstract byte[] ExportPkcs8(ICertificatePalCore certificatePal, ReadOnlySpan<char> password);
 
        public byte[]? Export(X509ContentType contentType, SafePasswordHandle password)
        {
            Debug.Assert(password != null);
            switch (contentType)
            {
                case X509ContentType.Cert:
                    return ExportX509Der();
                case X509ContentType.Pfx:
                    return ExportPfx(password);
                case X509ContentType.Pkcs7:
                    return ExportPkcs7();
                case X509ContentType.SerializedCert:
                case X509ContentType.SerializedStore:
                    throw new PlatformNotSupportedException(SR.Cryptography_Unix_X509_SerializedExport);
                default:
                    throw new CryptographicException(SR.Cryptography_X509_InvalidContentType);
            }
        }
 
        private byte[]? ExportX509Der()
        {
            if (_singleCertPal != null)
            {
                return _singleCertPal.RawData;
            }
 
            // Windows/Desktop compatibility: Exporting a collection (or store) as
            // X509ContentType.Cert returns the equivalent of FirstOrDefault(),
            // so anything past _certs[0] is ignored, and an empty collection is
            // null (not an Exception)
            if (_certs!.Count == 0)
            {
                return null;
            }
 
            return _certs[0].RawData;
        }
 
        private byte[] ExportPfx(SafePasswordHandle password)
        {
            int certCount = 1;
 
            if (_singleCertPal == null)
            {
                Debug.Assert(_certs != null);
                certCount = _certs.Count;
            }
 
            CertBagAsn[] certBags = ArrayPool<CertBagAsn>.Shared.Rent(certCount);
            SafeBagAsn[] keyBags = ArrayPool<SafeBagAsn>.Shared.Rent(certCount);
            AttributeAsn[] certAttrs = ArrayPool<AttributeAsn>.Shared.Rent(certCount);
            certAttrs.AsSpan(0, certCount).Clear();
 
            AsnWriter tmpWriter = new AsnWriter(AsnEncodingRules.DER);
            ArraySegment<byte> encodedAuthSafe = default;
 
            bool gotRef = false;
 
            try
            {
                password.DangerousAddRef(ref gotRef);
                ReadOnlySpan<char> passwordSpan = password.DangerousGetSpan();
 
                int keyIdx = 0;
                int certIdx = 0;
 
                if (_singleCertPal != null)
                {
                    BuildBags(
                        _singleCertPal,
                        passwordSpan,
                        tmpWriter,
                        certBags,
                        certAttrs,
                        keyBags,
                        ref certIdx,
                        ref keyIdx);
                }
                else
                {
                    foreach (X509Certificate2 cert in _certs!)
                    {
                        BuildBags(
                            cert.Pal,
                            passwordSpan,
                            tmpWriter,
                            certBags,
                            certAttrs,
                            keyBags,
                            ref certIdx,
                            ref keyIdx);
                    }
                }
 
                encodedAuthSafe = EncodeAuthSafe(
                    tmpWriter,
                    keyBags,
                    keyIdx,
                    certBags,
                    certAttrs,
                    certIdx,
                    passwordSpan);
 
                return MacAndEncode(tmpWriter, encodedAuthSafe, passwordSpan);
            }
            finally
            {
                if (gotRef)
                {
                    password.DangerousRelease();
                }
 
                certAttrs.AsSpan(0, certCount).Clear();
                certBags.AsSpan(0, certCount).Clear();
                keyBags.AsSpan(0, certCount).Clear();
                ArrayPool<AttributeAsn>.Shared.Return(certAttrs);
                ArrayPool<CertBagAsn>.Shared.Return(certBags);
                ArrayPool<SafeBagAsn>.Shared.Return(keyBags);
 
                if (encodedAuthSafe.Array != null)
                {
                    CryptoPool.Return(encodedAuthSafe);
                }
            }
        }
 
        private void BuildBags(
            ICertificatePalCore certPal,
            ReadOnlySpan<char> passwordSpan,
            AsnWriter tmpWriter,
            CertBagAsn[] certBags,
            AttributeAsn[] certAttrs,
            SafeBagAsn[] keyBags,
            ref int certIdx,
            ref int keyIdx)
        {
            tmpWriter.WriteOctetString(certPal.RawData);
 
            certBags[certIdx] = new CertBagAsn
            {
                CertId = Oids.Pkcs12X509CertBagType,
                CertValue = tmpWriter.Encode(),
            };
 
            tmpWriter.Reset();
 
            if (certPal.HasPrivateKey)
            {
                byte[] attrBytes = new byte[6];
                attrBytes[0] = (byte)UniversalTagNumber.OctetString;
                attrBytes[1] = sizeof(int);
                MemoryMarshal.Write(attrBytes.AsSpan(2), in keyIdx);
 
                AttributeAsn attribute = new AttributeAsn
                {
                    AttrType = Oids.LocalKeyId,
                    AttrValues = new ReadOnlyMemory<byte>[]
                    {
                        attrBytes,
                    }
                };
                keyBags[keyIdx] = new SafeBagAsn
                {
                    BagId = Oids.Pkcs12ShroudedKeyBag,
                    BagValue = ExportPkcs8(certPal, passwordSpan),
                    BagAttributes = new[] { attribute }
                };
 
                // Reuse the attribute between the cert and the key.
                certAttrs[certIdx] = attribute;
                keyIdx++;
            }
 
            certIdx++;
        }
 
        private static unsafe ArraySegment<byte> EncodeAuthSafe(
            AsnWriter tmpWriter,
            SafeBagAsn[] keyBags,
            int keyCount,
            CertBagAsn[] certBags,
            AttributeAsn[] certAttrs,
            int certIdx,
            ReadOnlySpan<char> passwordSpan)
        {
            string? encryptionAlgorithmOid = null;
            bool certsIsPkcs12Encryption = false;
            string? certsHmacOid = null;
 
            ArraySegment<byte> encodedKeyContents = default;
            ArraySegment<byte> encodedCertContents = default;
 
            try
            {
                if (keyCount > 0)
                {
                    encodedKeyContents = EncodeKeys(tmpWriter, keyBags, keyCount);
                }
 
                Span<byte> salt = stackalloc byte[16];
                RandomNumberGenerator.Fill(salt);
                Span<byte> certContentsIv = stackalloc byte[8];
 
                if (certIdx > 0)
                {
                    encodedCertContents = EncodeCerts(
                        tmpWriter,
                        certBags,
                        certAttrs,
                        certIdx,
                        salt,
                        passwordSpan,
                        certContentsIv,
                        out certsHmacOid,
                        out encryptionAlgorithmOid,
                        out certsIsPkcs12Encryption);
                }
 
                return EncodeAuthSafe(
                    tmpWriter,
                    encodedKeyContents,
                    encodedCertContents,
                    certsIsPkcs12Encryption,
                    certsHmacOid!,
                    encryptionAlgorithmOid!,
                    salt,
                    certContentsIv);
            }
            finally
            {
                if (encodedCertContents.Array != null)
                {
                    CryptoPool.Return(encodedCertContents);
                }
 
                if (encodedKeyContents.Array != null)
                {
                    CryptoPool.Return(encodedKeyContents);
                }
            }
        }
 
        private static ArraySegment<byte> EncodeKeys(AsnWriter tmpWriter, SafeBagAsn[] keyBags, int keyCount)
        {
            Debug.Assert(tmpWriter.GetEncodedLength() == 0);
 
            using (tmpWriter.PushSequence())
            {
                for (int i = 0; i < keyCount; i++)
                {
                    keyBags[i].Encode(tmpWriter);
                }
            }
 
            int length = tmpWriter.GetEncodedLength();
            byte[] keyBuf = CryptoPool.Rent(length);
 
            if (!tmpWriter.TryEncode(keyBuf, out length))
            {
                Debug.Fail("TryEncode failed with a pre-allocated buffer");
                throw new InvalidOperationException();
            }
 
            // Explicitly clear the internal buffer before it goes out of scope.
            tmpWriter.Reset();
 
            return new ArraySegment<byte>(keyBuf, 0, length);
        }
 
        private static ArraySegment<byte> EncodeCerts(
            AsnWriter tmpWriter,
            CertBagAsn[] certBags,
            AttributeAsn[] certAttrs,
            int certCount,
            Span<byte> salt,
            ReadOnlySpan<char> passwordSpan,
            Span<byte> certContentsIv,
            out string hmacOid,
            out string encryptionAlgorithmOid,
            out bool isPkcs12)
        {
            Debug.Assert(tmpWriter.GetEncodedLength() == 0);
            tmpWriter.PushSequence();
 
            PasswordBasedEncryption.InitiateEncryption(
                s_windowsPbe,
                out SymmetricAlgorithm cipher,
                out hmacOid,
                out encryptionAlgorithmOid,
                out isPkcs12);
 
            using (cipher)
            {
                Debug.Assert(certContentsIv.Length * 8 == cipher.BlockSize);
 
                for (int i = certCount - 1; i >= 0; --i)
                {
                    // Manually write the SafeBagAsn
                    // https://tools.ietf.org/html/rfc7292#section-4.2
                    //
                    // SafeBag ::= SEQUENCE {
                    //   bagId          BAG-TYPE.&id ({PKCS12BagSet})
                    //   bagValue       [0] EXPLICIT BAG-TYPE.&Type({PKCS12BagSet}{@bagId}),
                    //   bagAttributes  SET OF PKCS12Attribute OPTIONAL
                    // }
                    tmpWriter.PushSequence();
 
                    tmpWriter.WriteObjectIdentifierForCrypto(Oids.Pkcs12CertBag);
 
                    tmpWriter.PushSequence(s_contextSpecific0);
                    certBags[i].Encode(tmpWriter);
                    tmpWriter.PopSequence(s_contextSpecific0);
 
                    if (certAttrs[i].AttrType != null)
                    {
                        tmpWriter.PushSetOf();
                        certAttrs[i].Encode(tmpWriter);
                        tmpWriter.PopSetOf();
                    }
 
                    tmpWriter.PopSequence();
                }
 
                tmpWriter.PopSequence();
 
                // The padding applied will add at most a block to the output,
                // so ask for contentsSpan.Length + the number of bytes in a cipher block.
                int cipherBlockBytes = cipher.BlockSize >> 3;
                int requestedSize = checked(tmpWriter.GetEncodedLength() + cipherBlockBytes);
                byte[] certContents = CryptoPool.Rent(requestedSize);
 
                int encryptedLength = PasswordBasedEncryption.Encrypt(
                    passwordSpan,
                    ReadOnlySpan<byte>.Empty,
                    cipher,
                    isPkcs12,
                    tmpWriter,
                    s_windowsPbe,
                    salt,
                    certContents,
                    certContentsIv);
 
                Debug.Assert(encryptedLength <= requestedSize);
                tmpWriter.Reset();
 
                return new ArraySegment<byte>(certContents, 0, encryptedLength);
            }
        }
 
        private static ArraySegment<byte> EncodeAuthSafe(
            AsnWriter tmpWriter,
            ReadOnlyMemory<byte> encodedKeyContents,
            ReadOnlyMemory<byte> encodedCertContents,
            bool isPkcs12,
            string hmacOid,
            string encryptionAlgorithmOid,
            Span<byte> salt,
            Span<byte> certContentsIv)
        {
            Debug.Assert(tmpWriter.GetEncodedLength() == 0);
 
            tmpWriter.PushSequence();
 
            if (!encodedKeyContents.IsEmpty)
            {
                tmpWriter.PushSequence();
                tmpWriter.WriteObjectIdentifier(Oids.Pkcs7Data);
                tmpWriter.PushSequence(s_contextSpecific0);
 
                ReadOnlySpan<byte> keyContents = encodedKeyContents.Span;
                tmpWriter.WriteOctetString(keyContents);
 
                tmpWriter.PopSequence(s_contextSpecific0);
                tmpWriter.PopSequence();
            }
 
            if (!encodedCertContents.IsEmpty)
            {
                tmpWriter.PushSequence();
 
                {
                    tmpWriter.WriteObjectIdentifier(Oids.Pkcs7Encrypted);
 
                    tmpWriter.PushSequence(s_contextSpecific0);
                    tmpWriter.PushSequence();
 
                    {
                        // No unprotected attributes: version 0 data
                        tmpWriter.WriteInteger(0);
 
                        tmpWriter.PushSequence();
 
                        {
                            tmpWriter.WriteObjectIdentifier(Oids.Pkcs7Data);
 
                            PasswordBasedEncryption.WritePbeAlgorithmIdentifier(
                                tmpWriter,
                                isPkcs12,
                                encryptionAlgorithmOid,
                                salt,
                                s_windowsPbe.IterationCount,
                                hmacOid,
                                certContentsIv);
 
                            tmpWriter.WriteOctetString(encodedCertContents.Span, s_contextSpecific0);
                            tmpWriter.PopSequence();
                        }
 
                        tmpWriter.PopSequence();
                        tmpWriter.PopSequence(s_contextSpecific0);
                    }
 
                    tmpWriter.PopSequence();
                }
            }
 
            tmpWriter.PopSequence();
 
            int authSafeLength = tmpWriter.GetEncodedLength();
            byte[] authSafe = CryptoPool.Rent(authSafeLength);
 
            if (!tmpWriter.TryEncode(authSafe, out authSafeLength))
            {
                Debug.Fail("TryEncode failed with a pre-allocated buffer");
                throw new InvalidOperationException();
            }
 
            tmpWriter.Reset();
 
            return new ArraySegment<byte>(authSafe, 0, authSafeLength);
        }
 
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA5350", Justification = "HMACSHA1 is required for compat with other platforms")]
        private static unsafe byte[] MacAndEncode(
            AsnWriter tmpWriter,
            ReadOnlyMemory<byte> encodedAuthSafe,
            ReadOnlySpan<char> passwordSpan)
        {
            Span<byte> macKey = stackalloc byte[HMACSHA1.HashSizeInBytes];
            Span<byte> macSalt = stackalloc byte[HMACSHA1.HashSizeInBytes];
            Span<byte> macSpan = stackalloc byte[HMACSHA1.HashSizeInBytes];
            RandomNumberGenerator.Fill(macSalt);
 
            Pkcs12Kdf.DeriveMacKey(
                passwordSpan,
                HashAlgorithmName.SHA1,
                s_windowsPbe.IterationCount,
                macSalt,
                macKey);
 
            int bytesWritten = HMACSHA1.HashData(macKey, encodedAuthSafe.Span, macSpan);
 
            if (bytesWritten != HMACSHA1.HashSizeInBytes)
            {
                Debug.Fail($"HMACSHA1.HashData wrote {bytesWritten} of {HMACSHA1.HashSizeInBytes} bytes");
                throw new CryptographicException();
            }
 
            CryptographicOperations.ZeroMemory(macKey);
 
            // https://tools.ietf.org/html/rfc7292#section-4
            //
            // PFX ::= SEQUENCE {
            //   version    INTEGER {v3(3)}(v3,...),
            //   authSafe   ContentInfo,
            //   macData    MacData OPTIONAL
            // }
            Debug.Assert(tmpWriter.GetEncodedLength() == 0);
            tmpWriter.PushSequence();
 
            tmpWriter.WriteInteger(3);
 
            tmpWriter.PushSequence();
            {
                tmpWriter.WriteObjectIdentifier(Oids.Pkcs7Data);
 
                tmpWriter.PushSequence(s_contextSpecific0);
                {
                    tmpWriter.WriteOctetString(encodedAuthSafe.Span);
                    tmpWriter.PopSequence(s_contextSpecific0);
                }
 
                tmpWriter.PopSequence();
            }
 
            // https://tools.ietf.org/html/rfc7292#section-4
            //
            // MacData ::= SEQUENCE {
            //   mac        DigestInfo,
            //   macSalt    OCTET STRING,
            //   iterations INTEGER DEFAULT 1
            //   -- Note: The default is for historical reasons and its use is
            //   -- deprecated.
            // }
            tmpWriter.PushSequence();
            {
                tmpWriter.PushSequence();
                {
                    tmpWriter.PushSequence();
                    {
                        tmpWriter.WriteObjectIdentifier(Oids.Sha1);
                        tmpWriter.PopSequence();
                    }
 
                    tmpWriter.WriteOctetString(macSpan);
                    tmpWriter.PopSequence();
                }
 
                tmpWriter.WriteOctetString(macSalt);
                tmpWriter.WriteInteger(s_windowsPbe.IterationCount);
 
                tmpWriter.PopSequence();
            }
 
            tmpWriter.PopSequence();
            return tmpWriter.Encode();
        }
    }
}