From 0378b532c36108de37eae799a9f015180afbe631 Mon Sep 17 00:00:00 2001 From: Jonathon Duerig Date: Mon, 8 Sep 2014 09:22:55 -0600 Subject: [PATCH] Add a new optional authentication protocol. If the new authentication protocol is used, the tool using the signer will not only get a speaks-for credential, but an authentication token which allows it to ensure that the new credential was just authorized by the user. This only works in the postMessage flow and not in the new PHP/post flow. --- geni-auth.js | 64 +++++++++++++++++++++++++++++++++++---- xml-signer.js | 84 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 136 insertions(+), 12 deletions(-) diff --git a/geni-auth.js b/geni-auth.js index 40e12c5..785de9f 100755 --- a/geni-auth.js +++ b/geni-auth.js @@ -3,8 +3,29 @@ var genilib = {}; genilib.trustedHost = 'https://www.emulab.net'; genilib.trustedPath = '/protogeni/speaks-for/index.html'; -genilib.authorize = function(id, cert, callback, defaultMA, userBundle) +genilib.authorize = function(idOrObject, cert, callback, defaultMA, userBundle) { + var id; + var authenticate; + + // If the user invokes with just a single object, it is a dictionary of options + if (typeof(idOrObject) === 'object') + { + id = idOrObject.id; + cert = idOrObject.toolCertificate; + callback = idOrObject.complete; + defaultMA = idOrObject.defaultMA; + userBundle = idOrObject.userBundle; + authenticate = idOrObject.authenticate; + console.log('set args based on dictionary'); + } + else + { + id = idOrObject; + } + + shouldAuthenticate = (authenticate !== undefined) + var wrapper = {}; wrapper.other = window.open(genilib.trustedHost + genilib.trustedPath + @@ -21,7 +42,8 @@ genilib.authorize = function(id, cert, callback, defaultMA, userBundle) { data = { certificate: cert, - tool: true + tool: true, + auth: shouldAuthenticate }; if (userBundle) { @@ -40,19 +62,49 @@ genilib.authorize = function(id, cert, callback, defaultMA, userBundle) event.origin === genilib.trustedHost && event.data.id && event.data.id === id && event.data.credential) { + wrapper.credential = event.data.credential; + wrapper.credentialId = event.data.id; + wrapper.authenticationToken = event.data.userToken; + wrapper.encryptedCredential = event.data.encryptedCredential; + if (shouldAuthenticate && event.data.userCertificate) + { + console.log('authenticating'); + authenticate(event.data.userCertificate, wrapper.authenticationToken, wrapper.toolAuthSuccess, wrapper.toolAuthFailure); + } + else + { + console.log('sendAck'); + wrapper.sendAck(); + } + } + }; + + wrapper.sendAck = function () { window.removeEventListener('message', wrapper.listener, false); // wrapper.other.removeEventListener('close', wrapper.close, false); data = { - id: event.data.id, + id: wrapper.credentialId, ack: true }; console.log('Sending ack to ' + genilib.trustedHost); wrapper.other.postMessage(data, genilib.trustedHost); - callback(event.data.credential); - } - }; + callback(wrapper.credential, wrapper.encryptedCredential); + }; + + wrapper.toolAuthSuccess = function (toolToken) { + data = { + id: wrapper.credentialId, + toolToken: toolToken + }; + console.log('Sending tool token to ' + genilib.trustedHost); + wrapper.other.postMessage(data, genilib.trustedHost); + }; + + wrapper.toolAuthFailure = function () { + wrapper.sendAck(); + }; wrapper.close = function (event) { window.removeEventListener('message', wrapper.message, false); diff --git a/xml-signer.js b/xml-signer.js index d2a47fa..63d9b1b 100755 --- a/xml-signer.js +++ b/xml-signer.js @@ -87,6 +87,7 @@ function ($, _, error, forge, sigExport, xmlText, noKeyText, authorizeText) { if (window.opener) { window.addEventListener('message', messageToolCert, false); window.opener.postMessage(data, '*'); + console.log('posting ready message'); } else { var fake_event = { data: { certificate: myCert, tool: true } } messageToolCert(fake_event); @@ -185,6 +186,19 @@ function ($, _, error, forge, sigExport, xmlText, noKeyText, authorizeText) { return result; } + function wrapCertificates(list) + { + var result = ""; + var i = 0; + for (; i < list.length; i += 1) + { + result += "-----BEGIN CERTIFICATE-----\n"; + result += list[i]; + result += "-----END CERTIFICATE-----\n"; + } + return result; + } + function extractKey(pem) { var inKey = false; @@ -339,6 +353,10 @@ function ($, _, error, forge, sigExport, xmlText, noKeyText, authorizeText) { }; } + var speaksForCredential; + var decryptedPrivateKey; + var userSecret; + function clickSign(event) { event.preventDefault(); @@ -358,6 +376,7 @@ function ($, _, error, forge, sigExport, xmlText, noKeyText, authorizeText) { try { var decrypted = forge.pki.decryptRsaPrivateKey(encryptedKey, password); + decryptedPrivateKey = decrypted; var sig = new SignedXml(); sig.addReference("//*[local-name(.)='credential']"); sig.signingKey = decrypted; @@ -367,12 +386,17 @@ function ($, _, error, forge, sigExport, xmlText, noKeyText, authorizeText) { sig.keyInfoProvider = new GENIKeyInfo(certList); } sig.computeSignature(xml); + speaksForCredential = sig.getSignedXml(); + var userToken = generateUserToken(speakerCert); var data = { id: toolId, - credential: sig.getSignedXml() + credential: speaksForCredential, + userToken: userToken, + userCertificate: wrapCertificates(certList) }; if (window.opener) { window.opener.postMessage(data, '*'); + console.log('posting credential'); } else { // This can't be the only way to redirect with POST, sigh... var backForm = document.createElement('form'); @@ -433,15 +457,62 @@ function ($, _, error, forge, sigExport, xmlText, noKeyText, authorizeText) { function messageAck(event) { - if (event.source === window.opener && event.data && event.data.id && - event.data.id === toolId && event.data.ack) - { - window.close(); - } + console.log('messageAck'); + if (event.source === window.opener && event.data && event.data.id && + event.data.id === toolId) + { + if (event.data.ack) + { + window.close(); + } + else if (event.data.toolToken) + { + sendAuthorizationToken(event.data.toolToken); + } + } + } + + function sendAuthorizationToken(toolToken) + { + var encryptedCredential = encryptWithSecrets(userSecret, getToolSecret(toolToken)); + var data = { + id: toolId, + encryptedCredential: encryptedCredential, + credential: speaksForCredential + }; + window.opener.postMessage(data, '*'); + console.log('posting auth token'); + } + + function generateUserToken(speakerCert) + { + var secretBytes = forge.random.getBytesSync(32); + userSecret = forge.util.bytesToHex(secretBytes); + var p7 = forge.pkcs7.createEnvelopedData(); + var cert = forge.pki.certificateFromPem(wrapCertificates([speakerCert])); + p7.addRecipient(cert); + p7.content = forge.util.createBuffer(userSecret); + p7.encrypt(); + return forge.pkcs7.messageToPem(p7); + } + + function getToolSecret(toolToken) + { + var p7 = forge.pkcs7.messageFromPem(toolToken); + p7.decrypt(p7.recipients[0], decryptedPrivateKey); + return p7.content.toString(); + } + + function encryptWithSecrets(userSecret, toolSecret) + { + var md = forge.md.sha256.create(); + md.update(speaksForCredential + userSecret + toolSecret); + return md.digest().toHex(); } function messageCert(event) { + console.log('messageCert'); if (event.source === certWindow && event.data && event.data.authority) { if (event.data.certificate) @@ -483,6 +554,7 @@ function ($, _, error, forge, sigExport, xmlText, noKeyText, authorizeText) { credential: cred }; window.opener.postMessage(data, '*'); + console.log('posting reflect cred'); window.removeEventListener('message', messageCert); window.addEventListener('message', messageAck); }