未验证 提交 f0845bde 编写于 作者: J Jeremy Barton 提交者: GitHub

Ask SChannel for stapled OCSP when the client will do revocation

上级 79874806
......@@ -14,6 +14,7 @@ internal enum CertContextPropId : int
CERT_ARCHIVED_PROP_ID = 19,
CERT_KEY_IDENTIFIER_PROP_ID = 20,
CERT_PUBKEY_ALG_PARA_PROP_ID = 22,
CERT_OCSP_RESPONSE_PROP_ID = 70,
CERT_NCRYPT_KEY_HANDLE_PROP_ID = 78,
CERT_DELETE_KEYSET_PROP_ID = 101,
CERT_CLR_DELETE_KEY_PROP_ID = 125,
......
......@@ -203,6 +203,9 @@ public enum Flags
SCH_CRED_MANUAL_CRED_VALIDATION = 0x08,
SCH_CRED_NO_DEFAULT_CREDS = 0x10,
SCH_CRED_AUTO_CRED_VALIDATION = 0x20,
SCH_CRED_REVOCATION_CHECK_END_CERT = 0x100,
SCH_CRED_IGNORE_NO_REVOCATION_CHECK = 0x800,
SCH_CRED_IGNORE_REVOCATION_OFFLINE = 0x1000,
SCH_SEND_AUX_RECORD = 0x00200000,
SCH_USE_STRONG_CRYPTO = 0x00400000,
}
......
......@@ -6,6 +6,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
namespace System.Net.Security
{
......@@ -26,18 +27,26 @@ internal static class SslSessionsCache
private readonly EncryptionPolicy _encryptionPolicy;
private readonly bool _isServerMode;
private readonly bool _sendTrustList;
private readonly bool _checkRevocation;
//
// SECURITY: X509Certificate.GetCertHash() is virtual hence before going here,
// the caller of this ctor has to ensure that a user cert object was inspected and
// optionally cloned.
//
internal SslCredKey(byte[]? thumbPrint, int allowedProtocols, bool isServerMode, EncryptionPolicy encryptionPolicy, bool sendTrustList)
internal SslCredKey(
byte[]? thumbPrint,
int allowedProtocols,
bool isServerMode,
EncryptionPolicy encryptionPolicy,
bool sendTrustList,
bool checkRevocation)
{
_thumbPrint = thumbPrint ?? Array.Empty<byte>();
_allowedProtocols = allowedProtocols;
_encryptionPolicy = encryptionPolicy;
_isServerMode = isServerMode;
_checkRevocation = checkRevocation;
_sendTrustList = sendTrustList;
}
......@@ -68,6 +77,7 @@ public override int GetHashCode()
hashCode ^= (int)_encryptionPolicy;
hashCode ^= _isServerMode ? 0x10000 : 0x20000;
hashCode ^= _sendTrustList ? 0x40000 : 0x80000;
hashCode ^= _checkRevocation ? 0x100000 : 0x200000;
return hashCode;
}
......@@ -86,6 +96,7 @@ public bool Equals(SslCredKey other)
_allowedProtocols == other._allowedProtocols &&
_isServerMode == other._isServerMode &&
_sendTrustList == other._sendTrustList &&
_checkRevocation == other._checkRevocation &&
thumbPrint.AsSpan().SequenceEqual(otherThumbPrint);
}
}
......@@ -96,7 +107,13 @@ public bool Equals(SslCredKey other)
// ATTN: The returned handle can be invalid, the callers of InitializeSecurityContext and AcceptSecurityContext
// must be prepared to execute a back-out code if the call fails.
//
internal static SafeFreeCredentials? TryCachedCredential(byte[]? thumbPrint, SslProtocols sslProtocols, bool isServer, EncryptionPolicy encryptionPolicy, bool sendTrustList = false)
internal static SafeFreeCredentials? TryCachedCredential(
byte[]? thumbPrint,
SslProtocols sslProtocols,
bool isServer,
EncryptionPolicy encryptionPolicy,
bool checkRevocation,
bool sendTrustList = false)
{
if (s_cachedCreds.IsEmpty)
{
......@@ -104,7 +121,7 @@ public bool Equals(SslCredKey other)
return null;
}
var key = new SslCredKey(thumbPrint, (int)sslProtocols, isServer, encryptionPolicy, sendTrustList);
var key = new SslCredKey(thumbPrint, (int)sslProtocols, isServer, encryptionPolicy, sendTrustList, checkRevocation);
//SafeCredentialReference? cached;
SafeFreeCredentials? credentials = GetCachedCredential(key);
......@@ -129,7 +146,14 @@ public bool Equals(SslCredKey other)
//
// ATTN: The thumbPrint must be from inspected and possibly cloned user Cert object or we get a security hole in SslCredKey ctor.
//
internal static void CacheCredential(SafeFreeCredentials creds, byte[]? thumbPrint, SslProtocols sslProtocols, bool isServer, EncryptionPolicy encryptionPolicy, bool sendTrustList = false)
internal static void CacheCredential(
SafeFreeCredentials creds,
byte[]? thumbPrint,
SslProtocols sslProtocols,
bool isServer,
EncryptionPolicy encryptionPolicy,
bool checkRevocation,
bool sendTrustList = false)
{
Debug.Assert(creds != null, "creds == null");
......@@ -139,7 +163,7 @@ internal static void CacheCredential(SafeFreeCredentials creds, byte[]? thumbPri
return;
}
SslCredKey key = new SslCredKey(thumbPrint, (int)sslProtocols, isServer, encryptionPolicy, sendTrustList);
SslCredKey key = new SslCredKey(thumbPrint, (int)sslProtocols, isServer, encryptionPolicy, sendTrustList, checkRevocation);
SafeFreeCredentials? credentials = GetCachedCredential(key);
......
......@@ -499,7 +499,12 @@ private bool AcquireClientCredentials(ref byte[]? thumbPrint)
// SECURITY: selectedCert ref if not null is a safe object that does not depend on possible **user** inherited X509Certificate type.
//
byte[]? guessedThumbPrint = selectedCert?.GetCertHash();
SafeFreeCredentials? cachedCredentialHandle = SslSessionsCache.TryCachedCredential(guessedThumbPrint, _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.IsServer, _sslAuthenticationOptions.EncryptionPolicy);
SafeFreeCredentials? cachedCredentialHandle = SslSessionsCache.TryCachedCredential(
guessedThumbPrint,
_sslAuthenticationOptions.EnabledSslProtocols,
_sslAuthenticationOptions.IsServer,
_sslAuthenticationOptions.EncryptionPolicy,
_sslAuthenticationOptions.CertificateRevocationCheckMode != X509RevocationMode.NoCheck);
// We can probably do some optimization here. If the selectedCert is returned by the delegate
// we can always go ahead and use the certificate to create our credential
......@@ -657,8 +662,7 @@ private bool AcquireServerCredentials(ref byte[]? thumbPrint)
private static SafeFreeCredentials AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions)
{
SafeFreeCredentials cred = SslStreamPal.AcquireCredentialsHandle(sslAuthenticationOptions.CertificateContext, sslAuthenticationOptions.EnabledSslProtocols,
sslAuthenticationOptions.EncryptionPolicy, sslAuthenticationOptions.IsServer);
SafeFreeCredentials cred = SslStreamPal.AcquireCredentialsHandle(sslAuthenticationOptions);
if (sslAuthenticationOptions.CertificateContext != null)
{
......@@ -819,7 +823,14 @@ private SecurityStatusPal GenerateToken(ReadOnlySpan<byte> inputBuffer, ref byte
//
if (!cachedCreds && _securityContext != null && !_securityContext.IsInvalid && _credentialsHandle != null && !_credentialsHandle.IsInvalid)
{
SslSessionsCache.CacheCredential(_credentialsHandle, thumbPrint, _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.IsServer, _sslAuthenticationOptions.EncryptionPolicy, sendTrustList);
SslSessionsCache.CacheCredential(
_credentialsHandle,
thumbPrint,
_sslAuthenticationOptions.EnabledSslProtocols,
_sslAuthenticationOptions.IsServer,
_sslAuthenticationOptions.EncryptionPolicy,
_sslAuthenticationOptions.CertificateRevocationCheckMode != X509RevocationMode.NoCheck,
sendTrustList);
}
}
}
......
......@@ -55,13 +55,12 @@ public static void VerifyPackageInfo()
throw new PlatformNotSupportedException();
}
public static SafeFreeCredentials AcquireCredentialsHandle(
SslStreamCertificateContext? certificateContext,
SslProtocols protocols,
EncryptionPolicy policy,
bool isServer)
public static SafeFreeCredentials AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions)
{
return new SafeFreeSslCredentials(certificateContext, protocols, policy);
return new SafeFreeSslCredentials(
sslAuthenticationOptions.CertificateContext,
sslAuthenticationOptions.EnabledSslProtocols,
sslAuthenticationOptions.EncryptionPolicy);
}
public static SecurityStatusPal EncryptMessage(
......
......@@ -62,13 +62,12 @@ public static void VerifyPackageInfo()
throw new PlatformNotSupportedException();
}
public static SafeFreeCredentials AcquireCredentialsHandle(
SslStreamCertificateContext? certificateContext,
SslProtocols protocols,
EncryptionPolicy policy,
bool isServer)
public static SafeFreeCredentials AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions)
{
return new SafeFreeSslCredentials(certificateContext, protocols, policy);
return new SafeFreeSslCredentials(
sslAuthenticationOptions.CertificateContext,
sslAuthenticationOptions.EnabledSslProtocols,
sslAuthenticationOptions.EncryptionPolicy);
}
public static SecurityStatusPal EncryptMessage(
......
......@@ -46,10 +46,13 @@ public static void VerifyPackageInfo()
return HandshakeInternal(credential!, ref context, inputBuffer, ref outputBuffer, sslAuthenticationOptions, clientCertificateSelectionCallback);
}
public static SafeFreeCredentials AcquireCredentialsHandle(SslStreamCertificateContext? certificateContext,
SslProtocols protocols, EncryptionPolicy policy, bool isServer)
public static SafeFreeCredentials AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions)
{
return new SafeFreeSslCredentials(certificateContext, protocols, policy, isServer);
return new SafeFreeSslCredentials(
sslAuthenticationOptions.CertificateContext,
sslAuthenticationOptions.EnabledSslProtocols,
sslAuthenticationOptions.EncryptionPolicy,
sslAuthenticationOptions.IsServer);
}
public static SecurityStatusPal EncryptMessage(SafeDeleteSslContext securityContext, ReadOnlyMemory<byte> input, int headerSize, int trailerSize, ref byte[] output, out int resultSize)
......
......@@ -136,16 +136,21 @@ public static byte[] ConvertAlpnProtocolListToByteArray(List<SslApplicationProto
return status;
}
public static SafeFreeCredentials AcquireCredentialsHandle(SslStreamCertificateContext? certificateContext, SslProtocols protocols, EncryptionPolicy policy, bool isServer)
public static SafeFreeCredentials AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions)
{
try
{
EncryptionPolicy policy = sslAuthenticationOptions.EncryptionPolicy;
// New crypto API supports TLS1.3 but it does not allow to force NULL encryption.
#pragma warning disable SYSLIB0040 // NoEncryption and AllowNoEncryption are obsolete
SafeFreeCredentials cred = !UseNewCryptoApi || policy == EncryptionPolicy.NoEncryption ?
AcquireCredentialsHandleSchannelCred(certificateContext, protocols, policy, isServer) :
AcquireCredentialsHandleSchCredentials(certificateContext, protocols, policy, isServer);
AcquireCredentialsHandleSchannelCred(sslAuthenticationOptions) :
AcquireCredentialsHandleSchCredentials(sslAuthenticationOptions);
#pragma warning restore SYSLIB0040
SslStreamCertificateContext? certificateContext = sslAuthenticationOptions.CertificateContext;
if (certificateContext != null && certificateContext.Trust != null && certificateContext.Trust._sendTrustInHandshake)
{
AttachCertificateStore(cred, certificateContext.Trust._store!);
......@@ -182,10 +187,11 @@ private static unsafe void AttachCertificateStore(SafeFreeCredentials cred, X509
// This is legacy crypto API used on .NET Framework and older Windows versions.
// It only supports TLS up to 1.2
public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchannelCred(SslStreamCertificateContext? certificateContext, SslProtocols protocols, EncryptionPolicy policy, bool isServer)
public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchannelCred(SslAuthenticationOptions authOptions)
{
X509Certificate2? certificate = certificateContext?.Certificate;
int protocolFlags = GetProtocolFlagsFromSslProtocols(protocols, isServer);
X509Certificate2? certificate = authOptions.CertificateContext?.Certificate;
bool isServer = authOptions.IsServer;
int protocolFlags = GetProtocolFlagsFromSslProtocols(authOptions.EnabledSslProtocols, isServer);
Interop.SspiCli.SCHANNEL_CRED.Flags flags;
Interop.SspiCli.CredentialUse direction;
......@@ -196,17 +202,29 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchannelCred(Ss
Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_CRED_MANUAL_CRED_VALIDATION |
Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_CRED_NO_DEFAULT_CREDS |
Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_SEND_AUX_RECORD;
// Request OCSP Stapling from the server
if (authOptions.CertificateRevocationCheckMode != X509RevocationMode.NoCheck)
{
flags |=
Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_CRED_REVOCATION_CHECK_END_CERT |
Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_CRED_IGNORE_NO_REVOCATION_CHECK |
Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_CRED_IGNORE_REVOCATION_OFFLINE;
}
}
else
{
direction = Interop.SspiCli.CredentialUse.SECPKG_CRED_INBOUND;
flags = Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_SEND_AUX_RECORD;
if (certificateContext?.Trust?._sendTrustInHandshake == true)
if (authOptions.CertificateContext?.Trust?._sendTrustInHandshake == true)
{
flags |= Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_CRED_NO_SYSTEM_MAPPER;
}
}
EncryptionPolicy policy = authOptions.EncryptionPolicy;
#pragma warning disable SYSLIB0040 // NoEncryption and AllowNoEncryption are obsolete
// Always opt-in SCH_USE_STRONG_CRYPTO for TLS.
if (((protocolFlags == 0) ||
......@@ -234,17 +252,19 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchannelCred(Ss
}
// This function uses new crypto API to support TLS 1.3 and beyond.
public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchCredentials(SslStreamCertificateContext? certificateContext, SslProtocols protocols, EncryptionPolicy policy, bool isServer)
public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchCredentials(SslAuthenticationOptions authOptions)
{
X509Certificate2? certificate = certificateContext?.Certificate;
int protocolFlags = GetProtocolFlagsFromSslProtocols(protocols, isServer);
X509Certificate2? certificate = authOptions.CertificateContext?.Certificate;
bool isServer = authOptions.IsServer;
int protocolFlags = GetProtocolFlagsFromSslProtocols(authOptions.EnabledSslProtocols, isServer);
Interop.SspiCli.SCH_CREDENTIALS.Flags flags;
Interop.SspiCli.CredentialUse direction;
if (isServer)
{
direction = Interop.SspiCli.CredentialUse.SECPKG_CRED_INBOUND;
flags = Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_SEND_AUX_RECORD;
if (certificateContext?.Trust?._sendTrustInHandshake == true)
if (authOptions.CertificateContext?.Trust?._sendTrustInHandshake == true)
{
flags |= Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_CRED_NO_SYSTEM_MAPPER;
}
......@@ -256,8 +276,19 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchCredentials(
Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_CRED_MANUAL_CRED_VALIDATION |
Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_CRED_NO_DEFAULT_CREDS |
Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_SEND_AUX_RECORD;
// Request OCSP Stapling from the server
if (authOptions.CertificateRevocationCheckMode != X509RevocationMode.NoCheck)
{
flags |=
Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_CRED_REVOCATION_CHECK_END_CERT |
Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_CRED_IGNORE_NO_REVOCATION_CHECK |
Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_CRED_IGNORE_REVOCATION_OFFLINE;
}
}
EncryptionPolicy policy = authOptions.EncryptionPolicy;
if (policy == EncryptionPolicy.RequireEncryption)
{
// Always opt-in SCH_USE_STRONG_CRYPTO for TLS.
......
......@@ -11,6 +11,7 @@
using System.Security.Cryptography.X509Certificates.Tests.Common;
using System.Threading.Tasks;
using Microsoft.DotNet.XUnitExtensions;
using Microsoft.Win32.SafeHandles;
using Xunit;
namespace System.Net.Security.Tests
......@@ -134,6 +135,62 @@ public Task ConnectWithRevocation_ServerCertWithoutContext_NoStapledOcsp()
return ConnectWithRevocation_WithCallback_Core(X509RevocationMode.Offline, offlineContext: null);
}
#if WINDOWS
[ConditionalTheory]
[OuterLoop("Uses external servers")]
[PlatformSpecific(TestPlatforms.Windows)]
[InlineData(X509RevocationMode.Offline)]
[InlineData(X509RevocationMode.Online)]
[InlineData(X509RevocationMode.NoCheck)]
public Task ConnectWithRevocation_RemoteServer_StapledOcsp_FromWindows(X509RevocationMode revocationMode)
{
// This test could ideally end at the Client Hello, because it really only wants to
// ensure that the status_request extension was asserted. Since the SslStream tests
// do not currently attempt to intercept and inspect the Client Hello, this test
// obtains the data indirectly: by talking to a host known to do OCSP Server Stapling
// with revocation in Offline mode.
// Unfortunately, this test will fail if the remote host stops doing server stapling,
// but it's the best we can do right now.
string serverName = Configuration.Http.Http2Host;
SslClientAuthenticationOptions clientOpts = new SslClientAuthenticationOptions
{
TargetHost = serverName,
RemoteCertificateValidationCallback = CertificateValidationCallback,
CertificateRevocationCheckMode = revocationMode,
};
return EndToEndHelper(clientOpts);
static bool CertificateValidationCallback(
object sender,
X509Certificate? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors)
{
Assert.NotNull(certificate);
using (SafeCertContextHandle ctx = new SafeCertContextHandle(certificate.Handle, ownsHandle: false))
{
bool hasStapledOcsp =
ctx.CertHasProperty(Interop.Crypt32.CertContextPropId.CERT_OCSP_RESPONSE_PROP_ID);
if (((SslStream)sender).CheckCertRevocationStatus)
{
Assert.True(hasStapledOcsp, "Cert has stapled OCSP data");
}
else
{
Assert.False(hasStapledOcsp, "Cert has stapled OCSP data");
}
}
return true;
}
}
#endif
private async Task ConnectWithRevocation_WithCallback_Core(
X509RevocationMode revocationMode,
bool? offlineContext = false)
......@@ -317,6 +374,27 @@ private async Task EndToEndHelper(string host)
}
}
private async Task EndToEndHelper(SslClientAuthenticationOptions clientOptions)
{
using (var client = new TcpClient())
{
try
{
await client.ConnectAsync(clientOptions.TargetHost, 443);
}
catch (Exception ex)
{
// if we cannot connect skip the test instead of failing.
throw new SkipTestException($"Unable to connect to '{clientOptions.TargetHost}': {ex.Message}");
}
using (SslStream sslStream = new SslStream(client.GetStream()))
{
await sslStream.AuthenticateAsClientAsync(clientOptions);
}
}
}
private bool RemoteHttpsCertValidation(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
Assert.Equal(SslPolicyErrors.None, sslPolicyErrors);
......
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.Win32.SafeHandles
{
/// <summary>
/// SafeHandle for the CERT_CONTEXT structure defined by crypt32.
/// </summary>
internal sealed class SafeCertContextHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeCertContextHandle()
: base(ownsHandle: true)
{
}
public SafeCertContextHandle(IntPtr handle, bool ownsHandle)
: base(ownsHandle)
{
SetHandle(handle);
}
protected override bool ReleaseHandle()
{
Interop.Crypt32.CertFreeCertificateContext(handle);
SetHandle(IntPtr.Zero);
return true;
}
internal bool CertHasProperty(Interop.Crypt32.CertContextPropId propertyId)
{
int cb = 0;
bool hasProperty = Interop.Crypt32.CertGetCertificateContextProperty(
this,
propertyId,
null,
ref cb);
return hasProperty;
}
}
}
......@@ -114,8 +114,23 @@
</ItemGroup>
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'windows'">
<Compile Include="IdentityValidator.Windows.cs" />
<Compile Include="Interop\SafeCertContextHandle.cs" />
<Compile Include="$(CommonTestPath)System\Net\Capability.Security.Windows.cs"
Link="Common\System\Net\Capability.Security.Windows.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Interop.Libraries.cs"
Link="Common\Interop\Windows\Interop.Libraries.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Crypt32\Interop.CertContextPropId.cs"
Link="Common\Interop\Windows\Crypt32\Interop.CertContextPropId.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Crypt32\Interop.CertDuplicateCertificateContext.cs"
Link="Common\Interop\Windows\Crypt32\Interop.CertDuplicateCertificateContext.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Crypt32\Interop.CertFreeCertificateContext.cs"
Link="Common\Interop\Windows\Crypt32\Interop.CertFreeCertificateContext.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Crypt32\Interop.CertGetCertificateContextProperty.cs"
Link="Common\Interop\Windows\Crypt32\Interop.CertGetCertificateContextProperty.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Crypt32\Interop.DATA_BLOB.cs"
Link="Common\Interop\Windows\Crypt32\Interop.DATA_BLOB.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Crypt32\Interop.MsgEncodingType.cs"
Link="Common\Interop\Windows\Crypt32\Interop.MsgEncodingType.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetPlatformIdentifier)' != 'windows' and '$(TargetPlatformIdentifier)' != 'Browser'">
<Compile Include="IdentityValidator.Unix.cs" />
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册