|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net;
using System.Xml;
namespace System.Security.Cryptography.Xml
{
public class Reference
{
internal const string DefaultDigestMethod = SignedXml.XmlDsigSHA256Url;
private string? _id;
private string? _uri;
private string? _type;
private TransformChain _transformChain;
private string _digestMethod;
private byte[]? _digestValue;
private HashAlgorithm? _hashAlgorithm;
private readonly object? _refTarget;
private readonly ReferenceTargetType _refTargetType;
private XmlElement? _cachedXml;
private SignedXml? _signedXml;
internal CanonicalXmlNodeList? _namespaces;
//
// public constructors
//
public Reference()
{
_transformChain = new TransformChain();
_refTarget = null;
_refTargetType = ReferenceTargetType.UriReference;
_cachedXml = null;
_digestMethod = DefaultDigestMethod;
}
public Reference(Stream stream)
{
_transformChain = new TransformChain();
_refTarget = stream;
_refTargetType = ReferenceTargetType.Stream;
_cachedXml = null;
_digestMethod = DefaultDigestMethod;
}
public Reference(string? uri)
{
_transformChain = new TransformChain();
_refTarget = uri;
_uri = uri;
_refTargetType = ReferenceTargetType.UriReference;
_cachedXml = null;
_digestMethod = DefaultDigestMethod;
}
internal Reference(XmlElement element)
{
_transformChain = new TransformChain();
_refTarget = element;
_refTargetType = ReferenceTargetType.XmlElement;
_cachedXml = null;
_digestMethod = DefaultDigestMethod;
}
//
// public properties
//
public string? Id
{
get { return _id; }
set { _id = value; }
}
public string? Uri
{
get { return _uri; }
set
{
_uri = value;
_cachedXml = null;
}
}
public string? Type
{
get { return _type; }
set
{
_type = value;
_cachedXml = null;
}
}
public string DigestMethod
{
get { return _digestMethod; }
set
{
_digestMethod = value;
_cachedXml = null;
}
}
public byte[]? DigestValue
{
get { return _digestValue; }
set
{
_digestValue = value;
_cachedXml = null;
}
}
public TransformChain TransformChain
{
get => _transformChain ??= new TransformChain();
set
{
_transformChain = value;
_cachedXml = null;
}
}
[MemberNotNullWhen(true, nameof(_cachedXml))]
internal bool CacheValid
{
get
{
return (_cachedXml != null);
}
}
internal SignedXml? SignedXml
{
get { return _signedXml; }
set { _signedXml = value; }
}
internal ReferenceTargetType ReferenceTargetType
{
get
{
return _refTargetType;
}
}
private static readonly string[] s_expectedAttrNames = new string[] { "Id", "URI", "Type" };
//
// public methods
//
public XmlElement GetXml()
{
if (CacheValid) return _cachedXml;
XmlDocument document = new XmlDocument();
document.PreserveWhitespace = true;
return GetXml(document);
}
internal XmlElement GetXml(XmlDocument document)
{
// Create the Reference
XmlElement referenceElement = document.CreateElement("Reference", SignedXml.XmlDsigNamespaceUrl);
if (!string.IsNullOrEmpty(_id))
referenceElement.SetAttribute("Id", _id);
if (_uri != null)
referenceElement.SetAttribute("URI", _uri);
if (!string.IsNullOrEmpty(_type))
referenceElement.SetAttribute("Type", _type);
// Add the transforms to the Reference
if (TransformChain.Count != 0)
referenceElement.AppendChild(TransformChain.GetXml(document, SignedXml.XmlDsigNamespaceUrl));
// Add the DigestMethod
if (string.IsNullOrEmpty(_digestMethod))
throw new CryptographicException(SR.Cryptography_Xml_DigestMethodRequired);
XmlElement digestMethodElement = document.CreateElement("DigestMethod", SignedXml.XmlDsigNamespaceUrl);
digestMethodElement.SetAttribute("Algorithm", _digestMethod);
referenceElement.AppendChild(digestMethodElement);
if (DigestValue == null)
{
if (_hashAlgorithm!.Hash == null)
throw new CryptographicException(SR.Cryptography_Xml_DigestValueRequired);
DigestValue = _hashAlgorithm.Hash;
}
XmlElement digestValueElement = document.CreateElement("DigestValue", SignedXml.XmlDsigNamespaceUrl);
digestValueElement.AppendChild(document.CreateTextNode(Convert.ToBase64String(_digestValue!)));
referenceElement.AppendChild(digestValueElement);
return referenceElement;
}
[RequiresDynamicCode(CryptoHelpers.XsltRequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(CryptoHelpers.CreateFromNameUnreferencedCodeMessage)]
public void LoadXml(XmlElement value)
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
_id = Utils.GetAttribute(value, "Id", SignedXml.XmlDsigNamespaceUrl);
_uri = Utils.GetAttribute(value, "URI", SignedXml.XmlDsigNamespaceUrl);
_type = Utils.GetAttribute(value, "Type", SignedXml.XmlDsigNamespaceUrl);
if (!Utils.VerifyAttributes(value, s_expectedAttrNames))
throw new CryptographicException(SR.Cryptography_Xml_InvalidElement, "Reference");
XmlNamespaceManager nsm = new XmlNamespaceManager(value.OwnerDocument.NameTable);
nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl);
// Transforms
bool hasTransforms = false;
TransformChain = new TransformChain();
XmlNodeList? transformsNodes = value.SelectNodes("ds:Transforms", nsm);
if (transformsNodes != null && transformsNodes.Count != 0)
{
if (transformsNodes.Count > 1)
{
throw new CryptographicException(SR.Cryptography_Xml_InvalidElement, "Reference/Transforms");
}
hasTransforms = true;
XmlElement transformsElement = (transformsNodes[0] as XmlElement)!;
if (!Utils.VerifyAttributes(transformsElement, (string[]?)null))
{
throw new CryptographicException(SR.Cryptography_Xml_InvalidElement, "Reference/Transforms");
}
XmlNodeList? transformNodes = transformsElement.SelectNodes("ds:Transform", nsm);
if (transformNodes != null)
{
if (transformNodes.Count != transformsElement.SelectNodes("*")!.Count)
{
throw new CryptographicException(SR.Cryptography_Xml_InvalidElement, "Reference/Transforms");
}
if (transformNodes.Count > Utils.MaxTransformsPerReference)
{
throw new CryptographicException(SR.Cryptography_Xml_InvalidElement, "Reference/Transforms");
}
foreach (XmlNode transformNode in transformNodes)
{
XmlElement transformElement = (transformNode as XmlElement)!;
string? algorithm = Utils.GetAttribute(transformElement, "Algorithm", SignedXml.XmlDsigNamespaceUrl);
if (algorithm == null || !Utils.VerifyAttributes(transformElement, "Algorithm"))
{
throw new CryptographicException(SR.Cryptography_Xml_UnknownTransform);
}
Transform? transform = CryptoHelpers.CreateFromName<Transform>(algorithm);
if (transform == null)
{
throw new CryptographicException(SR.Cryptography_Xml_UnknownTransform);
}
AddTransform(transform);
// let the transform read the children of the transformElement for data
transform.LoadInnerXml(transformElement.ChildNodes);
// Hack! this is done to get around the lack of here() function support in XPath
if (transform is XmlDsigEnvelopedSignatureTransform)
{
// Walk back to the Signature tag. Find the nearest signature ancestor
// Signature-->SignedInfo-->Reference-->Transforms-->Transform
XmlNode? signatureTag = transformElement.SelectSingleNode("ancestor::ds:Signature[1]", nsm);
// Resolve the reference to get starting point for position calculation.
// This needs to match the way CalculateSignature resolves URI references.
XmlNode? referenceTarget = null;
if (_uri == null || _uri.Length == 0)
{
referenceTarget = transformElement.OwnerDocument;
}
else if (_uri[0] == '#')
{
string idref = Utils.ExtractIdFromLocalUri(_uri);
if (idref == "xpointer(/)")
{
referenceTarget = transformElement.OwnerDocument;
}
else
{
referenceTarget = SignedXml!.GetIdElement(transformElement.OwnerDocument, idref);
}
}
XmlNodeList? signatureList = referenceTarget?.SelectNodes(".//ds:Signature", nsm);
if (signatureList != null)
{
int position = 0;
foreach (XmlNode node in signatureList)
{
position++;
if (node == signatureTag)
{
((XmlDsigEnvelopedSignatureTransform)transform).SignaturePosition = position;
break;
}
}
}
}
}
}
}
// DigestMethod
XmlNodeList? digestMethodNodes = value.SelectNodes("ds:DigestMethod", nsm);
if (digestMethodNodes == null || digestMethodNodes.Count == 0 || digestMethodNodes.Count > 1)
throw new CryptographicException(SR.Cryptography_Xml_InvalidElement, "Reference/DigestMethod");
XmlElement digestMethodElement = (digestMethodNodes[0] as XmlElement)!;
_digestMethod = Utils.GetAttribute(digestMethodElement, "Algorithm", SignedXml.XmlDsigNamespaceUrl)!;
if (_digestMethod == null || !Utils.VerifyAttributes(digestMethodElement, "Algorithm"))
throw new CryptographicException(SR.Cryptography_Xml_InvalidElement, "Reference/DigestMethod");
// DigestValue
XmlNodeList? digestValueNodes = value.SelectNodes("ds:DigestValue", nsm);
if (digestValueNodes == null || digestValueNodes.Count == 0 || digestValueNodes.Count > 1)
throw new CryptographicException(SR.Cryptography_Xml_InvalidElement, "Reference/DigestValue");
XmlElement digestValueElement = (digestValueNodes[0] as XmlElement)!;
_digestValue = Convert.FromBase64String(Utils.DiscardWhiteSpaces(digestValueElement.InnerText));
if (!Utils.VerifyAttributes(digestValueElement, (string[]?)null))
throw new CryptographicException(SR.Cryptography_Xml_InvalidElement, "Reference/DigestValue");
// Verify that there aren't any extra nodes that aren't allowed
int expectedChildNodeCount = hasTransforms ? 3 : 2;
if (value.SelectNodes("*")!.Count != expectedChildNodeCount)
throw new CryptographicException(SR.Cryptography_Xml_InvalidElement, "Reference");
// cache the Xml
_cachedXml = value;
}
public void AddTransform(Transform transform)
{
if (transform is null)
{
throw new ArgumentNullException(nameof(transform));
}
transform.Reference = this;
TransformChain.Add(transform);
}
[RequiresUnreferencedCode(CryptoHelpers.CreateFromNameUnreferencedCodeMessage)]
internal void UpdateHashValue(XmlDocument document, CanonicalXmlNodeList refList)
{
DigestValue = CalculateHashValue(document, refList);
}
// What we want to do is pump the input through the TransformChain and then
// hash the output of the chain document is the document context for resolving relative references
[RequiresUnreferencedCode(CryptoHelpers.CreateFromNameUnreferencedCodeMessage)]
internal byte[]? CalculateHashValue(XmlDocument document, CanonicalXmlNodeList refList)
{
// refList is a list of elements that might be targets of references
// Now's the time to create our hashing algorithm
_hashAlgorithm = CryptoHelpers.CreateNonTransformFromName<HashAlgorithm>(_digestMethod);
if (_hashAlgorithm == null)
throw new CryptographicException(SR.Cryptography_Xml_CreateHashAlgorithmFailed);
// Let's go get the target.
string baseUri = (document == null ? System.Environment.CurrentDirectory + "\\" : document.BaseURI);
Stream? hashInputStream = null;
WebResponse? response = null;
Stream? inputStream = null;
XmlResolver? resolver = null;
byte[] hashval;
try
{
switch (_refTargetType)
{
case ReferenceTargetType.Stream:
// This is the easiest case. We already have a stream, so just pump it through the TransformChain
resolver = (SignedXml!.ResolverSet ? SignedXml._xmlResolver : XmlResolverHelper.GetThrowingResolver());
hashInputStream = TransformChain.TransformToOctetStream((Stream?)_refTarget, resolver, baseUri);
break;
case ReferenceTargetType.UriReference:
// Second-easiest case -- dereference the URI & pump through the TransformChain
// handle the special cases where the URI is null (meaning whole doc)
// or the URI is just a fragment (meaning a reference to an embedded Object)
if (_uri == null)
{
// We need to create a DocumentNavigator out of the XmlElement
resolver = (SignedXml!.ResolverSet ? SignedXml._xmlResolver : XmlResolverHelper.GetThrowingResolver());
// In the case of a Uri-less reference, we will simply pass null to the transform chain.
// The first transform in the chain is expected to know how to retrieve the data to hash.
hashInputStream = TransformChain.TransformToOctetStream((Stream?)null, resolver!, baseUri);
}
else if (_uri.Length == 0)
{
// This is the self-referential case. First, check that we have a document context.
// The Enveloped Signature does not discard comments as per spec; those will be omitted during the transform chain process
if (document == null)
throw new CryptographicException(SR.Format(SR.Cryptography_Xml_SelfReferenceRequiresContext, _uri));
// Normalize the containing document
resolver = (SignedXml!.ResolverSet ? SignedXml._xmlResolver : XmlResolverHelper.GetThrowingResolver());
XmlDocument docWithNoComments = Utils.DiscardComments(Utils.PreProcessDocumentInput(document, resolver!, baseUri));
hashInputStream = TransformChain.TransformToOctetStream(docWithNoComments, resolver, baseUri);
}
else if (_uri[0] == '#')
{
// If we get here, then we are constructing a Reference to an embedded DataObject
// referenced by an Id = attribute. Go find the relevant object
string idref = Utils.GetIdFromLocalUri(_uri, out bool discardComments);
if (idref == "xpointer(/)")
{
// This is a self referential case
if (document == null)
throw new CryptographicException(SR.Format(SR.Cryptography_Xml_SelfReferenceRequiresContext, _uri));
// We should not discard comments here!!!
resolver = (SignedXml!.ResolverSet ? SignedXml._xmlResolver : XmlResolverHelper.GetThrowingResolver());
hashInputStream = TransformChain.TransformToOctetStream(Utils.PreProcessDocumentInput(document, resolver!, baseUri), resolver, baseUri);
break;
}
XmlElement? elem = SignedXml!.GetIdElement(document, idref);
if (elem != null)
_namespaces = Utils.GetPropagatedAttributes(elem.ParentNode as XmlElement);
if (elem == null)
{
// Go throw the referenced items passed in
if (refList != null)
{
foreach (XmlNode node in refList)
{
XmlElement? tempElem = node as XmlElement;
if ((tempElem != null) && (Utils.HasAttribute(tempElem, "Id", SignedXml.XmlDsigNamespaceUrl))
&& (Utils.GetAttribute(tempElem, "Id", SignedXml.XmlDsigNamespaceUrl)!.Equals(idref)))
{
elem = tempElem;
if (_signedXml!._context != null)
_namespaces = Utils.GetPropagatedAttributes(_signedXml._context);
break;
}
}
}
}
if (elem == null)
throw new CryptographicException(SR.Cryptography_Xml_InvalidReference);
XmlDocument normDocument = Utils.PreProcessElementInput(elem, resolver!, baseUri);
// Add the propagated attributes
Utils.AddNamespaces(normDocument.DocumentElement!, _namespaces);
resolver = (SignedXml.ResolverSet ? SignedXml._xmlResolver : XmlResolverHelper.GetThrowingResolver());
if (discardComments)
{
// We should discard comments before going into the transform chain
XmlDocument docWithNoComments = Utils.DiscardComments(normDocument);
hashInputStream = TransformChain.TransformToOctetStream(docWithNoComments, resolver, baseUri);
}
else
{
// This is an XPointer reference, do not discard comments!!!
hashInputStream = TransformChain.TransformToOctetStream(normDocument, resolver, baseUri);
}
}
else
{
throw new CryptographicException(SR.Cryptography_Xml_UriNotResolved, _uri);
}
break;
case ReferenceTargetType.XmlElement:
// We need to create a DocumentNavigator out of the XmlElement
resolver = (SignedXml!.ResolverSet ? SignedXml._xmlResolver : XmlResolverHelper.GetThrowingResolver());
hashInputStream = TransformChain.TransformToOctetStream(Utils.PreProcessElementInput((XmlElement)_refTarget!, resolver!, baseUri), resolver, baseUri);
break;
default:
throw new CryptographicException(SR.Cryptography_Xml_UriNotResolved, _uri);
}
// Compute the new hash value
hashInputStream = SignedXmlDebugLog.LogReferenceData(this, hashInputStream);
hashval = _hashAlgorithm.ComputeHash(hashInputStream!);
}
finally
{
hashInputStream?.Close();
response?.Close();
inputStream?.Close();
}
return hashval;
}
}
}
|