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();
}