diff options
22 files changed, 2237 insertions, 0 deletions
diff --git a/examples/pki/certs/cacert.pem b/examples/pki/certs/cacert.pem new file mode 100644 index 0000000..8cf663c --- /dev/null +++ b/examples/pki/certs/cacert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC0TCCAjqgAwIBAgIJAP2TNFqmE1KUMA0GCSqGSIb3DQEBBQUAMIGeMQowCAYD +VQQFEwE1MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1bm55 +dmFsZTESMBAGA1UEChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTElMCMG +CSqGSIb3DQEJARYWa2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxMLU2Vs +ZiBTaWduZWQwIBcNMTIxMTExMTA1NDA2WhgPMjA3MTA1MDYxMDU0MDZaMIGeMQow +CAYDVQQFEwE1MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1 +bm55dmFsZTESMBAGA1UEChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTEl +MCMGCSqGSIb3DQEJARYWa2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxML +U2VsZiBTaWduZWQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMXgnd5wlHAp +GxZ58LrpEkHU995lT9PxtMgkp0tpFhg7R5HQw9K7TfQk5NHB28hNzf8UE/c0z2pJ +XggPnAzvdx27NQeJGX5CWsi6fITZ8vH/+SxgfxxC+CE/6BkDpzw21MgBtq11vWL7 +XVaxNeU12Ax889U66i3CrObuCYt2mbpzAgMBAAGjEzARMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEFBQADgYEAkFIbnr2/0/XWp+f80Gl6GAC7tdmZFlT9udVF +q794rXyMlYY64pq34SzfQAn+4DztT4B9yzrTx03tLNr6Uf+5TS+ubcwG41UBBMs/ +Icf9zBMRqr+IXhijS49gQ7dPjqNTCqX+6ILbRWjdXP15ZWymI3ayQL/CMwFt/E+0 +kT6MLes= +-----END CERTIFICATE----- diff --git a/examples/pki/certs/middleware.pem b/examples/pki/certs/middleware.pem new file mode 100644 index 0000000..ae5b8db --- /dev/null +++ b/examples/pki/certs/middleware.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIICoTCCAgoCARAwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV +BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQK +EwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr +ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0x +MjExMTExMDU0MDZaGA8yMDcxMDUwNjEwNTQwNlowgZAxCzAJBgNVBAYTAlVTMQsw +CQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3Rh +Y2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv +cGVuc3RhY2sub3JnMRIwEAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEB +BQADgY0AMIGJAoGBALVu4bjaOH33yAx0WdpEqj4UDVsLxVjWxEpIbOlDlc6IfJd+ +cUriQtxf6ahjxtzLPERS81SnwZmrICWZngbOn733pULMTZktTJH+o7C74NdKwUSN +xjlCeWUy+FqIQoje4ygoJRPpMdkp1wHNO0ZERwRN9e8M5TIlx/LRtk+q8bT5AgMB +AAEwDQYJKoZIhvcNAQEFBQADgYEAcp9ancue9Oq+MkaPucCrIqFhiUsdUThulJlB +etPpUDGgStBSHgze/oxG2+flIjRoI6gG9Chfw//vWHOwDT7N32AHSgaI4b8/k/+s +hAV2khYkV4PW2oS1TfeU/vxQzXbgApqhLBNqfFmJVW48aGAr/aqsJi3MYWN3269+ +6vChaVw= +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALVu4bjaOH33yAx0 +WdpEqj4UDVsLxVjWxEpIbOlDlc6IfJd+cUriQtxf6ahjxtzLPERS81SnwZmrICWZ +ngbOn733pULMTZktTJH+o7C74NdKwUSNxjlCeWUy+FqIQoje4ygoJRPpMdkp1wHN +O0ZERwRN9e8M5TIlx/LRtk+q8bT5AgMBAAECgYAmwq6EYFJrTvE0//JmN/8qzfvg +dI5PoWpD+F8UInUxr2T2tHOdrOLd07vGVrKYXu7cJeCIOGKa4r02azAggioL/nE9 +FgPpqEC+QROvLuhFsk1gLZ2pGQ06sveKZVMH22h59BKZkYlhjh5qd4vlmhPqkmPp +gdXj7ZjDCJhhQdFVkQJBANp18k2mVksn8q29LMieVTSIZNN3ucDA1QHbim+3fp/O +GxCzU7Mv1Xfnu1zoRFu5/sF3YG0Zy3TGPDrEljBC3rUCQQDUnBjVFXL35OkBZqXW +taJPzGbsPoqAO+Ls2juS97zNzeGxUNhvcKuEvHO63PXqDxp1535DpvJEBN1rT2FF +iaO1AkEAt/QTWWFUTqrPxY6DNFdm5fpn9E1fg7icZJkKBDJeFJCH59MpCryfovzl +n0ERtq9ynlQ4RQYwdR8rvkylLvRP9QJAOiXHFOAc5XeR0nREfwiGL9TzgUFJl/DJ +C4ZULMnctVzNkTVPPItQHal87WppR26CCiUZ/161e6zo8eRv8hjG0QJABWqfYQuK +dWH8nxlXS+NFUDbsCdL+XpOVE7iEH7hvSw/A/kz40mLx8sDp/Fz1ysrogR/L+NGC +Vrlwm4q/WYJO0Q== +-----END PRIVATE KEY----- diff --git a/examples/pki/certs/signing_cert.pem b/examples/pki/certs/signing_cert.pem new file mode 100644 index 0000000..1491b55 --- /dev/null +++ b/examples/pki/certs/signing_cert.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICoDCCAgkCAREwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV +BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQK +EwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr +ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0x +MjExMTExMDU0MDZaGA8yMDcxMDUwNjEwNTQwNlowgY8xCzAJBgNVBAYTAlVTMQsw +CQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3Rh +Y2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv +cGVuc3RhY2sub3JnMREwDwYDVQQDEwhLZXlzdG9uZTCBnzANBgkqhkiG9w0BAQEF +AAOBjQAwgYkCgYEAuoQC6IBqMxC5845c/ZkLsdcQbTHqIpYJHEkwEoxyeEjwiGFf +iZmiZ91pSFNc9MfjdJnN+be/ndVS19w1nrrJvV/udVsf6JZWkTPX5HyxnllwznCH +pP7gfvMZzGsqzWlSdiD6mcRbCYRX9hCCauG3jhCtISINCVYMYQGH6QSib9sCAwEA +ATANBgkqhkiG9w0BAQUFAAOBgQBCssELi+1RSjEmzeqSnpgUqmtpvB9oxbcwl+xH +rIrYvqMU6pV2aSxgLDqpGjjusLHUau9Bmu3Myc/fm9/mlPUQHNj0AWl8vvfSlq1b +vsWMUa1h4UFlPWoF2DIUFd+noBxe5CbcLUV6K0oyJAcPO433OyuGl5oQkhxmoy1J +w59KRg== +-----END CERTIFICATE----- diff --git a/examples/pki/certs/ssl_cert.pem b/examples/pki/certs/ssl_cert.pem new file mode 100644 index 0000000..0a0bc21 --- /dev/null +++ b/examples/pki/certs/ssl_cert.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICoTCCAgoCARAwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV +BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQK +EwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr +ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0x +MjExMTExMDU0MDZaGA8yMDcxMDUwNjEwNTQwNlowgZAxCzAJBgNVBAYTAlVTMQsw +CQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3Rh +Y2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv +cGVuc3RhY2sub3JnMRIwEAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEB +BQADgY0AMIGJAoGBALVu4bjaOH33yAx0WdpEqj4UDVsLxVjWxEpIbOlDlc6IfJd+ +cUriQtxf6ahjxtzLPERS81SnwZmrICWZngbOn733pULMTZktTJH+o7C74NdKwUSN +xjlCeWUy+FqIQoje4ygoJRPpMdkp1wHNO0ZERwRN9e8M5TIlx/LRtk+q8bT5AgMB +AAEwDQYJKoZIhvcNAQEFBQADgYEAcp9ancue9Oq+MkaPucCrIqFhiUsdUThulJlB +etPpUDGgStBSHgze/oxG2+flIjRoI6gG9Chfw//vWHOwDT7N32AHSgaI4b8/k/+s +hAV2khYkV4PW2oS1TfeU/vxQzXbgApqhLBNqfFmJVW48aGAr/aqsJi3MYWN3269+ +6vChaVw= +-----END CERTIFICATE----- diff --git a/examples/pki/cms/auth_token_revoked.json b/examples/pki/cms/auth_token_revoked.json new file mode 100644 index 0000000..92c6922 --- /dev/null +++ b/examples/pki/cms/auth_token_revoked.json @@ -0,0 +1 @@ +{"access": {"serviceCatalog": [{"endpoints": [{"adminURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", "region": "regionOne", "internalURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", "publicURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a"}], "endpoints_links": [], "type": "volume", "name": "volume"}, {"endpoints": [{"adminURL": "http://127.0.0.1:9292/v1", "region": "regionOne", "internalURL": "http://127.0.0.1:9292/v1", "publicURL": "http://127.0.0.1:9292/v1"}], "endpoints_links": [], "type": "image", "name": "glance"}, {"endpoints": [{"adminURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", "region": "regionOne", "internalURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", "publicURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a"}], "endpoints_links": [], "type": "compute", "name": "nova"}, {"endpoints": [{"adminURL": "http://127.0.0.1:35357/v2.0", "region": "RegionOne", "internalURL": "http://127.0.0.1:35357/v2.0", "publicURL": "http://127.0.0.1:5000/v2.0"}], "endpoints_links": [], "type": "identity", "name": "keystone"}],"token": {"expires": "2012-06-02T14:47:34Z", "id": "placeholder", "tenant": {"enabled": true, "description": null, "name": "tenant_name1", "id": "tenant_id1"}}, "user": {"username": "revoked_username1", "roles_links": ["role1","role2"], "id": "revoked_user_id1", "roles": [{"name": "role1"}, {"name": "role2"}], "name": "revoked_username1"}}} diff --git a/examples/pki/cms/auth_token_revoked.pem b/examples/pki/cms/auth_token_revoked.pem new file mode 100644 index 0000000..0bd7a70 --- /dev/null +++ b/examples/pki/cms/auth_token_revoked.pem @@ -0,0 +1,42 @@ +-----BEGIN CMS----- +MIIHVgYJKoZIhvcNAQcCoIIHRzCCB0MCAQExCTAHBgUrDgMCGjCCBeQGCSqGSIb3 +DQEHAaCCBdUEggXReyJhY2Nlc3MiOiB7InNlcnZpY2VDYXRhbG9nIjogW3siZW5k +cG9pbnRzIjogW3siYWRtaW5VUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc2L3Yx +LzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwgInJlZ2lvbiI6ICJy +ZWdpb25PbmUiLCAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc2 +L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwgInB1YmxpY1VS +TCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzYvdjEvNjRiNmYzZmJjYzUzNDM1ZThh +NjBmY2Y4OWJiNjYxN2EifV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUi +OiAidm9sdW1lIiwgIm5hbWUiOiAidm9sdW1lIn0sIHsiZW5kcG9pbnRzIjogW3si +YWRtaW5VUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5MjkyL3YxIiwgInJlZ2lvbiI6 +ICJyZWdpb25PbmUiLCAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5 +MjkyL3YxIiwgInB1YmxpY1VSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjkyOTIvdjEi +fV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUiOiAiaW1hZ2UiLCAibmFt +ZSI6ICJnbGFuY2UifSwgeyJlbmRwb2ludHMiOiBbeyJhZG1pblVSTCI6ICJodHRw +Oi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5 +YmI2NjE3YSIsICJyZWdpb24iOiAicmVnaW9uT25lIiwgImludGVybmFsVVJMIjog +Imh0dHA6Ly8xMjcuMC4wLjE6ODc3NC92MS4xLzY0YjZmM2ZiY2M1MzQzNWU4YTYw +ZmNmODliYjY2MTdhIiwgInB1YmxpY1VSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3 +NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSJ9XSwgImVu +ZHBvaW50c19saW5rcyI6IFtdLCAidHlwZSI6ICJjb21wdXRlIiwgIm5hbWUiOiAi +bm92YSJ9LCB7ImVuZHBvaW50cyI6IFt7ImFkbWluVVJMIjogImh0dHA6Ly8xMjcu +MC4wLjE6MzUzNTcvdjIuMCIsICJyZWdpb24iOiAiUmVnaW9uT25lIiwgImludGVy +bmFsVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6MzUzNTcvdjIuMCIsICJwdWJsaWNV +UkwiOiAiaHR0cDovLzEyNy4wLjAuMTo1MDAwL3YyLjAifV0sICJlbmRwb2ludHNf +bGlua3MiOiBbXSwgInR5cGUiOiAiaWRlbnRpdHkiLCAibmFtZSI6ICJrZXlzdG9u +ZSJ9XSwidG9rZW4iOiB7ImV4cGlyZXMiOiAiMjAxMi0wNi0wMlQxNDo0NzozNFoi +LCAiaWQiOiAicGxhY2Vob2xkZXIiLCAidGVuYW50IjogeyJlbmFibGVkIjogdHJ1 +ZSwgImRlc2NyaXB0aW9uIjogbnVsbCwgIm5hbWUiOiAidGVuYW50X25hbWUxIiwg +ImlkIjogInRlbmFudF9pZDEifX0sICJ1c2VyIjogeyJ1c2VybmFtZSI6ICJyZXZv +a2VkX3VzZXJuYW1lMSIsICJyb2xlc19saW5rcyI6IFsicm9sZTEiLCJyb2xlMiJd +LCAiaWQiOiAicmV2b2tlZF91c2VyX2lkMSIsICJyb2xlcyI6IFt7Im5hbWUiOiAi +cm9sZTEifSwgeyJuYW1lIjogInJvbGUyIn1dLCAibmFtZSI6ICJyZXZva2VkX3Vz +ZXJuYW1lMSJ9fX0NCjGCAUkwggFFAgEBMIGkMIGeMQowCAYDVQQFEwE1MQswCQYD +VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1bm55dmFsZTESMBAGA1UE +ChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTElMCMGCSqGSIb3DQEJARYW +a2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxMLU2VsZiBTaWduZWQCAREw +BwYFKw4DAhowDQYJKoZIhvcNAQEBBQAEgYBhV5KrVjcdACPUNafkPY+lgCSlh6uc +N55SATBcQmg1/argEUFg/cx2GcF7ftQV384iGepLEgsq+6om2wPw6DWA0RknpVLJ +vMsHbWdGoXIZ5jRuAQTPtkXcJQOR677baDHvGJ+5zwBBDT2CmN2Tcv348+Xpjp7D +hF/cmAXnYYo00g== +-----END CMS----- diff --git a/examples/pki/cms/auth_token_scoped.json b/examples/pki/cms/auth_token_scoped.json new file mode 100644 index 0000000..16eb644 --- /dev/null +++ b/examples/pki/cms/auth_token_scoped.json @@ -0,0 +1 @@ +{"access": {"serviceCatalog": [{"endpoints": [{"adminURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", "region": "regionOne", "internalURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", "publicURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a"}], "endpoints_links": [], "type": "volume", "name": "volume"}, {"endpoints": [{"adminURL": "http://127.0.0.1:9292/v1", "region": "regionOne", "internalURL": "http://127.0.0.1:9292/v1", "publicURL": "http://127.0.0.1:9292/v1"}], "endpoints_links": [], "type": "image", "name": "glance"}, {"endpoints": [{"adminURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", "region": "regionOne", "internalURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", "publicURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a"}], "endpoints_links": [], "type": "compute", "name": "nova"}, {"endpoints": [{"adminURL": "http://127.0.0.1:35357/v2.0", "region": "RegionOne", "internalURL": "http://127.0.0.1:35357/v2.0", "publicURL": "http://127.0.0.1:5000/v2.0"}], "endpoints_links": [], "type": "identity", "name": "keystone"}],"token": {"expires": "2012-06-02T14:47:34Z", "id": "placeholder", "tenant": {"enabled": true, "description": null, "name": "tenant_name1", "id": "tenant_id1"}}, "user": {"username": "user_name1", "roles_links": ["role1","role2"], "id": "user_id1", "roles": [{"name": "role1"}, {"name": "role2"}], "name": "user_name1"}}} diff --git a/examples/pki/cms/auth_token_scoped.pem b/examples/pki/cms/auth_token_scoped.pem new file mode 100644 index 0000000..529d7f8 --- /dev/null +++ b/examples/pki/cms/auth_token_scoped.pem @@ -0,0 +1,41 @@ +-----BEGIN CMS----- +MIIHQAYJKoZIhvcNAQcCoIIHMTCCBy0CAQExCTAHBgUrDgMCGjCCBc4GCSqGSIb3 +DQEHAaCCBb8EggW7eyJhY2Nlc3MiOiB7InNlcnZpY2VDYXRhbG9nIjogW3siZW5k +cG9pbnRzIjogW3siYWRtaW5VUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc2L3Yx +LzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwgInJlZ2lvbiI6ICJy +ZWdpb25PbmUiLCAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc2 +L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwgInB1YmxpY1VS +TCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzYvdjEvNjRiNmYzZmJjYzUzNDM1ZThh +NjBmY2Y4OWJiNjYxN2EifV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUi +OiAidm9sdW1lIiwgIm5hbWUiOiAidm9sdW1lIn0sIHsiZW5kcG9pbnRzIjogW3si +YWRtaW5VUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5MjkyL3YxIiwgInJlZ2lvbiI6 +ICJyZWdpb25PbmUiLCAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5 +MjkyL3YxIiwgInB1YmxpY1VSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjkyOTIvdjEi +fV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUiOiAiaW1hZ2UiLCAibmFt +ZSI6ICJnbGFuY2UifSwgeyJlbmRwb2ludHMiOiBbeyJhZG1pblVSTCI6ICJodHRw +Oi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5 +YmI2NjE3YSIsICJyZWdpb24iOiAicmVnaW9uT25lIiwgImludGVybmFsVVJMIjog +Imh0dHA6Ly8xMjcuMC4wLjE6ODc3NC92MS4xLzY0YjZmM2ZiY2M1MzQzNWU4YTYw +ZmNmODliYjY2MTdhIiwgInB1YmxpY1VSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3 +NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSJ9XSwgImVu +ZHBvaW50c19saW5rcyI6IFtdLCAidHlwZSI6ICJjb21wdXRlIiwgIm5hbWUiOiAi +bm92YSJ9LCB7ImVuZHBvaW50cyI6IFt7ImFkbWluVVJMIjogImh0dHA6Ly8xMjcu +MC4wLjE6MzUzNTcvdjIuMCIsICJyZWdpb24iOiAiUmVnaW9uT25lIiwgImludGVy +bmFsVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6MzUzNTcvdjIuMCIsICJwdWJsaWNV +UkwiOiAiaHR0cDovLzEyNy4wLjAuMTo1MDAwL3YyLjAifV0sICJlbmRwb2ludHNf +bGlua3MiOiBbXSwgInR5cGUiOiAiaWRlbnRpdHkiLCAibmFtZSI6ICJrZXlzdG9u +ZSJ9XSwidG9rZW4iOiB7ImV4cGlyZXMiOiAiMjAxMi0wNi0wMlQxNDo0NzozNFoi +LCAiaWQiOiAicGxhY2Vob2xkZXIiLCAidGVuYW50IjogeyJlbmFibGVkIjogdHJ1 +ZSwgImRlc2NyaXB0aW9uIjogbnVsbCwgIm5hbWUiOiAidGVuYW50X25hbWUxIiwg +ImlkIjogInRlbmFudF9pZDEifX0sICJ1c2VyIjogeyJ1c2VybmFtZSI6ICJ1c2Vy +X25hbWUxIiwgInJvbGVzX2xpbmtzIjogWyJyb2xlMSIsInJvbGUyIl0sICJpZCI6 +ICJ1c2VyX2lkMSIsICJyb2xlcyI6IFt7Im5hbWUiOiAicm9sZTEifSwgeyJuYW1l +IjogInJvbGUyIn1dLCAibmFtZSI6ICJ1c2VyX25hbWUxIn19fQ0KMYIBSTCCAUUC +AQEwgaQwgZ4xCjAIBgNVBAUTATUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTES +MBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3RhY2sxETAPBgNVBAsT +CEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVuc3RhY2sub3Jn +MRQwEgYDVQQDEwtTZWxmIFNpZ25lZAIBETAHBgUrDgMCGjANBgkqhkiG9w0BAQEF +AASBgFizBVs3dCvlHx04nUHgXHpaA9RL+e3uaaNszK9UwCBpBlv8c6+74sz6i3+G +eYDIpL9bc6QgNJ6cKhmW5yLmS8/+mmAMAcm06bdWc7p/mqC3Ild+xmQ+OHDYyyJg +DvtRUgtidFUCvxne/nwKK0WHJlpY+iwWqel5F+Xqmb8vheb1 +-----END CMS----- diff --git a/examples/pki/cms/auth_token_unscoped.json b/examples/pki/cms/auth_token_unscoped.json new file mode 100644 index 0000000..b2340a7 --- /dev/null +++ b/examples/pki/cms/auth_token_unscoped.json @@ -0,0 +1 @@ +{"access": {"token": {"expires": "2012-08-17T15:35:34Z", "id": "01e032c996ef4406b144335915a41e79"}, "serviceCatalog": {}, "user": {"username": "user_name1", "roles_links": [], "id": "c9c89e3be3ee453fbf00c7966f6d3fbd", "roles": [{'name': 'role1'},{'name': 'role2'},], "name": "user_name1"}}}
\ No newline at end of file diff --git a/examples/pki/cms/auth_token_unscoped.pem b/examples/pki/cms/auth_token_unscoped.pem new file mode 100644 index 0000000..d42d1f1 --- /dev/null +++ b/examples/pki/cms/auth_token_unscoped.pem @@ -0,0 +1,17 @@ +-----BEGIN CMS----- +MIICpwYJKoZIhvcNAQcCoIICmDCCApQCAQExCTAHBgUrDgMCGjCCATUGCSqGSIb3 +DQEHAaCCASYEggEieyJhY2Nlc3MiOiB7InRva2VuIjogeyJleHBpcmVzIjogIjIw +MTItMDgtMTdUMTU6MzU6MzRaIiwgImlkIjogIjAxZTAzMmM5OTZlZjQ0MDZiMTQ0 +MzM1OTE1YTQxZTc5In0sICJzZXJ2aWNlQ2F0YWxvZyI6IHt9LCAidXNlciI6IHsi +dXNlcm5hbWUiOiAidXNlcl9uYW1lMSIsICJyb2xlc19saW5rcyI6IFtdLCAiaWQi +OiAiYzljODllM2JlM2VlNDUzZmJmMDBjNzk2NmY2ZDNmYmQiLCAicm9sZXMiOiBb +eyduYW1lJzogJ3JvbGUxJ30seyduYW1lJzogJ3JvbGUyJ30sXSwgIm5hbWUiOiAi +dXNlcl9uYW1lMSJ9fX0xggFJMIIBRQIBATCBpDCBnjEKMAgGA1UEBRMBNTELMAkG +A1UEBhMCVVMxCzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlTdW5ueXZhbGUxEjAQBgNV +BAoTCU9wZW5TdGFjazERMA8GA1UECxMIS2V5c3RvbmUxJTAjBgkqhkiG9w0BCQEW +FmtleXN0b25lQG9wZW5zdGFjay5vcmcxFDASBgNVBAMTC1NlbGYgU2lnbmVkAgER +MAcGBSsOAwIaMA0GCSqGSIb3DQEBAQUABIGAITCwkW7cAbcWbCBD5GfGMGHB9hP/ +UagaCZ8HFhlzjdQoJjvC+Mtu+3lWlwqPGR8ztY9kBc1401S2qJxD4FGo+M3CkNpF +s0mtaT2PUJfFkDCzHqeBQNFHyZeqLjkPYnokPcw4s3i60DBGTFfAiUT3xumn8a4h +C+zEAee35C/A+Iw= +-----END CMS----- diff --git a/examples/pki/cms/revocation_list.json b/examples/pki/cms/revocation_list.json new file mode 100644 index 0000000..c3401b0 --- /dev/null +++ b/examples/pki/cms/revocation_list.json @@ -0,0 +1 @@ +{"revoked":[{"id":"7acfcfdaf6a14aebe97c61c5947bc4d3","expires":"2012-08-14T17:58:48Z"}]} diff --git a/examples/pki/cms/revocation_list.pem b/examples/pki/cms/revocation_list.pem new file mode 100644 index 0000000..969d10d --- /dev/null +++ b/examples/pki/cms/revocation_list.pem @@ -0,0 +1,12 @@ +-----BEGIN CMS----- +MIIB2QYJKoZIhvcNAQcCoIIByjCCAcYCAQExCTAHBgUrDgMCGjBpBgkqhkiG9w0B +BwGgXARaeyJyZXZva2VkIjpbeyJpZCI6IjdhY2ZjZmRhZjZhMTRhZWJlOTdjNjFj +NTk0N2JjNGQzIiwiZXhwaXJlcyI6IjIwMTItMDgtMTRUMTc6NTg6NDhaIn1dfQ0K +MYIBSTCCAUUCAQEwgaQwgZ4xCjAIBgNVBAUTATUxCzAJBgNVBAYTAlVTMQswCQYD +VQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3RhY2sx +ETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVu +c3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZAIBETAHBgUrDgMCGjANBgkq +hkiG9w0BAQEFAASBgDNDhvViAo8EqTVVvZ00pWUWjajTwoV1w1os1XDJ1XacBUo+ +rsh7gljIIVuvHL2F9C660I5jxhb7QVsTge3CwSiDmexxBAPOs4lNR5hFH7FdT47b +OK2qd0XnRjo5F7odUxIkozuQ/UISaNTPeWxGEMNVhpTXo2Dwn8wN1wrs/Z2E +-----END CMS----- diff --git a/examples/pki/gen_pki.sh b/examples/pki/gen_pki.sh new file mode 100755 index 0000000..9bf6c32 --- /dev/null +++ b/examples/pki/gen_pki.sh @@ -0,0 +1,222 @@ +#!/bin/bash + +# Copyright 2012 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# This script generates the crypto necessary for the SSL tests. + +DIR=`dirname "$0"` +CURRENT_DIR=`cd "$DIR" && pwd` +CERTS_DIR=$CURRENT_DIR/certs +PRIVATE_DIR=$CURRENT_DIR/private +CMS_DIR=$CURRENT_DIR/cms + + +function rm_old { + rm -rf $CERTS_DIR/*.pem + rm -rf $PRIVATE_DIR/*.pem +} + +function cleanup { + rm -rf *.conf > /dev/null 2>&1 + rm -rf index* > /dev/null 2>&1 + rm -rf *.crt > /dev/null 2>&1 + rm -rf newcerts > /dev/null 2>&1 + rm -rf *.pem > /dev/null 2>&1 + rm -rf serial* > /dev/null 2>&1 +} + +function generate_ca_conf { + echo ' +[ req ] +default_bits = 1024 +default_keyfile = cakey.pem +default_md = sha1 + +prompt = no +distinguished_name = ca_distinguished_name + +x509_extensions = ca_extensions + +[ ca_distinguished_name ] +serialNumber = 5 +countryName = US +stateOrProvinceName = CA +localityName = Sunnyvale +organizationName = OpenStack +organizationalUnitName = Keystone +emailAddress = keystone@openstack.org +commonName = Self Signed + +[ ca_extensions ] +basicConstraints = critical,CA:true +' > ca.conf +} + +function generate_ssl_req_conf { + echo ' +[ req ] +default_bits = 1024 +default_keyfile = keystonekey.pem +default_md = sha1 + +prompt = no +distinguished_name = distinguished_name + +[ distinguished_name ] +countryName = US +stateOrProvinceName = CA +localityName = Sunnyvale +organizationName = OpenStack +organizationalUnitName = Keystone +commonName = localhost +emailAddress = keystone@openstack.org +' > ssl_req.conf +} + +function generate_cms_signing_req_conf { + echo ' +[ req ] +default_bits = 1024 +default_keyfile = keystonekey.pem +default_md = sha1 + +prompt = no +distinguished_name = distinguished_name + +[ distinguished_name ] +countryName = US +stateOrProvinceName = CA +localityName = Sunnyvale +organizationName = OpenStack +organizationalUnitName = Keystone +commonName = Keystone +emailAddress = keystone@openstack.org +' > cms_signing_req.conf +} + +function generate_signing_conf { + echo ' +[ ca ] +default_ca = signing_ca + +[ signing_ca ] +dir = . +database = $dir/index.txt +new_certs_dir = $dir/newcerts + +certificate = $dir/certs/cacert.pem +serial = $dir/serial +private_key = $dir/private/cakey.pem + +default_days = 21360 +default_crl_days = 30 +default_md = sha1 + +policy = policy_any + +[ policy_any ] +countryName = supplied +stateOrProvinceName = supplied +localityName = optional +organizationName = supplied +organizationalUnitName = supplied +emailAddress = supplied +commonName = supplied +' > signing.conf +} + +function setup { + touch index.txt + echo '10' > serial + generate_ca_conf + mkdir newcerts +} + +function check_error { + if [ $1 != 0 ] ; then + echo "Failed! rc=${1}" + echo 'Bailing ...' + cleanup + exit $1 + else + echo 'Done' + fi +} + +function generate_ca { + echo 'Generating New CA Certificate ...' + openssl req -x509 -newkey rsa:1024 -days 21360 -out $CERTS_DIR/cacert.pem -keyout $PRIVATE_DIR/cakey.pem -outform PEM -config ca.conf -nodes + check_error $? +} + +function ssl_cert_req { + echo 'Generating SSL Certificate Request ...' + generate_ssl_req_conf + openssl req -newkey rsa:1024 -keyout $PRIVATE_DIR/ssl_key.pem -keyform PEM -out ssl_req.pem -outform PEM -config ssl_req.conf -nodes + check_error $? + #openssl req -in req.pem -text -noout +} + +function cms_signing_cert_req { + echo 'Generating CMS Signing Certificate Request ...' + generate_cms_signing_req_conf + openssl req -newkey rsa:1024 -keyout $PRIVATE_DIR/signing_key.pem -keyform PEM -out cms_signing_req.pem -outform PEM -config cms_signing_req.conf -nodes + check_error $? + #openssl req -in req.pem -text -noout +} + +function issue_certs { + generate_signing_conf + echo 'Issuing SSL Certificate ...' + openssl ca -in ssl_req.pem -config signing.conf -batch + check_error $? + openssl x509 -in $CURRENT_DIR/newcerts/10.pem -out $CERTS_DIR/ssl_cert.pem + check_error $? + echo 'Issuing CMS Signing Certificate ...' + openssl ca -in cms_signing_req.pem -config signing.conf -batch + check_error $? + openssl x509 -in $CURRENT_DIR/newcerts/11.pem -out $CERTS_DIR/signing_cert.pem + check_error $? +} + +function create_middleware_cert { + cp $CERTS_DIR/ssl_cert.pem $CERTS_DIR/middleware.pem + cat $PRIVATE_DIR/ssl_key.pem >> $CERTS_DIR/middleware.pem +} + +function check_openssl { + echo 'Checking openssl availability ...' + which openssl + check_error $? +} + +function gen_sample_cms { + for json_file in "${CMS_DIR}/auth_token_revoked.json" "${CMS_DIR}/auth_token_unscoped.json" "${CMS_DIR}/auth_token_scoped.json" "${CMS_DIR}/revocation_list.json" + do + openssl cms -sign -in $json_file -nosmimecap -signer $CERTS_DIR/signing_cert.pem -inkey $PRIVATE_DIR/signing_key.pem -outform PEM -nodetach -nocerts -noattr -out ${json_file/.json/.pem} + done +} + +check_openssl +rm_old +cleanup +setup +generate_ca +ssl_cert_req +cms_signing_cert_req +issue_certs +create_middleware_cert +gen_sample_cms +cleanup diff --git a/examples/pki/private/cakey.pem b/examples/pki/private/cakey.pem new file mode 100644 index 0000000..3db9c22 --- /dev/null +++ b/examples/pki/private/cakey.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMXgnd5wlHApGxZ5 +8LrpEkHU995lT9PxtMgkp0tpFhg7R5HQw9K7TfQk5NHB28hNzf8UE/c0z2pJXggP +nAzvdx27NQeJGX5CWsi6fITZ8vH/+SxgfxxC+CE/6BkDpzw21MgBtq11vWL7XVax +NeU12Ax889U66i3CrObuCYt2mbpzAgMBAAECgYEAligxJE9CFSrcR14Zc3zSQeqe +fcFbpnXQveAyo2MHRTQWx2wobY19RjuI+DOn2IRSQbK2w+zrSLiMBonx3U8Kj8nx +A4EQ75GLJEEr81TvBoIZSJAqrowNrkXNq8W++qwjlGXRjKiBAYlKMrFvR4lij4XN +6cdB7kGdSIUmhvC20sECQQD4ebCGfsgFWnrqOrco6T9eQRTvP3+gJuqYXYLuVSTC +R4gHxT5QVXSZt/Hv3UWJ0BLDbyLzLGHf30w1AqgwsUP5AkEAy96qXq6j2+IRa5w7 +2G+KZHF5N/MK/Hyy27Jw67GBVeGQj1Dwq2ZGAJBZrfXjTtQQAGdQ7EfOTCAOzHgX +2Bx0ywJAYqfGbBBIkL+VEA0SDh9WNrE2g6u9m7P371kplEGgH7dRDmzFShYz/pin +aep8IrTHzmsBAHY9wiqh0mZkqzim2QJADTYdxkr89WfeByI1wp3f0wiDeXu3j4sp +MBGNPcjf/8fBTXhKUGEtUiYImbxggaA+dTg8x0MT/FzreJajvO6DJwJARMc6rhzv +aTlm4IgApcDPBeuz6SKex9TfvDUJpqACoFM4lMgyHADi9NrJBslxFHPP5eTiM2Ag +vI7EuW837e6raQ== +-----END PRIVATE KEY----- diff --git a/examples/pki/private/signing_key.pem b/examples/pki/private/signing_key.pem new file mode 100644 index 0000000..b6ad710 --- /dev/null +++ b/examples/pki/private/signing_key.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALqEAuiAajMQufOO +XP2ZC7HXEG0x6iKWCRxJMBKMcnhI8IhhX4mZomfdaUhTXPTH43SZzfm3v53VUtfc +NZ66yb1f7nVbH+iWVpEz1+R8sZ5ZcM5wh6T+4H7zGcxrKs1pUnYg+pnEWwmEV/YQ +gmrht44QrSEiDQlWDGEBh+kEom/bAgMBAAECgYBywfSUHya4gqsW2toGQps6cauu +s85uN0glujY0w2tO7Pnpv5errvaI12cG1BvWlAIz5MohwlfIgc919wyavCyRJgQN +xQo5v5MEMYKKc8ppmXpRr03HLwoPLOHVs6UHRJQT9dhOBfmLzMZIP7P/lJlt2/1X +Okwxft/PWorczKX1aQJBAORlVqP+Cj4r5kz1A77agnCvINioV1VM5n9PvzPVzYLH +5r1I53RWFooy1Hx2RUCmtSRQMZMeI9iGMg9c8d3LJ4UCQQDRDuIAd3AoNBcwXKC4 +BPNkbI9BSqnpIdZo87BzpY8rJ/ra3VHMHuq4w+gQsmmEy3pp01AZd1uBqv3s1wHy +muffAkEAn2ZmiH+lUGy9B5q8qXfBL7naF7utb/gCqnnSvO+LxamUTSjTeKsYgg0l +pVO503xF0fkyEDYp2FUYHQbGOwAtLQJAHkJ3N/YRx9/yU0+0+63LxQdpnNu/yDzb +mglbywF1vZtl1fQe+NqowuGoX3JTj6McLuElQOpj1lr3siZU49bEJQJBANRazUzj +Xfoja7wGuZ3PwHdxxoNDlJ2u0rYjcfK9VZuPGSz/25iCOkaar3OralJ3lfCWbFKA +vvRp8Hl2Yk4hdKM= +-----END PRIVATE KEY----- diff --git a/examples/pki/private/ssl_key.pem b/examples/pki/private/ssl_key.pem new file mode 100644 index 0000000..2e4667f --- /dev/null +++ b/examples/pki/private/ssl_key.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALVu4bjaOH33yAx0 +WdpEqj4UDVsLxVjWxEpIbOlDlc6IfJd+cUriQtxf6ahjxtzLPERS81SnwZmrICWZ +ngbOn733pULMTZktTJH+o7C74NdKwUSNxjlCeWUy+FqIQoje4ygoJRPpMdkp1wHN +O0ZERwRN9e8M5TIlx/LRtk+q8bT5AgMBAAECgYAmwq6EYFJrTvE0//JmN/8qzfvg +dI5PoWpD+F8UInUxr2T2tHOdrOLd07vGVrKYXu7cJeCIOGKa4r02azAggioL/nE9 +FgPpqEC+QROvLuhFsk1gLZ2pGQ06sveKZVMH22h59BKZkYlhjh5qd4vlmhPqkmPp +gdXj7ZjDCJhhQdFVkQJBANp18k2mVksn8q29LMieVTSIZNN3ucDA1QHbim+3fp/O +GxCzU7Mv1Xfnu1zoRFu5/sF3YG0Zy3TGPDrEljBC3rUCQQDUnBjVFXL35OkBZqXW +taJPzGbsPoqAO+Ls2juS97zNzeGxUNhvcKuEvHO63PXqDxp1535DpvJEBN1rT2FF +iaO1AkEAt/QTWWFUTqrPxY6DNFdm5fpn9E1fg7icZJkKBDJeFJCH59MpCryfovzl +n0ERtq9ynlQ4RQYwdR8rvkylLvRP9QJAOiXHFOAc5XeR0nREfwiGL9TzgUFJl/DJ +C4ZULMnctVzNkTVPPItQHal87WppR26CCiUZ/161e6zo8eRv8hjG0QJABWqfYQuK +dWH8nxlXS+NFUDbsCdL+XpOVE7iEH7hvSw/A/kz40mLx8sDp/Fz1ysrogR/L+NGC +Vrlwm4q/WYJO0Q== +-----END PRIVATE KEY----- diff --git a/keystoneclient/common/cms.py b/keystoneclient/common/cms.py new file mode 100644 index 0000000..93c0505 --- /dev/null +++ b/keystoneclient/common/cms.py @@ -0,0 +1,169 @@ +import hashlib + +import logging + + +subprocess = None +LOG = logging.getLogger(__name__) +PKI_ANS1_PREFIX = 'MII' + + +def _ensure_subprocess(): + # NOTE(vish): late loading subprocess so we can + # use the green version if we are in + # eventlet. + global subprocess + if not subprocess: + try: + from eventlet import patcher + if patcher.already_patched.get('os'): + from eventlet.green import subprocess + else: + import subprocess + except ImportError: + import subprocess + + +def cms_verify(formatted, signing_cert_file_name, ca_file_name): + """ + verifies the signature of the contents IAW CMS syntax + """ + _ensure_subprocess() + process = subprocess.Popen(["openssl", "cms", "-verify", + "-certfile", signing_cert_file_name, + "-CAfile", ca_file_name, + "-inform", "PEM", + "-nosmimecap", "-nodetach", + "-nocerts", "-noattr"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + output, err = process.communicate(formatted) + retcode = process.poll() + if retcode: + LOG.error('Verify error: %s' % err) + raise subprocess.CalledProcessError(retcode, "openssl", output=err) + return output + + +def token_to_cms(signed_text): + copy_of_text = signed_text.replace('-', '/') + + formatted = "-----BEGIN CMS-----\n" + line_length = 64 + while len(copy_of_text) > 0: + if (len(copy_of_text) > line_length): + formatted += copy_of_text[:line_length] + copy_of_text = copy_of_text[line_length:] + else: + formatted += copy_of_text + copy_of_text = "" + formatted += "\n" + + formatted += "-----END CMS-----\n" + + return formatted + + +def verify_token(token, signing_cert_file_name, ca_file_name): + return cms_verify(token_to_cms(token), + signing_cert_file_name, + ca_file_name) + + +def is_ans1_token(token): + ''' + thx to ayoung for sorting this out. + + base64 decoded hex representation of MII is 3082 + In [3]: binascii.hexlify(base64.b64decode('MII=')) + Out[3]: '3082' + + re: http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf + + pg4: For tags from 0 to 30 the first octet is the identfier + pg10: Hex 30 means sequence, followed by the length of that sequence. + pg5: Second octet is the length octet + first bit indicates short or long form, next 7 bits encode the number + of subsequent octets that make up the content length octets as an + unsigned binary int + + 82 = 10000010 (first bit indicates long form) + 0000010 = 2 octets of content length + so read the next 2 octets to get the length of the content. + + In the case of a very large content length there could be a requirement to + have more than 2 octets to designate the content length, therefore + requiring us to check for MIM, MIQ, etc. + In [4]: base64.b64encode(binascii.a2b_hex('3083')) + Out[4]: 'MIM=' + In [5]: base64.b64encode(binascii.a2b_hex('3084')) + Out[5]: 'MIQ=' + Checking for MI would become invalid at 16 octets of content length + 10010000 = 90 + In [6]: base64.b64encode(binascii.a2b_hex('3090')) + Out[6]: 'MJA=' + Checking for just M is insufficient + + But we will only check for MII: + Max length of the content using 2 octets is 7FFF or 32767 + It's not practical to support a token of this length or greater in http + therefore, we will check for MII only and ignore the case of larger tokens + ''' + return token[:3] == PKI_ANS1_PREFIX + + +def cms_sign_text(text, signing_cert_file_name, signing_key_file_name): + """ Uses OpenSSL to sign a document + Produces a Base64 encoding of a DER formatted CMS Document + http://en.wikipedia.org/wiki/Cryptographic_Message_Syntax + """ + _ensure_subprocess() + process = subprocess.Popen(["openssl", "cms", "-sign", + "-signer", signing_cert_file_name, + "-inkey", signing_key_file_name, + "-outform", "PEM", + "-nosmimecap", "-nodetach", + "-nocerts", "-noattr"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + output, err = process.communicate(text) + retcode = process.poll() + if retcode or "Error" in err: + LOG.error('Signing error: %s' % err) + raise subprocess.CalledProcessError(retcode, "openssl") + return output + + +def cms_sign_token(text, signing_cert_file_name, signing_key_file_name): + output = cms_sign_text(text, signing_cert_file_name, signing_key_file_name) + return cms_to_token(output) + + +def cms_to_token(cms_text): + + start_delim = "-----BEGIN CMS-----" + end_delim = "-----END CMS-----" + signed_text = cms_text + signed_text = signed_text.replace('/', '-') + signed_text = signed_text.replace(start_delim, '') + signed_text = signed_text.replace(end_delim, '') + signed_text = signed_text.replace('\n', '') + + return signed_text + + +def cms_hash_token(token_id): + """ + return: for ans1_token, returns the hash of the passed in token + otherwise, returns what it was passed in. + """ + if token_id is None: + return None + if is_ans1_token(token_id): + hasher = hashlib.md5() + hasher.update(token_id) + return hasher.hexdigest() + else: + return token_id diff --git a/keystoneclient/middleware/auth_token.py b/keystoneclient/middleware/auth_token.py new file mode 100644 index 0000000..93af6e1 --- /dev/null +++ b/keystoneclient/middleware/auth_token.py @@ -0,0 +1,854 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2012 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +TOKEN-BASED AUTH MIDDLEWARE + +This WSGI component: + +* Verifies that incoming client requests have valid tokens by validating + tokens with the auth service. +* Rejects unauthenticated requests UNLESS it is in 'delay_auth_decision' + mode, which means the final decision is delegated to the downstream WSGI + component (usually the OpenStack service) +* Collects and forwards identity information based on a valid token + such as user name, tenant, etc + +Refer to: http://keystone.openstack.org/middlewarearchitecture.html + +HEADERS +------- + +* Headers starting with HTTP\_ is a standard http header +* Headers starting with HTTP_X is an extended http header + +Coming in from initial call from client or customer +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +HTTP_X_AUTH_TOKEN + The client token being passed in. + +HTTP_X_STORAGE_TOKEN + The client token being passed in (legacy Rackspace use) to support + swift/cloud files + +Used for communication between components +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +WWW-Authenticate + HTTP header returned to a user indicating which endpoint to use + to retrieve a new token + +What we add to the request for use by the OpenStack service +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +HTTP_X_IDENTITY_STATUS + 'Confirmed' or 'Invalid' + The underlying service will only see a value of 'Invalid' if the Middleware + is configured to run in 'delay_auth_decision' mode + +HTTP_X_TENANT_ID + Identity service managed unique identifier, string + +HTTP_X_TENANT_NAME + Unique tenant identifier, string + +HTTP_X_USER_ID + Identity-service managed unique identifier, string + +HTTP_X_USER_NAME + Unique user identifier, string + +HTTP_X_ROLES + Comma delimited list of case-sensitive Roles + +HTTP_X_SERVICE_CATALOG + json encoded keystone service catalog (optional). + +HTTP_X_TENANT + *Deprecated* in favor of HTTP_X_TENANT_ID and HTTP_X_TENANT_NAME + Keystone-assigned unique identifier, deprecated + +HTTP_X_USER + *Deprecated* in favor of HTTP_X_USER_ID and HTTP_X_USER_NAME + Unique user name, string + +HTTP_X_ROLE + *Deprecated* in favor of HTTP_X_ROLES + This is being renamed, and the new header contains the same data. + +""" + +import datetime +import httplib +import json +import logging +import os +import stat +import time +import webob +import webob.exc + +from keystoneclient.openstack.common import jsonutils +from keystoneclient.common import cms +from keystoneclient import utils +from keystoneclient.openstack.common import timeutils + +CONF = None +try: + from openstack.common import cfg + CONF = cfg.CONF +except ImportError: + # cfg is not a library yet, try application copies + for app in 'nova', 'glance', 'quantum', 'cinder': + try: + cfg = __import__('%s.openstack.common.cfg' % app, + fromlist=['%s.openstack.common' % app]) + # test which application middleware is running in + if hasattr(cfg, 'CONF') and 'config_file' in cfg.CONF: + CONF = cfg.CONF + break + except ImportError: + pass +if not CONF: + from keystoneclient.openstack.common import cfg + CONF = cfg.CONF +LOG = logging.getLogger(__name__) + +# alternative middleware configuration in the main application's +# configuration file e.g. in nova.conf +# [keystone_authtoken] +# auth_host = 127.0.0.1 +# auth_port = 35357 +# auth_protocol = http +# admin_tenant_name = admin +# admin_user = admin +# admin_password = badpassword +opts = [ + cfg.StrOpt('auth_admin_prefix', default=''), + cfg.StrOpt('auth_host', default='127.0.0.1'), + cfg.IntOpt('auth_port', default=35357), + cfg.StrOpt('auth_protocol', default='https'), + cfg.StrOpt('auth_uri', default=None), + cfg.BoolOpt('delay_auth_decision', default=False), + cfg.StrOpt('admin_token'), + cfg.StrOpt('admin_user'), + cfg.StrOpt('admin_password'), + cfg.StrOpt('admin_tenant_name', default='admin'), + cfg.StrOpt('certfile'), + cfg.StrOpt('keyfile'), + cfg.StrOpt('signing_dir'), + cfg.ListOpt('memcache_servers'), + cfg.IntOpt('token_cache_time', default=300), +] +CONF.register_opts(opts, group='keystone_authtoken') + + +def will_expire_soon(expiry): + """ Determines if expiration is about to occur. + + :param expiry: a datetime of the expected expiration + :returns: boolean : true if expiration is within 30 seconds + """ + soon = (timeutils.utcnow() + datetime.timedelta(seconds=30)) + return expiry < soon + + +class InvalidUserToken(Exception): + pass + + +class ServiceError(Exception): + pass + + +class ConfigurationError(Exception): + pass + + +class AuthProtocol(object): + """Auth Middleware that handles authenticating client calls.""" + + def __init__(self, app, conf): + LOG.info('Starting keystone auth_token middleware') + self.conf = conf + self.app = app + + # delay_auth_decision means we still allow unauthenticated requests + # through and we let the downstream service make the final decision + self.delay_auth_decision = (self._conf_get('delay_auth_decision') in + (True, 'true', 't', '1', 'on', 'yes', 'y')) + + # where to find the auth service (we use this to validate tokens) + self.auth_host = self._conf_get('auth_host') + self.auth_port = int(self._conf_get('auth_port')) + self.auth_protocol = self._conf_get('auth_protocol') + if self.auth_protocol == 'http': + self.http_client_class = httplib.HTTPConnection + else: + self.http_client_class = httplib.HTTPSConnection + + self.auth_admin_prefix = self._conf_get('auth_admin_prefix') + self.auth_uri = self._conf_get('auth_uri') + if self.auth_uri is None: + self.auth_uri = '%s://%s:%s' % (self.auth_protocol, + self.auth_host, + self.auth_port) + + # SSL + self.cert_file = self._conf_get('certfile') + self.key_file = self._conf_get('keyfile') + + #signing + self.signing_dirname = self._conf_get('signing_dir') + if self.signing_dirname is None: + self.signing_dirname = '%s/keystone-signing' % os.environ['HOME'] + LOG.info('Using %s as cache directory for signing certificate' % + self.signing_dirname) + if (os.path.exists(self.signing_dirname) and + not os.access(self.signing_dirname, os.W_OK)): + raise ConfigurationError("unable to access signing dir %s" % + self.signing_dirname) + + if not os.path.exists(self.signing_dirname): + os.makedirs(self.signing_dirname) + #will throw IOError if it cannot change permissions + os.chmod(self.signing_dirname, stat.S_IRWXU) + + val = '%s/signing_cert.pem' % self.signing_dirname + self.signing_cert_file_name = val + val = '%s/cacert.pem' % self.signing_dirname + self.ca_file_name = val + val = '%s/revoked.pem' % self.signing_dirname + self.revoked_file_name = val + + # Credentials used to verify this component with the Auth service since + # validating tokens is a privileged call + self.admin_token = self._conf_get('admin_token') + self.admin_token_expiry = None + self.admin_user = self._conf_get('admin_user') + self.admin_password = self._conf_get('admin_password') + self.admin_tenant_name = self._conf_get('admin_tenant_name') + + # Token caching via memcache + self._cache = None + self._iso8601 = None + memcache_servers = self._conf_get('memcache_servers') + # By default the token will be cached for 5 minutes + self.token_cache_time = int(self._conf_get('token_cache_time')) + self._token_revocation_list = None + self._token_revocation_list_fetched_time = None + cache_timeout = datetime.timedelta(seconds=0) + self.token_revocation_list_cache_timeout = cache_timeout + if memcache_servers: + try: + import memcache + import iso8601 + LOG.info('Using memcache for caching token') + self._cache = memcache.Client(memcache_servers.split(',')) + self._iso8601 = iso8601 + except ImportError as e: + LOG.warn('disabled caching due to missing libraries %s', e) + + def _conf_get(self, name): + # try config from paste-deploy first + if name in self.conf: + return self.conf[name] + else: + return CONF.keystone_authtoken[name] + + def __call__(self, env, start_response): + """Handle incoming request. + + Authenticate send downstream on success. Reject request if + we can't authenticate. + + """ + LOG.debug('Authenticating user token') + try: + self._remove_auth_headers(env) + user_token = self._get_user_token_from_header(env) + token_info = self._validate_user_token(user_token) + user_headers = self._build_user_headers(token_info) + self._add_headers(env, user_headers) + return self.app(env, start_response) + + except InvalidUserToken: + if self.delay_auth_decision: + LOG.info('Invalid user token - deferring reject downstream') + self._add_headers(env, {'X-Identity-Status': 'Invalid'}) + return self.app(env, start_response) + else: + LOG.info('Invalid user token - rejecting request') + return self._reject_request(env, start_response) + + except ServiceError as e: + LOG.critical('Unable to obtain admin token: %s' % e) + resp = webob.exc.HTTPServiceUnavailable() + return resp(env, start_response) + + def _remove_auth_headers(self, env): + """Remove headers so a user can't fake authentication. + + :param env: wsgi request environment + + """ + auth_headers = ( + 'X-Identity-Status', + 'X-Tenant-Id', + 'X-Tenant-Name', + 'X-User-Id', + 'X-User-Name', + 'X-Roles', + 'X-Service-Catalog', + # Deprecated + 'X-User', + 'X-Tenant', + 'X-Role', + ) + LOG.debug('Removing headers from request environment: %s' % + ','.join(auth_headers)) + self._remove_headers(env, auth_headers) + + def _get_user_token_from_header(self, env): + """Get token id from request. + + :param env: wsgi request environment + :return token id + :raises InvalidUserToken if no token is provided in request + + """ + token = self._get_header(env, 'X-Auth-Token', + self._get_header(env, 'X-Storage-Token')) + if token: + return token + else: + LOG.warn("Unable to find authentication token in headers: %s", env) + raise InvalidUserToken('Unable to find token in headers') + + def _reject_request(self, env, start_response): + """Redirect client to auth server. + + :param env: wsgi request environment + :param start_response: wsgi response callback + :returns HTTPUnauthorized http response + + """ + headers = [('WWW-Authenticate', 'Keystone uri=\'%s\'' % self.auth_uri)] + resp = webob.exc.HTTPUnauthorized('Authentication required', headers) + return resp(env, start_response) + + def get_admin_token(self): + """Return admin token, possibly fetching a new one. + + if self.admin_token_expiry is set from fetching an admin token, check + it for expiration, and request a new token is the existing token + is about to expire. + + :return admin token id + :raise ServiceError when unable to retrieve token from keystone + + """ + if self.admin_token_expiry: + if will_expire_soon(self.admin_token_expiry): + self.admin_token = None + + if not self.admin_token: + (self.admin_token, + self.admin_token_expiry) = self._request_admin_token() + + return self.admin_token + + def _get_http_connection(self): + if self.auth_protocol == 'http': + return self.http_client_class(self.auth_host, self.auth_port) + else: + return self.http_client_class(self.auth_host, + self.auth_port, + self.key_file, + self.cert_file) + + def _http_request(self, method, path): + """HTTP request helper used to make unspecified content type requests. + + :param method: http method + :param path: relative request url + :return (http response object) + :raise ServerError when unable to communicate with keystone + + """ + conn = self._get_http_connection() + + try: + conn.request(method, path) + response = conn.getresponse() + body = response.read() + except Exception as e: + LOG.error('HTTP connection exception: %s' % e) + raise ServiceError('Unable to communicate with keystone') + finally: + conn.close() + + return response, body + + def _json_request(self, method, path, body=None, additional_headers=None): + """HTTP request helper used to make json requests. + + :param method: http method + :param path: relative request url + :param body: dict to encode to json as request body. Optional. + :param additional_headers: dict of additional headers to send with + http request. Optional. + :return (http response object, response body parsed as json) + :raise ServerError when unable to communicate with keystone + + """ + conn = self._get_http_connection() + + kwargs = { + 'headers': { + 'Content-type': 'application/json', + 'Accept': 'application/json', + }, + } + + if additional_headers: + kwargs['headers'].update(additional_headers) + + if body: + kwargs['body'] = jsonutils.dumps(body) + + full_path = self.auth_admin_prefix + path + try: + conn.request(method, full_path, **kwargs) + response = conn.getresponse() + body = response.read() + except Exception as e: + LOG.error('HTTP connection exception: %s' % e) + raise ServiceError('Unable to communicate with keystone') + finally: + conn.close() + + try: + data = jsonutils.loads(body) + except ValueError: + LOG.debug('Keystone did not return json-encoded body') + data = {} + + return response, data + + def _request_admin_token(self): + """Retrieve new token as admin user from keystone. + + :return token id upon success + :raises ServerError when unable to communicate with keystone + + """ + params = { + 'auth': { + 'passwordCredentials': { + 'username': self.admin_user, + 'password': self.admin_password, + }, + 'tenantName': self.admin_tenant_name, + } + } + + response, data = self._json_request('POST', + '/v2.0/tokens', + body=params) + + try: + token = data['access']['token']['id'] + expiry = data['access']['token']['expires'] + assert token + assert expiry + datetime_expiry = timeutils.parse_isotime(expiry) + return (token, timeutils.normalize_time(datetime_expiry)) + except (AssertionError, KeyError): + LOG.warn("Unexpected response from keystone service: %s", data) + raise ServiceError('invalid json response') + except (ValueError): + LOG.warn("Unable to parse expiration time from token: %s", data) + raise ServiceError('invalid json response') + + def _validate_user_token(self, user_token, retry=True): + """Authenticate user using PKI + + :param user_token: user's token id + :param retry: Ignored, as it is not longer relevant + :return uncrypted body of the token if the token is valid + :raise InvalidUserToken if token is rejected + :no longer raises ServiceError since it no longer makes RPC + + """ + try: + token_id = cms.cms_hash_token(user_token) + cached = self._cache_get(token_id) + if cached: + return cached + if cms.is_ans1_token(user_token): + verified = self.verify_signed_token(user_token) + data = json.loads(verified) + else: + data = self.verify_uuid_token(user_token, retry) + self._cache_put(token_id, data) + return data + except Exception as e: + LOG.debug('Token validation failure.', exc_info=True) + self._cache_store_invalid(user_token) + LOG.warn("Authorization failed for token %s", user_token) + raise InvalidUserToken('Token authorization failed') + + def _build_user_headers(self, token_info): + """Convert token object into headers. + + Build headers that represent authenticated user: + * X_IDENTITY_STATUS: Confirmed or Invalid + * X_TENANT_ID: id of tenant if tenant is present + * X_TENANT_NAME: name of tenant if tenant is present + * X_USER_ID: id of user + * X_USER_NAME: name of user + * X_ROLES: list of roles + * X_SERVICE_CATALOG: service catalog + + Additional (deprecated) headers include: + * X_USER: name of user + * X_TENANT: For legacy compatibility before we had ID and Name + * X_ROLE: list of roles + + :param token_info: token object returned by keystone on authentication + :raise InvalidUserToken when unable to parse token object + + """ + user = token_info['access']['user'] + token = token_info['access']['token'] + roles = ','.join([role['name'] for role in user.get('roles', [])]) + + def get_tenant_info(): + """Returns a (tenant_id, tenant_name) tuple from context.""" + def essex(): + """Essex puts the tenant ID and name on the token.""" + return (token['tenant']['id'], token['tenant']['name']) + + def pre_diablo(): + """Pre-diablo, Keystone only provided tenantId.""" + return (token['tenantId'], token['tenantId']) + + def default_tenant(): + """Assume the user's default tenant.""" + return (user['tenantId'], user['tenantName']) + + for method in [essex, pre_diablo, default_tenant]: + try: + return method() + except KeyError: + pass + + raise InvalidUserToken('Unable to determine tenancy.') + + tenant_id, tenant_name = get_tenant_info() + + user_id = user['id'] + user_name = user['name'] + + rval = { + 'X-Identity-Status': 'Confirmed', + 'X-Tenant-Id': tenant_id, + 'X-Tenant-Name': tenant_name, + 'X-User-Id': user_id, + 'X-User-Name': user_name, + 'X-Roles': roles, + # Deprecated + 'X-User': user_name, + 'X-Tenant': tenant_name, + 'X-Role': roles, + } + + try: + catalog = token_info['access']['serviceCatalog'] + rval['X-Service-Catalog'] = jsonutils.dumps(catalog) + except KeyError: + pass + + return rval + + def _header_to_env_var(self, key): + """Convert header to wsgi env variable. + + :param key: http header name (ex. 'X-Auth-Token') + :return wsgi env variable name (ex. 'HTTP_X_AUTH_TOKEN') + + """ + return 'HTTP_%s' % key.replace('-', '_').upper() + + def _add_headers(self, env, headers): + """Add http headers to environment.""" + for (k, v) in headers.iteritems(): + env_key = self._header_to_env_var(k) + env[env_key] = v + + def _remove_headers(self, env, keys): + """Remove http headers from environment.""" + for k in keys: + env_key = self._header_to_env_var(k) + try: + del env[env_key] + except KeyError: + pass + + def _get_header(self, env, key, default=None): + """Get http header from environment.""" + env_key = self._header_to_env_var(key) + return env.get(env_key, default) + + def _cache_get(self, token): + """Return token information from cache. + + If token is invalid raise InvalidUserToken + return token only if fresh (not expired). + """ + if self._cache and token: + key = 'tokens/%s' % token + cached = self._cache.get(key) + if cached == 'invalid': + LOG.debug('Cached Token %s is marked unauthorized', token) + raise InvalidUserToken('Token authorization failed') + if cached: + data, expires = cached + if time.time() < float(expires): + LOG.debug('Returning cached token %s', token) + return data + else: + LOG.debug('Cached Token %s seems expired', token) + + def _cache_put(self, token, data): + """Put token data into the cache. + + Stores the parsed expire date in cache allowing + quick check of token freshness on retrieval. + """ + if self._cache and data: + key = 'tokens/%s' % token + if 'token' in data.get('access', {}): + timestamp = data['access']['token']['expires'] + expires = self._iso8601.parse_date(timestamp).strftime('%s') + else: + LOG.error('invalid token format') + return + LOG.debug('Storing %s token in memcache', token) + self._cache.set(key, + (data, expires), + time=self.token_cache_time) + + def _cache_store_invalid(self, token): + """Store invalid token in cache.""" + if self._cache: + key = 'tokens/%s' % token + LOG.debug('Marking token %s as unauthorized in memcache', token) + self._cache.set(key, + 'invalid', + time=self.token_cache_time) + + def cert_file_missing(self, called_proc_err, file_name): + return (called_proc_err.output.find(file_name) + and not os.path.exists(file_name)) + + def verify_uuid_token(self, user_token, retry=True): + """Authenticate user token with keystone. + + :param user_token: user's token id + :param retry: flag that forces the middleware to retry + user authentication when an indeterminate + response is received. Optional. + :return token object received from keystone on success + :raise InvalidUserToken if token is rejected + :raise ServiceError if unable to authenticate token + + """ + + headers = {'X-Auth-Token': self.get_admin_token()} + response, data = self._json_request('GET', + '/v2.0/tokens/%s' % user_token, + additional_headers=headers) + + if response.status == 200: + self._cache_put(user_token, data) + return data + if response.status == 404: + # FIXME(ja): I'm assuming the 404 status means that user_token is + # invalid - not that the admin_token is invalid + self._cache_store_invalid(user_token) + LOG.warn("Authorization failed for token %s", user_token) + raise InvalidUserToken('Token authorization failed') + if response.status == 401: + LOG.info('Keystone rejected admin token %s, resetting', headers) + self.admin_token = None + else: + LOG.error('Bad response code while validating token: %s' % + response.status) + if retry: + LOG.info('Retrying validation') + return self._validate_user_token(user_token, False) + else: + LOG.warn("Invalid user token: %s. Keystone response: %s.", + user_token, data) + + raise InvalidUserToken() + + def is_signed_token_revoked(self, signed_text): + """Indicate whether the token appears in the revocation list.""" + revocation_list = self.token_revocation_list + revoked_tokens = revocation_list.get('revoked', []) + if not revoked_tokens: + return + revoked_ids = (x['id'] for x in revoked_tokens) + token_id = utils.hash_signed_token(signed_text) + for revoked_id in revoked_ids: + if token_id == revoked_id: + LOG.debug('Token %s is marked as having been revoked', + token_id) + return True + return False + + def cms_verify(self, data): + """Verifies the signature of the provided data's IAW CMS syntax. + + If either of the certificate files are missing, fetch them and + retry. + """ + while True: + try: + output = cms.cms_verify(data, self.signing_cert_file_name, + self.ca_file_name) + except cms.subprocess.CalledProcessError as err: + if self.cert_file_missing(err, self.signing_cert_file_name): + self.fetch_signing_cert() + continue + if self.cert_file_missing(err, self.ca_file_name): + self.fetch_ca_cert() + continue + raise err + return output + + def verify_signed_token(self, signed_text): + """Check that the token is unrevoked and has a valid signature.""" + if self.is_signed_token_revoked(signed_text): + raise InvalidUserToken('Token has been revoked') + + formatted = cms.token_to_cms(signed_text) + return self.cms_verify(formatted) + + @property + def token_revocation_list_fetched_time(self): + if not self._token_revocation_list_fetched_time: + # If the fetched list has been written to disk, use its + # modification time. + if os.path.exists(self.revoked_file_name): + mtime = os.path.getmtime(self.revoked_file_name) + fetched_time = datetime.datetime.fromtimestamp(mtime) + # Otherwise the list will need to be fetched. + else: + fetched_time = datetime.datetime.min + self._token_revocation_list_fetched_time = fetched_time + return self._token_revocation_list_fetched_time + + @token_revocation_list_fetched_time.setter + def token_revocation_list_fetched_time(self, value): + self._token_revocation_list_fetched_time = value + + @property + def token_revocation_list(self): + timeout = (self.token_revocation_list_fetched_time + + self.token_revocation_list_cache_timeout) + list_is_current = timeutils.utcnow() < timeout + if list_is_current: + # Load the list from disk if required + if not self._token_revocation_list: + with open(self.revoked_file_name, 'r') as f: + self._token_revocation_list = jsonutils.loads(f.read()) + else: + self.token_revocation_list = self.fetch_revocation_list() + return self._token_revocation_list + + @token_revocation_list.setter + def token_revocation_list(self, value): + """Save a revocation list to memory and to disk. + + :param value: A json-encoded revocation list + + """ + self._token_revocation_list = jsonutils.loads(value) + self.token_revocation_list_fetched_time = timeutils.utcnow() + with open(self.revoked_file_name, 'w') as f: + f.write(value) + + def fetch_revocation_list(self, retry=True): + headers = {'X-Auth-Token': self.get_admin_token()} + response, data = self._json_request('GET', '/v2.0/tokens/revoked', + additional_headers=headers) + if response.status == 401: + if retry: + LOG.info('Keystone rejected admin token %s, resetting admin ' + 'token', headers) + self.admin_token = None + return self.fetch_revocation_list(retry=False) + if response.status != 200: + raise ServiceError('Unable to fetch token revocation list.') + if (not 'signed' in data): + raise ServiceError('Revocation list inmproperly formatted.') + return self.cms_verify(data['signed']) + + def fetch_signing_cert(self): + response, data = self._http_request('GET', + '/v2.0/certificates/signing') + try: + #todo check response + certfile = open(self.signing_cert_file_name, 'w') + certfile.write(data) + certfile.close() + except (AssertionError, KeyError): + LOG.warn("Unexpected response from keystone service: %s", data) + raise ServiceError('invalid json response') + + def fetch_ca_cert(self): + response, data = self._http_request('GET', + '/v2.0/certificates/ca') + try: + #todo check response + certfile = open(self.ca_file_name, 'w') + certfile.write(data) + certfile.close() + except (AssertionError, KeyError): + LOG.warn("Unexpected response from keystone service: %s", data) + raise ServiceError('invalid json response') + + +def filter_factory(global_conf, **local_conf): + """Returns a WSGI filter app for use with paste.deploy.""" + conf = global_conf.copy() + conf.update(local_conf) + + def auth_filter(app): + return AuthProtocol(app, conf) + return auth_filter + + +def app_factory(global_conf, **local_conf): + conf = global_conf.copy() + conf.update(local_conf) + return AuthProtocol(None, conf) diff --git a/keystoneclient/middleware/test.py b/keystoneclient/middleware/test.py new file mode 100644 index 0000000..e5c1171 --- /dev/null +++ b/keystoneclient/middleware/test.py @@ -0,0 +1,67 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# +# Test support for middleware authentication +# + +import os +import sys + + +ROOTDIR = os.path.dirname(os.path.abspath(os.curdir)) + + +def rootdir(*p): + return os.path.join(ROOTDIR, *p) + + +class NoModule(object): + """A mixin class to provide support for unloading/disabling modules.""" + + def __init__(self, *args, **kw): + super(NoModule, self).__init__(*args, **kw) + self._finders = [] + self._cleared_modules = {} + + def tearDown(self): + super(NoModule, self).tearDown() + for finder in self._finders: + sys.meta_path.remove(finder) + sys.modules.update(self._cleared_modules) + + def clear_module(self, module): + cleared_modules = {} + for fullname in sys.modules.keys(): + if fullname == module or fullname.startswith(module + '.'): + cleared_modules[fullname] = sys.modules.pop(fullname) + return cleared_modules + + def disable_module(self, module): + """Ensure ImportError for the specified module.""" + + # Clear 'module' references in sys.modules + self._cleared_modules.update(self.clear_module(module)) + + # Disallow further imports of 'module' + class NoModule(object): + def find_module(self, fullname, path): + if fullname == module or fullname.startswith(module + '.'): + raise ImportError + + finder = NoModule() + self._finders.append(finder) + sys.meta_path.insert(0, finder) diff --git a/keystoneclient/utils.py b/keystoneclient/utils.py index 36ec41b..225afe7 100644 --- a/keystoneclient/utils.py +++ b/keystoneclient/utils.py @@ -1,4 +1,5 @@ import uuid +import hashlib import prettytable @@ -114,3 +115,9 @@ def string_to_bool(arg): return arg return arg.strip().lower() in ('t', 'true', 'yes', '1') + + +def hash_signed_token(signed_text): + hash_ = hashlib.md5() + hash_.update(signed_text) + return hash_.hexdigest() diff --git a/tests/test_auth_token_middleware.py b/tests/test_auth_token_middleware.py new file mode 100644 index 0000000..0c15504 --- /dev/null +++ b/tests/test_auth_token_middleware.py @@ -0,0 +1,667 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime +import iso8601 +import os +import string +import tempfile +import unittest2 as unittest + +import webob + +from keystoneclient.common import cms +from keystoneclient import utils +from keystoneclient.middleware import auth_token +from keystoneclient.openstack.common import jsonutils +from keystoneclient.openstack.common import timeutils +from keystoneclient.middleware import test + + +CERTDIR = test.rootdir("python-keystoneclient/examples/pki/certs") +KEYDIR = test.rootdir("python-keystoneclient/examples/pki/private") +CMSDIR = test.rootdir("python-keystoneclient/examples/pki/cms") +SIGNING_CERT = os.path.join(CERTDIR, 'signing_cert.pem') +SIGNING_KEY = os.path.join(KEYDIR, 'signing_key.pem') +CA = os.path.join(CERTDIR, 'ca.pem') + +REVOCATION_LIST = None +REVOKED_TOKEN = None +REVOKED_TOKEN_HASH = None +SIGNED_REVOCATION_LIST = None +SIGNED_TOKEN_SCOPED = None +SIGNED_TOKEN_UNSCOPED = None +SIGNED_TOKEN_SCOPED_KEY = None +SIGNED_TOKEN_UNSCOPED_KEY = None + +VALID_SIGNED_REVOCATION_LIST = None + +UUID_TOKEN_DEFAULT = "ec6c0710ec2f471498484c1b53ab4f9d" +UUID_TOKEN_NO_SERVICE_CATALOG = '8286720fbe4941e69fa8241723bb02df' +UUID_TOKEN_UNSCOPED = '731f903721c14827be7b2dc912af7776' +VALID_DIABLO_TOKEN = 'b0cf19b55dbb4f20a6ee18e6c6cf1726' + +INVALID_SIGNED_TOKEN = string.replace( + """AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB +CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC +DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD +EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE +FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +0000000000000000000000000000000000000000000000000000000000000000 +1111111111111111111111111111111111111111111111111111111111111111 +2222222222222222222222222222222222222222222222222222222222222222 +3333333333333333333333333333333333333333333333333333333333333333 +4444444444444444444444444444444444444444444444444444444444444444 +5555555555555555555555555555555555555555555555555555555555555555 +6666666666666666666666666666666666666666666666666666666666666666 +7777777777777777777777777777777777777777777777777777777777777777 +8888888888888888888888888888888888888888888888888888888888888888 +9999999999999999999999999999999999999999999999999999999999999999 +0000000000000000000000000000000000000000000000000000000000000000 +xg==""", "\n", "") + +# JSON responses keyed by token ID +TOKEN_RESPONSES = { + UUID_TOKEN_DEFAULT: { + 'access': { + 'token': { + 'id': UUID_TOKEN_DEFAULT, + 'expires': '2999-01-01T00:00:10Z', + 'tenant': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + }, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + 'serviceCatalog': {} + }, + }, + VALID_DIABLO_TOKEN: { + 'access': { + 'token': { + 'id': VALID_DIABLO_TOKEN, + 'expires': '2999-01-01T00:00:10', + 'tenantId': 'tenant_id1', + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + }, + }, + UUID_TOKEN_UNSCOPED: { + 'access': { + 'token': { + 'id': UUID_TOKEN_UNSCOPED, + 'expires': '2999-01-01T00:00:10Z', + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + }, + }, + UUID_TOKEN_NO_SERVICE_CATALOG: { + 'access': { + 'token': { + 'id': 'valid-token', + 'expires': '2999-01-01T00:00:10Z', + 'tenant': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + }, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + } + }, + }, +} + +FAKE_RESPONSE_STACK = [] + + +# The data for these tests are signed using openssl and are stored in files +# in the signing subdirectory. In order to keep the values consistent between +# the tests and the signed documents, we read them in for use in the tests. +def setUpModule(self): + signing_path = CMSDIR + with open(os.path.join(signing_path, 'auth_token_scoped.pem')) as f: + self.SIGNED_TOKEN_SCOPED = cms.cms_to_token(f.read()) + with open(os.path.join(signing_path, 'auth_token_unscoped.pem')) as f: + self.SIGNED_TOKEN_UNSCOPED = cms.cms_to_token(f.read()) + with open(os.path.join(signing_path, 'auth_token_revoked.pem')) as f: + self.REVOKED_TOKEN = cms.cms_to_token(f.read()) + self.REVOKED_TOKEN_HASH = utils.hash_signed_token(self.REVOKED_TOKEN) + with open(os.path.join(signing_path, 'revocation_list.json')) as f: + self.REVOCATION_LIST = jsonutils.loads(f.read()) + with open(os.path.join(signing_path, 'revocation_list.pem')) as f: + self.VALID_SIGNED_REVOCATION_LIST = jsonutils.dumps( + {'signed': f.read()}) + self.SIGNED_TOKEN_SCOPED_KEY =\ + cms.cms_hash_token(self.SIGNED_TOKEN_SCOPED) + self.SIGNED_TOKEN_UNSCOPED_KEY =\ + cms.cms_hash_token(self.SIGNED_TOKEN_UNSCOPED) + + self.TOKEN_RESPONSES[self.SIGNED_TOKEN_SCOPED_KEY] = { + 'access': { + 'token': { + 'id': self.SIGNED_TOKEN_SCOPED_KEY, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'tenantId': 'tenant_id1', + 'tenantName': 'tenant_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + }, + } + + self.TOKEN_RESPONSES[SIGNED_TOKEN_UNSCOPED_KEY] = { + 'access': { + 'token': { + 'id': SIGNED_TOKEN_UNSCOPED_KEY, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + }, + }, + + +class FakeMemcache(object): + def __init__(self): + self.set_key = None + self.set_value = None + self.token_expiration = None + + def get(self, key): + data = TOKEN_RESPONSES[SIGNED_TOKEN_SCOPED_KEY].copy() + if not data or key != "tokens/%s" % (data['access']['token']['id']): + return + if not self.token_expiration: + dt = datetime.datetime.now() + datetime.timedelta(minutes=5) + self.token_expiration = dt.strftime("%s") + dt = datetime.datetime.now() + datetime.timedelta(hours=24) + ks_expires = dt.isoformat() + data['access']['token']['expires'] = ks_expires + return (data, str(self.token_expiration)) + + def set(self, key, value, time=None): + self.set_value = value + self.set_key = key + + +class FakeHTTPResponse(object): + def __init__(self, status, body): + self.status = status + self.body = body + + def read(self): + return self.body + + +class FakeStackHTTPConnection(object): + + def __init__(self, *args, **kwargs): + pass + + def getresponse(self): + if len(FAKE_RESPONSE_STACK): + return FAKE_RESPONSE_STACK.pop() + return FakeHTTPResponse(500, jsonutils.dumps('UNEXPECTED RESPONSE')) + + def request(self, *_args, **_kwargs): + pass + + def close(self): + pass + + +class FakeHTTPConnection(object): + + last_requested_url = '' + + def __init__(self, *args): + self.send_valid_revocation_list = True + + def request(self, method, path, **kwargs): + """Fakes out several http responses. + + If a POST request is made, we assume the calling code is trying + to get a new admin token. + + If a GET request is made to validate a token, return success + if the token is 'token1'. If a different token is provided, return + a 404, indicating an unknown (therefore unauthorized) token. + + """ + FakeHTTPConnection.last_requested_url = path + if method == 'POST': + status = 200 + body = jsonutils.dumps({ + 'access': { + 'token': {'id': 'admin_token2'}, + }, + }) + + else: + token_id = path.rsplit('/', 1)[1] + if token_id in TOKEN_RESPONSES.keys(): + status = 200 + body = jsonutils.dumps(TOKEN_RESPONSES[token_id]) + elif token_id == "revoked": + status = 200 + body = SIGNED_REVOCATION_LIST + else: + status = 404 + body = str() + + self.resp = FakeHTTPResponse(status, body) + + def getresponse(self): + return self.resp + + def close(self): + pass + + +class FakeApp(object): + """This represents a WSGI app protected by the auth_token middleware.""" + def __init__(self, expected_env=None): + expected_env = expected_env or {} + self.expected_env = { + 'HTTP_X_IDENTITY_STATUS': 'Confirmed', + 'HTTP_X_TENANT_ID': 'tenant_id1', + 'HTTP_X_TENANT_NAME': 'tenant_name1', + 'HTTP_X_USER_ID': 'user_id1', + 'HTTP_X_USER_NAME': 'user_name1', + 'HTTP_X_ROLES': 'role1,role2', + 'HTTP_X_USER': 'user_name1', # deprecated (diablo-compat) + 'HTTP_X_TENANT': 'tenant_name1', # deprecated (diablo-compat) + 'HTTP_X_ROLE': 'role1,role2', # deprecated (diablo-compat) + } + self.expected_env.update(expected_env) + + def __call__(self, env, start_response): + for k, v in self.expected_env.items(): + assert env[k] == v, '%s != %s' % (env[k], v) + + resp = webob.Response() + resp.body = 'SUCCESS' + return resp(env, start_response) + + +class BaseAuthTokenMiddlewareTest(unittest.TestCase): + + def setUp(self, expected_env=None): + expected_env = expected_env or {} + + conf = { + 'admin_token': 'admin_token1', + 'auth_host': 'keystone.example.com', + 'auth_port': 1234, + 'auth_admin_prefix': '/testadmin', + 'signing_dir': CERTDIR, + } + + self.middleware = auth_token.AuthProtocol(FakeApp(expected_env), conf) + self.middleware.http_client_class = FakeHTTPConnection + self.middleware._iso8601 = iso8601 + + self.response_status = None + self.response_headers = None + self.middleware.revoked_file_name = tempfile.mkstemp()[1] + cache_timeout = datetime.timedelta(days=1) + self.middleware.token_revocation_list_cache_timeout = cache_timeout + self.middleware.token_revocation_list = jsonutils.dumps( + {"revoked": [], "extra": "success"}) + + signed_list = 'SIGNED_REVOCATION_LIST' + valid_signed_list = 'VALID_SIGNED_REVOCATION_LIST' + globals()[signed_list] = globals()[valid_signed_list] + + super(BaseAuthTokenMiddlewareTest, self).setUp() + + def tearDown(self): + super(BaseAuthTokenMiddlewareTest, self).tearDown() + try: + os.remove(self.middleware.revoked_file_name) + except OSError: + pass + + def start_fake_response(self, status, headers): + self.response_status = int(status.split(' ', 1)[0]) + self.response_headers = dict(headers) + + +class StackResponseAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest): + """Auth Token middleware test setup that allows the tests to define + a stack of responses to HTTP requests in the test and get those + responses back in sequence for testing. + + Example:: + + resp1 = FakeHTTPResponse(401, jsonutils.dumps('')) + resp2 = FakeHTTPResponse(200, jsonutils.dumps({ + 'access': { + 'token': {'id': 'admin_token2'}, + }, + }) + FAKE_RESPONSE_STACK.append(resp1) + FAKE_RESPONSE_STACK.append(resp2) + + ... do your testing code here ... + + """ + + def setUp(self, expected_env=None): + super(StackResponseAuthTokenMiddlewareTest, self).setUp(expected_env) + self.middleware.http_client_class = FakeStackHTTPConnection + + def test_fetch_revocation_list_with_expire(self): + # first response to revocation list should return 401 Unauthorized + # to pretend to be an expired token + resp1 = FakeHTTPResponse(200, jsonutils.dumps({ + 'access': { + 'token': {'id': 'admin_token2'}, + }, + })) + resp2 = FakeHTTPResponse(401, jsonutils.dumps('')) + resp3 = FakeHTTPResponse(200, jsonutils.dumps({ + 'access': { + 'token': {'id': 'admin_token2'}, + }, + })) + resp4 = FakeHTTPResponse(200, SIGNED_REVOCATION_LIST) + + # first get_admin_token() call + FAKE_RESPONSE_STACK.append(resp1) + # request revocation list, get "unauthorized" due to simulated expired + # token + FAKE_RESPONSE_STACK.append(resp2) + # request a new admin_token + FAKE_RESPONSE_STACK.append(resp3) + # request revocation list, get the revocation list properly + FAKE_RESPONSE_STACK.append(resp4) + + fetched_list = jsonutils.loads(self.middleware.fetch_revocation_list()) + self.assertEqual(fetched_list, REVOCATION_LIST) + + +class DiabloAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest): + """Auth Token middleware should understand Diablo keystone responses.""" + def setUp(self): + # pre-diablo only had Tenant ID, which was also the Name + expected_env = { + 'HTTP_X_TENANT_ID': 'tenant_id1', + 'HTTP_X_TENANT_NAME': 'tenant_id1', + # now deprecated (diablo-compat) + 'HTTP_X_TENANT': 'tenant_id1', + } + super(DiabloAuthTokenMiddlewareTest, self).setUp(expected_env) + + def test_valid_diablo_response(self): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = VALID_DIABLO_TOKEN + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + + +class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest): + def assert_valid_request_200(self, token): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = token + body = self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + self.assertTrue(req.headers.get('X-Service-Catalog')) + self.assertEqual(body, ['SUCCESS']) + + def test_valid_uuid_request(self): + self.assert_valid_request_200(UUID_TOKEN_DEFAULT) + self.assertEqual("/testadmin/v2.0/tokens/%s" % UUID_TOKEN_DEFAULT, + FakeHTTPConnection.last_requested_url) + + def test_valid_signed_request(self): + FakeHTTPConnection.last_requested_url = '' + self.assert_valid_request_200(SIGNED_TOKEN_SCOPED) + self.assertEqual(self.middleware.conf['auth_admin_prefix'], + "/testadmin") + #ensure that signed requests do not generate HTTP traffic + self.assertEqual('', FakeHTTPConnection.last_requested_url) + + def assert_unscoped_default_tenant_auto_scopes(self, token): + """Unscoped requests with a default tenant should "auto-scope." + + The implied scope is the user's tenant ID. + + """ + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = token + body = self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + self.assertEqual(body, ['SUCCESS']) + + def test_default_tenant_uuid_token(self): + self.assert_unscoped_default_tenant_auto_scopes(UUID_TOKEN_DEFAULT) + + def test_default_tenant_signed_token(self): + self.assert_unscoped_default_tenant_auto_scopes(SIGNED_TOKEN_SCOPED) + + def assert_unscoped_token_receives_401(self, token): + """Unscoped requests with no default tenant ID should be rejected.""" + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = token + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + 'Keystone uri=\'https://keystone.example.com:1234\'') + + def test_unscoped_uuid_token_receives_401(self): + self.assert_unscoped_token_receives_401(UUID_TOKEN_UNSCOPED) + + def test_unscoped_pki_token_receives_401(self): + self.assert_unscoped_token_receives_401(SIGNED_TOKEN_UNSCOPED) + + def test_revoked_token_receives_401(self): + self.middleware.token_revocation_list = self.get_revocation_list_json() + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = REVOKED_TOKEN + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + + def get_revocation_list_json(self, token_ids=None): + if token_ids is None: + token_ids = [REVOKED_TOKEN_HASH] + revocation_list = {'revoked': [{'id': x, 'expires': timeutils.utcnow()} + for x in token_ids]} + return jsonutils.dumps(revocation_list) + + def test_is_signed_token_revoked_returns_false(self): + #explicitly setting an empty revocation list here to document intent + self.middleware.token_revocation_list = jsonutils.dumps( + {"revoked": [], "extra": "success"}) + result = self.middleware.is_signed_token_revoked(REVOKED_TOKEN) + self.assertFalse(result) + + def test_is_signed_token_revoked_returns_true(self): + self.middleware.token_revocation_list = self.get_revocation_list_json() + result = self.middleware.is_signed_token_revoked(REVOKED_TOKEN) + self.assertTrue(result) + + def test_verify_signed_token_raises_exception_for_revoked_token(self): + self.middleware.token_revocation_list = self.get_revocation_list_json() + with self.assertRaises(auth_token.InvalidUserToken): + self.middleware.verify_signed_token(REVOKED_TOKEN) + + def test_verify_signed_token_succeeds_for_unrevoked_token(self): + self.middleware.token_revocation_list = self.get_revocation_list_json() + self.middleware.verify_signed_token(SIGNED_TOKEN_SCOPED) + + def test_get_token_revocation_list_fetched_time_returns_min(self): + self.middleware.token_revocation_list_fetched_time = None + self.middleware.revoked_file_name = '' + self.assertEqual(self.middleware.token_revocation_list_fetched_time, + datetime.datetime.min) + + def test_get_token_revocation_list_fetched_time_returns_mtime(self): + self.middleware.token_revocation_list_fetched_time = None + mtime = os.path.getmtime(self.middleware.revoked_file_name) + fetched_time = datetime.datetime.fromtimestamp(mtime) + self.assertEqual(self.middleware.token_revocation_list_fetched_time, + fetched_time) + + def test_get_token_revocation_list_fetched_time_returns_value(self): + expected = self.middleware._token_revocation_list_fetched_time + self.assertEqual(self.middleware.token_revocation_list_fetched_time, + expected) + + def test_get_revocation_list_returns_fetched_list(self): + self.middleware.token_revocation_list_fetched_time = None + os.remove(self.middleware.revoked_file_name) + self.assertEqual(self.middleware.token_revocation_list, + REVOCATION_LIST) + + def test_get_revocation_list_returns_current_list_from_memory(self): + self.assertEqual(self.middleware.token_revocation_list, + self.middleware._token_revocation_list) + + def test_get_revocation_list_returns_current_list_from_disk(self): + in_memory_list = self.middleware.token_revocation_list + self.middleware._token_revocation_list = None + self.assertEqual(self.middleware.token_revocation_list, in_memory_list) + + def test_invalid_revocation_list_raises_service_error(self): + globals()['SIGNED_REVOCATION_LIST'] = "{}" + with self.assertRaises(auth_token.ServiceError): + self.middleware.fetch_revocation_list() + + def test_fetch_revocation_list(self): + fetched_list = jsonutils.loads(self.middleware.fetch_revocation_list()) + self.assertEqual(fetched_list, REVOCATION_LIST) + + def test_request_invalid_uuid_token(self): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = 'invalid-token' + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + 'Keystone uri=\'https://keystone.example.com:1234\'') + + def test_request_invalid_signed_token(self): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = INVALID_SIGNED_TOKEN + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + 'Keystone uri=\'https://keystone.example.com:1234\'') + + def test_request_no_token(self): + req = webob.Request.blank('/') + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + 'Keystone uri=\'https://keystone.example.com:1234\'') + + def test_request_blank_token(self): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = '' + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + 'Keystone uri=\'https://keystone.example.com:1234\'') + + def test_memcache(self): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = SIGNED_TOKEN_SCOPED + self.middleware._cache = FakeMemcache() + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.middleware._cache.set_value, None) + + def test_memcache_set_invalid(self): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = 'invalid-token' + self.middleware._cache = FakeMemcache() + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.middleware._cache.set_value, "invalid") + + def test_memcache_set_expired(self): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = SIGNED_TOKEN_SCOPED + self.middleware._cache = FakeMemcache() + expired = datetime.datetime.now() - datetime.timedelta(minutes=1) + self.middleware._cache.token_expiration = float(expired.strftime("%s")) + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(len(self.middleware._cache.set_value), 2) + + def test_nomemcache(self): + self.disable_module('memcache') + + conf = { + 'admin_token': 'admin_token1', + 'auth_host': 'keystone.example.com', + 'auth_port': 1234, + 'memcache_servers': 'localhost:11211', + } + + auth_token.AuthProtocol(FakeApp(), conf) + + def test_request_prevent_service_catalog_injection(self): + req = webob.Request.blank('/') + req.headers['X-Service-Catalog'] = '[]' + req.headers['X-Auth-Token'] = UUID_TOKEN_NO_SERVICE_CATALOG + body = self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + self.assertFalse(req.headers.get('X-Service-Catalog')) + self.assertEqual(body, ['SUCCESS']) + + def test_will_expire_soon(self): + tenseconds = datetime.datetime.utcnow() + datetime.timedelta( + seconds=10) + self.assertTrue(auth_token.will_expire_soon(tenseconds)) + fortyseconds = datetime.datetime.utcnow() + datetime.timedelta( + seconds=40) + self.assertFalse(auth_token.will_expire_soon(fortyseconds)) diff --git a/tools/test-requires b/tools/test-requires index 59afe9d..8d9547b 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -10,5 +10,7 @@ nosehtmloutput pep8==1.2 sphinx>=1.1.2 unittest2>=0.5.1 +WebOb==1.0.8 +iso8601>=0.1.4 Babel |