@@ -56,6 +56,7 @@ export class SignedXml {
5656 keyInfoAttributes : { [ attrName : string ] : string } = { } ;
5757 getKeyInfoContent = SignedXml . getKeyInfoContent ;
5858 getCertFromKeyInfo = SignedXml . getCertFromKeyInfo ;
59+ getObjectContent = SignedXml . getObjectContent ;
5960
6061 // Internal state
6162 private id = 0 ;
@@ -126,6 +127,16 @@ export class SignedXml {
126127
127128 static noop = ( ) => null ;
128129
130+ /**
131+ * Default implementation for getObjectContent that returns null (no Objects)
132+ */
133+ static getObjectContent ( ) : Array < {
134+ content : string ;
135+ attributes ?: Record < string , string | undefined > ;
136+ } > | null {
137+ return null ;
138+ }
139+
129140 /**
130141 * The SignedXml constructor provides an abstraction for sign and verify xml documents. The object is constructed using
131142 * @param options {@link SignedXmlOptions }
@@ -143,6 +154,7 @@ export class SignedXml {
143154 keyInfoAttributes,
144155 getKeyInfoContent,
145156 getCertFromKeyInfo,
157+ getObjectContent,
146158 } = options ;
147159
148160 // Options
@@ -164,6 +176,7 @@ export class SignedXml {
164176 this . keyInfoAttributes = keyInfoAttributes ?? this . keyInfoAttributes ;
165177 this . getKeyInfoContent = getKeyInfoContent ?? this . getKeyInfoContent ;
166178 this . getCertFromKeyInfo = getCertFromKeyInfo ?? SignedXml . noop ;
179+ this . getObjectContent = getObjectContent ?? this . getObjectContent ;
167180 this . CanonicalizationAlgorithms ;
168181 this . HashAlgorithms ;
169182 this . SignatureAlgorithms ;
@@ -796,6 +809,7 @@ export class SignedXml {
796809 * @param digestValue The expected digest value for the reference.
797810 * @param inclusiveNamespacesPrefixList The prefix list for inclusive namespace canonicalization.
798811 * @param isEmptyUri Indicates whether the URI is empty. Defaults to `false`.
812+ * @param isSignatureReference Indicates whether this reference points to an element in the signature itself (like an Object element).
799813 */
800814 addReference ( {
801815 xpath,
@@ -805,6 +819,7 @@ export class SignedXml {
805819 digestValue,
806820 inclusiveNamespacesPrefixList = [ ] ,
807821 isEmptyUri = false ,
822+ isSignatureReference = false ,
808823 } : Partial < Reference > & Pick < Reference , "xpath" > ) : void {
809824 if ( digestAlgorithm == null ) {
810825 throw new Error ( "digestAlgorithm is required" ) ;
@@ -822,6 +837,7 @@ export class SignedXml {
822837 digestValue,
823838 inclusiveNamespacesPrefixList,
824839 isEmptyUri,
840+ isSignatureReference,
825841 getValidatedNode : ( ) => {
826842 throw new Error (
827843 "Reference has not been validated yet; Did you call `sig.checkSignature()`?" ,
@@ -965,6 +981,7 @@ export class SignedXml {
965981
966982 signatureXml += this . createSignedInfo ( doc , prefix ) ;
967983 signatureXml += this . getKeyInfo ( prefix ) ;
984+ signatureXml += this . getObjects ( prefix ) ;
968985 signatureXml += `</${ currentPrefix } Signature>` ;
969986
970987 this . originalXmlWithIds = doc . toString ( ) ;
@@ -979,6 +996,9 @@ export class SignedXml {
979996 const dummySignatureWrapper = `<Dummy ${ existingPrefixesString } >${ signatureXml } </Dummy>` ;
980997 const nodeXml = new xmldom . DOMParser ( ) . parseFromString ( dummySignatureWrapper ) ;
981998
999+ // Process any signature references after the signature has been created
1000+ this . processSignatureReferences ( nodeXml , prefix ) ;
1001+
9821002 // Because we are using a dummy wrapper hack described above, we know there will be a `firstChild`
9831003 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
9841004 const signatureDoc = nodeXml . documentElement . firstChild ! ;
@@ -1070,6 +1090,39 @@ export class SignedXml {
10701090 return "" ;
10711091 }
10721092
1093+ /**
1094+ * Creates XML for Object elements to be included in the signature
1095+ *
1096+ * @param prefix Optional namespace prefix
1097+ * @returns XML string with Object elements or empty string if none
1098+ */
1099+ private getObjects ( prefix ?: string ) {
1100+ const currentPrefix = prefix ? `${ prefix } :` : "" ;
1101+ const objects = this . getObjectContent ?.( ) ;
1102+
1103+ if ( ! objects || objects . length === 0 ) {
1104+ return "" ;
1105+ }
1106+
1107+ let result = "" ;
1108+
1109+ for ( const obj of objects ) {
1110+ let objectAttrs = "" ;
1111+ if ( obj . attributes ) {
1112+ Object . keys ( obj . attributes ) . forEach ( ( name ) => {
1113+ const value = obj . attributes ?. [ name ] ;
1114+ if ( value !== undefined ) {
1115+ objectAttrs += ` ${ name } ="${ value } "` ;
1116+ }
1117+ } ) ;
1118+ }
1119+
1120+ result += `<${ currentPrefix } Object${ objectAttrs } >${ obj . content } </${ currentPrefix } Object>` ;
1121+ }
1122+
1123+ return result ;
1124+ }
1125+
10731126 /**
10741127 * Generate the Reference nodes (as part of the signature process)
10751128 *
@@ -1082,6 +1135,10 @@ export class SignedXml {
10821135
10831136 /* eslint-disable-next-line deprecation/deprecation */
10841137 for ( const ref of this . getReferences ( ) ) {
1138+ if ( ref . isSignatureReference ) {
1139+ // For signature references, we'll handle them separately after the signature is created
1140+ continue ;
1141+ }
10851142 const nodes = xpath . selectWithResolver ( ref . xpath ?? "" , doc , this . namespaceResolver ) ;
10861143
10871144 if ( ! utils . isArrayHasLength ( nodes ) ) {
@@ -1262,6 +1319,115 @@ export class SignedXml {
12621319 return doc . documentElement . firstChild ! ;
12631320 }
12641321
1322+ /**
1323+ * Process references that point to elements within the Signature element
1324+ * This is called after the initial signature has been created
1325+ */
1326+ private processSignatureReferences ( signatureDoc : Document , prefix ?: string ) {
1327+ // Get signature references
1328+ const signatureReferences = this . references . filter ( ( ref ) => ref . isSignatureReference ) ;
1329+ if ( signatureReferences . length === 0 ) {
1330+ return ;
1331+ }
1332+
1333+ prefix = prefix || "" ;
1334+ prefix = prefix ? `${ prefix } :` : prefix ;
1335+ const signatureNamespace = "http://www.w3.org/2000/09/xmldsig#" ;
1336+
1337+ // Find the SignedInfo element to append to
1338+ const signedInfoNode = xpath . select1 (
1339+ `.//*[local-name(.)='SignedInfo']` ,
1340+ signatureDoc ,
1341+ ) as Element ;
1342+ if ( ! signedInfoNode ) {
1343+ throw new Error ( "Could not find SignedInfo element in signature" ) ;
1344+ }
1345+
1346+ // Process each signature reference
1347+ for ( const ref of signatureReferences ) {
1348+ const nodes = xpath . selectWithResolver ( ref . xpath ?? "" , signatureDoc , this . namespaceResolver ) ;
1349+
1350+ if ( ! utils . isArrayHasLength ( nodes ) ) {
1351+ throw new Error (
1352+ `the following xpath cannot be signed because it was not found: ${ ref . xpath } ` ,
1353+ ) ;
1354+ }
1355+
1356+ // Process the reference
1357+ for ( const node of nodes ) {
1358+ // Create the reference element directly using DOM methods to avoid namespace issues
1359+ const referenceElem = signatureDoc . createElementNS (
1360+ signatureNamespace ,
1361+ `${ prefix } Reference` ,
1362+ ) ;
1363+ if ( ref . isEmptyUri ) {
1364+ referenceElem . setAttribute ( "URI" , "" ) ;
1365+ } else {
1366+ const id = this . ensureHasId ( node ) ;
1367+ ref . uri = id ;
1368+ referenceElem . setAttribute ( "URI" , `#${ id } ` ) ;
1369+ }
1370+
1371+ const transformsElem = signatureDoc . createElementNS (
1372+ signatureNamespace ,
1373+ `${ prefix } Transforms` ,
1374+ ) ;
1375+
1376+ for ( const trans of ref . transforms || [ ] ) {
1377+ const transform = this . findCanonicalizationAlgorithm ( trans ) ;
1378+ const transformElem = signatureDoc . createElementNS (
1379+ signatureNamespace ,
1380+ `${ prefix } Transform` ,
1381+ ) ;
1382+ transformElem . setAttribute ( "Algorithm" , transform . getAlgorithmName ( ) ) ;
1383+
1384+ if ( utils . isArrayHasLength ( ref . inclusiveNamespacesPrefixList ) ) {
1385+ const inclusiveNamespacesElem = signatureDoc . createElementNS (
1386+ transform . getAlgorithmName ( ) ,
1387+ "InclusiveNamespaces" ,
1388+ ) ;
1389+ inclusiveNamespacesElem . setAttribute (
1390+ "PrefixList" ,
1391+ ref . inclusiveNamespacesPrefixList . join ( " " ) ,
1392+ ) ;
1393+ transformElem . appendChild ( inclusiveNamespacesElem ) ;
1394+ }
1395+
1396+ transformsElem . appendChild ( transformElem ) ;
1397+ }
1398+
1399+ // Get the canonicalized XML
1400+ const canonXml = this . getCanonReferenceXml ( signatureDoc , ref , node ) ;
1401+
1402+ // Calculate the digest
1403+ const digestAlgorithm = this . findHashAlgorithm ( ref . digestAlgorithm ) ;
1404+ const digestValue = digestAlgorithm . getHash ( canonXml ) ;
1405+
1406+ // Store the digest value for later validation
1407+ ref . digestValue = digestValue ;
1408+
1409+ const digestMethodElem = signatureDoc . createElementNS (
1410+ signatureNamespace ,
1411+ `${ prefix } DigestMethod` ,
1412+ ) ;
1413+ digestMethodElem . setAttribute ( "Algorithm" , digestAlgorithm . getAlgorithmName ( ) ) ;
1414+
1415+ const digestValueElem = signatureDoc . createElementNS (
1416+ signatureNamespace ,
1417+ `${ prefix } DigestValue` ,
1418+ ) ;
1419+ digestValueElem . textContent = digestValue ;
1420+
1421+ referenceElem . appendChild ( transformsElem ) ;
1422+ referenceElem . appendChild ( digestMethodElem ) ;
1423+ referenceElem . appendChild ( digestValueElem ) ;
1424+
1425+ // Append the reference element to SignedInfo
1426+ signedInfoNode . appendChild ( referenceElem ) ;
1427+ }
1428+ }
1429+ }
1430+
12651431 /**
12661432 * Returns just the signature part, must be called only after {@link computeSignature}
12671433 *
0 commit comments