/// <summary> /// Downloads the XRDS document for this XRI. /// </summary> /// <param name="identifier">The identifier.</param> /// <param name="requestHandler">The request handler.</param> /// <returns>The XRDS document.</returns> private static XrdsDocument DownloadXrds(XriIdentifier identifier, IDirectWebRequestHandler requestHandler) { Requires.NotNull(identifier, "identifier"); Requires.NotNull(requestHandler, "requestHandler"); XrdsDocument doc; using (var xrdsResponse = Yadis.Request(requestHandler, GetXrdsUrl(identifier), identifier.IsDiscoverySecureEndToEnd)) { var readerSettings = MessagingUtilities.CreateUntrustedXmlReaderSettings(); doc = new XrdsDocument(XmlReader.Create(xrdsResponse.ResponseStream, readerSettings)); } ErrorUtilities.VerifyProtocol(doc.IsXrdResolutionSuccessful, OpenIdStrings.XriResolutionFailed); return doc; }
/// <summary> /// Downloads the XRDS document for this XRI. /// </summary> /// <param name="identifier">The identifier.</param> /// <param name="requestHandler">The request handler.</param> /// <returns>The XRDS document.</returns> private static XrdsDocument DownloadXrds(XriIdentifier identifier, IDirectWebRequestHandler requestHandler) { Requires.NotNull(identifier, "identifier"); Requires.NotNull(requestHandler, "requestHandler"); Contract.Ensures(Contract.Result<XrdsDocument>() != null); XrdsDocument doc; using (var xrdsResponse = Yadis.Request(requestHandler, GetXrdsUrl(identifier), identifier.IsDiscoverySecureEndToEnd)) { doc = new XrdsDocument(XmlReader.Create(xrdsResponse.ResponseStream)); } ErrorUtilities.VerifyProtocol(doc.IsXrdResolutionSuccessful, OpenIdStrings.XriResolutionFailed); return doc; }
/// <summary> /// Performs discovery on the specified identifier. /// </summary> /// <param name="identifier">The identifier to perform discovery on.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns> /// A sequence of service endpoints yielded by discovery. Must not be null, but may be empty. /// </returns> public async Task<IdentifierDiscoveryServiceResult> DiscoverAsync(Identifier identifier, CancellationToken cancellationToken) { Requires.NotNull(identifier, "identifier"); Verify.Operation(this.HostFactories != null, Strings.HostFactoriesRequired); cancellationToken.ThrowIfCancellationRequested(); var uriIdentifier = identifier as UriIdentifier; if (uriIdentifier == null) { return new IdentifierDiscoveryServiceResult(Enumerable.Empty<IdentifierDiscoveryResult>()); } var endpoints = new List<IdentifierDiscoveryResult>(); // Attempt YADIS discovery DiscoveryResult yadisResult = await Yadis.DiscoverAsync(this.HostFactories, uriIdentifier, identifier.IsDiscoverySecureEndToEnd, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); if (yadisResult != null) { if (yadisResult.IsXrds) { try { XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText); var xrdsEndpoints = xrds.XrdElements.CreateServiceEndpoints(yadisResult.NormalizedUri, uriIdentifier); // Filter out insecure endpoints if high security is required. if (uriIdentifier.IsDiscoverySecureEndToEnd) { xrdsEndpoints = xrdsEndpoints.Where(se => se.ProviderEndpoint.IsTransportSecure()); } endpoints.AddRange(xrdsEndpoints); } catch (XmlException ex) { Logger.Yadis.Error("Error while parsing the XRDS document. Falling back to HTML discovery.", ex); } } // Failing YADIS discovery of an XRDS document, we try HTML discovery. if (endpoints.Count == 0) { await yadisResult.TryRevertToHtmlResponseAsync(); var htmlEndpoints = new List<IdentifierDiscoveryResult>(DiscoverFromHtml(yadisResult.NormalizedUri, uriIdentifier, yadisResult.ResponseText)); if (htmlEndpoints.Any()) { Logger.Yadis.DebugFormat("Total services discovered in HTML: {0}", htmlEndpoints.Count); Logger.Yadis.Debug(htmlEndpoints.ToStringDeferred(true)); endpoints.AddRange(htmlEndpoints.Where(ep => !uriIdentifier.IsDiscoverySecureEndToEnd || ep.ProviderEndpoint.IsTransportSecure())); if (endpoints.Count == 0) { Logger.Yadis.Info("No HTML discovered endpoints met the security requirements."); } } else { Logger.Yadis.Debug("HTML discovery failed to find any endpoints."); } } else { Logger.Yadis.Debug("Skipping HTML discovery because XRDS contained service endpoints."); } } return new IdentifierDiscoveryServiceResult(endpoints); }
/// <summary> /// Performs discovery on the specified identifier. /// </summary> /// <param name="identifier">The identifier to perform discovery on.</param> /// <param name="requestHandler">The means to place outgoing HTTP requests.</param> /// <param name="abortDiscoveryChain">if set to <c>true</c>, no further discovery services will be called for this identifier.</param> /// <returns> /// A sequence of service endpoints yielded by discovery. Must not be null, but may be empty. /// </returns> public IEnumerable<IdentifierDiscoveryResult> Discover(Identifier identifier, IDirectWebRequestHandler requestHandler, out bool abortDiscoveryChain) { abortDiscoveryChain = false; var uriIdentifier = identifier as UriIdentifier; if (uriIdentifier == null) { return Enumerable.Empty<IdentifierDiscoveryResult>(); } var endpoints = new List<IdentifierDiscoveryResult>(); // Attempt YADIS discovery DiscoveryResult yadisResult = Yadis.Discover(requestHandler, uriIdentifier, identifier.IsDiscoverySecureEndToEnd); if (yadisResult != null) { if (yadisResult.IsXrds) { try { XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText); var xrdsEndpoints = xrds.XrdElements.CreateServiceEndpoints(yadisResult.NormalizedUri, uriIdentifier); // Filter out insecure endpoints if high security is required. if (uriIdentifier.IsDiscoverySecureEndToEnd) { xrdsEndpoints = xrdsEndpoints.Where(se => se.ProviderEndpoint.IsTransportSecure()); } endpoints.AddRange(xrdsEndpoints); } catch (XmlException ex) { Logger.Yadis.Error("Error while parsing the XRDS document. Falling back to HTML discovery.", ex); } } // Failing YADIS discovery of an XRDS document, we try HTML discovery. if (endpoints.Count == 0) { yadisResult.TryRevertToHtmlResponse(); var htmlEndpoints = new List<IdentifierDiscoveryResult>(DiscoverFromHtml(yadisResult.NormalizedUri, uriIdentifier, yadisResult.ResponseText)); if (htmlEndpoints.Any()) { Logger.Yadis.DebugFormat("Total services discovered in HTML: {0}", htmlEndpoints.Count); Logger.Yadis.Debug(htmlEndpoints.ToStringDeferred(true)); endpoints.AddRange(htmlEndpoints.Where(ep => !uriIdentifier.IsDiscoverySecureEndToEnd || ep.ProviderEndpoint.IsTransportSecure())); if (endpoints.Count == 0) { Logger.Yadis.Info("No HTML discovered endpoints met the security requirements."); } } else { Logger.Yadis.Debug("HTML discovery failed to find any endpoints."); } } else { Logger.Yadis.Debug("Skipping HTML discovery because XRDS contained service endpoints."); } } return endpoints; }
/// <summary> /// Validates the XML digital signature on an XRDS document. /// </summary> /// <param name="document">The XRDS document whose signature should be validated.</param> /// <param name="identifier">The identifier under discovery.</param> /// <param name="response">The response.</param> /// <param name="signingHost">The host name on the certificate that should be used to verify the signature in the XRDS.</param> /// <exception cref="ProtocolException">Thrown if the XRDS document has an invalid or a missing signature.</exception> private static void ValidateXmlDSig(XrdsDocument document, UriIdentifier identifier, IncomingWebResponse response, string signingHost) { Contract.Requires<ArgumentNullException>(document != null); Contract.Requires<ArgumentNullException>(identifier != null); Contract.Requires<ArgumentNullException>(response != null); var signatureNode = document.Node.SelectSingleNode("/xrds:XRDS/ds:Signature", document.XmlNamespaceResolver); ErrorUtilities.VerifyProtocol(signatureNode != null, OpenIdStrings.MissingElement, "Signature"); var signedInfoNode = signatureNode.SelectSingleNode("ds:SignedInfo", document.XmlNamespaceResolver); ErrorUtilities.VerifyProtocol(signedInfoNode != null, OpenIdStrings.MissingElement, "SignedInfo"); ErrorUtilities.VerifyProtocol( signedInfoNode.SelectSingleNode("ds:CanonicalizationMethod[@Algorithm='http://docs.oasis-open.org/xri/xrd/2009/01#canonicalize-raw-octets']", document.XmlNamespaceResolver) != null, "Unrecognized or missing canonicalization method."); ErrorUtilities.VerifyProtocol( signedInfoNode.SelectSingleNode("ds:SignatureMethod[@Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1']", document.XmlNamespaceResolver) != null, "Unrecognized or missing signature method."); var certNodes = signatureNode.Select("ds:KeyInfo/ds:X509Data/ds:X509Certificate", document.XmlNamespaceResolver); ErrorUtilities.VerifyProtocol(certNodes.Count > 0, OpenIdStrings.MissingElement, "X509Certificate"); var certs = certNodes.Cast<XPathNavigator>().Select(n => new X509Certificate2(Convert.FromBase64String(n.Value.Trim()))).ToList(); // Verify that we trust the signer of the certificates. // Start by trying to validate just the certificate used to sign the XRDS document, // since we can do that with partial trust. Logger.OpenId.Debug("Verifying that we trust the certificate used to sign the discovery document."); if (!certs[0].Verify()) { // We couldn't verify just the signing certificate, so try to verify the whole certificate chain. try { Logger.OpenId.Debug("Verifying the whole certificate chain."); VerifyCertChain(certs); Logger.OpenId.Debug("Certificate chain verified."); } catch (SecurityException) { Logger.Yadis.Warn("Signing certificate verification failed and we have insufficient code access security permissions to perform certificate chain validation."); ErrorUtilities.ThrowProtocol(OpenIdStrings.X509CertificateNotTrusted); } } // Verify that the certificate is issued to the host on whom we are performing discovery. string hostName = certs[0].GetNameInfo(X509NameType.DnsName, false); ErrorUtilities.VerifyProtocol(string.Equals(hostName, signingHost, StringComparison.OrdinalIgnoreCase), "X.509 signing certificate issued to {0}, but a certificate for {1} was expected.", hostName, signingHost); // Verify the signature itself byte[] signature = Convert.FromBase64String(response.Headers["Signature"]); var provider = (RSACryptoServiceProvider)certs.First().PublicKey.Key; byte[] data = new byte[response.ResponseStream.Length]; response.ResponseStream.Seek(0, SeekOrigin.Begin); response.ResponseStream.Read(data, 0, data.Length); ErrorUtilities.VerifyProtocol(provider.VerifyData(data, "SHA1", signature), "Invalid XmlDSig signature on XRDS document."); }
/// <summary> /// Gets the services for an identifier that are described by an external XRDS document. /// </summary> /// <param name="xrds">The XRD elements to search for described-by services.</param> /// <param name="identifier">The identifier under discovery.</param> /// <param name="requestHandler">The request handler.</param> /// <returns>The discovered services.</returns> private static IEnumerable<IdentifierDiscoveryResult> GetExternalServices(IEnumerable<XrdElement> xrds, UriIdentifier identifier, IDirectWebRequestHandler requestHandler) { Contract.Requires<ArgumentNullException>(xrds != null); Contract.Requires<ArgumentNullException>(identifier != null); Contract.Requires<ArgumentNullException>(requestHandler != null); Contract.Ensures(Contract.Result<IEnumerable<IdentifierDiscoveryResult>>() != null); var results = new List<IdentifierDiscoveryResult>(); foreach (var serviceElement in GetDescribedByServices(xrds)) { var templateNode = serviceElement.Node.SelectSingleNode("google:URITemplate", serviceElement.XmlNamespaceResolver); var nextAuthorityNode = serviceElement.Node.SelectSingleNode("google:NextAuthority", serviceElement.XmlNamespaceResolver); if (templateNode != null) { Uri externalLocation = new Uri(templateNode.Value.Trim().Replace("{%uri}", Uri.EscapeDataString(identifier.Uri.AbsoluteUri))); string nextAuthority = nextAuthorityNode != null ? nextAuthorityNode.Value.Trim() : identifier.Uri.Host; try { using (var externalXrdsResponse = GetXrdsResponse(identifier, requestHandler, externalLocation)) { XrdsDocument externalXrds = new XrdsDocument(XmlReader.Create(externalXrdsResponse.ResponseStream)); ValidateXmlDSig(externalXrds, identifier, externalXrdsResponse, nextAuthority); results.AddRange(GetXrdElements(externalXrds, identifier).CreateServiceEndpoints(identifier, identifier)); } } catch (ProtocolException ex) { Logger.Yadis.WarnFormat("HTTP GET error while retrieving described-by XRDS document {0}: {1}", externalLocation.AbsoluteUri, ex); } catch (XmlException ex) { Logger.Yadis.ErrorFormat("Error while parsing described-by XRDS document {0}: {1}", externalLocation.AbsoluteUri, ex); } } } return results; }
/// <summary> /// Gets the XRD elements that have a given CanonicalID. /// </summary> /// <param name="document">The XRDS document.</param> /// <param name="canonicalId">The CanonicalID to match on.</param> /// <returns>A sequence of XRD elements.</returns> private static IEnumerable<XrdElement> GetXrdElements(XrdsDocument document, string canonicalId) { // filter to include only those XRD elements describing the host whose host-meta pointed us to this document. return document.XrdElements.Where(xrd => string.Equals(xrd.CanonicalID, canonicalId, StringComparison.Ordinal)); }
/// <summary> /// Performs discovery on the specified identifier. /// </summary> /// <param name="identifier">The identifier to perform discovery on.</param> /// <param name="requestHandler">The means to place outgoing HTTP requests.</param> /// <param name="abortDiscoveryChain">if set to <c>true</c>, no further discovery services will be called for this identifier.</param> /// <returns> /// A sequence of service endpoints yielded by discovery. Must not be null, but may be empty. /// </returns> public IEnumerable<IdentifierDiscoveryResult> Discover(Identifier identifier, IDirectWebRequestHandler requestHandler, out bool abortDiscoveryChain) { abortDiscoveryChain = false; // Google Apps are always URIs -- not XRIs. var uriIdentifier = identifier as UriIdentifier; if (uriIdentifier == null) { return Enumerable.Empty<IdentifierDiscoveryResult>(); } var results = new List<IdentifierDiscoveryResult>(); string signingHost; using (var response = GetXrdsResponse(uriIdentifier, requestHandler, out signingHost)) { if (response != null) { try { var document = new XrdsDocument(XmlReader.Create(response.ResponseStream)); ValidateXmlDSig(document, uriIdentifier, response, signingHost); var xrds = GetXrdElements(document, uriIdentifier.Uri.Host); // Look for claimed identifier template URIs for an additional XRDS document. results.AddRange(GetExternalServices(xrds, uriIdentifier, requestHandler)); // If we couldn't find any claimed identifiers, look for OP identifiers. // Normally this would be the opposite (OP Identifiers take precedence over // claimed identifiers, but for Google Apps, XRDS' always have OP Identifiers // mixed in, which the OpenID spec mandate should eclipse Claimed Identifiers, // which would break positive assertion checks). if (results.Count == 0) { results.AddRange(xrds.CreateServiceEndpoints(uriIdentifier, uriIdentifier)); } abortDiscoveryChain = true; } catch (XmlException ex) { Logger.Yadis.ErrorFormat("Error while parsing XRDS document at {0} pointed to by host-meta: {1}", response.FinalUri, ex); } } } return results; }
/// <summary> /// Initializes a new instance of the <see cref="XrdElement"/> class. /// </summary> /// <param name="xrdElement">The XRD element.</param> /// <param name="parent">The parent.</param> public XrdElement(XPathNavigator xrdElement, XrdsDocument parent) : base(xrdElement, parent) { }
/// <summary> /// Downloads the XRDS document for this XRI. /// </summary> /// <param name="identifier">The identifier.</param> /// <param name="hostFactories">The host factories.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns> /// The XRDS document. /// </returns> private static async Task<XrdsDocument> DownloadXrdsAsync(XriIdentifier identifier, IHostFactories hostFactories, CancellationToken cancellationToken) { Requires.NotNull(identifier, "identifier"); Requires.NotNull(hostFactories, "hostFactories"); XrdsDocument doc; using (var xrdsResponse = await Yadis.RequestAsync(GetXrdsUrl(identifier), identifier.IsDiscoverySecureEndToEnd, hostFactories, cancellationToken)) { xrdsResponse.EnsureSuccessStatusCode(); var readerSettings = MessagingUtilities.CreateUntrustedXmlReaderSettings(); ErrorUtilities.VerifyProtocol(xrdsResponse.Content != null, "XRDS request \"{0}\" returned no response.", GetXrdsUrl(identifier)); await xrdsResponse.Content.LoadIntoBufferAsync(); using (var xrdsStream = await xrdsResponse.Content.ReadAsStreamAsync()) { doc = new XrdsDocument(XmlReader.Create(xrdsStream, readerSettings)); } } ErrorUtilities.VerifyProtocol(doc.IsXrdResolutionSuccessful, OpenIdStrings.XriResolutionFailed); return doc; }
/// <summary> /// Searches for an XRDS document at the realm URL, and if found, searches /// for a description of a relying party endpoints (OpenId login pages). /// </summary> /// <param name="requestHandler">The mechanism to use for sending HTTP requests.</param> /// <param name="allowRedirects">Whether redirects may be followed when discovering the Realm. /// This may be true when creating an unsolicited assertion, but must be /// false when performing return URL verification per 2.0 spec section 9.2.1.</param> /// <returns> /// The details of the endpoints if found; or <c>null</c> if no service document was discovered. /// </returns> internal virtual IEnumerable<RelyingPartyEndpointDescription> Discover(IDirectWebRequestHandler requestHandler, bool allowRedirects) { // Attempt YADIS discovery DiscoveryResult yadisResult = Yadis.Discover(requestHandler, this.UriWithWildcardChangedToWww, false); if (yadisResult != null) { // Detect disallowed redirects, since realm discovery never allows them for security. ErrorUtilities.VerifyProtocol(allowRedirects || yadisResult.NormalizedUri == yadisResult.RequestUri, OpenIdStrings.RealmCausedRedirectUponDiscovery, yadisResult.RequestUri); if (yadisResult.IsXrds) { try { XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText); return xrds.FindRelyingPartyReceivingEndpoints(); } catch (XmlException ex) { throw ErrorUtilities.Wrap(ex, XrdsStrings.InvalidXRDSDocument); } } } return null; }
private static void ValidateXmlDSig(XrdsDocument document, UriIdentifier identifier, IncomingWebResponse response, string signingHost) { Requires.NotNull(document, "document"); Requires.NotNull(identifier, "identifier"); Requires.NotNull(response, "response"); var signatureNode = document.Node.SelectSingleNode("/xrds:XRDS/ds:Signature", document.XmlNamespaceResolver); ErrorUtilities.VerifyProtocol(signatureNode != null, OpenIdStrings.MissingElement, "Signature"); var signedInfoNode = signatureNode.SelectSingleNode("ds:SignedInfo", document.XmlNamespaceResolver); ErrorUtilities.VerifyProtocol(signedInfoNode != null, OpenIdStrings.MissingElement, "SignedInfo"); ErrorUtilities.VerifyProtocol( signedInfoNode.SelectSingleNode("ds:CanonicalizationMethod[@Algorithm='http://docs.oasis-open.org/xri/xrd/2009/01#canonicalize-raw-octets']", document.XmlNamespaceResolver) != null, OpenIdStrings.UnsupportedCanonicalizationMethod); ErrorUtilities.VerifyProtocol( signedInfoNode.SelectSingleNode("ds:SignatureMethod[@Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1']", document.XmlNamespaceResolver) != null, OpenIdStrings.UnsupportedSignatureMethod); var certNodes = signatureNode.Select("ds:KeyInfo/ds:X509Data/ds:X509Certificate", document.XmlNamespaceResolver); ErrorUtilities.VerifyProtocol(certNodes.Count > 0, OpenIdStrings.MissingElement, "X509Certificate"); var certs = certNodes.Cast<XPathNavigator>().Select(n => new X509Certificate2(Convert.FromBase64String(n.Value.Trim()))).ToList(); VerifyCertificateChain(certs); // Verify that the certificate is issued to the host on whom we are performing discovery. string hostName = certs[0].GetNameInfo(X509NameType.DnsName, false); ErrorUtilities.VerifyProtocol(string.Equals(hostName, signingHost, StringComparison.OrdinalIgnoreCase), OpenIdStrings.MisdirectedSigningCertificate, hostName, signingHost); // Verify the signature itself byte[] signature = Convert.FromBase64String(response.Headers["Signature"]); var provider = (RSACryptoServiceProvider)certs.First().PublicKey.Key; byte[] data = new byte[response.ResponseStream.Length]; response.ResponseStream.Seek(0, SeekOrigin.Begin); response.ResponseStream.Read(data, 0, data.Length); ErrorUtilities.VerifyProtocol(provider.VerifyData(data, "SHA1", signature), OpenIdStrings.InvalidDSig); }
/// <summary> /// Performs discovery on the Identifier. /// </summary> /// <param name="requestHandler">The web request handler to use for discovery.</param> /// <returns> /// An initialized structure containing the discovered provider endpoint information. /// </returns> internal override IEnumerable<ServiceEndpoint> Discover(IDirectWebRequestHandler requestHandler) { List<ServiceEndpoint> endpoints = new List<ServiceEndpoint>(); // Attempt YADIS discovery DiscoveryResult yadisResult = Yadis.Discover(requestHandler, this, IsDiscoverySecureEndToEnd); if (yadisResult != null) { if (yadisResult.IsXrds) { XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText); var xrdsEndpoints = xrds.CreateServiceEndpoints(yadisResult.NormalizedUri, this); // Filter out insecure endpoints if high security is required. if (IsDiscoverySecureEndToEnd) { xrdsEndpoints = xrdsEndpoints.Where(se => se.IsSecure); } endpoints.AddRange(xrdsEndpoints); } // Failing YADIS discovery of an XRDS document, we try HTML discovery. if (endpoints.Count == 0) { var htmlEndpoints = new List<ServiceEndpoint>(DiscoverFromHtml(yadisResult.NormalizedUri, this, yadisResult.ResponseText)); if (htmlEndpoints.Any()) { Logger.Yadis.DebugFormat("Total services discovered in HTML: {0}", htmlEndpoints.Count); Logger.Yadis.Debug(htmlEndpoints.ToStringDeferred(true)); endpoints.AddRange(htmlEndpoints.Where(ep => !IsDiscoverySecureEndToEnd || ep.IsSecure)); if (endpoints.Count == 0) { Logger.Yadis.Info("No HTML discovered endpoints met the security requirements."); } } else { Logger.Yadis.Debug("HTML discovery failed to find any endpoints."); } } else { Logger.Yadis.Debug("Skipping HTML discovery because XRDS contained service endpoints."); } } return endpoints; }