diff options
-rw-r--r-- | tests/packager/test_csar.py | 34 | ||||
-rw-r--r-- | tests/packager/test_manifest.py | 47 | ||||
-rw-r--r-- | tests/packager/test_utils.py | 26 | ||||
-rw-r--r-- | tests/resources/csar/test.crt | 19 | ||||
-rw-r--r-- | tests/resources/signature/ca.crt | 21 | ||||
-rw-r--r-- | tests/resources/signature/manifest.mf | 9 | ||||
-rw-r--r-- | tests/resources/signature/test.crt | 19 | ||||
-rw-r--r-- | tests/resources/signature/test.key | 27 | ||||
-rw-r--r-- | vnfsdk_pkgtools/cli/__main__.py | 18 | ||||
-rw-r--r-- | vnfsdk_pkgtools/packager/csar.py | 58 | ||||
-rw-r--r-- | vnfsdk_pkgtools/packager/manifest.py | 32 | ||||
-rw-r--r-- | vnfsdk_pkgtools/packager/utils.py | 56 |
12 files changed, 350 insertions, 16 deletions
diff --git a/tests/packager/test_csar.py b/tests/packager/test_csar.py index f8875f3..e9e441c 100644 --- a/tests/packager/test_csar.py +++ b/tests/packager/test_csar.py @@ -26,7 +26,9 @@ CSAR_ENTRY_FILE = 'test_entry.yaml' CSAR_OUTPUT_FILE = 'output.csar' Args = collections.namedtuple('Args', - ['source', 'entry', 'manifest', 'history', 'tests', 'licenses', 'digest']) + ['source', 'entry', 'manifest', 'history', 'tests', + 'licenses', 'digest', 'certificate', 'privkey']) + ARGS_MANIFEST = { 'source': CSAR_RESOURCE_DIR, @@ -35,7 +37,9 @@ ARGS_MANIFEST = { 'history': 'ChangeLog.txt', 'tests': 'Tests', 'licenses': 'Licenses', - 'digest': None + 'digest': None, + 'certificate': None, + 'privkey': None, } ARGS_MANIFEST_DIGEST = { @@ -45,9 +49,22 @@ ARGS_MANIFEST_DIGEST = { 'history': 'ChangeLog.txt', 'tests': 'Tests', 'licenses': 'Licenses', - 'digest': 'sha256' + 'digest': 'sha256', + 'certificate': None, + 'privkey': None, } +ARGS_MANIFEST_DIGEST_CERT = { + 'source': CSAR_RESOURCE_DIR, + 'entry': CSAR_ENTRY_FILE, + 'manifest': 'test_entry.mf', + 'history': 'ChangeLog.txt', + 'tests': 'Tests', + 'licenses': 'Licenses', + 'digest': 'sha256', + 'certificate': 'test.crt', + 'privkey': 'tests/resources/signature/test.key', + } ARGS_NO_MANIFEST = { 'source': CSAR_RESOURCE_DIR, @@ -57,6 +74,8 @@ ARGS_NO_MANIFEST = { 'tests': None, 'licenses': None, 'digest': None, + 'certificate': None, + 'privkey': None, } @@ -65,7 +84,7 @@ def csar_write_test(args): csar_extract_dir = tempfile.mkdtemp() try: csar.write(args.source, args.entry, csar_target_dir + '/' + CSAR_OUTPUT_FILE, args) - csar.read(csar_target_dir + '/' + CSAR_OUTPUT_FILE, csar_extract_dir) + csar.read(csar_target_dir + '/' + CSAR_OUTPUT_FILE, csar_extract_dir, True) assert filecmp.cmp(args.source + '/' + args.entry, csar_extract_dir + '/' + args.entry) if(args.manifest and not args.digest): assert filecmp.cmp(args.source + '/' + args.manifest, @@ -96,3 +115,10 @@ def test_CSARWrite_manifest_digest(): if not os.path.exists(license_path): os.makedirs(license_path) csar_write_test(Args(**ARGS_MANIFEST_DIGEST)) + +def test_CSARWrite_manifest_digest_cert(): + # Because git can not store emptry directory, we need to create manually here + license_path = ARGS_MANIFEST['source'] + '/' + ARGS_MANIFEST['licenses'] + if not os.path.exists(license_path): + os.makedirs(license_path) + csar_write_test(Args(**ARGS_MANIFEST_DIGEST_CERT)) diff --git a/tests/packager/test_manifest.py b/tests/packager/test_manifest.py index b95d7c6..2383284 100644 --- a/tests/packager/test_manifest.py +++ b/tests/packager/test_manifest.py @@ -13,6 +13,9 @@ # under the License. # +import os +import os.path + import pytest from vnfsdk_pkgtools.packager import manifest @@ -38,6 +41,24 @@ FILE_DIGEST = '\n'.join(['Source: digest', 'Hash: 20a480339aa4371099f9503511dcc5a8051ce3884846678ced5611ec64bbfc9c', ]) +CMS = '\n'.join(['-----BEGIN CMS-----', + 'MIICmAYJKoZIhvcNAQcCoIICiTCCAoUCAQExDTALBglghkgBZQMEAgEwCwYJKoZI', + 'hvcNAQcBMYICYjCCAl4CAQEwUjBFMQswCQYDVQQGEwJQVDEPMA0GA1UECAwGTGlz', + 'Ym9hMQ8wDQYDVQQHDAZMaXNib2ExFDASBgNVBAoMC0V4YW1wbGUgT3JnAgkA6w7o', + '0SBbUUwwCwYJYIZIAWUDBAIBoIHkMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEw', + 'HAYJKoZIhvcNAQkFMQ8XDTE4MDgyNzAzMjY1MlowLwYJKoZIhvcNAQkEMSIEIFDv', + '62qcyvy9rbeUjjg0odflTyXt7GjP7xMyQe/k/joJMHkGCSqGSIb3DQEJDzFsMGow', + 'CwYJYIZIAWUDBAEqMAsGCWCGSAFlAwQBFjALBglghkgBZQMEAQIwCgYIKoZIhvcN', + 'AwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3DQMCAgFAMAcGBSsOAwIHMA0GCCqG', + 'SIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIIBAJzPsQ0tR9O7dXVJ7XywGLKrO/xG', + 'C9z7EMqxbjCX+bfkGh5b67mSWlHnN2Yox33YBV8cTz/NzHS8UW9x3CTNvt0wJ+5m', + 'Pcv+3w52XHu67b3LmMiJugpsyEIeB/qm1PzXPAqWAk+figwNtbhw994C6EzPQz+x', + 'eoS386Bie7kf/y/ac+xWiOdYYdC+SFhbko6sEJSCBzOIs1m3ufrsBukMxhxema5h', + 'pqE+DUlSFyilc9CQWnSLubkHmM4dZnU7qnNoTBqplDYpOYH3WSNN9Cv322JusAzt', + 'SzFEv182phI2C5pmjUnf7VG1WMKCH2WNtkYwMUCDcGvbHrh8n+kR8hL/BAs=', + '-----END CMS-----', + ]) + def test_metadata(tmpdir): p = tmpdir.mkdir('csar').join('test.mf') p.write(METADATA) @@ -100,10 +121,34 @@ def test_update_to_file(tmpdir): m1 = manifest.Manifest(mf.dirname, 'test.mf') m1.add_file('digest2', 'SHA256') + m1.signature = CMS m1.update_to_file() m2 = manifest.Manifest(mf.dirname, 'test.mf') assert m1.metadata['vnf_provider_id'] == m2.metadata['vnf_provider_id'] assert m1.digests['digest'] == m2.digests['digest2'] assert len(m2.digests.keys()) == 2 + assert m2.signature == CMS + +def test_signature(tmpdir): + p = tmpdir.mkdir('csar').join('test.mf') + p.write(METADATA + "\n\n" + CMS) + m = manifest.Manifest(p.dirname, 'test.mf') + assert m.signature == CMS - +def test_illegal_signature(tmpdir): + p = tmpdir.mkdir('csar').join('test.mf') + p.write(METADATA + "\n\n" + CMS[:-17]) + with pytest.raises(manifest.ManifestException) as excinfo: + manifest.Manifest(p.dirname, 'test.mf') + excinfo.match(r"Can NOT find end of sigature block") + +def test_signature_strip(tmpdir): + p = tmpdir.mkdir('csar').join('test.mf') + p.write(METADATA + "\n\n" + CMS) + m1 = manifest.Manifest(p.dirname, 'test.mf') + newfile = m1.save_to_temp_without_cms() + m2 = manifest.Manifest(os.path.dirname(newfile), + os.path.basename(newfile)) + assert m1.metadata == m2.metadata + assert m2.signature is None + os.unlink(newfile) diff --git a/tests/packager/test_utils.py b/tests/packager/test_utils.py index 91fc72b..3124ea5 100644 --- a/tests/packager/test_utils.py +++ b/tests/packager/test_utils.py @@ -14,6 +14,9 @@ # import os +import subprocess + +import pytest from vnfsdk_pkgtools.packager import utils @@ -34,3 +37,26 @@ def test_cal_file_hash_remote(mocker): self.content = CONTENT mocker.patch('requests.get', new=FakeRequest) assert SHA256 == utils.cal_file_hash("", "http://fake", 'sha256') + + +MSG_FILE = "tests/resources/signature/manifest.mf" +CERT_FILE = "tests/resources/signature/test.crt" +KEY_FILE = "tests/resources/signature/test.key" + +def test_sign_verify_pairwise(): + cms = utils.sign(MSG_FILE, CERT_FILE, KEY_FILE) + # We can't examine the exact content of cms because it contains timestamp + assert "---BEGIN CMS---" in cms + assert "---END CMS---" in cms + utils.verify(MSG_FILE, CERT_FILE, cms, no_verify_cert=True) + + +def test_verify_bad(tmpdir): + cms = utils.sign(MSG_FILE, CERT_FILE, KEY_FILE) + + p = tmpdir.join("file_msg.txt") + p.write("BAD") + + with pytest.raises(subprocess.CalledProcessError): + utils.verify(str(p), CERT_FILE, cms, no_verify_cert=True) + diff --git a/tests/resources/csar/test.crt b/tests/resources/csar/test.crt new file mode 100644 index 0000000..63c85d4 --- /dev/null +++ b/tests/resources/csar/test.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDIDCCAggCCQDrDujRIFtRTDANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJQ +VDEPMA0GA1UECAwGTGlzYm9hMQ8wDQYDVQQHDAZMaXNib2ExFDASBgNVBAoMC0V4 +YW1wbGUgT3JnMCAXDTE4MDgyNDA2MjY1OVoYDzIxMTYwMjEyMDYyNjU5WjBdMQsw +CQYDVQQGEwJQVDEPMA0GA1UECAwGTGlzYm9hMQ8wDQYDVQQHDAZMaXNib2ExFDAS +BgNVBAoMC0V4YW1wbGUgT3JnMRYwFAYDVQQDDA0qLmV4YW1wbGUub3JnMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwVDDqoO+C5dxgi1nnky+D4qqVdFG +mX3H4q6zFPUAkve3gElfttuDfbmN+OYCWhvKcjUN1Y2xjt+0aeRJVuQ+eumEX/1F +76i2t9c66fWPtdZ0V8IuDc2ajNxbKiAwYrwVl3AS2tJ32psHRLvpmLoOVz9UXY0J +rDwr274Z38wIqEGrUQ9hdOebEggeVu6Mv3pZUBYGGo9VX1/PTZguOaP85nC193Ux +SJe2+KV6aoc0odiokFmWK2JJrNb8bMjrQcQqp86JMW1DHyon5sF6edTIilxgC+SH +gapT5hZeoNnh3rAgHiWXF8ZOvho341s+7I78pbEtqCXNbF3VqikFlWmStQIDAQAB +MA0GCSqGSIb3DQEBCwUAA4IBAQCh8CffE1amceKSb7USEfkpsDbNYo+IWMDyVo9g +WQOYVIqIFGS8RMzs43Y6nIYJ/9pJUG10Qc4Yq1ZEqsV771Fz6WHx3zlJakVww/Ph +CxbakjO3EzIHVjEWIu3sUfMdyOeF0ZDHDnfQZYWC17d2jE+s8rH2epl2h1jhi8fS +i+eT2QDv8lHAM2mdM4jSwoCSsN7FImRxcYPoCxYwVkjVkmHhEMaUdqa1LKY/0YBf +PFm0pVDCBJZZvKGql44eKiaY/GNW9IyzQFprT8V1rhD1fbTBFXghVGVaUi2Am3JD ++eZYMzd4rzFLZm8bjNm0Oler1UJSR1K91lOEig3M8FTN6JRE +-----END CERTIFICATE----- diff --git a/tests/resources/signature/ca.crt b/tests/resources/signature/ca.crt new file mode 100644 index 0000000..89ff01a --- /dev/null +++ b/tests/resources/signature/ca.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgIJANdjlIoTLDroMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAlBUMQ8wDQYDVQQIDAZMaXNib2ExDzANBgNVBAcMBkxpc2JvYTEUMBIGA1UE +CgwLRXhhbXBsZSBPcmcwIBcNMTgwODI0MDYyMDI1WhgPMjExNjAyMTIwNjIwMjVa +MEUxCzAJBgNVBAYTAlBUMQ8wDQYDVQQIDAZMaXNib2ExDzANBgNVBAcMBkxpc2Jv +YTEUMBIGA1UECgwLRXhhbXBsZSBPcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDGe3tVz3JvBdkWl7od9ji0XoK8NQBogqSjVluw6ulRf2HR/3XGzehw +3KskDHkxINpfVDoTqUPaCw3Q4as/lu58FstxRZEDf39XjSNgd6igPMzXaUal7ImK +hl3Qi+XU7CgexnooJBNvr08y+Tbr53UmzXvWAHpTCvwghB7CvGnWaLSB5kQMgaRt +BD6Hudri0PKiwN6gdsSjlU00MCq7MSwnWXn1vG7XI7CliRzxWQ4c2Xpm7X82xQyo +lJNNjkhEY8wwTStFMyDdpD0mJTn1t6Y34KEDC9jJdEK8b6VDggpmnbSlNmsbmBUt +JqMeiDvIKHnNk1GrjkRRyYb2FsL1qn4dAgMBAAGjUDBOMB0GA1UdDgQWBBRW1/Ag +e8qM1WlXWB0QXf1u36EYBzAfBgNVHSMEGDAWgBRW1/Age8qM1WlXWB0QXf1u36EY +BzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCS6WS/w7wyQqljtc+r +39BPQ4Mbbqlws+GSjDcQn7ffes/PxSTSvKE/NxWrAFiG0Im1jWvBfEiCKDSMuBVg +C6aZmqYib2TlybXKUEKG9wfS/BFMqYkC2eQybwGjuKcaVlzIEtUctfychkRW6K7r +mf2FJXxtgrMjM2w6aFyk5n7VE/0rxgd8AznW39Z3Lg2gcmEzEFTOqB7DvXeMZffx ++G45//Id377lakpJGH92ALsCNdKkKZddxxp3554PZNsYRYFQoSsl7wfj7UV7IwNx +i48xlPZTbB4L0GEtgTxIXcYRC3bWOByjfP0YjOWnFJU6caSI77cQmlRuXg9k3jfD +2Fvh +-----END CERTIFICATE----- diff --git a/tests/resources/signature/manifest.mf b/tests/resources/signature/manifest.mf new file mode 100644 index 0000000..06895d3 --- /dev/null +++ b/tests/resources/signature/manifest.mf @@ -0,0 +1,9 @@ +metadata: +vnf_product_name: test +vnf_provider_id: test +vnf_package_version: 1.0 +vnf_release_data_time: 2017-09-15T15:00:03+08:00 + +Source: digest +Algorithm: SHA256 +Hash: 20a480339aa4371099f9503511dcc5a8051ce3884846678ced5611ec64bbfc9c diff --git a/tests/resources/signature/test.crt b/tests/resources/signature/test.crt new file mode 100644 index 0000000..63c85d4 --- /dev/null +++ b/tests/resources/signature/test.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDIDCCAggCCQDrDujRIFtRTDANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJQ +VDEPMA0GA1UECAwGTGlzYm9hMQ8wDQYDVQQHDAZMaXNib2ExFDASBgNVBAoMC0V4 +YW1wbGUgT3JnMCAXDTE4MDgyNDA2MjY1OVoYDzIxMTYwMjEyMDYyNjU5WjBdMQsw +CQYDVQQGEwJQVDEPMA0GA1UECAwGTGlzYm9hMQ8wDQYDVQQHDAZMaXNib2ExFDAS +BgNVBAoMC0V4YW1wbGUgT3JnMRYwFAYDVQQDDA0qLmV4YW1wbGUub3JnMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwVDDqoO+C5dxgi1nnky+D4qqVdFG +mX3H4q6zFPUAkve3gElfttuDfbmN+OYCWhvKcjUN1Y2xjt+0aeRJVuQ+eumEX/1F +76i2t9c66fWPtdZ0V8IuDc2ajNxbKiAwYrwVl3AS2tJ32psHRLvpmLoOVz9UXY0J +rDwr274Z38wIqEGrUQ9hdOebEggeVu6Mv3pZUBYGGo9VX1/PTZguOaP85nC193Ux +SJe2+KV6aoc0odiokFmWK2JJrNb8bMjrQcQqp86JMW1DHyon5sF6edTIilxgC+SH +gapT5hZeoNnh3rAgHiWXF8ZOvho341s+7I78pbEtqCXNbF3VqikFlWmStQIDAQAB +MA0GCSqGSIb3DQEBCwUAA4IBAQCh8CffE1amceKSb7USEfkpsDbNYo+IWMDyVo9g +WQOYVIqIFGS8RMzs43Y6nIYJ/9pJUG10Qc4Yq1ZEqsV771Fz6WHx3zlJakVww/Ph +CxbakjO3EzIHVjEWIu3sUfMdyOeF0ZDHDnfQZYWC17d2jE+s8rH2epl2h1jhi8fS +i+eT2QDv8lHAM2mdM4jSwoCSsN7FImRxcYPoCxYwVkjVkmHhEMaUdqa1LKY/0YBf +PFm0pVDCBJZZvKGql44eKiaY/GNW9IyzQFprT8V1rhD1fbTBFXghVGVaUi2Am3JD ++eZYMzd4rzFLZm8bjNm0Oler1UJSR1K91lOEig3M8FTN6JRE +-----END CERTIFICATE----- diff --git a/tests/resources/signature/test.key b/tests/resources/signature/test.key new file mode 100644 index 0000000..06781d1 --- /dev/null +++ b/tests/resources/signature/test.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAwVDDqoO+C5dxgi1nnky+D4qqVdFGmX3H4q6zFPUAkve3gElf +ttuDfbmN+OYCWhvKcjUN1Y2xjt+0aeRJVuQ+eumEX/1F76i2t9c66fWPtdZ0V8Iu +Dc2ajNxbKiAwYrwVl3AS2tJ32psHRLvpmLoOVz9UXY0JrDwr274Z38wIqEGrUQ9h +dOebEggeVu6Mv3pZUBYGGo9VX1/PTZguOaP85nC193UxSJe2+KV6aoc0odiokFmW +K2JJrNb8bMjrQcQqp86JMW1DHyon5sF6edTIilxgC+SHgapT5hZeoNnh3rAgHiWX +F8ZOvho341s+7I78pbEtqCXNbF3VqikFlWmStQIDAQABAoIBAEWZ+pjuDgLGaAvi +pSgNoXCfEG0NiEnDS7CGDdxByg8gOWVrBZckudcOc9tllPs0flNYXr7A4Wj7ik2F +8BHk48oFQa/91KxRJlhSmdeanj9uEz11oHc/y6FjxT8Jj+I8rdl0dJVsqdJ/dsJ8 +lcJzWq53Er0MBquIjpHzhAVbxNIvEjQvuIyKPEomrvNN49fR5J7ExH8n1OnkuFRJ +L4L98inXPtcQF2Xgorh2YwhV0DiY5X88TZMCZ8m7CB0DhwZSMueFWa6Aj4m5G0HQ +kalXU3bCeBu66s3sXZHuGg1oF3MwwO7BX3/1fg8LoaLEvYHAOXH5PNRUzAgh7/mA +dneVYoECgYEA69oHQ9VvXGpqHnE5spAtmyJn7v32zXEPUibdqPCNrGXznvzXM+Do +xHB4K51H8wE/WGT/LGgVcubqr62f/SvmyO6RVOS+DRY4aYKyFif0+7mVHgWhoYOg +oEaODZrKdxC4SrSYaogsDCH7bdkh1VdJhCDx6ppUGXs9/ZfKJn9kG+0CgYEA0dR9 +BQ6qnrBDrZg7vW/xV/iL/PGsJVBUt7oAkN7g4Ub42eTjAhIPdMxipgAkj6EbhoBO +KpWELvRqC3oTUVutdTvRhdgummNKToZNaYG7CK5X6bsQh7l6tqLVkE1WqoT4yXvA +1ez7iOhOsTvugYLlRJd3MTNiKw6c9Pr5ihIHyOkCgYEAsMRtk4LBfzNmq8BS7Skw +dUhZO39u3dVyFTF6CqeYl3CmIjAAnypfavOcrZupFFGIB5lSwxaTlNXLabYo5hhk +VY6wsD7szmQsDbBeB0sVSyFxyNmRpbtAxT98MmgkfQ60AZHPdnDvCnzhtBf8Sqis +OfJzPlFC3QJImOuOIEJ0ZpUCgYAtwj0RYUVsHdxkexJsGYF0Qculeb6k1IPlHTcK +YNCB0ExsmDG5Pd8JBZltwhI3EFdEWa6yMrEE3GBT6GdWErwdGNJ0cdSX7pdB0KAJ +JjV3iy241waf0p8kPRZ9xZ/kx+LJbad7/5ZLIP4oQH2e2MDO2IkxvZbm9OTdt+27 +Yns4IQKBgEuQv0JTt+o6/1cwZkMvXDKf0vW2nbPG9gbP3gy9Js0mOG0eWYCsNehO +DGLViyV2XURCskM4ZSA7Xc71lbCFhOV+jDXEipAjw8S8WJWdTraoF1QV12YEdlL/ +KdcxuJKVX8vdE9UCOxH/wcDG/JXAHX3qrMudJgAp598nyk6g2cNt +-----END RSA PRIVATE KEY----- diff --git a/vnfsdk_pkgtools/cli/__main__.py b/vnfsdk_pkgtools/cli/__main__.py index 23dbe02..2b262b9 100644 --- a/vnfsdk_pkgtools/cli/__main__.py +++ b/vnfsdk_pkgtools/cli/__main__.py @@ -25,6 +25,7 @@ import tempfile from vnfsdk_pkgtools import validator def csar_create_func(namespace): + csar.write(namespace.source, namespace.entry, namespace.destination, @@ -32,14 +33,16 @@ def csar_create_func(namespace): def csar_open_func(namespace): csar.read(namespace.source, - namespace.destination) + namespace.destination, + namespace.no_verify_cert) def csar_validate_func(namespace): workdir = tempfile.mkdtemp() try: reader = None reader = csar.read(namespace.source, - workdir) + workdir, + no_verify_cert=True) driver = validator.get_validator(namespace.parser) driver.validate(reader) @@ -87,6 +90,12 @@ def parse_args(args_list): '--digest', choices=['SHA256', 'SHA512'], help='If present, means to check the file deigest in manifest; compute the digest using the specified hash algorithm of all files in the csar package to be put into the manifest file') + csar_create.add_argument( + '--certificate', + help='Certificate file for certification, relative to service template directory') + csar_create.add_argument( + '--privkey', + help='Private key file for certification, absoluate or relative path') csar_open = subparsers.add_parser('csar-open') @@ -98,6 +107,11 @@ def parse_args(args_list): '-d', '--destination', help='Output directory to extract the CSAR into', required=True) + csar_open.add_argument( + '--no-verify-cert', + action='store_true', + help="Do NOT verify the signer's certificate") + csar_validate = subparsers.add_parser('csar-validate') csar_validate.set_defaults(func=csar_validate_func) diff --git a/vnfsdk_pkgtools/packager/csar.py b/vnfsdk_pkgtools/packager/csar.py index 162985f..a397f2e 100644 --- a/vnfsdk_pkgtools/packager/csar.py +++ b/vnfsdk_pkgtools/packager/csar.py @@ -23,6 +23,7 @@ import requests from ruamel import yaml # @UnresolvedImport from vnfsdk_pkgtools.packager import manifest +from vnfsdk_pkgtools.packager import utils LOG = logging.getLogger(__name__) @@ -38,6 +39,7 @@ META_ENTRY_MANIFEST_FILE_KEY = 'Entry-Manifest' META_ENTRY_HISTORY_FILE_KEY = 'Entry-Change-Log' META_ENTRY_TESTS_DIR_KEY = 'Entry-Tests' META_ENTRY_LICENSES_DIR_KEY = 'Entry-Licenses' +META_ENTRY_CERT_FILE_KEY = 'Entry-Certificate' BASE_METADATA = { META_FILE_VERSION_KEY: META_FILE_VERSION_VALUE, @@ -108,6 +110,19 @@ def write(source, entry, destination, args): check_dir=False) metadata[META_ENTRY_HISTORY_FILE_KEY] = args.history + if args.certificate: + check_file_dir(root=source, + entry=args.certificate, + msg='Please specify a valid certificate file.', + check_dir=False) + metadata[META_ENTRY_CERT_FILE_KEY] = args.certificate + if not args.privkey: + raise RuntimeError('Need private key file for signing') + check_file_dir(root='', + entry=args.privkey, + msg='Please specify a valid private key file.', + check_dir=False) + if(args.tests): check_file_dir(root=source, entry=args.tests, @@ -144,8 +159,14 @@ def write(source, entry, destination, args): f.write(dir_full_path + os.sep, dir_relative_path) if manifest_file: - if args.digest: - LOG.debug('Update manifest file to temporary file') + LOG.debug('Update manifest file to temporary file') + manifest_file_full_path = manifest_file.update_to_file(True) + if args.certificate and args.privkey: + LOG.debug('calculate signature') + manifest_file.signature = utils.sign(msg_file=manifest_file_full_path, + cert_file=os.path.join(source, args.certificate), + key_file=args.privkey) + # write cms into it manifest_file_full_path = manifest_file.update_to_file(True) LOG.debug('Writing to archive: {0}'.format(args.manifest)) f.write(manifest_file_full_path, args.manifest) @@ -156,7 +177,7 @@ def write(source, entry, destination, args): class _CSARReader(object): - def __init__(self, source, destination): + def __init__(self, source, destination, no_verify_cert=True): if os.path.isdir(destination) and os.listdir(destination): raise ValueError('{0} already exists and is not empty. ' 'Please specify the location where the CSAR ' @@ -179,7 +200,7 @@ class _CSARReader(object): raise ValueError('{0} is not a valid CSAR.'.format(self.source)) self._extract() self._read_metadata() - self._validate() + self._validate(no_verify_cert) finally: if downloaded_csar: os.remove(self.source) @@ -221,6 +242,10 @@ class _CSARReader(object): def entry_licenses_dir(self): return self.metadata.get(META_ENTRY_LICENSES_DIR_KEY) + @property + def entry_certificate_file(self): + return self.metadata.get(META_ENTRY_CERT_FILE_KEY) + def _extract(self): LOG.debug('Extracting CSAR contents') if not os.path.exists(self.destination): @@ -239,7 +264,7 @@ class _CSARReader(object): self.metadata.update(yaml.load(f)) LOG.debug('CSAR metadata:\n{0}'.format(pprint.pformat(self.metadata))) - def _validate(self): + def _validate(self, no_verify_cert): def validate_key(key, expected=None): if not self.metadata.get(key): raise ValueError('{0} is missing from the metadata file.'.format(key)) @@ -256,6 +281,7 @@ class _CSARReader(object): LOG.debug('CSAR change history file: {0}'.format(self.entry_history_file)) LOG.debug('CSAR tests directory: {0}'.format(self.entry_tests_dir)) LOG.debug('CSAR licenses directory: {0}'.format(self.entry_licenses_dir)) + LOG.debug('CSAR certificate file: {0}'.format(self.entry_certificate_file)) check_file_dir(self.destination, self.entry_definitions, @@ -294,6 +320,22 @@ class _CSARReader(object): 'file does not exist.'.format(self.entry_licenses_dir), check_dir=True) + if(self.entry_certificate_file): + # check certificate + check_file_dir(self.destination, + self.entry_certificate_file, + 'The certificate file {0} referenced by the metadata ' + 'file does not exist.'.format(self.entry_certificate_file), + check_dir=False) + tmp_manifest = self.manifest.save_to_temp_without_cms() + utils.verify(tmp_manifest, + os.path.join(self.destination, self.entry_certificate_file), + self.manifest.signature, + no_verify_cert) + os.unlink(tmp_manifest) + + + def _download(self, url, target): response = requests.get(url, stream=True) if response.status_code != 200: @@ -306,5 +348,7 @@ class _CSARReader(object): f.write(chunk) -def read(source, destination): - return _CSARReader(source=source, destination=destination) +def read(source, destination, no_verify_cert=False): + return _CSARReader(source=source, + destination=destination, + no_verify_cert=no_verify_cert) diff --git a/vnfsdk_pkgtools/packager/manifest.py b/vnfsdk_pkgtools/packager/manifest.py index e5bceb0..937f14e 100644 --- a/vnfsdk_pkgtools/packager/manifest.py +++ b/vnfsdk_pkgtools/packager/manifest.py @@ -123,9 +123,12 @@ class Manifest(object): self.digests[desc['Source']] = (desc['Algorithm'], desc['Hash']) elif key: raise ManifestException("Unknown key in line '%s:%s'" % (key, value)) + elif '--BEGIN CMS--' in remain: + if '--END CMS--' not in block[-1]: + raise ManifestException("Can NOT find end of sigature block") + self.signature = remain + '\n' + '\n'.join(block) else: - # TODO signature block - pass + raise ManifestException("Unknown content: '%s'" % remain) if not self.metadata: raise ManifestException("No metadata") @@ -155,6 +158,10 @@ class Manifest(object): ret += "Source: %s\n" % key ret += "Algorithm: %s\n" % digest[0] ret += "Hash: %s\n" % digest[1] + # signature + if self.signature: + ret += "\n" + ret += self.signature return ret def update_to_file(self, temporary=False): @@ -167,3 +174,24 @@ class Manifest(object): with open(abs_path, 'w') as fp: fp.write(content) return abs_path + + def save_to_temp_without_cms(self): + # we need to strip cms block with out changing the order of the + # file digest content before we verify the signature + tmpfile = tempfile.NamedTemporaryFile(delete=False) + skip = False + lines = [] + with open(os.path.join(self.root, self.path),'rU') as fp: + for line in fp: + if '--BEGIN CMS--' in line: + skip = True + elif '--END CMS--' in line: + skip = False + elif not skip: + lines.append(line) + # strip trailing empty lines + content = ''.join(lines).rstrip(' \n\t') + content += '\n' + tmpfile.write(content) + tmpfile.close() + return tmpfile.name diff --git a/vnfsdk_pkgtools/packager/utils.py b/vnfsdk_pkgtools/packager/utils.py index 2d74943..7027e2b 100644 --- a/vnfsdk_pkgtools/packager/utils.py +++ b/vnfsdk_pkgtools/packager/utils.py @@ -15,11 +15,17 @@ import hashlib from io import BytesIO +import logging import os +import os.path import urlparse +import subprocess +import tempfile import requests +LOG = logging.getLogger(__name__) + def _hash_value_for_file(f, hash_function, block_size=2**20): while True: @@ -43,3 +49,53 @@ def cal_file_hash(root, path, algo): else: with open(os.path.join(root, path), 'rb') as fp: return _hash_value_for_file(fp, h) + + +def _run_cmd(cmd, **kwargs): + if isinstance(cmd, list): + args = cmd + elif isinstance(cmd, string): + args = [cmd] + else: + raise RuntimeError("cmd must be string or list") + + for key, value in kwargs.iteritems(): + args.append(key) + if value: + args.append(value) + try: + LOG.debug("Executing %s", args) + return subprocess.check_output(args) + except subprocess.CalledProcessError as e: + LOG.error("Executing %s failed with return code %d, output: %s", + e.cmd, e.returncode, e.output) + raise e + + +def sign(msg_file, cert_file, key_file): + args = ["openssl", "cms", "-sign", "-binary", "-nocerts"] + kwargs = { + '-in': os.path.abspath(msg_file), + '-signer': os.path.abspath(cert_file), + '-inkey': os.path.abspath(key_file), + '-outform': 'PEM', + } + + return _run_cmd(args, **kwargs) + + +def verify(msg_file, cert_file, cms, no_verify_cert=False): + args = ["openssl", "cms", "-verify"] + if no_verify_cert: + args.append("-no_signer_cert_verify") + + with tempfile.NamedTemporaryFile() as f: + f.write(cms) + f.flush() + kwargs = { + '-in': f.name, + '-inform': 'PEM', + '-content': os.path.abspath(msg_file), + '-certfile': os.path.abspath(cert_file), + } + return _run_cmd(args, **kwargs) |