mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-06-16 12:41:28 +00:00
Initial work for verifying login ticket signatures
This commit is contained in:
parent
c6ddeaf154
commit
806e4e898d
4 changed files with 139 additions and 34 deletions
|
@ -149,6 +149,7 @@
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=PCSA/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=PCSA/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=PCSD/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=PCSD/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=PCSF/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=PCSF/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Rpcn/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Sublevels/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Sublevels/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Swingy/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Swingy/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=thumbsdown/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=thumbsdown/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
<PackageReference Include="SharpZipLib" Version="1.4.0" />
|
<PackageReference Include="SharpZipLib" Version="1.4.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||||
<PackageReference Include="YamlDotNet" Version="12.0.2" />
|
<PackageReference Include="YamlDotNet" Version="12.0.2" />
|
||||||
|
<PackageReference Include="BouncyCastle.Cryptography" Version="2.0.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using LBPUnion.ProjectLighthouse.Configuration;
|
using LBPUnion.ProjectLighthouse.Configuration;
|
||||||
|
@ -9,6 +11,12 @@ using LBPUnion.ProjectLighthouse.Helpers;
|
||||||
using LBPUnion.ProjectLighthouse.Logging;
|
using LBPUnion.ProjectLighthouse.Logging;
|
||||||
using LBPUnion.ProjectLighthouse.PlayerData;
|
using LBPUnion.ProjectLighthouse.PlayerData;
|
||||||
using LBPUnion.ProjectLighthouse.Tickets.Data;
|
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;
|
using Version = LBPUnion.ProjectLighthouse.Types.Version;
|
||||||
|
|
||||||
namespace LBPUnion.ProjectLighthouse.Tickets;
|
namespace LBPUnion.ProjectLighthouse.Tickets;
|
||||||
|
@ -30,9 +38,53 @@ public class NPTicket
|
||||||
|
|
||||||
private string? titleId { get; set; }
|
private string? titleId { get; set; }
|
||||||
|
|
||||||
|
private byte[] ticketBody { get; set; } = Array.Empty<byte>();
|
||||||
|
|
||||||
|
private byte[] ticketSignature { get; set; } = Array.Empty<byte>();
|
||||||
|
|
||||||
public GameVersion GameVersion { get; set; }
|
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<Platform, byte[]> 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
|
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.IssuedDate = reader.ReadTicketUInt64();
|
||||||
npTicket.ExpireDate = 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();
|
npTicket.Username = reader.ReadTicketString();
|
||||||
|
|
||||||
|
@ -48,12 +101,57 @@ public class NPTicket
|
||||||
reader.ReadTicketString(); // Domain
|
reader.ReadTicketString(); // Domain
|
||||||
|
|
||||||
npTicket.titleId = reader.ReadTicketString();
|
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
|
// 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(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -76,10 +174,9 @@ public class NPTicket
|
||||||
GameVersion = GameVersion.LittleBigPlanet2,
|
GameVersion = GameVersion.LittleBigPlanet2,
|
||||||
ExpireDate = 0,
|
ExpireDate = 0,
|
||||||
IssuedDate = 0,
|
IssuedDate = 0,
|
||||||
|
Username = dataStr["unitTestTicket".Length..],
|
||||||
};
|
};
|
||||||
|
|
||||||
npTicket.Username = dataStr.Substring(14);
|
|
||||||
|
|
||||||
return npTicket;
|
return npTicket;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,24 +186,11 @@ public class NPTicket
|
||||||
using MemoryStream ms = new(data);
|
using MemoryStream ms = new(data);
|
||||||
using TicketReader reader = new(ms);
|
using TicketReader reader = new(ms);
|
||||||
|
|
||||||
npTicket.ticketVersion = reader.ReadTicketVersion();
|
bool validTicket = ReadTicket(data, npTicket, reader);
|
||||||
|
if (!validTicket)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
case "2.1":
|
Logger.Warn($"Failed to parse ticket from {npTicket.Username}", LogArea.Login);
|
||||||
Read21Ticket(npTicket, reader);
|
return null;
|
||||||
break;
|
|
||||||
case "3.0":
|
|
||||||
Read30Ticket(npTicket, reader);
|
|
||||||
break;
|
|
||||||
default: throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (npTicket.titleId == null) throw new ArgumentNullException($"{nameof(npTicket)}.{nameof(npTicket.titleId)}");
|
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.
|
// We already read the title id, however we need to do some post-processing to get what we want.
|
||||||
// Current data: UP9000-BCUS98245_00
|
// Current data: UP9000-BCUS98245_00
|
||||||
// We need to chop this to get the titleId we're looking for
|
// 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[7..]; // Trim UP9000-
|
||||||
npTicket.titleId = npTicket.titleId.Substring(0, npTicket.titleId.Length - 3); // Trim _00 at the end
|
npTicket.titleId = npTicket.titleId[..^3]; // Trim _00 at the end
|
||||||
// Data now (hopefully): BCUS98245
|
// Data now (hopefully): BCUS98245
|
||||||
|
|
||||||
Logger.Debug($"titleId is {npTicket.titleId}", LogArea.Login);
|
Logger.Debug($"titleId is {npTicket.titleId}", LogArea.Login);
|
||||||
|
@ -145,6 +229,15 @@ public class NPTicket
|
||||||
return null;
|
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
|
#if DEBUG
|
||||||
Logger.Debug("npTicket data:", LogArea.Login);
|
Logger.Debug("npTicket data:", LogArea.Login);
|
||||||
Logger.Debug(JsonSerializer.Serialize(npTicket), LogArea.Login);
|
Logger.Debug(JsonSerializer.Serialize(npTicket), LogArea.Login);
|
||||||
|
|
|
@ -19,18 +19,22 @@ public class TicketReader : BinaryReader
|
||||||
{
|
{
|
||||||
this.ReadByte();
|
this.ReadByte();
|
||||||
|
|
||||||
SectionHeader sectionHeader = new();
|
SectionHeader sectionHeader = new()
|
||||||
sectionHeader.Type = (SectionType)this.ReadByte();
|
{
|
||||||
sectionHeader.Length = this.ReadUInt16BE();
|
Type = (SectionType)this.ReadByte(),
|
||||||
|
Length = this.ReadUInt16BE(),
|
||||||
|
};
|
||||||
|
|
||||||
return sectionHeader;
|
return sectionHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DataHeader ReadDataHeader()
|
private DataHeader ReadDataHeader()
|
||||||
{
|
{
|
||||||
DataHeader dataHeader = new();
|
DataHeader dataHeader = new()
|
||||||
dataHeader.Type = (DataType)this.ReadUInt16BE();
|
{
|
||||||
dataHeader.Length = this.ReadUInt16BE();
|
Type = (DataType)this.ReadUInt16BE(),
|
||||||
|
Length = this.ReadUInt16BE(),
|
||||||
|
};
|
||||||
|
|
||||||
return dataHeader;
|
return dataHeader;
|
||||||
}
|
}
|
||||||
|
@ -38,7 +42,7 @@ public class TicketReader : BinaryReader
|
||||||
public byte[] ReadTicketBinary()
|
public byte[] ReadTicketBinary()
|
||||||
{
|
{
|
||||||
DataHeader dataHeader = this.ReadDataHeader();
|
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);
|
return this.ReadBytes(dataHeader.Length);
|
||||||
}
|
}
|
||||||
|
@ -53,10 +57,16 @@ public class TicketReader : BinaryReader
|
||||||
return this.ReadUInt32BE();
|
return this.ReadUInt32BE();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ReadTicketEmpty()
|
||||||
|
{
|
||||||
|
DataHeader dataHeader = this.ReadDataHeader();
|
||||||
|
Debug.Assert(dataHeader.Type == DataType.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
public ulong ReadTicketUInt64()
|
public ulong ReadTicketUInt64()
|
||||||
{
|
{
|
||||||
DataHeader dataHeader = this.ReadDataHeader();
|
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();
|
return this.ReadUInt64BE();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue