Support signing and certificate 19/63019/1
authorLianhao Lu <lianhao.lu@intel.com>
Mon, 27 Aug 2018 09:11:10 +0000 (17:11 +0800)
committerLianhao Lu <lianhao.lu@intel.com>
Mon, 27 Aug 2018 09:11:10 +0000 (17:11 +0800)
Added the support of certificate and signing based on SOL-004.

Change-Id: I864f298edbcd85a9da2126d369a5b98d7950d590
Issue-ID: VNFSDK-144
Signed-off-by: Lianhao Lu <lianhao.lu@intel.com>
tests/packager/test_csar.py
tests/packager/test_manifest.py
tests/resources/csar/test.crt [new file with mode: 0644]
vnfsdk_pkgtools/cli/__main__.py
vnfsdk_pkgtools/packager/csar.py
vnfsdk_pkgtools/packager/manifest.py

index f8875f3..e9e441c 100644 (file)
@@ -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))
index b95d7c6..2383284 100644 (file)
@@ -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/resources/csar/test.crt b/tests/resources/csar/test.crt
new file mode 100644 (file)
index 0000000..63c85d4
--- /dev/null
@@ -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-----
index 23dbe02..2b262b9 100644 (file)
@@ -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)
index 162985f..a397f2e 100644 (file)
@@ -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)
index e5bceb0..937f14e 100644 (file)
@@ -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