using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Security.Cryptography;
using System.Globalization;
using System.Diagnostics;
namespace DotNetOpenId.RelyingParty {
///
/// A state-containing bit of non-confidential data that is sent to the
/// user agent as part of the return_to URL so we can read from it later.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design",
"CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "CryptoStream is not stored in a field.")]
[DebuggerDisplay("Nonce: {Nonce}, Endpoint: {Endpoint.ClaimedIdentifier}")]
class Token {
public static readonly string TokenKey = "token";
///
/// This nonce will only be used if the provider is pre-2.0.
///
public Nonce Nonce { get; set; }
public ServiceEndpoint Endpoint { get; private set; }
public Token(ServiceEndpoint provider)
: this(new Nonce(), provider) {
}
Token(Nonce nonce, ServiceEndpoint provider) {
Nonce = nonce;
Endpoint = provider;
}
///
/// Serializes this instance as a string that can be
/// included as part of a return_to variable in a querystring.
/// This string is cryptographically signed to protect against tampering.
///
public string Serialize(INonceStore store) {
using (MemoryStream dataStream = new MemoryStream()) {
if (!persistSignature(store)) {
Debug.Assert(!persistNonce(Endpoint, store), "Without a signature, a nonce is meaningless.");
dataStream.WriteByte(0); // there will be NO signature.
StreamWriter writer = new StreamWriter(dataStream);
Endpoint.Serialize(writer);
writer.Flush();
return Convert.ToBase64String(dataStream.ToArray());
} else {
using (HashAlgorithm shaHash = createHashAlgorithm(store))
using (CryptoStream shaStream = new CryptoStream(dataStream, shaHash, CryptoStreamMode.Write)) {
StreamWriter writer = new StreamWriter(shaStream);
Endpoint.Serialize(writer);
if (persistNonce(Endpoint, store))
writer.WriteLine(Nonce.Code);
writer.Flush();
shaStream.Flush();
shaStream.FlushFinalBlock();
byte[] hash = shaHash.Hash;
byte[] data = new byte[1 + hash.Length + dataStream.Length];
data[0] = 1; // there is a signature
Buffer.BlockCopy(hash, 0, data, 1, hash.Length);
Buffer.BlockCopy(dataStream.ToArray(), 0, data, 1 + hash.Length, (int)dataStream.Length);
return Convert.ToBase64String(data);
}
}
}
}
///
/// Deserializes a token returned to us from the provider and verifies its integrity.
///
///
/// As part of deserialization, the signature is verified to check
/// for tampering, and the nonce (if included by the RP) is also checked.
/// If no signature is present (due to stateless mode), the endpoint is verified
/// by discovery (slow but secure).
///
public static Token Deserialize(string token, INonceStore store) {
byte[] tok;
try {
tok = Convert.FromBase64String(token);
} catch (FormatException ex) {
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.ExpectedBase64OpenIdQueryParameter, token), null, ex);
}
if (tok.Length < 1) throw new OpenIdException(Strings.InvalidSignature);
bool signaturePresent = tok[0] == 1;
bool signatureVerified = false;
MemoryStream dataStream;
if (signaturePresent) {
if (persistSignature(store)) {
// Verify the signature to guarantee that our state hasn't been
// tampered with in transit or on the provider.
HashAlgorithm hmac = createHashAlgorithm(store);
int signatureLength = hmac.HashSize / 8;
dataStream = new MemoryStream(tok, 1 + signatureLength, tok.Length - 1 - signatureLength);
byte[] newSig = hmac.ComputeHash(dataStream);
dataStream.Position = 0;
if (tok.Length - 1 < newSig.Length)
throw new OpenIdException(Strings.InvalidSignature);
for (int i = 0; i < newSig.Length; i++)
if (tok[i + 1] != newSig[i])
throw new OpenIdException(Strings.InvalidSignature);
signatureVerified = true;
} else {
// Oops, we have no application state, so we have no way of validating the signature.
throw new OpenIdException(Strings.InconsistentAppState);
}
} else {
dataStream = new MemoryStream(tok, 1, tok.Length - 1);
}
StreamReader reader = new StreamReader(dataStream);
ServiceEndpoint endpoint = ServiceEndpoint.Deserialize(reader);
Nonce nonce = null;
if (signatureVerified && persistNonce(endpoint, store)) {
nonce = new Nonce(reader.ReadLine(), false);
nonce.Consume(store);
}
if (!signatureVerified) {
verifyEndpointByDiscovery(endpoint);
}
return new Token(nonce, endpoint);
}
static HashAlgorithm createHashAlgorithm(INonceStore store) {
return new HMACSHA256(store.SecretSigningKey);
}
///
/// Whether a relying party-side nonce should be used to protect
/// against replay attacks.
///
///
/// When communicating with an OP using OpenID 2.0, the provider takes
/// care of the nonce, so we don't have to.
///
/// If operating under stateless mode, nonces can't be used on the RP
/// side, so we rely on the Provider to be using some nonce mechanism.
/// In OpenID 2.0, this is guaranteed, but in 1.x it's just an
/// assumption, which allows for replay attacks if the assumption is false.
///
static bool persistNonce(ServiceEndpoint endpoint, INonceStore store) {
return endpoint.Protocol.Version.Major < 2 && persistSignature(store);
}
///
/// Whether to sign a token.
///
///
/// If an application store exists, we should sign the token. If it doesn't,
/// we haven't any means to keep a secret, so we can't sign the token.
///
static bool persistSignature(INonceStore store) {
return store != null;
}
///
/// Performs discovery on the information in the token to detect any tampering.
///
///
/// Manual re-discovery of a Claimed Identifier is the slow way to perform verification.
/// The best way is to check a signature on a deserialized token. That is the primary method,
/// but when stateless mode is used and no place exists to store a secret for signature
/// verification, this is the only alternative.
///
static void verifyEndpointByDiscovery(ServiceEndpoint endpoint) {
// If the user entered an OP Identifier then the ClaimedIdentifier will be the special
// identifier that we can't perform discovery on. We need to be careful about that.
Identifier identifierToDiscover;
if (endpoint.ClaimedIdentifier == endpoint.Protocol.ClaimedIdentifierForOPIdentifier) {
identifierToDiscover = endpoint.UserSuppliedIdentifier;
} else {
identifierToDiscover = endpoint.ClaimedIdentifier;
}
var discoveredEndpoints = new List(identifierToDiscover.Discover());
if (!discoveredEndpoints.Contains(endpoint)) {
throw new OpenIdException(Strings.InvalidSignature);
}
}
}
}