|
//------------------------------------------------------------------------------
// <copyright file="FormsAuthenticationTicketSerializer.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
namespace System.Web.Security {
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Web.Util;
// A helper class which can serialize / deserialize FormsAuthenticationTicket instances.
//
// MSRC 11838 / DevDiv #292994 (http://vstfdevdiv:8080/DevDiv2/web/wi.aspx?id=292994):
// We need to fix the format of the serialized FormsAuthenticationTicket to account for
// the fact that the string payloads can contain any arbitrary characters, including
// embedded nulls. In particular, because of that vulnerability, we must assume that *any*
// FormsAuthenticationTicket generated by a pre-patch system is potentially the result
// of a malicious action. This new serialized format was chosen because it guarantees
// a compatibility break between either old format and the new format: pre-patch systems
// will reject post-patch tickets as having an invalid format, and post-patch systems
// will also reject pre-patch tickets as having an invalid format.
/* Current (v1) ticket format
* ==========================
*
* Serialized ticket format version number: 1 byte
* FormsAuthenticationTicket.Version: 1 byte
* FormsAuthenticationTicket.IssueDateUtc: 8 bytes
* {spacer}: 1 byte
* FormsAuthenticationTicket.ExpirationUtc: 8 bytes
* FormsAuthenticationTicket.IsPersistent: 1 byte
* FormsAuthenticationTicket.Name: 1+ bytes (1+ length prefix, 0+ payload)
* FormsAuthenticationTicket.UserData: 1+ bytes (1+ length prefix, 0+ payload)
* FormsAuthenticationTicket.CookiePath: 1+ bytes (1+ length prefix, 0+ payload)
* {footer}: 1 byte
*/
internal static class FormsAuthenticationTicketSerializer {
private const byte CURRENT_TICKET_SERIALIZED_VERSION = 0x01;
// Resurrects a FormsAuthenticationTicket from its serialized blob representation.
// The input blob must be unsigned and unencrypted. This function returns null if
// the serialized ticket format is invalid. The caller must also verify that the
// ticket is still valid, as this method doesn't check expiration.
public static FormsAuthenticationTicket Deserialize(byte[] serializedTicket, int serializedTicketLength) {
try {
using (MemoryStream ticketBlobStream = new MemoryStream(serializedTicket)) {
using (SerializingBinaryReader ticketReader = new SerializingBinaryReader(ticketBlobStream)) {
// Step 1: Read the serialized format version number from the stream.
// Currently the only supported format is 0x01.
// LENGTH: 1 byte
byte serializedFormatVersion = ticketReader.ReadByte();
if (serializedFormatVersion != CURRENT_TICKET_SERIALIZED_VERSION) {
return null; // unexpected value
}
// Step 2: Read the ticket version number from the stream.
// LENGTH: 1 byte
int ticketVersion = ticketReader.ReadByte();
// Step 3: Read the ticket issue date from the stream.
// LENGTH: 8 bytes
long ticketIssueDateUtcTicks = ticketReader.ReadInt64();
DateTime ticketIssueDateUtc = new DateTime(ticketIssueDateUtcTicks, DateTimeKind.Utc);
DateTime ticketIssueDateLocal = ticketIssueDateUtc.ToLocalTime();
// Step 4: Read the spacer from the stream.
// LENGTH: 1 byte
byte spacer = ticketReader.ReadByte();
if (spacer != 0xfe) {
return null; // unexpected value
}
// Step 5: Read the ticket expiration date from the stream.
// LENGTH: 8 bytes
long ticketExpirationDateUtcTicks = ticketReader.ReadInt64();
DateTime ticketExpirationDateUtc = new DateTime(ticketExpirationDateUtcTicks, DateTimeKind.Utc);
DateTime ticketExpirationDateLocal = ticketExpirationDateUtc.ToLocalTime();
// Step 6: Read the ticket persistence field from the stream.
// LENGTH: 1 byte
byte ticketPersistenceFieldValue = ticketReader.ReadByte();
bool ticketIsPersistent;
switch (ticketPersistenceFieldValue) {
case 0:
ticketIsPersistent = false;
break;
case 1:
ticketIsPersistent = true;
break;
default:
return null; // unexpected value
}
// Step 7: Read the ticket username from the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
string ticketName = ticketReader.ReadBinaryString();
// Step 8: Read the ticket custom data from the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
string ticketUserData = ticketReader.ReadBinaryString();
// Step 9: Read the ticket cookie path from the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
string ticketCookiePath = ticketReader.ReadBinaryString();
// Step 10: Read the footer from the stream.
// LENGTH: 1 byte
byte footer = ticketReader.ReadByte();
if (footer != 0xff) {
return null; // unexpected value
}
// Step 11: Verify that we have consumed the entire payload.
// We don't expect there to be any more information after the footer.
// The caller is responsible for telling us when the actual payload
// is finished, as he may have handed us a byte array that contains
// the payload plus signature as an optimization, and we don't want
// to misinterpet the signature as a continuation of the payload.
if (ticketBlobStream.Position != serializedTicketLength) {
return null;
}
// Success.
return FormsAuthenticationTicket.FromUtc(
ticketVersion /* version */,
ticketName /* name */,
ticketIssueDateUtc /* issueDateUtc */,
ticketExpirationDateUtc /* expirationUtc */,
ticketIsPersistent /* isPersistent */,
ticketUserData /* userData */,
ticketCookiePath /* cookiePath */);
}
}
}
catch {
// If anything goes wrong while parsing the token, just treat the token as invalid.
return null;
}
}
// Turns a FormsAuthenticationTicket into a serialized blob.
// The resulting blob is not encrypted or signed.
public static byte[] Serialize(FormsAuthenticationTicket ticket) {
using (MemoryStream ticketBlobStream = new MemoryStream()) {
using (SerializingBinaryWriter ticketWriter = new SerializingBinaryWriter(ticketBlobStream)) {
// SECURITY NOTE:
// Earlier versions of the serializer (Framework20 / Framework40) wrote out a
// random 8-byte header as the first part of the payload. This random header
// was used as an IV when the ticket was encrypted, since the early encryption
// routines didn't automatically append an IV when encrypting data. However,
// the MSRC 10405 (Pythia) patch causes all of our crypto routines to use an
// IV automatically, so there's no need for us to include a random IV in the
// serialized stream any longer. We can just write out only the data, and the
// crypto routines will do the right thing.
// Step 1: Write the ticket serialized format version number (currently 0x01) to the stream.
// LENGTH: 1 byte
ticketWriter.Write(CURRENT_TICKET_SERIALIZED_VERSION);
// Step 2: Write the ticket version number to the stream.
// This is the developer-specified FormsAuthenticationTicket.Version property,
// which is just ticket metadata. Technically it should be stored as a 32-bit
// integer instead of just a byte, but we have historically been storing it
// as just a single byte forever and nobody has complained.
// LENGTH: 1 byte
ticketWriter.Write((byte)ticket.Version);
// Step 3: Write the ticket issue date to the stream.
// We store this value as UTC ticks. We can't use DateTime.ToBinary() since it
// isn't compatible with .NET v1.1.
// LENGTH: 8 bytes (64-bit little-endian in payload)
ticketWriter.Write(ticket.IssueDateUtc.Ticks);
// Step 4: Write a one-byte spacer (0xfe) to the stream.
// One of the old ticket formats (Framework40) expects the unencrypted payload
// to contain 0x000000 (3 null bytes) beginning at position 9 in the stream.
// Since we're currently at offset 10 in the serialized stream, we can take
// this opportunity to purposely inject a non-null byte at this offset, which
// intentionally breaks compatibility with Framework40 mode.
// LENGTH: 1 byte
Debug.Assert(ticketBlobStream.Position == 10, "Critical that we be at position 10 in the stream at this point.");
ticketWriter.Write((byte)0xfe);
// Step 5: Write the ticket expiration date to the stream.
// We store this value as UTC ticks.
// LENGTH: 8 bytes (64-bit little endian in payload)
ticketWriter.Write(ticket.ExpirationUtc.Ticks);
// Step 6: Write the ticket persistence field to the stream.
// LENGTH: 1 byte
ticketWriter.Write(ticket.IsPersistent);
// Step 7: Write the ticket username to the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
ticketWriter.WriteBinaryString(ticket.Name);
// Step 8: Write the ticket custom data to the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
ticketWriter.WriteBinaryString(ticket.UserData);
// Step 9: Write the ticket cookie path to the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
ticketWriter.WriteBinaryString(ticket.CookiePath);
// Step 10: Write a one-byte footer (0xff) to the stream.
// One of the old FormsAuthenticationTicket formats (Framework20) requires
// that the payload end in 0x0000 (U+0000). By making the very last byte
// of this format non-null, we can guarantee a compatiblity break between
// this format and Framework20.
// LENGTH: 1 byte
ticketWriter.Write((byte)0xff);
// Finished.
return ticketBlobStream.ToArray();
}
}
}
// see comments on SerializingBinaryWriter
private sealed class SerializingBinaryReader : BinaryReader {
public SerializingBinaryReader(Stream input)
: base(input) {
}
public string ReadBinaryString() {
int charCount = Read7BitEncodedInt();
byte[] bytes = ReadBytes(charCount * 2);
char[] chars = new char[charCount];
for (int i = 0; i < chars.Length; i++) {
chars[i] = (char)(bytes[2 * i] | (bytes[2 * i + 1] << 8));
}
return new String(chars);
}
public override string ReadString() {
// should never call this method since it will produce wrong results
throw new NotImplementedException();
}
}
// This is a special BinaryWriter which serializes strings in a way that is
// entirely round-trippable. For example, the string "\ud800" is a valid .NET
// Framework string, but since U+D800 is an unpaired Unicode surrogate the
// built-in Encoding types will not round-trip it. Strings are serialized as a
// 7-bit character count (not byte count!) followed by a UTF-16LE payload.
private sealed class SerializingBinaryWriter : BinaryWriter {
public SerializingBinaryWriter(Stream output)
: base(output) {
}
public override void Write(string value) {
// should never call this method since it will produce wrong results
throw new NotImplementedException();
}
public void WriteBinaryString(string value) {
byte[] bytes = new byte[value.Length * 2];
for (int i = 0; i < value.Length; i++) {
char c = value[i];
bytes[2 * i] = (byte)c;
bytes[2 * i + 1] = (byte)(c >> 8);
}
Write7BitEncodedInt(value.Length);
Write(bytes);
}
}
}
}
|