I have a working SAML2.0 Single Sign On system built in coffeescript on express / nodejs.
I use SSOCircle to verify my SSO and can successfully authenticate there with an HTTP-POST or HTTP-REDIRECT binding. So far so good.
Now I need to digitally sign my authentication request.
For this, I used the nodejs xml-crytpo library . This causes SAML to look like this:
<?xml version="1.0"?> <samlp:AuthnRequest Version="2.0" ID="_1C8A2EFA-2644-4330-9A36-2B45547ECFAF" IssueInstant="2014-10-14T16:06:27.769Z" Destination="https://idp.ssocircle.com:443/sso/SSOPOST/metaAlias/ssocircle" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"> <saml:Issuer>http://app.localhost</saml:Issuer> <samlp:NameIDPolicy AllowCreate="true" /> <Signature xmlns="http://www.w3.org/2000/09/xmldsig#"> <SignedInfo> <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /> <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" /> <Reference URI="#_1C8A2EFA-2644-4330-9A36-2B45547ECFAF"> <Transforms> <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /> </Transforms> <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" /> <DigestValue>m4qHiXM82TuxY31l6+QSECHEHc0=</DigestValue> </Reference> </SignedInfo> <SignatureValue>fps0I0Rp02qDK0BPTK7Lh+ ...</SignatureValue> <KeyInfo> <X509Data>MIICuDCCAaCgAwIBAgIQEVFtJk ...</X509Data> </KeyInfo> </Signature> </samlp:AuthnRequest>
The certificate information contained in the digital signature has been deleted for brevity, but it comes from the pem file that I created locally (self-signed).
I also updated the service provider's metadata provided in SSOCircle to include the SAML AuthnRequestsSigned and WantAssertionsSigned , for example:
<md:EntityDescriptor entityID="http://app.localhost" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"> <md:SPSSODescriptor AuthnRequestsSigned="true" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> ... </md:SPSSODescriptor> </md:EntityDescriptor>
Sending my AuthnRequest without a digital signature works fine. When I enable the digital signature, it fails with a status code of 500 and the general message "Invalid request."
How to correctly sign a number in an AuthnRequest figure AuthnRequest that it works against SSOCircle?
Thanks.
Edit 1 - Add certificate key to SP metadata
Following the recommendations in the comments, I have now added the appropriate KeyDescriptor attributes for my sp-metadata to enable an open, self-signed certificate certificate.
<md:EntityDescriptor entityID="http://app.localhost" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"> <md:SPSSODescriptor AuthnRequestsSigned="true" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> <md:KeyDescriptor use="signing"> <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> <ds:X509Data> <ds:X509SubjectName>CN=app.localhost</ds:X509SubjectName> <ds:X509Certificate>MIICu ... wg==</ds:X509Certificate> </ds:X509Data> </ds:KeyInfo> </md:KeyDescriptor> <md:KeyDescriptor use="encryption"> <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> <ds:X509Data> <ds:X509Certificate>MIICu ... wg==</ds:X509Certificate> </ds:X509Data> </ds:KeyInfo> </md:KeyDescriptor> ... </md:EntityDescriptor>
I have included two KeyDescriptor elements, one of which indicates use = signing , and the other is use = encryption , although I think the latter is redundant (since I am doing TLS) and IdP will be ignored.
This does not work, and I get the same error:
Reason: SAML request is invalid.
Edit 2 - Saturated Certificates
Theory I cannot use a self-signed certificate. Instead, I should use a certificate that is known and trusted by SSOCircle
Result . This has been disproved. You can use a self-signed certificate as much as the correct public key is included in sp-metadata.
Edit 3 - Added correct Transform Algorithm
Now I updated my signature to indicate the conversion algorithm #enveloped-signature .
<Transforms> <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" /> </Transforms>
I also found that the correct x509 certificate public key structure was as follows (note the internal X509Certificate element):
<KeyInfo> <X509Data> <X509Certificate>MIID...MMxZ</X509Certificate> </X509Data> </KeyInfo>
Now I use the TestShib website, as it allows access to IdP logs that provide more detailed and useful information.
Edit 4 - Reordering SAML Elements to Match Schema
According to the answer given by @Hos (see below), the order of SAML elements should be consistent with the Schema. Therefore, the signature element should appear immediately after the issuer element, for example:
<samlp:AuthnRequest Version="2.0" ID="_782F6F1E-1B80-4D7D-B26C-AC85030D9300" IssueInstant="2014-10-28T11:45:49.412Z" Destination="https://idp.ssocircle.com:443/sso/SSOPOST/metaAlias/ssocircle" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"> <saml:Issuer>http://app.localhost9de83841</saml:Issuer> <Signature xmlns="http://www.w3.org/2000/09/xmldsig#"> <SignedInfo> <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /> <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" /> <Reference URI="#_782F6F1E-1B80-4D7D-B26C-AC85030D9300"> <Transforms> <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" /> </Transforms> <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" /> <DigestValue>pjXEtbAFMJA3hWhD/f6lxJTshTg=</DigestValue> </Reference> </SignedInfo> <SignatureValue>FY1...Qg==</SignatureValue> <KeyInfo> <X509Data> <X509Certificate>MIID...MMxZ</X509Certificate> </X509Data> </KeyInfo> </Signature> <samlp:NameIDPolicy AllowCreate="true" /> </samlp:AuthnRequest>
This still does not work SSOCircle with an opaque error of 500. However, TestShib shows the following log:
11:02:37.292 - DEBUG [edu.internet2.middleware.shibboleth.common.security.MetadataPKIXValidationInformationResolver:631] - Write lock over cache acquired 11:02:37.292 - DEBUG [edu.internet2.middleware.shibboleth.common.security.MetadataPKIXValidationInformationResolver:634] - Added new PKIX info to entity cache with key: [http://app.localhost9de83841,{urn:oasis:names:tc:SAML:2.0:metadata}SPSSODescriptor,urn:oasis:names:tc:SAML:2.0:protocol,SIGNING] 11:02:37.292 - DEBUG [edu.internet2.middleware.shibboleth.common.security.MetadataPKIXValidationInformationResolver:637] - Write lock over cache released 11:02:37.293 - WARN [edu.internet2.middleware.shibboleth.idp.profile.saml2.SSOProfileHandler:406] - Message did not meet security requirements org.opensaml.ws.security.SecurityPolicyException: **Validation of protocol message signature failed**
When I read this, the public key used to verify the signature was extracted from sp-metadata:
Added new PKIX info to entity cache with key: [http://app.localhost9de83841,{urn:oasis:names:tc:SAML:2.0:metadata}SPSSODescriptor,urn:oasis:names:tc:SAML:2.0:protocol,SIGNING]
However, the actual verification failed, apparently because the digest values do not match. This means that the digest calculated using xmlcrypto is different from the digest calculated by ShibTest. This can only be for one of two reasons:
- The algorithms used by xmlcrypto and TestShib to generate a digest are somehow different
- Public keys do not match.
An instant look at the keys shows that they are the same. So, here is my code if you can notice something.
Note This code is based on nodejs xmlbuilder and xml-crytpo )
getSamlRequest:(idpUrl, requestId, next)-> request = @xmlbuilder.create 'samlp:AuthnRequest': '@xmlns:samlp':'urn:oasis:names:tc:SAML:2.0:protocol' '@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion' '@Version': '2.0' '@ID': requestId '@IssueInstant': (new Date()).toISOString() '@Destination': idpUrl 'saml:Issuer': '@@spEntityId' ,null ,headless: true request.comment 'insert-signature' request.element 'samlp:NameIDPolicy': '@AllowCreate': 'true' saml = request.end() #Your self-signed pem file that contains both public and private keys. #The public key must also be included in your sp-metadata certFilePath = "certs/sp-certificate.pem" @fs.readFile certFilePath, (err, certificate)=> signer = new @xmlcrypto.SignedXml() signer.signingKey = certificate signer.addReference "//*[local-name(.)='AuthnRequest']", ['http://www.w3.org/2000/09/xmldsig#enveloped-signature'] signer.keyInfoProvider = new => getKeyInfo: (key)=> public_key = /-----BEGIN CERTIFICATE-----([^-]*)-----END CERTIFICATE-----/g.exec(key)[1].replace /[\r\n|\n]/g, '' "<X509Data><X509Certificate>#{public_key}</X509Certificate></X509Data>" signer.computeSignature saml signature = signer.getSignatureXml() signed = saml.replace '<!-- insert-signature -->', signature return next null, signed
Edit 5 - Request Tracing
The following is a list of requests made during a SAML handshake using SSOCircle. The result was generated by the nodejs request module in conjunction with request-debug .
I have included some clarifications below this trace output.
{ request: { debugId: 17, uri: 'http://localhost:8082/security/sso/subscription/dev2/096b75a2-4a55-4eec-83c8-d2509e948b07', method: 'GET', headers: { host: 'localhost:8082' } } } { response: { debugId: 17, headers: { 'transfer-encoding': 'chunked', 'content-type': 'application/json; charset=utf-8', server: 'Microsoft-HTTPAPI/2.0', 'access-control-allow-origin': '*', 'access-control-allow-headers': 'origin, x-requested-with, accept, content-type', 'access-control-allow-methods': 'GET, POST, PATCH, PUT, DELETE', date: 'Wed, 05 Nov 2014 16:55:49 GMT' }, statusCode: 200, body: '{"RequestId":"_F7DDDD24-32C6-420E-A550-95872D30033B","SubscriptionName":"Develop-2","SubscriptionURL":"dev2","IdentityProviderCertificateFile":"ssocircle.cer","IdentityProviderDestinationUrl":"https://idp.ssocircle.com:443/sso/SSOPOST/metaAlias/ssocircle","SAMLBinding":"HttpPost"}' } } { request: { debugId: 18, uri: 'https://idp.ssocircle.com:443/sso/SSOPOST/metaAlias/ssocircle', method: 'POST', headers: { host: 'idp.ssocircle.com:443', 'content-type': 'application/x-www-form-urlencoded; charset=utf-8', 'content-length': 3492 }, body: 'SAMLRequest=%2BPFRyYW5zZm9ybXM%2BPFRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIiAvPjwvVHJhbnNmb3Jtcz48RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiIC8%2BPERpZ2VzdFZhbHVlPjBLdUtNVG1Zc29Wb1BlTXhtdDhuNml2M3RxZz08L0RpZ2VzdFZhbHVlPjwvUmVmZXJlbmNlPjwvU2lnbmVkSW5mbz48U2lnbmF0dXJlVmFsdWU%%2BPC9LZXlJbmZvPjwvU2lnbmF0dXJlPjxzYW1scDpOYW1lSURQb2xpY3kgQWxsb3dDcmVhdGU9InRydWUiLz48L3NhbWxwOkF1dGhuUmVxdWVzdD4%3D' } } { response: { debugId: 18, headers: { server: '"SSOCircle Web Server"', date: 'Wed, 05 Nov 2014 16:55:48 GMT', 'content-type': 'text/html;charset=UTF-8', connection: 'close', 'set-cookie': [Object], 'transfer-encoding': 'chunked' }, statusCode: 500, body: '\n\n\n\n<html><head><title>\n Error Page\n \n </title>\n <link href="/css/bx.css" rel="stylesheet" type="text/css" /></head>\n <body>\n <div id="myheader">\n <h1><img src="/logo.png" alt="IDPee - Put your LOGO here" height="65" width="180"> </h1>\n </div>\n <div id="mycontent">\n <div id="mynav">\n <ul>\n \n </ul>\n </div>\n <div id="mybox">\n <p>\n \n <h2>Error occurred</h2>\n<p>\nReason: Invalid signature in Request.\n</p>\n\n \n </p>\n </div>\n \n </div>\n <div id="myfooter">\n \n Copyright © SSOCircle/IDPee.com\n \n \n </div>\n </body>\n</html>\n\n\n\n' } } error: message=Something went wrong whilst trying to automatically log you in. Please try again., name=SSOFailure, message=An error occurred whilst processing the SSO Request., name=SSORequestFailure, message=The IdP failed to successfully process the SAMLRequest and returned an statuscode of 500., name=SamlRequestInvalid, url=http://app.localhost/security/saml2/request 2BPFRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIiAvPjwvVHJhbnNmb3Jtcz48RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiIC8% 2BPERpZ2VzdFZhbHVlPjBLdUtNVG1Zc29Wb1BlTXhtdDhuNml2M3RxZz08L0RpZ2VzdFZhbHVlPjwvUmVmZXJlbmNlPjwvU2lnbmVkSW5mbz48U2lnbmF0dXJlVmFsdWU %% 2BPC9LZXlJbmZvPjwvU2lnbmF0dXJlPjxzYW1scDpOYW1lSURQb2xpY3kgQWxsb3dDcmVhdGU9InRydWUiLz48L3NhbWxwOkF1dGhuUmVxdWVzdD4% 3D'}} { request: { debugId: 17, uri: 'http://localhost:8082/security/sso/subscription/dev2/096b75a2-4a55-4eec-83c8-d2509e948b07', method: 'GET', headers: { host: 'localhost:8082' } } } { response: { debugId: 17, headers: { 'transfer-encoding': 'chunked', 'content-type': 'application/json; charset=utf-8', server: 'Microsoft-HTTPAPI/2.0', 'access-control-allow-origin': '*', 'access-control-allow-headers': 'origin, x-requested-with, accept, content-type', 'access-control-allow-methods': 'GET, POST, PATCH, PUT, DELETE', date: 'Wed, 05 Nov 2014 16:55:49 GMT' }, statusCode: 200, body: '{"RequestId":"_F7DDDD24-32C6-420E-A550-95872D30033B","SubscriptionName":"Develop-2","SubscriptionURL":"dev2","IdentityProviderCertificateFile":"ssocircle.cer","IdentityProviderDestinationUrl":"https://idp.ssocircle.com:443/sso/SSOPOST/metaAlias/ssocircle","SAMLBinding":"HttpPost"}' } } { request: { debugId: 18, uri: 'https://idp.ssocircle.com:443/sso/SSOPOST/metaAlias/ssocircle', method: 'POST', headers: { host: 'idp.ssocircle.com:443', 'content-type': 'application/x-www-form-urlencoded; charset=utf-8', 'content-length': 3492 }, body: 'SAMLRequest=%2BPFRyYW5zZm9ybXM%2BPFRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIiAvPjwvVHJhbnNmb3Jtcz48RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiIC8%2BPERpZ2VzdFZhbHVlPjBLdUtNVG1Zc29Wb1BlTXhtdDhuNml2M3RxZz08L0RpZ2VzdFZhbHVlPjwvUmVmZXJlbmNlPjwvU2lnbmVkSW5mbz48U2lnbmF0dXJlVmFsdWU%%2BPC9LZXlJbmZvPjwvU2lnbmF0dXJlPjxzYW1scDpOYW1lSURQb2xpY3kgQWxsb3dDcmVhdGU9InRydWUiLz48L3NhbWxwOkF1dGhuUmVxdWVzdD4%3D' } } { response: { debugId: 18, headers: { server: '"SSOCircle Web Server"', date: 'Wed, 05 Nov 2014 16:55:48 GMT', 'content-type': 'text/html;charset=UTF-8', connection: 'close', 'set-cookie': [Object], 'transfer-encoding': 'chunked' }, statusCode: 500, body: '\n\n\n\n<html><head><title>\n Error Page\n \n </title>\n <link href="/css/bx.css" rel="stylesheet" type="text/css" /></head>\n <body>\n <div id="myheader">\n <h1><img src="/logo.png" alt="IDPee - Put your LOGO here" height="65" width="180"> </h1>\n </div>\n <div id="mycontent">\n <div id="mynav">\n <ul>\n \n </ul>\n </div>\n <div id="mybox">\n <p>\n \n <h2>Error occurred</h2>\n<p>\nReason: Invalid signature in Request.\n</p>\n\n \n </p>\n </div>\n \n </div>\n <div id="myfooter">\n \n Copyright © SSOCircle/IDPee.com\n \n \n </div>\n </body>\n</html>\n\n\n\n' } } error: message=Something went wrong whilst trying to automatically log you in. Please try again., name=SSOFailure, message=An error occurred whilst processing the SSO Request., name=SSORequestFailure, message=The IdP failed to successfully process the SAMLRequest and returned an statuscode of 500., name=SamlRequestInvalid, url=http://app.localhost/security/saml2/request
Request debugId = 17 . This is a call made by my client on my nodejs proxy that hits the SAML handshake.
Reply debugId = 17 : 200 OK response from the proxy server back to the client. Now the client is waiting for the redirect after completing the SAML handshake.
Request debugId = 18 : SAMLRequest is sent to the SAML SSOCircle endpoint
Answer debugId = 18 : Internal response to an internal server error from SSOCircle plus the page payload with an HTML error.