在.NET Core中使用MachineKey

在.NET Core中使用MachineKey

姐妹篇:《ASP.NET Cookie是怎么生成的》

姐妹篇:《.NET Core验证ASP.NET密码》

在上篇文章中,我介绍了Cookie是基于MachineKey生成的,MachineKey决定了Cookie生成的算法和密钥,并如果使用多台服务器做负载均衡时,必须指定一致的MachineKey

但在.NET Core中,官方似乎并没有提供MachineKey实现,这为兼容.NET FrameworkCookie造成了许多障碍。

今天我将深入探索MachineKey这个类,看看里面到底藏了什么东西,本文的最后我将使用.NET Core来解密一个ASP.NET MVC生成的Cookie

认识MachineKey

.NET Framework中,machineKey首先需要一个配置,写在app.config或者web.config中,格式一般如下:

<machineKey validationKey="128个hex字符" decryptionKey="64个hex字符" validation="SHA1" decryption="AES" />

网上能找到可以直接生成随机MachineKey的网站:https://www.developerfusion.com/tools/generatemachinekey/

MachineKeyvalidationKeydecryptionKey的内容只要符合长度和hex要求,都是可以随意指定的,所以machineKey生成器的意义其实不大。

探索MachineKey

打开MachineKey的源代码如下所示(有删减):

public static class MachineKey {
    public static byte[] Unprotect(byte[] protectedData, params string[] purposes) {
        // ...有删减

        return Unprotect(AspNetCryptoServiceProvider.Instance, protectedData, purposes);
    }

    // Internal method for unit testing.
    internal static byte[] Unprotect(ICryptoServiceProvider cryptoServiceProvider, byte[] protectedData, string[] purposes) {
        // If the user is calling this method, we want to use the ICryptoServiceProvider
        // regardless of whether or not it's the default provider.

        Purpose derivedPurpose = Purpose.User_MachineKey_Protect.AppendSpecificPurposes(purposes);
        ICryptoService cryptoService = cryptoServiceProvider.GetCryptoService(derivedPurpose);
        return cryptoService.Unprotect(protectedData);
    }
}

具体代码可参见:https://referencesource.microsoft.com/#system.web/Security/MachineKey.cs,209

可见它本质是使用了AspNetCryptoServiceProvider.Instance,然后调用其GetCryptoService方法,然后获取一个cryptoService,最后调用Unprotect,注意其中还使用了一个Purpose的类,依赖非常多。

AspNetCryptoServiceProvider

其中AspNetCryptoServiceProvider.Instance的定义如下(有删减和整合):

internal sealed class AspNetCryptoServiceProvider : ICryptoServiceProvider {
    private static readonly Lazy<AspNetCryptoServiceProvider> _singleton = new Lazy<AspNetCryptoServiceProvider>(GetSingletonCryptoServiceProvider);

    internal static AspNetCryptoServiceProvider Instance {
        get {
            return _singleton.Value;
        }
    }

    private static AspNetCryptoServiceProvider GetSingletonCryptoServiceProvider() {
        // Provides all of the necessary dependencies for an application-level
        // AspNetCryptoServiceProvider.

        MachineKeySection machineKeySection = MachineKeySection.GetApplicationConfig();

        return new AspNetCryptoServiceProvider(
            machineKeySection: machineKeySection,
            cryptoAlgorithmFactory: new MachineKeyCryptoAlgorithmFactory(machineKeySection),
            masterKeyProvider: new MachineKeyMasterKeyProvider(machineKeySection),
            dataProtectorFactory: new MachineKeyDataProtectorFactory(machineKeySection),
            keyDerivationFunction: SP800_108.DeriveKey);
    }
}

具体代码可见:https://referencesource.microsoft.com/#system.web/Security/Cryptography/AspNetCryptoServiceProvider.cs,68dbd1c184ea4e88

可见它本质是依赖于AspNetCryptoServiceProvider,它使用了MachineKeyCryptoAlgorithmFactoryMachineKeyMasterKeyProviderMachineKeyDataProtectorFactory,以及一个看上去有点奇怪的SP800_108.DeriveKey

AspNetCryptoServiceProviderGetCryptoService方法如下:

public ICryptoService GetCryptoService(Purpose purpose, CryptoServiceOptions options = CryptoServiceOptions.None) {
    ICryptoService cryptoService;
    if (_isDataProtectorEnabled && options == CryptoServiceOptions.None) {
        // We can only use DataProtector if it's configured and the caller didn't ask for any special behavior like cacheability
        cryptoService = GetDataProtectorCryptoService(purpose);
    }
    else {
        // Otherwise we fall back to using the <machineKey> algorithms for cryptography
        cryptoService = GetNetFXCryptoService(purpose, options);
    }

    // always homogenize errors returned from the crypto service
    return new HomogenizingCryptoServiceWrapper(cryptoService);
}

private NetFXCryptoService GetNetFXCryptoService(Purpose purpose, CryptoServiceOptions options) {
    // Extract the encryption and validation keys from the provided Purpose object
    CryptographicKey encryptionKey = purpose.GetDerivedEncryptionKey(_masterKeyProvider, _keyDerivationFunction);
    CryptographicKey validationKey = purpose.GetDerivedValidationKey(_masterKeyProvider, _keyDerivationFunction);

    // and return the ICryptoService
    // (predictable IV turned on if the caller requested cacheable output)
    return new NetFXCryptoService(_cryptoAlgorithmFactory, encryptionKey, validationKey, predictableIV: (options == CryptoServiceOptions.CacheableOutput));
}

注意其中有一个判断,我结合dnSpy做了认真的调试,发现它默认走的是GetNetFXCryptoService,也就是注释中所谓的<machineKey>算法。

然后GetNetFXCryptoService方法依赖于_masterKeyProvider_keyDerivationFunction用来生成两个CryptographicKey,这两个就是之前所说的MachineKeyMasterKeyProviderMachineKeyDataProtectorFactory

注意其中还有一个HomogenizingCryptoServiceWrapper类,故名思义,它的作用应该是统一管理加密解释过程中的报错,实际也确实如此,我不作深入,有兴趣的读者可以看看原始代码在这:https://referencesource.microsoft.com/#system.web/Security/Cryptography/HomogenizingCryptoServiceWrapper.cs,25

最后调用NetFXCryptoService来执行Unprotect任务。

NetFXCryptoService

这个是重点了,源代码如下(有删减):

internal sealed class NetFXCryptoService : ICryptoService {

    private readonly ICryptoAlgorithmFactory _cryptoAlgorithmFactory;
    private readonly CryptographicKey _encryptionKey;
    private readonly bool _predictableIV;
    private readonly CryptographicKey _validationKey;

    // ...有删减

    // [UNPROTECT]
    // INPUT: protectedData
    // OUTPUT: clearData
    // ALGORITHM:
    //   1) Assume protectedData := IV || Enc(Kenc, IV, clearData) || Sign(Kval, IV || Enc(Kenc, IV, clearData))
    //   2) Validate the signature over the payload and strip it from the end
    //   3) Strip off the IV from the beginning of the payload
    //   4) Decrypt what remains of the payload, and return it as clearData
    public byte[] Unprotect(byte[] protectedData) {
        // ...有删减
        using (SymmetricAlgorithm decryptionAlgorithm = _cryptoAlgorithmFactory.GetEncryptionAlgorithm()) {
            // 省略约100行代码😂
        }
    }
}

这个代码非常长,我直接一刀全部删减了,只保留注释。如果不理解先好好看注释,不理解它在干嘛,直接看代码可能非常难,有兴趣的可以直接先看看代码:https://referencesource.microsoft.com/#system.web/Security/Cryptography/NetFXCryptoService.cs,35

首先看注释:

protectedData := IV || Enc(Kenc, IV, clearData) || Sign(Kval, IV || Enc(Kenc, IV, clearData))

加密之后的数据由IV密文以及签名三部分组成;

其中密文使用encryptionKeyIV原始明文加密而来;

签名validationKey作验证,传入参数是IV以及密文(这一点有点像jwt)。

现在再来看看代码:

int ivByteCount = decryptionAlgorithm.BlockSize / 8; // IV length is equal to the block size
int signatureByteCount = validationAlgorithm.HashSize / 8;

IV的长度由解密算法的BlockSize决定,签名算法的长度由验证算法的BlockSize决定,有了IV签名的长度,就知道了密文的长度:

int encryptedPayloadByteCount = protectedData.Length - ivByteCount - signatureByteCount;

下文就应该是轻车熟路,依葫芦画瓢了,先验证签名:

byte[] computedSignature = validationAlgorithm.ComputeHash(protectedData, 0, ivByteCount + encryptedPayloadByteCount);
if (/*验证不成功*/) {
    return null;
}

然后直接解密:

using (MemoryStream memStream = new MemoryStream()) {
    using (ICryptoTransform decryptor = decryptionAlgorithm.CreateDecryptor()) {
        using (CryptoStream cryptoStream = new CryptoStream(memStream, decryptor, CryptoStreamMode.Write)) {
            cryptoStream.Write(protectedData, ivByteCount, encryptedPayloadByteCount);
            cryptoStream.FlushFinalBlock();

            // At this point
            // memStream := clearData

            byte[] clearData = memStream.ToArray();
            return clearData;
        }
    }
}

可见这个类都是一些“正常操作”。之后我们来补充一下遗漏的部分。

MachineKeyCryptoAlgorithmFactory

首先是MachineKeyCryptoAlgorithmFactory,代码如下(只保留了重点):

switch (algorithmName) {
    case "AES":
    case "Auto": // currently "Auto" defaults to AES
        return CryptoAlgorithms.CreateAes;

    case "DES":
        return CryptoAlgorithms.CreateDES;

    case "3DES":
        return CryptoAlgorithms.CreateTripleDES;

    default:
        return null; // unknown
}

switch (algorithmName) {
    case "SHA1":
        return CryptoAlgorithms.CreateHMACSHA1;

    case "HMACSHA256":
        return CryptoAlgorithms.CreateHMACSHA256;

    case "HMACSHA384":
        return CryptoAlgorithms.CreateHMACSHA384;

    case "HMACSHA512":
        return CryptoAlgorithms.CreateHMACSHA512;

    default:
        return null; // unknown
}

源代码链接在这:https://referencesource.microsoft.com/#system.web/Security/Cryptography/MachineKeyCryptoAlgorithmFactory.cs,14

可见非常地直白、浅显易懂。

MachineKeyMasterKeyProvider

然后是MachineKeyMasterKeyProvider,核心代码如下:

private CryptographicKey GenerateCryptographicKey(string configAttributeName, string configAttributeValue, int autogenKeyOffset, int autogenKeyCount, string errorResourceString) {
    byte[] keyMaterial = CryptoUtil.HexToBinary(configAttributeValue);

    // If <machineKey> contained a valid key, just use it verbatim.
    if (keyMaterial != null && keyMaterial.Length > 0) {
        return new CryptographicKey(keyMaterial);
    }

    // 有删减
}

 public CryptographicKey GetEncryptionKey() {
    if (_encryptionKey == null) {
        _encryptionKey = GenerateCryptographicKey(
            configAttributeName: "decryptionKey",
            configAttributeValue: _machineKeySection.DecryptionKey,
            autogenKeyOffset: AUTOGEN_ENCRYPTION_OFFSET,
            autogenKeyCount: AUTOGEN_ENCRYPTION_KEYLENGTH,
            errorResourceString: SR.Invalid_decryption_key);
    }
    return _encryptionKey;
}

public CryptographicKey GetValidationKey() {
    if (_validationKey == null) {
        _validationKey = GenerateCryptographicKey(
            configAttributeName: "validationKey",
            configAttributeValue: _machineKeySection.ValidationKey,
            autogenKeyOffset: AUTOGEN_VALIDATION_OFFSET,
            autogenKeyCount: AUTOGEN_VALIDATION_KEYLENGTH,
            errorResourceString: SR.Invalid_validation_key);
    }
    return _validationKey;
}

可见这个类就是从app.config/web.config中读取两个xml位置的值,并转换为CryptographicKey,然后CryptographicKey的本质就是一个字节数组byte[]

注意,原版的GenerateCrytographicKey函数其实很长,但重点确实就是前面这三行代码,后面的是一些骚操作,可以自动从一些配置的位置生成machineKey,这应该和machineKey节点缺失或者不写有关,不在本文考虑的范畴以内。有兴趣的读者可以参见原始代码:https://referencesource.microsoft.com/#system.web/Security/Cryptography/MachineKeyMasterKeyProvider.cs,87

MachineKeyDataProtectorFactory

其源代码如下(有删减):

internal sealed class MachineKeyDataProtectorFactory : IDataProtectorFactory {
    public DataProtector GetDataProtector(Purpose purpose) {
        if (_dataProtectorFactory == null) {
            _dataProtectorFactory = GetDataProtectorFactory();
        }
        return _dataProtectorFactory(purpose);
    }

    private Func<Purpose, DataProtector> GetDataProtectorFactory() {
        string applicationName = _machineKeySection.ApplicationName;
        string dataProtectorTypeName = _machineKeySection.DataProtectorType;

        Func<Purpose, DataProtector> factory = purpose => {
            // Since the custom implementation might depend on the impersonated
            // identity, we must instantiate it under app-level impersonation.
            using (new ApplicationImpersonationContext()) {
                return DataProtector.Create(dataProtectorTypeName, applicationName, purpose.PrimaryPurpose, purpose.SpecificPurposes);
            }
        };
        // 删减验证factory的部分代码和try-catch
        return factory; // we know at this point the factory is good
    }
}

其原始代码如下:https://referencesource.microsoft.com/#System.Web/Security/Cryptography/MachineKeyDataProtectorFactory.cs,cc110253450fcb16

注意_machineKeySectionApplicationNameDataProtectorType默认都是空字符串"",具体不细说,在这定义的:https://referencesource.microsoft.com/#System.Web/Configuration/MachineKeySection.cs,50

所以我们继续看DataProtector的代码:

public abstract class DataProtector
{
    public static DataProtector Create(string providerClass,
                                        string applicationName,
                                        string primaryPurpose,
                                        params string[] specificPurposes)
    {
        // Make sure providerClass is not null - Other parameters checked in constructor
        if (null == providerClass)
            throw new ArgumentNullException("providerClass");

        // Create a DataProtector based on this type using CryptoConfig
        return (DataProtector)CryptoConfig.CreateFromName(providerClass, applicationName, primaryPurpose, specificPurposes);
    }
}

注意它唯一的引用CryptoConfig,已经属于.NET Core已经包含的范畴了,因此没必要继续深入追踪。

Purpose

注意一开始时,我们说到的Purpose,相关定义如下:

public class Purpose {
    // ...有删减
    public static readonly Purpose User_MachineKey_Protect = new Purpose("User.MachineKey.Protect");
	internal Purpose AppendSpecificPurposes(IList<string> specificPurposes)
	{
		if (specificPurposes == null || specificPurposes.Count == 0)
		{
			return this;
		}
		string[] array = new string[SpecificPurposes.Length + specificPurposes.Count];
		Array.Copy(SpecificPurposes, array, SpecificPurposes.Length);
		specificPurposes.CopyTo(array, SpecificPurposes.Length);
		return new Purpose(PrimaryPurpose, array);
	}

    // Returns a label and context suitable for passing into the SP800-108 KDF.
    internal void GetKeyDerivationParameters(out byte[] label, out byte[] context) {
        // The primary purpose can just be used as the label directly, since ASP.NET
        // is always in full control of the primary purpose (it's never user-specified).
        if (_derivedKeyLabel == null) {
            _derivedKeyLabel = CryptoUtil.SecureUTF8Encoding.GetBytes(PrimaryPurpose);
        }

        // The specific purposes (which can contain nonce, identity, etc.) are concatenated
        // together to form the context. The BinaryWriter class prepends each element with
        // a 7-bit encoded length to guarantee uniqueness.
        if (_derivedKeyContext == null) {
            using (MemoryStream stream = new MemoryStream())
            using (BinaryWriter writer = new BinaryWriter(stream, CryptoUtil.SecureUTF8Encoding)) {
                foreach (string specificPurpose in SpecificPurposes) {
                    writer.Write(specificPurpose);
                }
                _derivedKeyContext = stream.ToArray();
            }
        }

        label = _derivedKeyLabel;
        context = _derivedKeyContext;
    }
}

注意其PrimaryPurpose值为:"User.MachineKey.Protect"

另外还需要记住这个GetKeyDerivationParameters方法,它将在接下来的SP800_108类中使用,它将PrimaryPurpose经过utf8编码生成label参数,然后用所有的SpecificPurposes通过二进制序列化,生成context参数。

原始代码链接:https://referencesource.microsoft.com/#System.Web/Security/Cryptography/Purpose.cs,6fd5fbe04ec71877

SP800_108

已经接近尾声了,我们知道一个字符串要转换为密钥,就必须经过一个安全的哈希算法。之前我们接触得最多的,是Rfc2898DeriveBytes,但它是为了保存密码而设计的。这里不需要这么复杂,因此…….NET另写了一个。

这个类代码非常长,但好在它所有内容都兼容.NET Core,因此可以直接复制粘贴。

它的目的是通过Purpose来生成密钥。有兴趣的读者可以了解一下其算法:https://referencesource.microsoft.com/#System.Web/Security/Cryptography/SP800_108.cs,38

收尾

关系图整理

我已经尽力将代码重点划出来,但仍然很复杂。这么多类,我最后理了一个关系图,用于了解其调用、依赖链:

MachineKey
    Purpose
    AspNetCryptoServiceProvider
        MachineKeySection
        MachineKeyCryptoAlgorithmFactory
            CryptoAlgorithms
        MachineKeyMasterKeyProvider
            CryptographicKey
        MachineKeyDataProtectorFactory
            DataProtector
                CryptoConfig
        SP800_108

祖传代码

整理了这么久,没有点干货怎么能行?基于以上的整理,我写了一份“祖传代码”,可以直接拿来在.NET Core中使用。代码较长,约200行,已经上传到我的博客数据网站,各位可以自取:https://github.com/sdcb/blog-data/tree/master/2020/20200222-machinekey-in-dotnetcore

其实只要一行代码?

直到后来,我发现有人将这些功能封闭成了一个NuGet包:AspNetTicketBridge,只需“一行”代码,就能搞定所有这些功能:

// https://github.com/dmarlow/AspNetTicketBridge
string cookie = "你的Cookie内容";
string validationKey = "machineKey中的validationKey";
string decryptionKey = "machineKey中的decryptionKey";

OwinAuthenticationTicket ticket = MachineKeyTicketUnprotector.UnprotectCookie(cookie, decryptionKey, validationKey);

LINQPad运行,结果如下(完美破解):

总结

喜欢的朋友请关注我的微信公众号:【DotNet骚操作】

DotNet骚操作

posted @ 2020-02-22 12:16  .NET骚操作  阅读(4189)  评论(4编辑  收藏  举报