diff options
| author | TSR Berry <20988865+TSRBerry@users.noreply.github.com> | 2023-04-08 01:22:00 +0200 |
|---|---|---|
| committer | Mary <thog@protonmail.com> | 2023-04-27 23:51:14 +0200 |
| commit | cee712105850ac3385cd0091a923438167433f9f (patch) | |
| tree | 4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx.HLE/HOS/Services/Ssl | |
| parent | cd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff) | |
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx.HLE/HOS/Services/Ssl')
16 files changed, 1424 insertions, 0 deletions
diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/BuiltInCertificateManager.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/BuiltInCertificateManager.cs new file mode 100644 index 00000000..abbc1354 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/BuiltInCertificateManager.cs @@ -0,0 +1,246 @@ +using LibHac; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Ncm; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.Exceptions; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Ssl.Types; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ssl +{ + class BuiltInCertificateManager + { + private const long CertStoreTitleId = 0x0100000000000800; + + private readonly string CertStoreTitleMissingErrorMessage = "CertStore system title not found! SSL CA retrieving will not work, provide the system archive to fix this error. (See https://github.com/Ryujinx/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide#initial-setup-continued---installation-of-firmware for more information)"; + + private static BuiltInCertificateManager _instance; + + public static BuiltInCertificateManager Instance + { + get + { + if (_instance == null) + { + _instance = new BuiltInCertificateManager(); + } + + return _instance; + } + } + + private VirtualFileSystem _virtualFileSystem; + private IntegrityCheckLevel _fsIntegrityCheckLevel; + private ContentManager _contentManager; + private bool _initialized; + private Dictionary<CaCertificateId, CertStoreEntry> _certificates; + + private object _lock = new object(); + + private struct CertStoreFileHeader + { + private const uint ValidMagic = 0x546C7373; + +#pragma warning disable CS0649 + public uint Magic; + public uint EntriesCount; +#pragma warning restore CS0649 + + public bool IsValid() + { + return Magic == ValidMagic; + } + } + + private struct CertStoreFileEntry + { +#pragma warning disable CS0649 + public CaCertificateId Id; + public TrustedCertStatus Status; + public uint DataSize; + public uint DataOffset; +#pragma warning restore CS0649 + } + + public class CertStoreEntry + { + public CaCertificateId Id; + public TrustedCertStatus Status; + public byte[] Data; + } + + public string GetCertStoreTitleContentPath() + { + return _contentManager.GetInstalledContentPath(CertStoreTitleId, StorageId.BuiltInSystem, NcaContentType.Data); + } + + public bool HasCertStoreTitle() + { + return !string.IsNullOrEmpty(GetCertStoreTitleContentPath()); + } + + private CertStoreEntry ReadCertStoreEntry(ReadOnlySpan<byte> buffer, CertStoreFileEntry entry) + { + string customCertificatePath = System.IO.Path.Join(AppDataManager.BaseDirPath, "system", "ssl", $"{entry.Id}.der"); + + byte[] data; + + if (File.Exists(customCertificatePath)) + { + data = File.ReadAllBytes(customCertificatePath); + } + else + { + data = buffer.Slice((int)entry.DataOffset, (int)entry.DataSize).ToArray(); + } + + return new CertStoreEntry + { + Id = entry.Id, + Status = entry.Status, + Data = data + }; + } + + public void Initialize(Switch device) + { + lock (_lock) + { + _certificates = new Dictionary<CaCertificateId, CertStoreEntry>(); + _initialized = false; + _contentManager = device.System.ContentManager; + _virtualFileSystem = device.FileSystem; + _fsIntegrityCheckLevel = device.System.FsIntegrityCheckLevel; + + if (HasCertStoreTitle()) + { + using LocalStorage ncaFile = new LocalStorage(_virtualFileSystem.SwitchPathToSystemPath(GetCertStoreTitleContentPath()), FileAccess.Read, FileMode.Open); + + Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile); + + IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, _fsIntegrityCheckLevel); + + using var trustedCertsFileRef = new UniqueRef<IFile>(); + + Result result = romfs.OpenFile(ref trustedCertsFileRef.Ref, "/ssl_TrustedCerts.bdf".ToU8Span(), OpenMode.Read); + + if (!result.IsSuccess()) + { + // [1.0.0 - 2.3.0] + if (ResultFs.PathNotFound.Includes(result)) + { + result = romfs.OpenFile(ref trustedCertsFileRef.Ref, "/ssl_TrustedCerts.tcf".ToU8Span(), OpenMode.Read); + } + + if (result.IsFailure()) + { + Logger.Error?.Print(LogClass.ServiceSsl, CertStoreTitleMissingErrorMessage); + + return; + } + } + + using IFile trustedCertsFile = trustedCertsFileRef.Release(); + + trustedCertsFile.GetSize(out long fileSize).ThrowIfFailure(); + + Span<byte> trustedCertsRaw = new byte[fileSize]; + + trustedCertsFile.Read(out _, 0, trustedCertsRaw).ThrowIfFailure(); + + CertStoreFileHeader header = MemoryMarshal.Read<CertStoreFileHeader>(trustedCertsRaw); + + if (!header.IsValid()) + { + Logger.Error?.Print(LogClass.ServiceSsl, "Invalid CertStore data found, skipping!"); + + return; + } + + ReadOnlySpan<byte> trustedCertsData = trustedCertsRaw[Unsafe.SizeOf<CertStoreFileHeader>()..]; + ReadOnlySpan<CertStoreFileEntry> trustedCertsEntries = MemoryMarshal.Cast<byte, CertStoreFileEntry>(trustedCertsData)[..(int)header.EntriesCount]; + + foreach (CertStoreFileEntry entry in trustedCertsEntries) + { + _certificates.Add(entry.Id, ReadCertStoreEntry(trustedCertsData, entry)); + } + + _initialized = true; + } + } + } + + public bool TryGetCertificates( + ReadOnlySpan<CaCertificateId> ids, + out CertStoreEntry[] entries, + out bool hasAllCertificates, + out int requiredSize) + { + lock (_lock) + { + if (!_initialized) + { + throw new InvalidSystemResourceException(CertStoreTitleMissingErrorMessage); + } + + requiredSize = 0; + hasAllCertificates = false; + + foreach (CaCertificateId id in ids) + { + if (id == CaCertificateId.All) + { + hasAllCertificates = true; + + break; + } + } + + if (hasAllCertificates) + { + entries = new CertStoreEntry[_certificates.Count]; + requiredSize = (_certificates.Count + 1) * Unsafe.SizeOf<BuiltInCertificateInfo>(); + + int i = 0; + + foreach (CertStoreEntry entry in _certificates.Values) + { + entries[i++] = entry; + requiredSize += (entry.Data.Length + 3) & ~3; + } + + return true; + } + else + { + entries = new CertStoreEntry[ids.Length]; + requiredSize = ids.Length * Unsafe.SizeOf<BuiltInCertificateInfo>(); + + for (int i = 0; i < ids.Length; i++) + { + if (!_certificates.TryGetValue(ids[i], out CertStoreEntry entry)) + { + return false; + } + + entries[i] = entry; + requiredSize += (entry.Data.Length + 3) & ~3; + } + + return true; + } + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/ISslService.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/ISslService.cs new file mode 100644 index 00000000..7741ef7e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/ISslService.cs @@ -0,0 +1,125 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ssl.SslService; +using Ryujinx.HLE.HOS.Services.Ssl.Types; +using Ryujinx.Memory; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ssl +{ + [Service("ssl")] + class ISslService : IpcService + { + // NOTE: The SSL service is used by games to connect it to various official online services, which we do not intend to support. + // In this case it is acceptable to stub all calls of the service. + public ISslService(ServiceCtx context) { } + + [CommandCmif(0)] + // CreateContext(nn::ssl::sf::SslVersion, u64, pid) -> object<nn::ssl::sf::ISslContext> + public ResultCode CreateContext(ServiceCtx context) + { + SslVersion sslVersion = (SslVersion)context.RequestData.ReadUInt32(); + ulong pidPlaceholder = context.RequestData.ReadUInt64(); + + MakeObject(context, new ISslContext(context.Request.HandleDesc.PId, sslVersion)); + + Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { sslVersion }); + + return ResultCode.Success; + } + + [CommandCmif(2)] + // GetCertificates(buffer<CaCertificateId, 5> ids) -> (u32 certificates_count, buffer<bytes, 6> certificates) + public ResultCode GetCertificates(ServiceCtx context) + { + ReadOnlySpan<CaCertificateId> ids = MemoryMarshal.Cast<byte, CaCertificateId>(context.Memory.GetSpan(context.Request.SendBuff[0].Position, (int)context.Request.SendBuff[0].Size)); + + if (!BuiltInCertificateManager.Instance.TryGetCertificates( + ids, + out BuiltInCertificateManager.CertStoreEntry[] entries, + out bool hasAllCertificates, + out int requiredSize)) + { + throw new InvalidOperationException(); + } + + if ((uint)requiredSize > (uint)context.Request.ReceiveBuff[0].Size) + { + return ResultCode.InvalidCertBufSize; + } + + int infosCount = entries.Length; + + if (hasAllCertificates) + { + infosCount++; + } + + using (WritableRegion region = context.Memory.GetWritableRegion(context.Request.ReceiveBuff[0].Position, (int)context.Request.ReceiveBuff[0].Size)) + { + Span<byte> rawData = region.Memory.Span; + Span<BuiltInCertificateInfo> infos = MemoryMarshal.Cast<byte, BuiltInCertificateInfo>(rawData)[..infosCount]; + Span<byte> certificatesData = rawData[(Unsafe.SizeOf<BuiltInCertificateInfo>() * infosCount)..]; + + for (int i = 0; i < entries.Length; i++) + { + entries[i].Data.CopyTo(certificatesData); + + infos[i] = new BuiltInCertificateInfo + { + Id = entries[i].Id, + Status = entries[i].Status, + CertificateDataSize = (ulong)entries[i].Data.Length, + CertificateDataOffset = (ulong)(rawData.Length - certificatesData.Length) + }; + + certificatesData = certificatesData[entries[i].Data.Length..]; + } + + if (hasAllCertificates) + { + infos[entries.Length] = new BuiltInCertificateInfo + { + Id = CaCertificateId.All, + Status = TrustedCertStatus.Invalid, + CertificateDataSize = 0, + CertificateDataOffset = 0 + }; + } + } + + context.ResponseData.Write(entries.Length); + + return ResultCode.Success; + } + + [CommandCmif(3)] + // GetCertificateBufSize(buffer<CaCertificateId, 5> ids) -> u32 buffer_size; + public ResultCode GetCertificateBufSize(ServiceCtx context) + { + ReadOnlySpan<CaCertificateId> ids = MemoryMarshal.Cast<byte, CaCertificateId>(context.Memory.GetSpan(context.Request.SendBuff[0].Position, (int)context.Request.SendBuff[0].Size)); + + if (!BuiltInCertificateManager.Instance.TryGetCertificates(ids, out _, out _, out int requiredSize)) + { + throw new InvalidOperationException(); + } + + context.ResponseData.Write(requiredSize); + + return ResultCode.Success; + } + + [CommandCmif(5)] + // SetInterfaceVersion(u32) + public ResultCode SetInterfaceVersion(ServiceCtx context) + { + // 1 = 3.0.0+, 2 = 5.0.0+, 3 = 6.0.0+ + uint interfaceVersion = context.RequestData.ReadUInt32(); + + Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { interfaceVersion }); + + return ResultCode.Success; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/ResultCode.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/ResultCode.cs new file mode 100644 index 00000000..862c79cd --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/ResultCode.cs @@ -0,0 +1,20 @@ +namespace Ryujinx.HLE.HOS.Services.Ssl +{ + public enum ResultCode + { + OsModuleId = 123, + ErrorCodeShift = 9, + + Success = 0, + NoSocket = (103 << ErrorCodeShift) | OsModuleId, + InvalidSocket = (106 << ErrorCodeShift) | OsModuleId, + InvalidCertBufSize = (112 << ErrorCodeShift) | OsModuleId, + InvalidOption = (126 << ErrorCodeShift) | OsModuleId, + CertBufferTooSmall = (202 << ErrorCodeShift) | OsModuleId, + AlreadyInUse = (203 << ErrorCodeShift) | OsModuleId, + WouldBlock = (204 << ErrorCodeShift) | OsModuleId, + Timeout = (205 << ErrorCodeShift) | OsModuleId, + ConnectionReset = (209 << ErrorCodeShift) | OsModuleId, + ConnectionAbort = (210 << ErrorCodeShift) | OsModuleId + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/ISslConnection.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/ISslConnection.cs new file mode 100644 index 00000000..b9087f40 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/ISslConnection.cs @@ -0,0 +1,519 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.Exceptions; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd; +using Ryujinx.HLE.HOS.Services.Ssl.Types; +using Ryujinx.Memory; +using System; +using System.Text; + +namespace Ryujinx.HLE.HOS.Services.Ssl.SslService +{ + class ISslConnection : IpcService, IDisposable + { + private bool _doNotClockSocket; + private bool _getServerCertChain; + private bool _skipDefaultVerify; + private bool _enableAlpn; + + private SslVersion _sslVersion; + private IoMode _ioMode; + private VerifyOption _verifyOption; + private SessionCacheMode _sessionCacheMode; + private string _hostName; + + private ISslConnectionBase _connection; + private BsdContext _bsdContext; + private readonly ulong _processId; + + private byte[] _nextAplnProto; + + public ISslConnection(ulong processId, SslVersion sslVersion) + { + _processId = processId; + _sslVersion = sslVersion; + _ioMode = IoMode.Blocking; + _sessionCacheMode = SessionCacheMode.None; + _verifyOption = VerifyOption.PeerCa | VerifyOption.HostName; + } + + [CommandCmif(0)] + // SetSocketDescriptor(u32) -> u32 + public ResultCode SetSocketDescriptor(ServiceCtx context) + { + if (_connection != null) + { + return ResultCode.AlreadyInUse; + } + + _bsdContext = BsdContext.GetContext(_processId); + + if (_bsdContext == null) + { + return ResultCode.InvalidSocket; + } + + int inputFd = context.RequestData.ReadInt32(); + + int internalFd = _bsdContext.DuplicateFileDescriptor(inputFd); + + if (internalFd == -1) + { + return ResultCode.InvalidSocket; + } + + InitializeConnection(internalFd); + + int outputFd = inputFd; + + if (_doNotClockSocket) + { + outputFd = -1; + } + + context.ResponseData.Write(outputFd); + + return ResultCode.Success; + } + + private void InitializeConnection(int socketFd) + { + ISocket bsdSocket = _bsdContext.RetrieveSocket(socketFd); + + _connection = new SslManagedSocketConnection(_bsdContext, _sslVersion, socketFd, bsdSocket); + } + + [CommandCmif(1)] + // SetHostName(buffer<bytes, 5>) + public ResultCode SetHostName(ServiceCtx context) + { + ulong hostNameDataPosition = context.Request.SendBuff[0].Position; + ulong hostNameDataSize = context.Request.SendBuff[0].Size; + + byte[] hostNameData = new byte[hostNameDataSize]; + + context.Memory.Read(hostNameDataPosition, hostNameData); + + _hostName = Encoding.ASCII.GetString(hostNameData).Trim('\0'); + + Logger.Info?.Print(LogClass.ServiceSsl, _hostName); + + return ResultCode.Success; + } + + [CommandCmif(2)] + // SetVerifyOption(nn::ssl::sf::VerifyOption) + public ResultCode SetVerifyOption(ServiceCtx context) + { + _verifyOption = (VerifyOption)context.RequestData.ReadUInt32(); + + Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { _verifyOption }); + + return ResultCode.Success; + } + + [CommandCmif(3)] + // SetIoMode(nn::ssl::sf::IoMode) + public ResultCode SetIoMode(ServiceCtx context) + { + if (_connection == null) + { + return ResultCode.NoSocket; + } + + _ioMode = (IoMode)context.RequestData.ReadUInt32(); + + _connection.Socket.Blocking = _ioMode == IoMode.Blocking; + + Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { _ioMode }); + + return ResultCode.Success; + } + + [CommandCmif(4)] + // GetSocketDescriptor() -> u32 + public ResultCode GetSocketDescriptor(ServiceCtx context) + { + context.ResponseData.Write(_connection.SocketFd); + + return ResultCode.Success; + } + + [CommandCmif(5)] + // GetHostName(buffer<bytes, 6>) -> u32 + public ResultCode GetHostName(ServiceCtx context) + { + ulong bufferAddress = context.Request.ReceiveBuff[0].Position; + ulong bufferLen = context.Request.ReceiveBuff[0].Size; + + using (var region = context.Memory.GetWritableRegion(bufferAddress, (int)bufferLen, true)) + { + Encoding.ASCII.GetBytes(_hostName, region.Memory.Span); + } + + context.ResponseData.Write((uint)_hostName.Length); + + Logger.Info?.Print(LogClass.ServiceSsl, _hostName); + + return ResultCode.Success; + } + + [CommandCmif(6)] + // GetVerifyOption() -> nn::ssl::sf::VerifyOption + public ResultCode GetVerifyOption(ServiceCtx context) + { + context.ResponseData.Write((uint)_verifyOption); + + Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { _verifyOption }); + + return ResultCode.Success; + } + + [CommandCmif(7)] + // GetIoMode() -> nn::ssl::sf::IoMode + public ResultCode GetIoMode(ServiceCtx context) + { + context.ResponseData.Write((uint)_ioMode); + + Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { _ioMode }); + + return ResultCode.Success; + } + + [CommandCmif(8)] + // DoHandshake() + public ResultCode DoHandshake(ServiceCtx context) + { + if (_connection == null) + { + return ResultCode.NoSocket; + } + + return _connection.Handshake(_hostName); + } + + [CommandCmif(9)] + // DoHandshakeGetServerCert() -> (u32, u32, buffer<bytes, 6>) + public ResultCode DoHandshakeGetServerCert(ServiceCtx context) + { + if (_connection == null) + { + return ResultCode.NoSocket; + } + + ResultCode result = _connection.Handshake(_hostName); + + if (result == ResultCode.Success) + { + if (_getServerCertChain) + { + using (WritableRegion region = context.Memory.GetWritableRegion(context.Request.ReceiveBuff[0].Position, (int)context.Request.ReceiveBuff[0].Size)) + { + result = _connection.GetServerCertificate(_hostName, region.Memory.Span, out uint bufferSize, out uint certificateCount); + + context.ResponseData.Write(bufferSize); + context.ResponseData.Write(certificateCount); + } + } + else + { + context.ResponseData.Write(0); + context.ResponseData.Write(0); + } + } + + return result; + } + + [CommandCmif(10)] + // Read() -> (u32, buffer<bytes, 6>) + public ResultCode Read(ServiceCtx context) + { + if (_connection == null) + { + return ResultCode.NoSocket; + } + + ResultCode result; + + using (WritableRegion region = context.Memory.GetWritableRegion(context.Request.ReceiveBuff[0].Position, (int)context.Request.ReceiveBuff[0].Size)) + { + // TODO: Better error management. + result = _connection.Read(out int readCount, region.Memory); + + if (result == ResultCode.Success) + { + context.ResponseData.Write(readCount); + } + } + + return result; + } + + [CommandCmif(11)] + // Write(buffer<bytes, 5>) -> s32 + public ResultCode Write(ServiceCtx context) + { + if (_connection == null) + { + return ResultCode.NoSocket; + } + + // We don't dispose as this isn't supposed to be modified + WritableRegion region = context.Memory.GetWritableRegion(context.Request.SendBuff[0].Position, (int)context.Request.SendBuff[0].Size); + + // TODO: Better error management. + ResultCode result = _connection.Write(out int writtenCount, region.Memory); + + if (result == ResultCode.Success) + { + context.ResponseData.Write(writtenCount); + } + + return result; + } + + [CommandCmif(12)] + // Pending() -> s32 + public ResultCode Pending(ServiceCtx context) + { + if (_connection == null) + { + return ResultCode.NoSocket; + } + + context.ResponseData.Write(_connection.Pending()); + + return ResultCode.Success; + } + + [CommandCmif(13)] + // Peek() -> (s32, buffer<bytes, 6>) + public ResultCode Peek(ServiceCtx context) + { + if (_connection == null) + { + return ResultCode.NoSocket; + } + + ResultCode result; + + using (WritableRegion region = context.Memory.GetWritableRegion(context.Request.ReceiveBuff[0].Position, (int)context.Request.ReceiveBuff[0].Size)) + { + // TODO: Better error management. + result = _connection.Peek(out int peekCount, region.Memory); + + if (result == ResultCode.Success) + { + context.ResponseData.Write(peekCount); + } + } + + return result; + } + + [CommandCmif(14)] + // Poll(nn::ssl::sf::PollEvent poll_event, u32 timeout) -> nn::ssl::sf::PollEvent + public ResultCode Poll(ServiceCtx context) + { + throw new ServiceNotImplementedException(this, context); + } + + [CommandCmif(15)] + // GetVerifyCertError() + public ResultCode GetVerifyCertError(ServiceCtx context) + { + throw new ServiceNotImplementedException(this, context); + } + + [CommandCmif(16)] + // GetNeededServerCertBufferSize() -> u32 + public ResultCode GetNeededServerCertBufferSize(ServiceCtx context) + { + throw new ServiceNotImplementedException(this, context); + } + + [CommandCmif(17)] + // SetSessionCacheMode(nn::ssl::sf::SessionCacheMode) + public ResultCode SetSessionCacheMode(ServiceCtx context) + { + SessionCacheMode sessionCacheMode = (SessionCacheMode)context.RequestData.ReadUInt32(); + + Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { sessionCacheMode }); + + _sessionCacheMode = sessionCacheMode; + + return ResultCode.Success; + } + + [CommandCmif(18)] + // GetSessionCacheMode() -> nn::ssl::sf::SessionCacheMode + public ResultCode GetSessionCacheMode(ServiceCtx context) + { + context.ResponseData.Write((uint)_sessionCacheMode); + + Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { _sessionCacheMode }); + + return ResultCode.Success; + } + + [CommandCmif(19)] + // FlushSessionCache() + public ResultCode FlushSessionCache(ServiceCtx context) + { + throw new ServiceNotImplementedException(this, context); + } + + [CommandCmif(20)] + // SetRenegotiationMode(nn::ssl::sf::RenegotiationMode) + public ResultCode SetRenegotiationMode(ServiceCtx context) + { + throw new ServiceNotImplementedException(this, context); + } + + [CommandCmif(21)] + // GetRenegotiationMode() -> nn::ssl::sf::RenegotiationMode + public ResultCode GetRenegotiationMode(ServiceCtx context) + { + throw new ServiceNotImplementedException(this, context); + } + + [CommandCmif(22)] + // SetOption(b8 value, nn::ssl::sf::OptionType option) + public ResultCode SetOption(ServiceCtx context) + { + bool value = context.RequestData.ReadUInt32() != 0; + OptionType option = (OptionType)context.RequestData.ReadUInt32(); + + Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { option, value }); + + return SetOption(option, value); + } + + [CommandCmif(23)] + // GetOption(nn::ssl::sf::OptionType) -> b8 + public ResultCode GetOption(ServiceCtx context) + { + OptionType option = (OptionType)context.RequestData.ReadUInt32(); + + Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { option }); + + ResultCode result = GetOption(option, out bool value); + + if (result == ResultCode.Success) + { + context.ResponseData.Write(value); + } + + return result; + } + + [CommandCmif(24)] + // GetVerifyCertErrors() -> (u32, u32, buffer<bytes, 6>) + public ResultCode GetVerifyCertErrors(ServiceCtx context) + { + throw new ServiceNotImplementedException(this, context); + } + + [CommandCmif(25)] // 4.0.0+ + // GetCipherInfo(u32) -> buffer<bytes, 6> + public ResultCode GetCipherInfo(ServiceCtx context) + { + throw new ServiceNotImplementedException(this, context); + } + + [CommandCmif(26)] + // SetNextAlpnProto(buffer<bytes, 5>) -> u32 + public ResultCode SetNextAlpnProto(ServiceCtx context) + { + ulong inputDataPosition = context.Request.SendBuff[0].Position; + ulong inputDataSize = context.Request.SendBuff[0].Size; + + _nextAplnProto = new byte[inputDataSize]; + + context.Memory.Read(inputDataPosition, _nextAplnProto); + + Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { inputDataSize }); + + return ResultCode.Success; + } + + [CommandCmif(27)] + // GetNextAlpnProto(buffer<bytes, 6>) -> u32 + public ResultCode GetNextAlpnProto(ServiceCtx context) + { + ulong outputDataPosition = context.Request.ReceiveBuff[0].Position; + ulong outputDataSize = context.Request.ReceiveBuff[0].Size; + + context.Memory.Write(outputDataPosition, _nextAplnProto); + + context.ResponseData.Write(_nextAplnProto.Length); + + Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { outputDataSize }); + + return ResultCode.Success; + } + + private ResultCode SetOption(OptionType option, bool value) + { + switch (option) + { + case OptionType.DoNotCloseSocket: + _doNotClockSocket = value; + break; + + case OptionType.GetServerCertChain: + _getServerCertChain = value; + break; + + case OptionType.SkipDefaultVerify: + _skipDefaultVerify = value; + break; + + case OptionType.EnableAlpn: + _enableAlpn = value; + break; + + default: + Logger.Warning?.Print(LogClass.ServiceSsl, $"Unsupported option {option}"); + return ResultCode.InvalidOption; + } + + return ResultCode.Success; + } + + private ResultCode GetOption(OptionType option, out bool value) + { + switch (option) + { + case OptionType.DoNotCloseSocket: + value = _doNotClockSocket; + break; + + case OptionType.GetServerCertChain: + value = _getServerCertChain; + break; + + case OptionType.SkipDefaultVerify: + value = _skipDefaultVerify; + break; + + case OptionType.EnableAlpn: + value = _enableAlpn; + break; + + default: + Logger.Warning?.Print(LogClass.ServiceSsl, $"Unsupported option {option}"); + + value = false; + return ResultCode.InvalidOption; + } + + return ResultCode.Success; + } + + public void Dispose() + { + _connection?.Dispose(); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/ISslConnectionBase.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/ISslConnectionBase.cs new file mode 100644 index 00000000..18e03e49 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/ISslConnectionBase.cs @@ -0,0 +1,24 @@ +using Ryujinx.HLE.HOS.Services.Sockets.Bsd; +using System; + +namespace Ryujinx.HLE.HOS.Services.Ssl.SslService +{ + interface ISslConnectionBase: IDisposable + { + int SocketFd { get; } + + ISocket Socket { get; } + + ResultCode Handshake(string hostName); + + ResultCode GetServerCertificate(string hostname, Span<byte> certificates, out uint storageSize, out uint certificateCount); + + ResultCode Write(out int writtenCount, ReadOnlyMemory<byte> buffer); + + ResultCode Read(out int readCount, Memory<byte> buffer); + + ResultCode Peek(out int peekCount, Memory<byte> buffer); + + int Pending(); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/ISslContext.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/ISslContext.cs new file mode 100644 index 00000000..b38ff921 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/ISslContext.cs @@ -0,0 +1,83 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ssl.Types; +using System.Text; + +namespace Ryujinx.HLE.HOS.Services.Ssl.SslService +{ + class ISslContext : IpcService + { + private uint _connectionCount; + + private readonly ulong _processId; + private readonly SslVersion _sslVersion; + private ulong _serverCertificateId; + private ulong _clientCertificateId; + + public ISslContext(ulong processId, SslVersion sslVersion) + { + _processId = processId; + _sslVersion = sslVersion; + } + + [CommandCmif(2)] + // CreateConnection() -> object<nn::ssl::sf::ISslConnection> + public ResultCode CreateConnection(ServiceCtx context) + { + MakeObject(context, new ISslConnection(_processId, _sslVersion)); + + _connectionCount++; + + return ResultCode.Success; + } + + [CommandCmif(3)] + // GetConnectionCount() -> u32 count + public ResultCode GetConnectionCount(ServiceCtx context) + { + context.ResponseData.Write(_connectionCount); + + Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { _connectionCount }); + + return ResultCode.Success; + } + + [CommandCmif(4)] + // ImportServerPki(nn::ssl::sf::CertificateFormat certificateFormat, buffer<bytes, 5> certificate) -> u64 certificateId + public ResultCode ImportServerPki(ServiceCtx context) + { + CertificateFormat certificateFormat = (CertificateFormat)context.RequestData.ReadUInt32(); + + ulong certificateDataPosition = context.Request.SendBuff[0].Position; + ulong certificateDataSize = context.Request.SendBuff[0].Size; + + context.ResponseData.Write(_serverCertificateId++); + + Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { certificateFormat }); + + return ResultCode.Success; + } + + [CommandCmif(5)] + // ImportClientPki(buffer<bytes, 5> certificate, buffer<bytes, 5> ascii_password) -> u64 certificateId + public ResultCode ImportClientPki(ServiceCtx context) + { + ulong certificateDataPosition = context.Request.SendBuff[0].Position; + ulong certificateDataSize = context.Request.SendBuff[0].Size; + + ulong asciiPasswordDataPosition = context.Request.SendBuff[1].Position; + ulong asciiPasswordDataSize = context.Request.SendBuff[1].Size; + + byte[] asciiPasswordData = new byte[asciiPasswordDataSize]; + + context.Memory.Read(asciiPasswordDataPosition, asciiPasswordData); + + string asciiPassword = Encoding.ASCII.GetString(asciiPasswordData).Trim('\0'); + + context.ResponseData.Write(_clientCertificateId++); + + Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { asciiPassword }); + + return ResultCode.Success; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs new file mode 100644 index 00000000..47d3eddb --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs @@ -0,0 +1,251 @@ +using Ryujinx.HLE.HOS.Services.Sockets.Bsd; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl; +using Ryujinx.HLE.HOS.Services.Ssl.Types; +using System; +using System.IO; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; + +namespace Ryujinx.HLE.HOS.Services.Ssl.SslService +{ + class SslManagedSocketConnection : ISslConnectionBase + { + public int SocketFd { get; } + + public ISocket Socket { get; } + + private BsdContext _bsdContext; + private SslVersion _sslVersion; + private SslStream _stream; + private bool _isBlockingSocket; + private int _previousReadTimeout; + + public SslManagedSocketConnection(BsdContext bsdContext, SslVersion sslVersion, int socketFd, ISocket socket) + { + _bsdContext = bsdContext; + _sslVersion = sslVersion; + + SocketFd = socketFd; + Socket = socket; + } + + private void StartSslOperation() + { + // Save blocking state + _isBlockingSocket = Socket.Blocking; + + // Force blocking for SslStream + Socket.Blocking = true; + } + + private void EndSslOperation() + { + // Restore blocking state + Socket.Blocking = _isBlockingSocket; + } + + private void StartSslReadOperation() + { + StartSslOperation(); + + if (!_isBlockingSocket) + { + _previousReadTimeout = _stream.ReadTimeout; + + _stream.ReadTimeout = 1; + } + } + + private void EndSslReadOperation() + { + if (!_isBlockingSocket) + { + _stream.ReadTimeout = _previousReadTimeout; + } + + EndSslOperation(); + } + +// NOTE: We silence warnings about TLS 1.0 and 1.1 as games will likely use it. +#pragma warning disable SYSLIB0039 + private static SslProtocols TranslateSslVersion(SslVersion version) + { + switch (version & SslVersion.VersionMask) + { + case SslVersion.Auto: + return SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13; + case SslVersion.TlsV10: + return SslProtocols.Tls; + case SslVersion.TlsV11: + return SslProtocols.Tls11; + case SslVersion.TlsV12: + return SslProtocols.Tls12; + case SslVersion.TlsV13: + return SslProtocols.Tls13; + default: + throw new NotImplementedException(version.ToString()); + } + } +#pragma warning restore SYSLIB0039 + + public ResultCode Handshake(string hostName) + { + StartSslOperation(); + _stream = new SslStream(new NetworkStream(((ManagedSocket)Socket).Socket, false), false, null, null); + _stream.AuthenticateAsClient(hostName, null, TranslateSslVersion(_sslVersion), false); + EndSslOperation(); + + return ResultCode.Success; + } + + public ResultCode Peek(out int peekCount, Memory<byte> buffer) + { + // NOTE: We cannot support that on .NET SSL API. + // As Nintendo's curl implementation detail check if a connection is alive via Peek, we just return that it would block to let it know that it's alive. + peekCount = -1; + + return ResultCode.WouldBlock; + } + + public int Pending() + { + // Unsupported + return 0; + } + + private static bool TryTranslateWinSockError(bool isBlocking, WsaError error, out ResultCode resultCode) + { + switch (error) + { + case WsaError.WSAETIMEDOUT: + resultCode = isBlocking ? ResultCode.Timeout : ResultCode.WouldBlock; + return true; + case WsaError.WSAECONNABORTED: + resultCode = ResultCode.ConnectionAbort; + return true; + case WsaError.WSAECONNRESET: + resultCode = ResultCode.ConnectionReset; + return true; + default: + resultCode = ResultCode.Success; + return false; + } + } + + public ResultCode Read(out int readCount, Memory<byte> buffer) + { + if (!Socket.Poll(0, SelectMode.SelectRead)) + { + readCount = -1; + + return ResultCode.WouldBlock; + } + + StartSslReadOperation(); + + try + { + readCount = _stream.Read(buffer.Span); + } + catch (IOException exception) + { + readCount = -1; + + if (exception.InnerException is SocketException socketException) + { + WsaError socketErrorCode = (WsaError)socketException.SocketErrorCode; + + if (TryTranslateWinSockError(_isBlockingSocket, socketErrorCode, out ResultCode result)) + { + return result; + } + else + { + throw socketException; + } + } + else + { + throw exception; + } + } + finally + { + EndSslReadOperation(); + } + + return ResultCode.Success; + } + + public ResultCode Write(out int writtenCount, ReadOnlyMemory<byte> buffer) + { + if (!Socket.Poll(0, SelectMode.SelectWrite)) + { + writtenCount = 0; + + return ResultCode.WouldBlock; + } + + StartSslOperation(); + + try + { + _stream.Write(buffer.Span); + } + catch (IOException exception) + { + writtenCount = -1; + + if (exception.InnerException is SocketException socketException) + { + WsaError socketErrorCode = (WsaError)socketException.SocketErrorCode; + + if (TryTranslateWinSockError(_isBlockingSocket, socketErrorCode, out ResultCode result)) + { + return result; + } + else + { + throw socketException; + } + } + else + { + throw exception; + } + } + finally + { + EndSslOperation(); + } + + // .NET API doesn't provide the size written, assume all written. + writtenCount = buffer.Length; + + return ResultCode.Success; + } + + public ResultCode GetServerCertificate(string hostname, Span<byte> certificates, out uint storageSize, out uint certificateCount) + { + byte[] rawCertData = _stream.RemoteCertificate.GetRawCertData(); + + storageSize = (uint)rawCertData.Length; + certificateCount = 1; + + if (rawCertData.Length > certificates.Length) + { + return ResultCode.CertBufferTooSmall; + } + + rawCertData.CopyTo(certificates); + + return ResultCode.Success; + } + + public void Dispose() + { + _bsdContext.CloseFileDescriptor(SocketFd); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/Types/BuiltInCertificateInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/BuiltInCertificateInfo.cs new file mode 100644 index 00000000..313220e1 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/BuiltInCertificateInfo.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.HLE.HOS.Services.Ssl.Types +{ + struct BuiltInCertificateInfo + { + public CaCertificateId Id; + public TrustedCertStatus Status; + public ulong CertificateDataSize; + public ulong CertificateDataOffset; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/Types/CaCertificateId.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/CaCertificateId.cs new file mode 100644 index 00000000..5c84579a --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/CaCertificateId.cs @@ -0,0 +1,68 @@ +namespace Ryujinx.HLE.HOS.Services.Ssl.Types +{ + enum CaCertificateId : uint + { + // Nintendo CAs + NintendoCAG3 = 1, + NintendoClass2CAG3, + + // External CAs + AmazonRootCA1 = 1000, + StarfieldServicesRootCertificateAuthorityG2, + AddTrustExternalCARoot, + COMODOCertificationAuthority, + UTNDATACorpSGC, + UTNUSERFirstHardware, + BaltimoreCyberTrustRoot, + CybertrustGlobalRoot, + VerizonGlobalRootCA, + DigiCertAssuredIDRootCA, + DigiCertAssuredIDRootG2, + DigiCertGlobalRootCA, + DigiCertGlobalRootG2, + DigiCertHighAssuranceEVRootCA, + EntrustnetCertificationAuthority2048, + EntrustRootCertificationAuthority, + EntrustRootCertificationAuthorityG2, + GeoTrustGlobalCA2, + GeoTrustGlobalCA, + GeoTrustPrimaryCertificationAuthorityG3, + GeoTrustPrimaryCertificationAuthority, + GlobalSignRootCA, + GlobalSignRootCAR2, + GlobalSignRootCAR3, + GoDaddyClass2CertificationAuthority, + GoDaddyRootCertificateAuthorityG2, + StarfieldClass2CertificationAuthority, + StarfieldRootCertificateAuthorityG2, + ThawtePrimaryRootCAG3, + ThawtePrimaryRootCA, + VeriSignClass3PublicPrimaryCertificationAuthorityG3, + VeriSignClass3PublicPrimaryCertificationAuthorityG5, + VeriSignUniversalRootCertificationAuthority, + DSTRootCAX3, + USERTrustRSACertificationAuthority, + ISRGRootX10, + USERTrustECCCertificationAuthority, + COMODORSACertificationAuthority, + COMODOECCCertificationAuthority, + AmazonRootCA2, + AmazonRootCA3, + AmazonRootCA4, + DigiCertAssuredIDRootG3, + DigiCertGlobalRootG3, + DigiCertTrustedRootG4, + EntrustRootCertificationAuthorityEC1, + EntrustRootCertificationAuthorityG4, + GlobalSignECCRootCAR4, + GlobalSignECCRootCAR5, + GlobalSignECCRootCAR6, + GTSRootR1, + GTSRootR2, + GTSRootR3, + GTSRootR4, + SecurityCommunicationRootCA, + + All = uint.MaxValue + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/Types/CertificateFormat.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/CertificateFormat.cs new file mode 100644 index 00000000..1d80f739 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/CertificateFormat.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Ssl.Types +{ + enum CertificateFormat : uint + { + Pem = 1, + Der = 2 + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/Types/IoMode.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/IoMode.cs new file mode 100644 index 00000000..1cd06d6d --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/IoMode.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Ssl.Types +{ + enum IoMode : uint + { + Blocking = 1, + NonBlocking = 2 + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/Types/OptionType.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/OptionType.cs new file mode 100644 index 00000000..3673200a --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/OptionType.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.HLE.HOS.Services.Ssl.Types +{ + enum OptionType : uint + { + DoNotCloseSocket, + GetServerCertChain, // 3.0.0+ + SkipDefaultVerify, // 5.0.0+ + EnableAlpn // 9.0.0+ + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/Types/SessionCacheMode.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/SessionCacheMode.cs new file mode 100644 index 00000000..cec7b745 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/SessionCacheMode.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.HLE.HOS.Services.Ssl.Types +{ + enum SessionCacheMode : uint + { + None, + SessionId, + SessionTicket + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/Types/SslVersion.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/SslVersion.cs new file mode 100644 index 00000000..7110fd85 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/SslVersion.cs @@ -0,0 +1,16 @@ +using System; + +namespace Ryujinx.HLE.HOS.Services.Ssl.Types +{ + [Flags] + enum SslVersion : uint + { + Auto = 1 << 0, + TlsV10 = 1 << 3, + TlsV11 = 1 << 4, + TlsV12 = 1 << 5, + TlsV13 = 1 << 6, // 11.0.0+ + + VersionMask = 0xFFFFFF + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/Types/TrustedCertStatus.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/TrustedCertStatus.cs new file mode 100644 index 00000000..7fd5efd6 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/TrustedCertStatus.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.HLE.HOS.Services.Ssl.Types +{ + enum TrustedCertStatus : uint + { + Removed, + EnabledTrusted, + EnabledNotTrusted, + Revoked, + + Invalid = uint.MaxValue + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/Types/VerifyOption.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/VerifyOption.cs new file mode 100644 index 00000000..d25bb6c3 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/Types/VerifyOption.cs @@ -0,0 +1,15 @@ +using System; + +namespace Ryujinx.HLE.HOS.Services.Ssl.Types +{ + [Flags] + enum VerifyOption : uint + { + PeerCa = 1 << 0, + HostName = 1 << 1, + DateCheck = 1 << 2, + EvCertPartial = 1 << 3, + EvPolicyOid = 1 << 4, // 6.0.0+ + EvCertFingerprint = 1 << 5 // 6.0.0+ + } +}
\ No newline at end of file |
