Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix XML Signature Removal Bug #548

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/libsaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,11 +263,13 @@ const libSaml = () => {
* @param {array} tagValues tag values
* @return {string}
*/
replaceTagsByValue(rawXML: string, tagValues: any): string {
replaceTagsByValue(rawXML: string, tagValues: Record<string, unknown>): string {
Object.keys(tagValues).forEach(t => {
let tagValue = tagValues[t];
tagValue = tagValue == null ? tagValue : tagValue.toString();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If value is null or undefined leave as is (this will produce an empty string), otherwise convert to string (fixes issue with false values). This fixes failure happening for tests:

  • flow > create login request with redirect binding using default template and parse it
  • flow > create login request with post simpleSign binding using default template and parse it
  • flow > create login request with post binding using default template and parse it
t.is(extract.nameIDPolicy.allowCreate, 'false');

  Difference:

  - ''
  + 'false'

rawXML = rawXML.replace(
new RegExp(`("?)\\{${t}\\}`, 'g'),
escapeTag(tagValues[t])
escapeTag(tagValue as string)
);
});
return rawXML;
Expand Down Expand Up @@ -453,7 +455,7 @@ const libSaml = () => {

sig.loadSignature(signatureNode);

doc.removeChild(signatureNode);
signatureNode.parentNode.removeChild(signatureNode);

verified = verified && sig.checkSignature(doc.toString());

Expand Down
34 changes: 33 additions & 1 deletion test/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const {

const binding = ref.namespace.binding;

const xmlProlog = '<?xml version="1.0" encoding="utf-8"?>';

// Custom template
const loginResponseTemplate = {
context: '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" InResponseTo="{InResponseTo}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="{StatusCode}"/></samlp:Status><saml:Assertion ID="{AssertionID}" Version="2.0" IssueInstant="{IssueInstant}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"><saml:Issuer>{Issuer}</saml:Issuer><saml:Subject><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="{SubjectConfirmationDataNotOnOrAfter}" Recipient="{SubjectRecipient}" InResponseTo="{InResponseTo}"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="{ConditionsNotBefore}" NotOnOrAfter="{ConditionsNotOnOrAfter}"><saml:AudienceRestriction><saml:Audience>{Audience}</saml:Audience></saml:AudienceRestriction></saml:Conditions>{AttributeStatement}</saml:Assertion></samlp:Response>',
Expand Down Expand Up @@ -146,6 +148,14 @@ const idp = identityProvider(defaultIdpConfig);
const sp = serviceProvider(defaultSpConfig);
const idpNoEncrypt = identityProvider({ ...defaultIdpConfig, isAssertionEncrypted: false });
const idpcustomNoEncrypt = identityProvider({ ...defaultIdpConfig, isAssertionEncrypted: false, loginResponseTemplate });
const idpcustomPrologNoEncrypt = identityProvider({
...defaultIdpConfig,
isAssertionEncrypted: false,
loginResponseTemplate: {
...loginResponseTemplate,
context: xmlProlog + loginResponseTemplate.context,
}
});
const idpcustom = identityProvider({ ...defaultIdpConfig, loginResponseTemplate });
const idpEncryptThenSign = identityProvider({ ...defaultIdpConfig, messageSigningOrder: 'encrypt-then-sign' });
const spWantLogoutReqSign = serviceProvider({ ...defaultSpConfig, wantLogoutRequestSigned: true });
Expand All @@ -155,7 +165,7 @@ const spNoAssertSignCustomConfig = serviceProvider({ ...defaultSpConfig,
metadata: spmetaNoAssertSign,
signatureConfig: {
prefix: 'ds',
location: { reference: "/*[local-name(.)='Response']/*[local-name(.)='Issuer']", action: 'after' },
location: { reference: "/*[local-name(.)='Response']", action: 'prepend' },
Comment on lines -158 to +168
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change makes it so that Login Responses produce for this SP will add their signature as the first element of the Response tag. This and the presence of and XML Prolog (making the Response tag not the owner doc) causes the bug in question where the XML returned by removeChild is incorrect.

ex.
Before replaceChild

<?xml version="1.0" encoding="utf-8"?><samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6" Version="2.0" IssueInstant="2024-10-02T23:03:36.499Z" Destination="" InResponseTo="_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4"><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>V4e4IN/fpSq2B5ygB1wE94TS5tS+U+giS4OWEH/NCak=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>n1sqIxMB3vcEYrUt8mlFJoF8M4IjTmLwXiZEqkn1yFPaxD87gLG7tfcRGeL4DcsUp/xjt0qIK4004nLGBfp9pIYsM+NMyPkr8SnoXObO75TuFZ7e0owe0WAs0QiAQ7gbNbzBSd/q5YHckYJWxDEJXchjN63f0phdEgB7wMatkBpqNFsKoT+VONoAVauQp7on7a3s42JgcRzvWIlROmMMiPSasSp2My1UA0Gx9kdUDQWeLVlmgTl2/W3EcE4K7VIe5L2EQcnBRmUE1Q5kngnFOmAkIok76jcqJOmzmheA7FpgrMe/kzqikyQy9ZsJQxS5SCFQNhCQLdenNHSpY/GCog==</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIDlzCCAn+gAwIBAgIJAO1ymQc33+bWMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAkhLMRMwEQYDVQQIDApTb21lLVN0YXRlMRowGAYDVQQKDBFJZGVudGl0eSBQcm92aWRlcjEUMBIGA1UECwwLRGV2ZWxvcG1lbnQxDDAKBgNVBAMMA0lEUDAeFw0xNTA3MDUxODAyMjdaFw0xODA3MDQxODAyMjdaMGIxCzAJBgNVBAYTAkhLMRMwEQYDVQQIDApTb21lLVN0YXRlMRowGAYDVQQKDBFJZGVudGl0eSBQcm92aWRlcjEUMBIGA1UECwwLRGV2ZWxvcG1lbnQxDDAKBgNVBAMMA0lEUDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAODZsWhCe+yG0PalQPTUoD7yko5MTWMCRxJ8hSm2k7mG3Eg/Y2v0EBdCmTw7iDCevRqUmbmFnq7MROyV4eriJzh0KabAdZf7/k6koghst3ZUtWOwzshyxkBtWDwGmBpQGTGsKxJ8M1js3aSqNRXBT4OBWM9w2Glt1+8ty30RhYv3pSF+/HHLH7Ac+vLSIAlokaFW34RWTcJ/8rADuRWlXih4GfnIu0W/ncm5nTSaJiRAvr3dGDRO/khiXoJdbbOj7dHPULxVGbH9IbPK76TCwLbF7ikIMsPovVbTrpyL6vsbVUKeEl/5GKppTwp9DLAOeoSYpCYkkDkYKu9TRQjF02MCAwEAAaNQME4wHQYDVR0OBBYEFP2ut2AQdy6D1dwdwK740IHmbh38MB8GA1UdIwQYMBaAFP2ut2AQdy6D1dwdwK740IHmbh38MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBANMZUoPNmHzgja2PYkbvBYMHmpvUkVoiuvQ9cJPlqGTB2CRfG68BNNs/Clz8P7cIrAdkhCUwi1rSBhDuslGFNrSaIpv6B10FpBuKwef3G7YrPWFNEN6khY7aHNWSTHqKgs1DrGef2B9hvkrnHWbQVSVXrBFKe1wTCqcgGcOpYoSK7L8C6iX6uIA/uZYnVQ4NgBrizJ0azkjdegz3hwO/gt4malEURy8D85/AAVt6PAzhpb9VJUGxSXr/EfntVUEz3L2gUFWWk1CnZFyz0rIOEt/zPmeAY8BLyd/Tjxm4Y+gwNazKq5y9AJS+m858b/nM4QdCnUE4yyoWAJDUHiAmvFA=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature><saml:Issuer>https://idp.example.com/metadata</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><saml:Assertion ID="_519cc29f-aba8-4517-aa4b-6ce2da634a47" Version="2.0" IssueInstant="2024-10-02T23:03:36.499Z" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"><saml:Issuer>https://idp.example.com/metadata</saml:Issuer><saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">[email protected]</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="2024-10-02T23:08:36.499Z" Recipient="https://sp.example.org/metadata" InResponseTo="_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="2024-10-02T23:03:36.499Z" NotOnOrAfter="2024-10-02T23:08:36.499Z"><saml:AudienceRestriction><saml:Audience>https://sp.example.org/metadata</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AttributeStatement><saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">[email protected]</saml:AttributeValue></saml:Attribute><saml:Attribute Name="name" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">mynameinsp</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>

After replaceChild (Broken XML)

<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://idp.example.com/metadata</saml:Issuer><samlp:Status xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><saml:Assertion ID="_519cc29f-aba8-4517-aa4b-6ce2da634a47" Version="2.0" IssueInstant="2024-10-02T23:03:36.499Z" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"><saml:Issuer>https://idp.example.com/metadata</saml:Issuer><saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">[email protected]</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="2024-10-02T23:08:36.499Z" Recipient="https://sp.example.org/metadata" InResponseTo="_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="2024-10-02T23:03:36.499Z" NotOnOrAfter="2024-10-02T23:08:36.499Z"><saml:AudienceRestriction><saml:Audience>https://sp.example.org/metadata</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AttributeStatement><saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">[email protected]</saml:AttributeValue></saml:Attribute><saml:Attribute Name="name" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">mynameinsp</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion>

},
});
const spWithClockDrift = serviceProvider({ ...defaultSpConfig, clockDrifts: [-2000, 2000] });
Expand Down Expand Up @@ -408,6 +418,28 @@ test('send response with signed assertion and parse it', async t => {
t.is(extract.response.inResponseTo, 'request_id');
});

// + XML prolog
test('send response with [prolog + custom template] signed assertion and parse it', async t => {
//const requestInfo = { extract: { request: { id: 'request_id' } } };
// sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
const user = { email: '[email protected]'};
const { id, context: SAMLResponse } = await idpcustomPrologNoEncrypt.createLoginResponse(
spNoAssertSignCustomConfig,
sampleRequestInfo,
'post',
user,
createTemplateCallback(idpcustomPrologNoEncrypt, spNoAssertSignCustomConfig, binding.post, user),
);
// receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
const { samlContent, extract } = await spNoAssertSignCustomConfig.parseLoginResponse(idpcustomPrologNoEncrypt, 'post', { body: { SAMLResponse } });
t.is(typeof id, 'string');
t.is(samlContent.startsWith(xmlProlog), true);
t.is(samlContent.endsWith('/samlp:Response>'), true);
t.is(extract.nameID, '[email protected]');
t.is(extract.attributes.name, 'mynameinsp');
t.is(extract.attributes.mail, '[email protected]');
});

// + REDIRECT
test('send response with signed assertion by redirect and parse it', async t => {
// sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
Expand Down