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:
Josh 2023-06-07 00:16:29 -05:00 committed by GitHub
parent 8b0aed9a61
commit 2f11731a8e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 506 additions and 247 deletions

57
Documentation/Tickets.md Normal file
View 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
```

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

View file

@ -1,21 +1,14 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Tickets.Parser;
using LBPUnion.ProjectLighthouse.Tickets.Signature;
using LBPUnion.ProjectLighthouse.Types.Logging;
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
using System.Text.Json;
#endif
@ -27,224 +20,78 @@ namespace LBPUnion.ProjectLighthouse.Tickets;
/// </summary>
public class NPTicket
{
public string? Username { get; set; }
public string Username { get; protected internal set; } = "";
private TicketVersion? ticketVersion { get; set; }
public Platform Platform { get; set; }
public Platform Platform { get; private set; }
public GameVersion GameVersion { get; private set; }
public string TicketHash { get; private set; } = "";
public uint IssuerId { get; set; }
public ulong IssuedDate { get; set; }
public ulong ExpireDate { get; set; }
public ulong IssueTime { get; set; }
public ulong ExpireTime { 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 byte[] ticketSignatureIdentifier { get; set; } = Array.Empty<byte>();
private TicketSignatureVerifier SignatureVerifier { get; set; } = new NullSignatureVerifier();
public GameVersion GameVersion { get; set; }
protected internal SectionHeader BodyHeader { get; set; }
private static ECDomainParameters FromX9EcParams(X9ECParameters param) =>
new(param.Curve, param.G, param.N, param.H, param.GetSeed());
protected internal byte[] Data { get; set; } = Array.Empty<byte>();
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"));
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
private static bool ReadTicket(NPTicket npTicket, TicketReader reader)
{
public string HashAlgo { get; set; }
public ECPoint PublicKey { get; set; }
public ECDomainParameters CurveParams { get; set; }
npTicket.TicketVersion = reader.ReadTicketVersion();
public SignatureParams(string hashAlgo, ECPoint pubKey, ECDomainParameters curve)
{
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
reader.SkipBytes(4); // Skip header
ushort ticketLen = reader.ReadUInt16BE();
// 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);
return false;
throw new TicketParseException(
@$"Ticket length mismatch, expected={ticketLen}, actual={npTicket.Data.Length - 0x8}");
}
long bodyStart = reader.BaseStream.Position;
SectionHeader bodyHeader = reader.ReadSectionHeader();
npTicket.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);
return false;
throw new TicketParseException(
@$"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
"3.0" => Read30Ticket(npTicket, reader), // used by ps vita
Tickets.TicketVersion.V21 => new TicketParser21(npTicket, reader), // used by ps3 and rpcs3
Tickets.TicketVersion.V30 => new TicketParser30(npTicket, reader), // used by ps vita
_ => 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
"5250434E" => data.AsSpan().Slice((int)bodyStart, bodyHeader.Length + 4).ToArray(),
// psn and unit test
"719F1D4A" or "54455354" => data.AsSpan()[..data.AsSpan().IndexOf(npTicket.ticketSignature)].ToArray(),
_ => throw new ArgumentOutOfRangeException(nameof(npTicket)),
0x5250434E => new RpcnSignatureVerifier(npTicket),
0x719F1D4A => new PsnSignatureVerifier(npTicket),
0x54455354 => new UnitTestSignatureVerifier(npTicket),
_ => throw new ArgumentOutOfRangeException(nameof(npTicket),
signatureIdentifier,
@"Invalid signature identifier"),
};
return true;
@ -255,46 +102,60 @@ public class NPTicket
/// </summary>
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
{
using MemoryStream ms = new(data);
using TicketReader reader = new(ms);
using TicketReader reader = new(new MemoryStream(data));
bool validTicket = ReadTicket(data, npTicket, reader);
bool validTicket = ReadTicket(npTicket, reader);
if (!validTicket)
{
Logger.Warn($"Failed to parse ticket from {npTicket.Username}", LogArea.Login);
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);
return null;
}
if ((ulong)TimeHelper.TimestampMillis > npTicket.ExpireDate)
{
Logger.Warn($"Ticket has expired from {npTicket.Username} ({(ulong)TimeHelper.TimestampMillis} > {npTicket.ExpireDate}", LogArea.Login);
Logger.Warn(
$"Ticket isn't valid yet from {npTicket.Username} ({npTicket.IssueTime} > {(ulong)TimeHelper.TimestampMillis})",
LogArea.Login);
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.
// Current data: UP9000-BCUS98245_00
// We need to chop this to get the titleId we're looking for
npTicket.titleId = npTicket.titleId[7..]; // Trim UP9000-
npTicket.titleId = npTicket.titleId[..^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);
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)
{
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;
}
@ -308,15 +169,16 @@ public class NPTicket
_ => 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);
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);
return null;
@ -332,12 +194,17 @@ public class NPTicket
return npTicket;
}
catch (TicketParseException e)
{
Logger.Error($"Parsing npTicket failed: {e.Message}", LogArea.Login);
return null;
}
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
(
"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
);
return null;
@ -347,7 +214,7 @@ public class NPTicket
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
("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);
return null;
}

View file

@ -0,0 +1,6 @@
namespace LBPUnion.ProjectLighthouse.Tickets.Parser;
public interface ITicketParser
{
public bool ParseTicket();
}

View 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; }
}

View 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;
}
}

View 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;
}
}

View 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;
}

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

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

View file

@ -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;
}
}

View file

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

View file

@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Text;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Tickets.Signature;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
@ -92,7 +93,7 @@ public class TicketBuilder
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");
signer.Init(true, key);
@ -126,7 +127,7 @@ public class TicketBuilder
return this;
}
public TicketBuilder SetIssuerId(ushort issuerId)
public TicketBuilder SetIssuerId(uint issuerId)
{
this.IssuerId = issuerId;
return this;
@ -155,5 +156,4 @@ public class TicketBuilder
this.Status = status;
return this;
}
}

View file

@ -32,6 +32,7 @@ public struct SectionHeader
{
public SectionType Type;
public ushort Length;
public int Position;
}
public class TicketReader : BinaryReader
@ -39,7 +40,9 @@ public class TicketReader : BinaryReader
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()
{
@ -49,6 +52,7 @@ public class TicketReader : BinaryReader
{
Type = (SectionType)this.ReadByte(),
Length = this.ReadUInt16BE(),
Position = (int)(this.BaseStream.Position - 4),
};
return sectionHeader;

View file

@ -8,7 +8,7 @@ namespace LBPUnion.ProjectLighthouse.Tickets;
public abstract class TicketData
{
public void WriteHeader(BinaryWriter writer)
protected void WriteHeader(BinaryWriter writer)
{
byte[] id = new byte[2];
byte[] len = new byte[2];
@ -19,7 +19,7 @@ public abstract class TicketData
}
public abstract void Write(BinaryWriter writer);
public abstract short Id();
protected abstract short Id();
public abstract short Len();
}
@ -31,7 +31,7 @@ public class EmptyData : TicketData
this.WriteHeader(writer);
}
public override short Id() => 0;
protected override short Id() => 0;
public override short Len() => 0;
}
#endregion
@ -54,7 +54,7 @@ public class UInt32Data : TicketData
writer.Write(data);
}
public override short Id() => 1;
protected override short Id() => 1;
public override short Len() => 4;
}
#endregion
@ -77,7 +77,7 @@ public class UInt64Data : TicketData
writer.Write(data);
}
public override short Id() => 2;
protected override short Id() => 2;
public override short Len() => 8;
}
#endregion
@ -103,7 +103,7 @@ public class StringData : TicketData
writer.Write(this.val);
}
public override short Id() => 4;
protected override short Id() => 4;
public override short Len() => (short)this.val.Length;
}
#endregion
@ -126,7 +126,7 @@ public class TimestampData : TicketData
writer.Write(data);
}
public override short Id() => 7;
protected override short Id() => 7;
public override short Len() => 8;
}
#endregion
@ -147,7 +147,7 @@ public class BinaryData : TicketData
writer.Write(this.val);
}
public override short Id() => 8;
protected override short Id() => 8;
public override short Len() => (short)this.val.Length;
}
#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);
}
#endregion

View file

@ -1,17 +1,7 @@
namespace LBPUnion.ProjectLighthouse.Tickets;
public struct TicketVersion
public enum TicketVersion : ushort
{
public byte Major { get; set; }
public byte Minor { get; set; }
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();
V21 = 0x2101,
V30 = 0x3100,
}