diff --git a/ProjectLighthouse.sln.DotSettings b/ProjectLighthouse.sln.DotSettings index e345dfeb..d3818928 100644 --- a/ProjectLighthouse.sln.DotSettings +++ b/ProjectLighthouse.sln.DotSettings @@ -149,6 +149,7 @@ True True True + True True True True diff --git a/ProjectLighthouse/ProjectLighthouse.csproj b/ProjectLighthouse/ProjectLighthouse.csproj index 8bfddee5..7f189d03 100644 --- a/ProjectLighthouse/ProjectLighthouse.csproj +++ b/ProjectLighthouse/ProjectLighthouse.csproj @@ -26,6 +26,7 @@ + diff --git a/ProjectLighthouse/Tickets/NPTicket.cs b/ProjectLighthouse/Tickets/NPTicket.cs index 84fa0723..3b05184b 100644 --- a/ProjectLighthouse/Tickets/NPTicket.cs +++ b/ProjectLighthouse/Tickets/NPTicket.cs @@ -1,6 +1,8 @@ #nullable enable using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Text.Json; using LBPUnion.ProjectLighthouse.Configuration; @@ -9,6 +11,12 @@ using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.Tickets.Data; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Math.EC; +using Org.BouncyCastle.Security; using Version = LBPUnion.ProjectLighthouse.Types.Version; namespace LBPUnion.ProjectLighthouse.Tickets; @@ -30,9 +38,53 @@ public class NPTicket private string? titleId { get; set; } + private byte[] ticketBody { get; set; } = Array.Empty(); + + private byte[] ticketSignature { get; set; } = Array.Empty(); + public GameVersion GameVersion { get; set; } - private static void Read21Ticket(NPTicket npTicket, TicketReader reader) + private static readonly ECDomainParameters secp224K1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp224k1")); + private static readonly ECDomainParameters secp192K1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp192k1")); + + private static readonly ECPoint rpcnPublic = secp224K1.Curve.CreatePoint( + new BigInteger("b07bc0f0addb97657e9f389039e8d2b9c97dc2a31d3042e7d0479b93", 16), + new BigInteger("d81c42b0abdf6c42191a31e31f93342f8f033bd529c2c57fdb5a0a7d", 16)); + + private ECDomainParameters getCurveParams() => this.IsRpcn() ? secp224K1 : secp192K1; + + private static ECPoint getPublicKey() => rpcnPublic; + + private static ECDomainParameters FromX9EcParams(X9ECParameters param) => + new(param.Curve, param.G, param.N, param.H, param.GetSeed()); + + private bool ValidateSignature() + { + //TODO support psn + if (!this.IsRpcn()) return true; + + ECPublicKeyParameters pubKey = new(getPublicKey(), this.getCurveParams()); + ISigner signer = SignerUtilities.GetSigner("SHA-224withECDSA"); + signer.Init(false, pubKey); + + signer.BlockUpdate(this.ticketBody); + + return signer.VerifySignature(this.ticketSignature); + } + + private bool IsRpcn() => this.IssuerId == 0x33333333; + + private static readonly Dictionary identifierByPlatform = new() + { + { + Platform.RPCS3, new byte[] { 0x52, 0x50, 0x43, 0x4E, } + }, + { + Platform.PS3, new byte[]{ 0x71, 0x9F, 0x1D, 0x4A, } + } + }; + + private static bool Read21Ticket(NPTicket npTicket, TicketReader reader) { reader.ReadTicketString(); // "Serial id", but its apparently not what we're looking for @@ -40,7 +92,8 @@ public class NPTicket npTicket.IssuedDate = reader.ReadTicketUInt64(); npTicket.ExpireDate = reader.ReadTicketUInt64(); - reader.ReadTicketUInt64(); // PSN User id, we don't care about this + ulong uid = reader.ReadTicketUInt64(); // PSN User id, we don't care about this + Console.WriteLine(@$"npTicket uid = {uid}"); npTicket.Username = reader.ReadTicketString(); @@ -48,12 +101,57 @@ public class NPTicket reader.ReadTicketString(); // Domain npTicket.titleId = reader.ReadTicketString(); + + reader.ReadTicketUInt32(); // status + + reader.ReadTicketEmpty(); // padding + reader.ReadTicketEmpty(); + + reader.ReadSectionHeader(); // footer header + + byte[] ident = reader.ReadTicketBinary(); // 4 byte identifier + Platform platform = npTicket.IsRpcn() ? Platform.RPCS3 : Platform.PS3; + if (!ident.SequenceEqual(identifierByPlatform[platform])) + { + Console.WriteLine(@$"Identity sequence mismatch, platform={npTicket.Platform} - {Convert.ToHexString(ident)} == {Convert.ToHexString(identifierByPlatform[npTicket.Platform])}"); + return false; + } + + //TODO check platform and ident + + npTicket.ticketSignature = reader.ReadTicketBinary(); + return true; } // Function is here for future use incase we ever need to read more from the ticket - private static void Read30Ticket(NPTicket npTicket, TicketReader reader) + private static bool Read30Ticket(NPTicket npTicket, TicketReader reader) => Read21Ticket(npTicket, reader); + + private static bool ReadTicket(byte[] data, NPTicket npTicket, TicketReader reader) { - Read21Ticket(npTicket, reader); + npTicket.ticketVersion = reader.ReadTicketVersion(); + + reader.ReadBytes(4); // Skip header + + ushort ticketLen = reader.ReadUInt16BE(); // Ticket length, we don't care about this + if (ticketLen != data.Length - 0x8) + { + Console.WriteLine("Ticket length mismatch"); + return false; + } + + long bodyStart = reader.BaseStream.Position; + SectionHeader bodyHeader = reader.ReadSectionHeader(); + + npTicket.ticketBody = data.AsSpan().Slice((int)bodyStart, bodyHeader.Length+4).ToArray(); + + Logger.Debug($"bodyHeader.Type is {bodyHeader.Type}, index={bodyStart}", LogArea.Login); + + return npTicket.ticketVersion.ToString() switch + { + "2.1" => Read21Ticket(npTicket, reader), + "3.0" => Read30Ticket(npTicket, reader), + _ => throw new NotImplementedException(), + }; } /// @@ -76,10 +174,9 @@ public class NPTicket GameVersion = GameVersion.LittleBigPlanet2, ExpireDate = 0, IssuedDate = 0, + Username = dataStr["unitTestTicket".Length..], }; - npTicket.Username = dataStr.Substring(14); - return npTicket; } } @@ -89,24 +186,11 @@ public class NPTicket using MemoryStream ms = new(data); using TicketReader reader = new(ms); - npTicket.ticketVersion = reader.ReadTicketVersion(); - - reader.ReadBytes(4); // Skip header - - reader.ReadUInt16BE(); // Ticket length, we don't care about this - - SectionHeader bodyHeader = reader.ReadSectionHeader(); - Logger.Debug($"bodyHeader.Type is {bodyHeader.Type}", LogArea.Login); - - switch (npTicket.ticketVersion) + bool validTicket = ReadTicket(data, npTicket, reader); + if (!validTicket) { - case "2.1": - Read21Ticket(npTicket, reader); - break; - case "3.0": - Read30Ticket(npTicket, reader); - break; - default: throw new NotImplementedException(); + Logger.Warn($"Failed to parse ticket from {npTicket.Username}", LogArea.Login); + return null; } if (npTicket.titleId == null) throw new ArgumentNullException($"{nameof(npTicket)}.{nameof(npTicket.titleId)}"); @@ -114,8 +198,8 @@ public class NPTicket // We already read the title id, however we need to do some post-processing to get what we want. // Current data: UP9000-BCUS98245_00 // We need to chop this to get the titleId we're looking for - npTicket.titleId = npTicket.titleId.Substring(7); // Trim UP9000- - npTicket.titleId = npTicket.titleId.Substring(0, npTicket.titleId.Length - 3); // Trim _00 at the end + npTicket.titleId = npTicket.titleId[7..]; // Trim UP9000- + npTicket.titleId = npTicket.titleId[..^3]; // Trim _00 at the end // Data now (hopefully): BCUS98245 Logger.Debug($"titleId is {npTicket.titleId}", LogArea.Login); @@ -145,6 +229,15 @@ public class NPTicket return null; } + bool valid = npTicket.ValidateSignature(); + if (!valid) + { + Logger.Warn($"Failed to verify authenticity of ticket from user {npTicket.Username}", LogArea.Login); + return null; + } + + Logger.Success($"Verified ticket signature from {npTicket.Username}", LogArea.Login); + #if DEBUG Logger.Debug("npTicket data:", LogArea.Login); Logger.Debug(JsonSerializer.Serialize(npTicket), LogArea.Login); diff --git a/ProjectLighthouse/Tickets/TicketReader.cs b/ProjectLighthouse/Tickets/TicketReader.cs index cfb7c067..d7928f33 100644 --- a/ProjectLighthouse/Tickets/TicketReader.cs +++ b/ProjectLighthouse/Tickets/TicketReader.cs @@ -19,18 +19,22 @@ public class TicketReader : BinaryReader { this.ReadByte(); - SectionHeader sectionHeader = new(); - sectionHeader.Type = (SectionType)this.ReadByte(); - sectionHeader.Length = this.ReadUInt16BE(); + SectionHeader sectionHeader = new() + { + Type = (SectionType)this.ReadByte(), + Length = this.ReadUInt16BE(), + }; return sectionHeader; } - public DataHeader ReadDataHeader() + private DataHeader ReadDataHeader() { - DataHeader dataHeader = new(); - dataHeader.Type = (DataType)this.ReadUInt16BE(); - dataHeader.Length = this.ReadUInt16BE(); + DataHeader dataHeader = new() + { + Type = (DataType)this.ReadUInt16BE(), + Length = this.ReadUInt16BE(), + }; return dataHeader; } @@ -38,7 +42,7 @@ public class TicketReader : BinaryReader public byte[] ReadTicketBinary() { DataHeader dataHeader = this.ReadDataHeader(); - Debug.Assert(dataHeader.Type == DataType.Binary || dataHeader.Type == DataType.String); + Debug.Assert(dataHeader.Type is DataType.Binary or DataType.String); return this.ReadBytes(dataHeader.Length); } @@ -53,10 +57,16 @@ public class TicketReader : BinaryReader return this.ReadUInt32BE(); } + public void ReadTicketEmpty() + { + DataHeader dataHeader = this.ReadDataHeader(); + Debug.Assert(dataHeader.Type == DataType.Empty); + } + public ulong ReadTicketUInt64() { DataHeader dataHeader = this.ReadDataHeader(); - Debug.Assert(dataHeader.Type == DataType.UInt64 || dataHeader.Type == DataType.Timestamp); + Debug.Assert(dataHeader.Type is DataType.UInt64 or DataType.Timestamp); return this.ReadUInt64BE(); }