mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-08-02 10:08:39 +00:00
Refactor NpTicket parser and add NpTicket signature documentation (#784)
* Refactor NpTicket parser and add NpTicket signature documentation * Update TickerBuilder chaining style * Fix SectionHeader position returning wrong value * Don't attempt to parse ticket if it is less than 8 bytes
This commit is contained in:
parent
8b0aed9a61
commit
2f11731a8e
16 changed files with 506 additions and 247 deletions
57
Documentation/Tickets.md
Normal file
57
Documentation/Tickets.md
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# PS3 Authentication Tickets
|
||||||
|
|
||||||
|
Authentication tickets, commonly referred to as NpTickets or X-I-5 tickets are binary blobs used by third-party game servers
|
||||||
|
to identify and authenticate users.
|
||||||
|
|
||||||
|
For more information about the format of tickets see either the [article written by the psdevwiki](https://www.psdevwiki.com/ps3/X-I-5-Ticket)
|
||||||
|
or the write-up done by the [Skate 3 server team](https://github.com/hallofmeat/Skateboard3Server/blob/master/docs/PS3Ticket.md)
|
||||||
|
|
||||||
|
PSN uses ECDSA for securely verifying that a ticket was indeed generated by PSN and hasn't been tampered with
|
||||||
|
|
||||||
|
The exact specifics of the system are unknown but it's believed that each game or in some instances groups of games (The LBP series for example)
|
||||||
|
are given a single unique identifier such that any tickets with that identifier are guaranteed to be signed
|
||||||
|
using the same private key. Therefore, with that identifier in hand you can determine what public
|
||||||
|
key you should use to verify a ticket.
|
||||||
|
|
||||||
|
## ECDSA specifics
|
||||||
|
The PSN implementation of ECDSA signing uses the `secp192r1` curve also known as `nistp192`. The hashing algorithm used is `SHA-1`.
|
||||||
|
The message is the entire ticket sans the signature (the last 56 bytes).
|
||||||
|
|
||||||
|
The RPCN implementation of ECDSA signing uses the `secp224k1` curve. The hashing algorithm used is `SHA-224`.
|
||||||
|
The message for RPCN is the body section of the ticket including the body header.
|
||||||
|
The public key for RPCN is available on [the RPCN repo](https://github.com/RipleyTom/rpcn/blob/master/ticket_public.pem) or in the [known keys section](#known-public-keys).
|
||||||
|
|
||||||
|
## Finding public keys
|
||||||
|
Without going into the specifics of ECDSA it's possible to derive the public key given that you have the original message and know the curve
|
||||||
|
parameters and hashing algorithm.
|
||||||
|
|
||||||
|
A C# program has been written that can parse tickets and give the public keys that correspond to the private key used to sign the message.
|
||||||
|
|
||||||
|
The project is available on GitHub at https://github.com/Slendy/PubKeyFinder
|
||||||
|
|
||||||
|
While it's possible to find the public key given a single ticket, due to the way ECDSA works there are always 2 possible
|
||||||
|
points that can validate a signature. If you have more than one ticket though then each ticket will have at least
|
||||||
|
one matching point which can be used to identify the actual public key.
|
||||||
|
|
||||||
|
## Known Public Keys
|
||||||
|
|
||||||
|
### RPCN (All Games)
|
||||||
|
```
|
||||||
|
identifier: 5250434E (RPCN in ASCII)
|
||||||
|
x: b07bc0f0addb97657e9f389039e8d2b9c97dc2a31d3042e7d0479b93
|
||||||
|
y: d81c42b0abdf6c42191a31e31f93342f8f033bd529c2c57fdb5a0a7d
|
||||||
|
```
|
||||||
|
|
||||||
|
### LittleBigPlanet Series (PSN)
|
||||||
|
```
|
||||||
|
identfier: 719F1D4A
|
||||||
|
x: 39c62d061d4ee35c5f3f7531de0af3cf918346526edac727
|
||||||
|
y: a5d578b55113e612bf1878d4cc939d61a41318403b5bdf86
|
||||||
|
```
|
||||||
|
|
||||||
|
### Skate 3 (PSN)
|
||||||
|
```
|
||||||
|
identifier: 382DE58D
|
||||||
|
x: a93f2d73da8fe51c59872fad192b832f8b9dabde8587233
|
||||||
|
y: 93131936a54a0ea51117f74518e56aae95f6baff4b29f999
|
||||||
|
```
|
48
ProjectLighthouse.Tests/Unit/TicketTests.cs
Normal file
48
ProjectLighthouse.Tests/Unit/TicketTests.cs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
using LBPUnion.ProjectLighthouse.Tickets;
|
||||||
|
using LBPUnion.ProjectLighthouse.Tickets.Signature;
|
||||||
|
using LBPUnion.ProjectLighthouse.Types.Users;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Tests.Unit;
|
||||||
|
|
||||||
|
[Trait("Category", "Unit")]
|
||||||
|
public class TicketTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CanReadTicket()
|
||||||
|
{
|
||||||
|
TicketBuilder builder = new TicketBuilder()
|
||||||
|
.SetCountry("br")
|
||||||
|
.SetUserId(21)
|
||||||
|
.SetDomain("us")
|
||||||
|
.SetStatus(0)
|
||||||
|
.SetIssuerId(0x74657374)
|
||||||
|
.setExpirationTime(ulong.MaxValue)
|
||||||
|
.SetUsername("unittest")
|
||||||
|
.SetIssueTime(0)
|
||||||
|
.SetTitleId("UP9000-BCUS98245_00");
|
||||||
|
|
||||||
|
byte[] ticketData = builder.Build();
|
||||||
|
|
||||||
|
NPTicket? ticket = NPTicket.CreateFromBytes(ticketData);
|
||||||
|
Assert.NotNull(ticket);
|
||||||
|
Assert.Equal((ulong)0, ticket.IssueTime);
|
||||||
|
Assert.Equal(ulong.MaxValue, ticket.ExpireTime);
|
||||||
|
Assert.Equal("unittest", ticket.Username);
|
||||||
|
Assert.Equal(GameVersion.LittleBigPlanet2, ticket.GameVersion);
|
||||||
|
Assert.Equal((ulong)0x74657374, ticket.IssuerId);
|
||||||
|
Assert.Equal((ulong)21, ticket.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanVerifyTicketSignature()
|
||||||
|
{
|
||||||
|
TicketBuilder builder = new();
|
||||||
|
|
||||||
|
byte[] ticketData = builder.Build();
|
||||||
|
|
||||||
|
NPTicket? ticket = NPTicket.CreateFromBytes(ticketData);
|
||||||
|
Assert.NotNull(ticket);
|
||||||
|
Assert.True(new UnitTestSignatureVerifier(ticket).ValidateSignature());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,14 @@
|
||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using LBPUnion.ProjectLighthouse.Extensions;
|
using LBPUnion.ProjectLighthouse.Extensions;
|
||||||
using LBPUnion.ProjectLighthouse.Helpers;
|
using LBPUnion.ProjectLighthouse.Helpers;
|
||||||
using LBPUnion.ProjectLighthouse.Logging;
|
using LBPUnion.ProjectLighthouse.Logging;
|
||||||
using LBPUnion.ProjectLighthouse.Configuration;
|
using LBPUnion.ProjectLighthouse.Configuration;
|
||||||
|
using LBPUnion.ProjectLighthouse.Tickets.Parser;
|
||||||
|
using LBPUnion.ProjectLighthouse.Tickets.Signature;
|
||||||
using LBPUnion.ProjectLighthouse.Types.Logging;
|
using LBPUnion.ProjectLighthouse.Types.Logging;
|
||||||
using LBPUnion.ProjectLighthouse.Types.Users;
|
using LBPUnion.ProjectLighthouse.Types.Users;
|
||||||
using Org.BouncyCastle.Asn1;
|
|
||||||
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;
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
#endif
|
#endif
|
||||||
|
@ -27,224 +20,78 @@ namespace LBPUnion.ProjectLighthouse.Tickets;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class NPTicket
|
public class NPTicket
|
||||||
{
|
{
|
||||||
public string? Username { get; set; }
|
public string Username { get; protected internal set; } = "";
|
||||||
|
|
||||||
private TicketVersion? ticketVersion { get; set; }
|
public Platform Platform { get; private set; }
|
||||||
|
public GameVersion GameVersion { get; private set; }
|
||||||
public Platform Platform { get; set; }
|
public string TicketHash { get; private set; } = "";
|
||||||
|
|
||||||
public uint IssuerId { get; set; }
|
public uint IssuerId { get; set; }
|
||||||
public ulong IssuedDate { get; set; }
|
public ulong IssueTime { get; set; }
|
||||||
public ulong ExpireDate { get; set; }
|
public ulong ExpireTime { get; set; }
|
||||||
public ulong UserId { get; set; }
|
public ulong UserId { get; set; }
|
||||||
public string TicketHash { get; set; } = "";
|
|
||||||
|
|
||||||
private string? titleId { get; set; }
|
private TicketVersion? TicketVersion { get; set; }
|
||||||
|
|
||||||
private byte[] ticketBody { get; set; } = Array.Empty<byte>();
|
protected internal string TitleId { get; set; } = "";
|
||||||
|
|
||||||
private byte[] ticketSignature { get; set; } = Array.Empty<byte>();
|
private TicketSignatureVerifier SignatureVerifier { get; set; } = new NullSignatureVerifier();
|
||||||
private byte[] ticketSignatureIdentifier { get; set; } = Array.Empty<byte>();
|
|
||||||
|
|
||||||
public GameVersion GameVersion { get; set; }
|
protected internal SectionHeader BodyHeader { get; set; }
|
||||||
|
|
||||||
private static ECDomainParameters FromX9EcParams(X9ECParameters param) =>
|
protected internal byte[] Data { get; set; } = Array.Empty<byte>();
|
||||||
new(param.Curve, param.G, param.N, param.H, param.GetSeed());
|
protected internal byte[] TicketSignature { get; set; } = Array.Empty<byte>();
|
||||||
|
protected internal byte[] TicketSignatureIdentifier { get; set; } = Array.Empty<byte>();
|
||||||
|
|
||||||
public static readonly ECDomainParameters Secp224K1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp224k1"));
|
private static bool ReadTicket(NPTicket npTicket, TicketReader reader)
|
||||||
public static readonly ECDomainParameters Secp192R1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp192r1"));
|
|
||||||
|
|
||||||
private static readonly ECPoint rpcnPublic = Secp224K1.Curve.CreatePoint(
|
|
||||||
new BigInteger("b07bc0f0addb97657e9f389039e8d2b9c97dc2a31d3042e7d0479b93", 16),
|
|
||||||
new BigInteger("d81c42b0abdf6c42191a31e31f93342f8f033bd529c2c57fdb5a0a7d", 16));
|
|
||||||
|
|
||||||
private static readonly ECPoint psnPublic = Secp192R1.Curve.CreatePoint(
|
|
||||||
new BigInteger("39c62d061d4ee35c5f3f7531de0af3cf918346526edac727", 16),
|
|
||||||
new BigInteger("a5d578b55113e612bf1878d4cc939d61a41318403b5bdf86", 16));
|
|
||||||
|
|
||||||
private static readonly ECPoint unitTestPublic = Secp192R1.Curve.CreatePoint(
|
|
||||||
new BigInteger("b6f3374bde4ec23a25e1508889e7d7e71870ba74daf8654f", 16),
|
|
||||||
new BigInteger("738de93dad0fffb5642045439afaaf8c6fda319a72d2a584", 16));
|
|
||||||
|
|
||||||
internal class SignatureParams
|
|
||||||
{
|
{
|
||||||
public string HashAlgo { get; set; }
|
npTicket.TicketVersion = reader.ReadTicketVersion();
|
||||||
public ECPoint PublicKey { get; set; }
|
|
||||||
public ECDomainParameters CurveParams { get; set; }
|
|
||||||
|
|
||||||
public SignatureParams(string hashAlgo, ECPoint pubKey, ECDomainParameters curve)
|
reader.SkipBytes(4); // Skip header
|
||||||
{
|
|
||||||
this.HashAlgo = hashAlgo;
|
|
||||||
this.PublicKey = pubKey;
|
|
||||||
this.CurveParams = curve;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Dictionary<string, SignatureParams> signatureParamsMap = new()
|
|
||||||
{
|
|
||||||
//psn
|
|
||||||
{ "719F1D4A", new SignatureParams("SHA-1", psnPublic, Secp192R1) },
|
|
||||||
//rpcn
|
|
||||||
{ "5250434E", new SignatureParams("SHA-224", rpcnPublic, Secp224K1) },
|
|
||||||
//unit test
|
|
||||||
{ "54455354", new SignatureParams("SHA-1", unitTestPublic, Secp192R1) },
|
|
||||||
};
|
|
||||||
|
|
||||||
private bool ValidateSignature()
|
|
||||||
{
|
|
||||||
string identifierHex = Convert.ToHexString(this.ticketSignatureIdentifier);
|
|
||||||
if (!this.signatureParamsMap.ContainsKey(identifierHex))
|
|
||||||
{
|
|
||||||
Logger.Warn($"Unknown signature identifier in ticket: {identifierHex}, platform={this.Platform}", LogArea.Login);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SignatureParams sigParams = this.signatureParamsMap[identifierHex];
|
|
||||||
ECPublicKeyParameters pubKey = new(sigParams.PublicKey, sigParams.CurveParams);
|
|
||||||
|
|
||||||
ISigner signer = SignerUtilities.GetSigner($"{sigParams.HashAlgo}withECDSA");
|
|
||||||
signer.Init(false, pubKey);
|
|
||||||
|
|
||||||
signer.BlockUpdate(this.ticketBody);
|
|
||||||
|
|
||||||
return signer.VerifySignature(this.ticketSignature);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sometimes psn signatures have one or two extra empty bytes
|
|
||||||
// This is slow but it's better than carelessly chopping 0's
|
|
||||||
private static byte[] ParseSignature(byte[] signature)
|
|
||||||
{
|
|
||||||
for (int i = 0; i <= 2; i++)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Asn1Object.FromByteArray(signature);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
signature = signature.SkipLast(1).ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return signature;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool Read21Ticket(NPTicket npTicket, TicketReader reader)
|
|
||||||
{
|
|
||||||
reader.ReadTicketString(); // serial id
|
|
||||||
|
|
||||||
npTicket.IssuerId = reader.ReadTicketUInt32();
|
|
||||||
npTicket.IssuedDate = reader.ReadTicketUInt64();
|
|
||||||
npTicket.ExpireDate = reader.ReadTicketUInt64();
|
|
||||||
|
|
||||||
npTicket.UserId = reader.ReadTicketUInt64();
|
|
||||||
|
|
||||||
npTicket.Username = reader.ReadTicketString();
|
|
||||||
|
|
||||||
reader.ReadTicketString(); // Country
|
|
||||||
reader.ReadTicketString(); // Domain
|
|
||||||
|
|
||||||
npTicket.titleId = reader.ReadTicketString();
|
|
||||||
|
|
||||||
reader.ReadTicketUInt32(); // status
|
|
||||||
|
|
||||||
reader.ReadTicketEmpty(); // padding
|
|
||||||
reader.ReadTicketEmpty();
|
|
||||||
|
|
||||||
SectionHeader footer = reader.ReadSectionHeader(); // footer header
|
|
||||||
if (footer.Type != SectionType.Footer)
|
|
||||||
{
|
|
||||||
Logger.Warn(@$"Unexpected ticket footer header: expected={SectionType.Footer}, actual={footer}",
|
|
||||||
LogArea.Login);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
npTicket.ticketSignatureIdentifier = reader.ReadTicketBinary();
|
|
||||||
|
|
||||||
npTicket.ticketSignature = ParseSignature(reader.ReadTicketBinary());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool Read30Ticket(NPTicket npTicket, TicketReader reader)
|
|
||||||
{
|
|
||||||
reader.ReadTicketString(); // serial id
|
|
||||||
|
|
||||||
npTicket.IssuerId = reader.ReadTicketUInt32();
|
|
||||||
npTicket.IssuedDate = reader.ReadTicketUInt64();
|
|
||||||
npTicket.ExpireDate = reader.ReadTicketUInt64();
|
|
||||||
|
|
||||||
npTicket.UserId = reader.ReadTicketUInt64();
|
|
||||||
|
|
||||||
npTicket.Username = reader.ReadTicketString();
|
|
||||||
|
|
||||||
reader.ReadTicketString(); // Country
|
|
||||||
reader.ReadTicketString(); // Domain
|
|
||||||
|
|
||||||
npTicket.titleId = reader.ReadTicketString();
|
|
||||||
|
|
||||||
reader.ReadSectionHeader(); // date of birth section
|
|
||||||
reader.ReadBytes(4); // 4 bytes for year month and day
|
|
||||||
reader.ReadTicketUInt32();
|
|
||||||
|
|
||||||
reader.ReadSectionHeader(); // empty section?
|
|
||||||
reader.ReadTicketEmpty();
|
|
||||||
|
|
||||||
SectionHeader footer = reader.ReadSectionHeader(); // footer header
|
|
||||||
if (footer.Type != SectionType.Footer)
|
|
||||||
{
|
|
||||||
Logger.Warn(@$"Unexpected ticket footer header: expected={SectionType.Footer}, actual={footer}",
|
|
||||||
LogArea.Login);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
npTicket.ticketSignatureIdentifier = reader.ReadTicketBinary();
|
|
||||||
|
|
||||||
npTicket.ticketSignature = ParseSignature(reader.ReadTicketBinary());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ReadTicket(byte[] data, NPTicket npTicket, TicketReader reader)
|
|
||||||
{
|
|
||||||
npTicket.ticketVersion = reader.ReadTicketVersion();
|
|
||||||
|
|
||||||
reader.ReadBytes(4); // Skip header
|
|
||||||
|
|
||||||
ushort ticketLen = reader.ReadUInt16BE();
|
ushort ticketLen = reader.ReadUInt16BE();
|
||||||
|
|
||||||
// Subtract 8 bytes to account for ticket header
|
// Subtract 8 bytes to account for ticket header
|
||||||
if (ticketLen != data.Length - 0x8)
|
if (ticketLen != npTicket.Data.Length - 0x8)
|
||||||
{
|
{
|
||||||
Logger.Warn(@$"Ticket length mismatch, expected={ticketLen}, actual={data.Length - 0x8}", LogArea.Login);
|
throw new TicketParseException(
|
||||||
return false;
|
@$"Ticket length mismatch, expected={ticketLen}, actual={npTicket.Data.Length - 0x8}");
|
||||||
}
|
}
|
||||||
|
|
||||||
long bodyStart = reader.BaseStream.Position;
|
npTicket.BodyHeader = reader.ReadSectionHeader();
|
||||||
SectionHeader bodyHeader = reader.ReadSectionHeader();
|
|
||||||
|
|
||||||
if (bodyHeader.Type != SectionType.Body)
|
if (npTicket.BodyHeader.Type != SectionType.Body)
|
||||||
{
|
{
|
||||||
Logger.Warn(@$"Unexpected ticket body header: expected={SectionType.Body}, actual={bodyHeader}", LogArea.Login);
|
throw new TicketParseException(
|
||||||
return false;
|
@$"Unexpected ticket body header: expected={SectionType.Body}, actual={npTicket.BodyHeader.Type}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Debug($"bodyHeader.Type is {bodyHeader.Type}, index={bodyStart}", LogArea.Login);
|
Logger.Debug($"bodyHeader.Type is {npTicket.BodyHeader.Type}, index={npTicket.BodyHeader.Position}",
|
||||||
|
LogArea.Login);
|
||||||
|
|
||||||
bool parsedSuccessfully = npTicket.ticketVersion.ToString() switch
|
ITicketParser ticketParser = npTicket.TicketVersion switch
|
||||||
{
|
{
|
||||||
"2.1" => Read21Ticket(npTicket, reader), // used by ps3 and rpcs3
|
Tickets.TicketVersion.V21 => new TicketParser21(npTicket, reader), // used by ps3 and rpcs3
|
||||||
"3.0" => Read30Ticket(npTicket, reader), // used by ps vita
|
Tickets.TicketVersion.V30 => new TicketParser30(npTicket, reader), // used by ps vita
|
||||||
_ => throw new NotImplementedException(),
|
_ => throw new NotImplementedException(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!parsedSuccessfully) return false;
|
if (!ticketParser.ParseTicket()) return false;
|
||||||
|
|
||||||
npTicket.ticketBody = Convert.ToHexString(npTicket.ticketSignatureIdentifier) switch
|
// Create a uint in big endian
|
||||||
|
uint signatureIdentifier = (uint)(npTicket.TicketSignatureIdentifier[0] << 24 |
|
||||||
|
npTicket.TicketSignatureIdentifier[1] << 16 |
|
||||||
|
npTicket.TicketSignatureIdentifier[2] << 8 |
|
||||||
|
npTicket.TicketSignatureIdentifier[3]);
|
||||||
|
|
||||||
|
npTicket.SignatureVerifier = signatureIdentifier switch
|
||||||
{
|
{
|
||||||
// rpcn
|
0x5250434E => new RpcnSignatureVerifier(npTicket),
|
||||||
"5250434E" => data.AsSpan().Slice((int)bodyStart, bodyHeader.Length + 4).ToArray(),
|
0x719F1D4A => new PsnSignatureVerifier(npTicket),
|
||||||
// psn and unit test
|
0x54455354 => new UnitTestSignatureVerifier(npTicket),
|
||||||
"719F1D4A" or "54455354" => data.AsSpan()[..data.AsSpan().IndexOf(npTicket.ticketSignature)].ToArray(),
|
_ => throw new ArgumentOutOfRangeException(nameof(npTicket),
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(npTicket)),
|
signatureIdentifier,
|
||||||
|
@"Invalid signature identifier"),
|
||||||
};
|
};
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -255,46 +102,60 @@ public class NPTicket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static NPTicket? CreateFromBytes(byte[] data)
|
public static NPTicket? CreateFromBytes(byte[] data)
|
||||||
{
|
{
|
||||||
NPTicket npTicket = new();
|
// Header should have at least 8 bytes
|
||||||
|
if (data.Length < 8)
|
||||||
|
{
|
||||||
|
Logger.Warn("NpTicket does not contain header", LogArea.Login);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
NPTicket npTicket = new()
|
||||||
|
{
|
||||||
|
Data = data,
|
||||||
|
};
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using MemoryStream ms = new(data);
|
using TicketReader reader = new(new MemoryStream(data));
|
||||||
using TicketReader reader = new(ms);
|
|
||||||
|
|
||||||
bool validTicket = ReadTicket(data, npTicket, reader);
|
bool validTicket = ReadTicket(npTicket, reader);
|
||||||
if (!validTicket)
|
if (!validTicket)
|
||||||
{
|
{
|
||||||
Logger.Warn($"Failed to parse ticket from {npTicket.Username}", LogArea.Login);
|
Logger.Warn($"Failed to parse ticket from {npTicket.Username}", LogArea.Login);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (npTicket.IssuedDate > (ulong)TimeHelper.TimestampMillis)
|
if (npTicket.IssueTime > (ulong)TimeHelper.TimestampMillis)
|
||||||
{
|
{
|
||||||
Logger.Warn($"Ticket isn't valid yet from {npTicket.Username} ({npTicket.IssuedDate} > {(ulong)TimeHelper.TimestampMillis})", LogArea.Login);
|
Logger.Warn(
|
||||||
return null;
|
$"Ticket isn't valid yet from {npTicket.Username} ({npTicket.IssueTime} > {(ulong)TimeHelper.TimestampMillis})",
|
||||||
}
|
LogArea.Login);
|
||||||
if ((ulong)TimeHelper.TimestampMillis > npTicket.ExpireDate)
|
|
||||||
{
|
|
||||||
Logger.Warn($"Ticket has expired from {npTicket.Username} ({(ulong)TimeHelper.TimestampMillis} > {npTicket.ExpireDate}", LogArea.Login);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (npTicket.titleId == null) throw new ArgumentNullException($"{nameof(npTicket)}.{nameof(npTicket.titleId)}");
|
if ((ulong)TimeHelper.TimestampMillis > npTicket.ExpireTime)
|
||||||
|
{
|
||||||
|
Logger.Warn(
|
||||||
|
$"Ticket has expired from {npTicket.Username} ({(ulong)TimeHelper.TimestampMillis} > {npTicket.ExpireTime}",
|
||||||
|
LogArea.Login);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (npTicket.TitleId == null)
|
||||||
|
throw new ArgumentNullException($"{nameof(npTicket)}.{nameof(npTicket.TitleId)}");
|
||||||
|
|
||||||
// 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[7..]; // Trim UP9000-
|
npTicket.TitleId = npTicket.TitleId[7..]; // Trim UP9000-
|
||||||
npTicket.titleId = npTicket.titleId[..^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);
|
||||||
|
|
||||||
npTicket.GameVersion = GameVersionHelper.FromTitleId(npTicket.titleId); // Finally, convert it to GameVersion
|
npTicket.GameVersion = GameVersionHelper.FromTitleId(npTicket.TitleId); // Finally, convert it to GameVersion
|
||||||
|
|
||||||
if (npTicket.GameVersion == GameVersion.Unknown)
|
if (npTicket.GameVersion == GameVersion.Unknown)
|
||||||
{
|
{
|
||||||
Logger.Warn($"Could not determine game version from title id {npTicket.titleId}", LogArea.Login);
|
Logger.Warn($"Could not determine game version from title id {npTicket.TitleId}", LogArea.Login);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -308,15 +169,16 @@ public class NPTicket
|
||||||
_ => Platform.Unknown,
|
_ => Platform.Unknown,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (npTicket.Platform == Platform.PS3 && npTicket.GameVersion == GameVersion.LittleBigPlanetVita) npTicket.Platform = Platform.Vita;
|
if (npTicket.GameVersion == GameVersion.LittleBigPlanetVita) npTicket.Platform = Platform.Vita;
|
||||||
|
|
||||||
if (npTicket.Platform == Platform.Unknown || (npTicket.Platform == Platform.UnitTest && !ServerStatics.IsUnitTesting))
|
if (npTicket.Platform == Platform.Unknown)
|
||||||
{
|
{
|
||||||
Logger.Warn($"Could not determine platform from IssuerId {npTicket.IssuerId} decimal", LogArea.Login);
|
Logger.Warn($"Could not determine platform from IssuerId {npTicket.IssuerId} decimal", LogArea.Login);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ServerConfiguration.Instance.Authentication.VerifyTickets && !npTicket.ValidateSignature())
|
if (ServerConfiguration.Instance.Authentication.VerifyTickets &&
|
||||||
|
!npTicket.SignatureVerifier.ValidateSignature())
|
||||||
{
|
{
|
||||||
Logger.Warn($"Failed to verify authenticity of ticket from user {npTicket.Username}", LogArea.Login);
|
Logger.Warn($"Failed to verify authenticity of ticket from user {npTicket.Username}", LogArea.Login);
|
||||||
return null;
|
return null;
|
||||||
|
@ -332,12 +194,17 @@ public class NPTicket
|
||||||
|
|
||||||
return npTicket;
|
return npTicket;
|
||||||
}
|
}
|
||||||
|
catch (TicketParseException e)
|
||||||
|
{
|
||||||
|
Logger.Error($"Parsing npTicket failed: {e.Message}", LogArea.Login);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
catch(NotImplementedException)
|
catch(NotImplementedException)
|
||||||
{
|
{
|
||||||
Logger.Error($"The ticket version {npTicket.ticketVersion} is not implemented yet.", LogArea.Login);
|
Logger.Error($"The ticket version {npTicket.TicketVersion} is not implemented yet.", LogArea.Login);
|
||||||
Logger.Error
|
Logger.Error
|
||||||
(
|
(
|
||||||
"Please let us know that this is a ticket version that is actually used on our issue tracker at https://github.com/LBPUnion/project-lighthouse/issues !",
|
"Please let us know that this is a ticket version that is actually used on our issue tracker at https://github.com/LBPUnion/ProjectLighthouse/issues !",
|
||||||
LogArea.Login
|
LogArea.Login
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
|
@ -347,7 +214,7 @@ public class NPTicket
|
||||||
Logger.Error("Failed to read npTicket!", LogArea.Login);
|
Logger.Error("Failed to read npTicket!", LogArea.Login);
|
||||||
Logger.Error("Either this is spam data, or the more likely that this is a bug.", LogArea.Login);
|
Logger.Error("Either this is spam data, or the more likely that this is a bug.", LogArea.Login);
|
||||||
Logger.Error
|
Logger.Error
|
||||||
("Please report the following exception to our issue tracker at https://github.com/LBPUnion/project-lighthouse/issues!", LogArea.Login);
|
("Please report the following exception to our issue tracker at https://github.com/LBPUnion/ProjectLighthouse/issues!", LogArea.Login);
|
||||||
Logger.Error(e.ToDetailedException(), LogArea.Login);
|
Logger.Error(e.ToDetailedException(), LogArea.Login);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
6
ProjectLighthouse/Tickets/Parser/ITicketParser.cs
Normal file
6
ProjectLighthouse/Tickets/Parser/ITicketParser.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Tickets.Parser;
|
||||||
|
|
||||||
|
public interface ITicketParser
|
||||||
|
{
|
||||||
|
public bool ParseTicket();
|
||||||
|
}
|
13
ProjectLighthouse/Tickets/Parser/TicketParseException.cs
Normal file
13
ProjectLighthouse/Tickets/Parser/TicketParseException.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Tickets.Parser;
|
||||||
|
|
||||||
|
public class TicketParseException : Exception
|
||||||
|
{
|
||||||
|
public TicketParseException(string message)
|
||||||
|
{
|
||||||
|
this.Message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Message { get; }
|
||||||
|
}
|
46
ProjectLighthouse/Tickets/Parser/TicketParser21.cs
Normal file
46
ProjectLighthouse/Tickets/Parser/TicketParser21.cs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Tickets.Parser;
|
||||||
|
|
||||||
|
public class TicketParser21 : ITicketParser
|
||||||
|
{
|
||||||
|
private readonly NPTicket ticket;
|
||||||
|
private readonly TicketReader reader;
|
||||||
|
|
||||||
|
public TicketParser21(NPTicket ticket, TicketReader reader)
|
||||||
|
{
|
||||||
|
this.ticket = ticket;
|
||||||
|
this.reader = reader;
|
||||||
|
}
|
||||||
|
public bool ParseTicket()
|
||||||
|
{
|
||||||
|
this.reader.ReadTicketString(); // serial id
|
||||||
|
|
||||||
|
this.ticket.IssuerId = this.reader.ReadTicketUInt32();
|
||||||
|
this.ticket.IssueTime = this.reader.ReadTicketUInt64();
|
||||||
|
this.ticket.ExpireTime = this.reader.ReadTicketUInt64();
|
||||||
|
|
||||||
|
this.ticket.UserId = this.reader.ReadTicketUInt64();
|
||||||
|
|
||||||
|
this.ticket.Username = this.reader.ReadTicketString();
|
||||||
|
|
||||||
|
this.reader.ReadTicketString(); // Country
|
||||||
|
this.reader.ReadTicketString(); // Domain
|
||||||
|
|
||||||
|
this.ticket.TitleId = this.reader.ReadTicketString();
|
||||||
|
|
||||||
|
this.reader.ReadTicketUInt32(); // status
|
||||||
|
|
||||||
|
this.reader.ReadTicketEmpty(); // padding
|
||||||
|
this.reader.ReadTicketEmpty();
|
||||||
|
|
||||||
|
SectionHeader footer = this.reader.ReadSectionHeader(); // footer header
|
||||||
|
if (footer.Type != SectionType.Footer)
|
||||||
|
{
|
||||||
|
throw new TicketParseException(@$"Unexpected ticket footer header: expected={SectionType.Footer}, actual={footer}");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ticket.TicketSignatureIdentifier = this.reader.ReadTicketBinary();
|
||||||
|
|
||||||
|
this.ticket.TicketSignature = this.reader.ReadTicketBinary();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
49
ProjectLighthouse/Tickets/Parser/TicketParser30.cs
Normal file
49
ProjectLighthouse/Tickets/Parser/TicketParser30.cs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Tickets.Parser;
|
||||||
|
|
||||||
|
public class TicketParser30 : ITicketParser
|
||||||
|
{
|
||||||
|
private readonly NPTicket ticket;
|
||||||
|
private readonly TicketReader reader;
|
||||||
|
|
||||||
|
public TicketParser30(NPTicket ticket, TicketReader reader)
|
||||||
|
{
|
||||||
|
this.ticket = ticket;
|
||||||
|
this.reader = reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ParseTicket()
|
||||||
|
{
|
||||||
|
this.reader.ReadTicketString(); // serial id
|
||||||
|
|
||||||
|
this.ticket.IssuerId = this.reader.ReadTicketUInt32();
|
||||||
|
this.ticket.IssueTime = this.reader.ReadTicketUInt64();
|
||||||
|
this.ticket.ExpireTime = this.reader.ReadTicketUInt64();
|
||||||
|
|
||||||
|
this.ticket.UserId = this.reader.ReadTicketUInt64();
|
||||||
|
|
||||||
|
this.ticket.Username = this.reader.ReadTicketString();
|
||||||
|
|
||||||
|
this.reader.ReadTicketString(); // Country
|
||||||
|
this.reader.ReadTicketString(); // Domain
|
||||||
|
|
||||||
|
this.ticket.TitleId = this.reader.ReadTicketString();
|
||||||
|
|
||||||
|
this.reader.ReadSectionHeader(); // date of birth section
|
||||||
|
this.reader.ReadBytes(4); // 4 bytes for year month and day
|
||||||
|
this.reader.ReadTicketUInt32();
|
||||||
|
|
||||||
|
this.reader.ReadSectionHeader(); // empty section?
|
||||||
|
this.reader.ReadTicketEmpty();
|
||||||
|
|
||||||
|
SectionHeader footer = this.reader.ReadSectionHeader(); // footer header
|
||||||
|
if (footer.Type != SectionType.Footer)
|
||||||
|
{
|
||||||
|
throw new TicketParseException(@$"Unexpected ticket footer header: expected={SectionType.Footer}, actual={footer}");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ticket.TicketSignatureIdentifier = this.reader.ReadTicketBinary();
|
||||||
|
|
||||||
|
this.ticket.TicketSignature = this.reader.ReadTicketBinary();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
11
ProjectLighthouse/Tickets/Signature/NullSignatureVerifier.cs
Normal file
11
ProjectLighthouse/Tickets/Signature/NullSignatureVerifier.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
using Org.BouncyCastle.Crypto;
|
||||||
|
using Org.BouncyCastle.Crypto.Parameters;
|
||||||
|
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Tickets.Signature;
|
||||||
|
|
||||||
|
public class NullSignatureVerifier : TicketSignatureVerifier
|
||||||
|
{
|
||||||
|
protected override ECPublicKeyParameters PublicKey => null;
|
||||||
|
protected override string HashAlgorithm => null;
|
||||||
|
protected override bool VerifySignature(ISigner signer) => false;
|
||||||
|
}
|
39
ProjectLighthouse/Tickets/Signature/PsnSignatureVerifier.cs
Normal file
39
ProjectLighthouse/Tickets/Signature/PsnSignatureVerifier.cs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
using System;
|
||||||
|
using Org.BouncyCastle.Asn1.X9;
|
||||||
|
using Org.BouncyCastle.Crypto;
|
||||||
|
using Org.BouncyCastle.Crypto.Parameters;
|
||||||
|
using Org.BouncyCastle.Math;
|
||||||
|
using Org.BouncyCastle.Math.EC;
|
||||||
|
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Tickets.Signature;
|
||||||
|
|
||||||
|
public class PsnSignatureVerifier : TicketSignatureVerifier
|
||||||
|
{
|
||||||
|
private static readonly Lazy<ECPublicKeyParameters> publicKeyParams = new(() =>
|
||||||
|
{
|
||||||
|
ECDomainParameters curve = FromX9EcParams(ECNamedCurveTable.GetByName("secp192r1"));
|
||||||
|
ECPoint publicKey = curve.Curve.CreatePoint(
|
||||||
|
new BigInteger("39c62d061d4ee35c5f3f7531de0af3cf918346526edac727", 16),
|
||||||
|
new BigInteger("a5d578b55113e612bf1878d4cc939d61a41318403b5bdf86", 16));
|
||||||
|
return new ECPublicKeyParameters(publicKey, curve);
|
||||||
|
});
|
||||||
|
|
||||||
|
protected override ECPublicKeyParameters PublicKey => publicKeyParams.Value;
|
||||||
|
protected override string HashAlgorithm => "SHA-1";
|
||||||
|
|
||||||
|
private readonly NPTicket ticket;
|
||||||
|
|
||||||
|
public PsnSignatureVerifier(NPTicket ticket)
|
||||||
|
{
|
||||||
|
this.ticket = ticket;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool VerifySignature(ISigner signer)
|
||||||
|
{
|
||||||
|
Span<byte> ticketBody = this.ticket.Data.AsSpan();
|
||||||
|
ticketBody = ticketBody[..ticketBody.IndexOf(this.ticket.TicketSignature)];
|
||||||
|
signer.BlockUpdate(ticketBody);
|
||||||
|
|
||||||
|
return signer.VerifySignature(TrimSignature(this.ticket.TicketSignature));
|
||||||
|
}
|
||||||
|
}
|
39
ProjectLighthouse/Tickets/Signature/RpcnSignatureVerifier.cs
Normal file
39
ProjectLighthouse/Tickets/Signature/RpcnSignatureVerifier.cs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
using System;
|
||||||
|
using Org.BouncyCastle.Asn1.X9;
|
||||||
|
using Org.BouncyCastle.Crypto;
|
||||||
|
using Org.BouncyCastle.Crypto.Parameters;
|
||||||
|
using Org.BouncyCastle.Math;
|
||||||
|
using Org.BouncyCastle.Math.EC;
|
||||||
|
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Tickets.Signature;
|
||||||
|
|
||||||
|
public class RpcnSignatureVerifier : TicketSignatureVerifier
|
||||||
|
{
|
||||||
|
private static readonly Lazy<ECPublicKeyParameters> publicKeyParams = new(() =>
|
||||||
|
{
|
||||||
|
ECDomainParameters curve = FromX9EcParams(ECNamedCurveTable.GetByName("secp224k1"));
|
||||||
|
ECPoint publicKey = curve.Curve.CreatePoint(
|
||||||
|
new BigInteger("b07bc0f0addb97657e9f389039e8d2b9c97dc2a31d3042e7d0479b93", 16),
|
||||||
|
new BigInteger("d81c42b0abdf6c42191a31e31f93342f8f033bd529c2c57fdb5a0a7d", 16));
|
||||||
|
return new ECPublicKeyParameters(publicKey, curve);
|
||||||
|
});
|
||||||
|
|
||||||
|
protected override ECPublicKeyParameters PublicKey => publicKeyParams.Value;
|
||||||
|
protected override string HashAlgorithm => "SHA-224";
|
||||||
|
|
||||||
|
private readonly NPTicket ticket;
|
||||||
|
|
||||||
|
public RpcnSignatureVerifier(NPTicket ticket)
|
||||||
|
{
|
||||||
|
this.ticket = ticket;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool VerifySignature(ISigner signer)
|
||||||
|
{
|
||||||
|
Span<byte> ticketBody = this.ticket.Data.AsSpan();
|
||||||
|
ticketBody = ticketBody.Slice(this.ticket.BodyHeader.Position, this.ticket.BodyHeader.Length + 4);
|
||||||
|
signer.BlockUpdate(ticketBody);
|
||||||
|
|
||||||
|
return signer.VerifySignature(TrimSignature(this.ticket.TicketSignature));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
using System.Linq;
|
||||||
|
using Org.BouncyCastle.Asn1;
|
||||||
|
using Org.BouncyCastle.Asn1.X9;
|
||||||
|
using Org.BouncyCastle.Crypto;
|
||||||
|
using Org.BouncyCastle.Crypto.Parameters;
|
||||||
|
using Org.BouncyCastle.Security;
|
||||||
|
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Tickets.Signature;
|
||||||
|
|
||||||
|
public abstract class TicketSignatureVerifier
|
||||||
|
{
|
||||||
|
protected abstract ECPublicKeyParameters PublicKey { get; }
|
||||||
|
protected abstract string HashAlgorithm { get; }
|
||||||
|
|
||||||
|
protected abstract bool VerifySignature(ISigner signer);
|
||||||
|
|
||||||
|
public bool ValidateSignature()
|
||||||
|
{
|
||||||
|
ISigner signer = SignerUtilities.GetSigner($"{this.HashAlgorithm}withECDSA");
|
||||||
|
signer.Init(false, this.PublicKey);
|
||||||
|
|
||||||
|
return this.VerifySignature(signer);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static ECDomainParameters FromX9EcParams(X9ECParameters param) =>
|
||||||
|
new(param.Curve, param.G, param.N, param.H, param.GetSeed());
|
||||||
|
|
||||||
|
// Sometimes psn signatures have one or two extra empty bytes
|
||||||
|
// This is slow but it's better than carelessly chopping 0's
|
||||||
|
private protected static byte[] TrimSignature(byte[] signature)
|
||||||
|
{
|
||||||
|
for (int i = 0; i <= 2; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Asn1Object.FromByteArray(signature);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
signature = signature.SkipLast(1).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
using System;
|
||||||
|
using LBPUnion.ProjectLighthouse.Configuration;
|
||||||
|
using Org.BouncyCastle.Asn1.X9;
|
||||||
|
using Org.BouncyCastle.Crypto;
|
||||||
|
using Org.BouncyCastle.Crypto.Parameters;
|
||||||
|
using Org.BouncyCastle.Math;
|
||||||
|
using Org.BouncyCastle.Math.EC;
|
||||||
|
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Tickets.Signature;
|
||||||
|
|
||||||
|
public class UnitTestSignatureVerifier : TicketSignatureVerifier
|
||||||
|
{
|
||||||
|
protected internal static readonly Lazy<ECPublicKeyParameters> PublicKeyParams = new(() =>
|
||||||
|
{
|
||||||
|
ECDomainParameters curve = FromX9EcParams(ECNamedCurveTable.GetByName("secp192r1"));
|
||||||
|
ECPoint publicKey = curve.Curve.CreatePoint(
|
||||||
|
new BigInteger("b6f3374bde4ec23a25e1508889e7d7e71870ba74daf8654f", 16),
|
||||||
|
new BigInteger("738de93dad0fffb5642045439afaaf8c6fda319a72d2a584", 16));
|
||||||
|
return new ECPublicKeyParameters(publicKey, curve);
|
||||||
|
});
|
||||||
|
|
||||||
|
protected override ECPublicKeyParameters PublicKey => PublicKeyParams.Value;
|
||||||
|
protected override string HashAlgorithm => "SHA-1";
|
||||||
|
|
||||||
|
private readonly NPTicket ticket;
|
||||||
|
|
||||||
|
public UnitTestSignatureVerifier(NPTicket ticket)
|
||||||
|
{
|
||||||
|
this.ticket = ticket;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool VerifySignature(ISigner signer)
|
||||||
|
{
|
||||||
|
// Don't allow tickets signed by the unit testing private key
|
||||||
|
if (!ServerStatics.IsUnitTesting) return false;
|
||||||
|
|
||||||
|
Span<byte> ticketBody = this.ticket.Data.AsSpan();
|
||||||
|
ticketBody = ticketBody[..ticketBody.IndexOf(this.ticket.TicketSignature)];
|
||||||
|
signer.BlockUpdate(ticketBody);
|
||||||
|
|
||||||
|
return signer.VerifySignature(TrimSignature(this.ticket.TicketSignature));
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using LBPUnion.ProjectLighthouse.Helpers;
|
using LBPUnion.ProjectLighthouse.Helpers;
|
||||||
|
using LBPUnion.ProjectLighthouse.Tickets.Signature;
|
||||||
using Org.BouncyCastle.Crypto;
|
using Org.BouncyCastle.Crypto;
|
||||||
using Org.BouncyCastle.Crypto.Parameters;
|
using Org.BouncyCastle.Crypto.Parameters;
|
||||||
using Org.BouncyCastle.Math;
|
using Org.BouncyCastle.Math;
|
||||||
|
@ -92,7 +93,7 @@ public class TicketBuilder
|
||||||
|
|
||||||
private static byte[] SignData(byte[] data)
|
private static byte[] SignData(byte[] data)
|
||||||
{
|
{
|
||||||
ECPrivateKeyParameters key = new(privateKey, NPTicket.Secp192R1);
|
ECPrivateKeyParameters key = new(privateKey, UnitTestSignatureVerifier.PublicKeyParams.Value.Parameters);
|
||||||
|
|
||||||
ISigner signer = SignerUtilities.GetSigner("SHA-1withECDSA");
|
ISigner signer = SignerUtilities.GetSigner("SHA-1withECDSA");
|
||||||
signer.Init(true, key);
|
signer.Init(true, key);
|
||||||
|
@ -126,7 +127,7 @@ public class TicketBuilder
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TicketBuilder SetIssuerId(ushort issuerId)
|
public TicketBuilder SetIssuerId(uint issuerId)
|
||||||
{
|
{
|
||||||
this.IssuerId = issuerId;
|
this.IssuerId = issuerId;
|
||||||
return this;
|
return this;
|
||||||
|
@ -155,5 +156,4 @@ public class TicketBuilder
|
||||||
this.Status = status;
|
this.Status = status;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -32,6 +32,7 @@ public struct SectionHeader
|
||||||
{
|
{
|
||||||
public SectionType Type;
|
public SectionType Type;
|
||||||
public ushort Length;
|
public ushort Length;
|
||||||
|
public int Position;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TicketReader : BinaryReader
|
public class TicketReader : BinaryReader
|
||||||
|
@ -39,7 +40,9 @@ public class TicketReader : BinaryReader
|
||||||
public TicketReader([NotNull] Stream input) : base(input)
|
public TicketReader([NotNull] Stream input) : base(input)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
public TicketVersion ReadTicketVersion() => new((byte)(this.ReadByte() >> 4), this.ReadByte());
|
public TicketVersion ReadTicketVersion() => (TicketVersion)this.ReadUInt16BE();
|
||||||
|
|
||||||
|
public void SkipBytes(long bytes) => this.BaseStream.Position += bytes;
|
||||||
|
|
||||||
public SectionHeader ReadSectionHeader()
|
public SectionHeader ReadSectionHeader()
|
||||||
{
|
{
|
||||||
|
@ -49,6 +52,7 @@ public class TicketReader : BinaryReader
|
||||||
{
|
{
|
||||||
Type = (SectionType)this.ReadByte(),
|
Type = (SectionType)this.ReadByte(),
|
||||||
Length = this.ReadUInt16BE(),
|
Length = this.ReadUInt16BE(),
|
||||||
|
Position = (int)(this.BaseStream.Position - 4),
|
||||||
};
|
};
|
||||||
|
|
||||||
return sectionHeader;
|
return sectionHeader;
|
||||||
|
|
|
@ -8,7 +8,7 @@ namespace LBPUnion.ProjectLighthouse.Tickets;
|
||||||
|
|
||||||
public abstract class TicketData
|
public abstract class TicketData
|
||||||
{
|
{
|
||||||
public void WriteHeader(BinaryWriter writer)
|
protected void WriteHeader(BinaryWriter writer)
|
||||||
{
|
{
|
||||||
byte[] id = new byte[2];
|
byte[] id = new byte[2];
|
||||||
byte[] len = new byte[2];
|
byte[] len = new byte[2];
|
||||||
|
@ -19,7 +19,7 @@ public abstract class TicketData
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void Write(BinaryWriter writer);
|
public abstract void Write(BinaryWriter writer);
|
||||||
public abstract short Id();
|
protected abstract short Id();
|
||||||
public abstract short Len();
|
public abstract short Len();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ public class EmptyData : TicketData
|
||||||
this.WriteHeader(writer);
|
this.WriteHeader(writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override short Id() => 0;
|
protected override short Id() => 0;
|
||||||
public override short Len() => 0;
|
public override short Len() => 0;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -54,7 +54,7 @@ public class UInt32Data : TicketData
|
||||||
writer.Write(data);
|
writer.Write(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override short Id() => 1;
|
protected override short Id() => 1;
|
||||||
public override short Len() => 4;
|
public override short Len() => 4;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -77,7 +77,7 @@ public class UInt64Data : TicketData
|
||||||
writer.Write(data);
|
writer.Write(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override short Id() => 2;
|
protected override short Id() => 2;
|
||||||
public override short Len() => 8;
|
public override short Len() => 8;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -103,7 +103,7 @@ public class StringData : TicketData
|
||||||
writer.Write(this.val);
|
writer.Write(this.val);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override short Id() => 4;
|
protected override short Id() => 4;
|
||||||
public override short Len() => (short)this.val.Length;
|
public override short Len() => (short)this.val.Length;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -126,7 +126,7 @@ public class TimestampData : TicketData
|
||||||
writer.Write(data);
|
writer.Write(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override short Id() => 7;
|
protected override short Id() => 7;
|
||||||
public override short Len() => 8;
|
public override short Len() => 8;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -147,7 +147,7 @@ public class BinaryData : TicketData
|
||||||
writer.Write(this.val);
|
writer.Write(this.val);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override short Id() => 8;
|
protected override short Id() => 8;
|
||||||
public override short Len() => (short)this.val.Length;
|
public override short Len() => (short)this.val.Length;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -173,7 +173,7 @@ public class BlobData : TicketData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override short Id() => (short)(0x3000 | this.id);
|
protected override short Id() => (short)(0x3000 | this.id);
|
||||||
public override short Len() => (short)this.data.Sum(d => d.Len() + 4);
|
public override short Len() => (short)this.data.Sum(d => d.Len() + 4);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
|
@ -1,17 +1,7 @@
|
||||||
namespace LBPUnion.ProjectLighthouse.Tickets;
|
namespace LBPUnion.ProjectLighthouse.Tickets;
|
||||||
|
|
||||||
public struct TicketVersion
|
public enum TicketVersion : ushort
|
||||||
{
|
{
|
||||||
public byte Major { get; set; }
|
V21 = 0x2101,
|
||||||
public byte Minor { get; set; }
|
V30 = 0x3100,
|
||||||
|
|
||||||
public TicketVersion(byte major, byte minor)
|
|
||||||
{
|
|
||||||
this.Major = major;
|
|
||||||
this.Minor = minor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => $"{this.Major}.{this.Minor}";
|
|
||||||
|
|
||||||
public static implicit operator string(TicketVersion v) => v.ToString();
|
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue