From 745eed006e93ae89dc0e6e5c366160d6239490e9 Mon Sep 17 00:00:00 2001 From: Ilya Etingof Date: Fri, 8 Nov 2019 23:20:51 +0100 Subject: Add support for RFC 7508 (#102) --- CHANGES.txt | 1 + pyasn1_modules/rfc7508.py | 90 +++++++++++++++++++++++++++++++ tests/__main__.py | 1 + tests/test_rfc7508.py | 132 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 224 insertions(+) create mode 100644 pyasn1_modules/rfc7508.py create mode 100644 tests/test_rfc7508.py diff --git a/CHANGES.txt b/CHANGES.txt index a3660b7..3edd67b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -24,6 +24,7 @@ Revision 0.2.8, released XX-XX-2019 - Added RFC5924 providing Extended Key Usage for Session Initiation Protocol (SIP) in X.509 certificates - Added RFC5916 providing Device Owner Attribute +- Added RFC7508 providing Securing Header Fields with S/MIME - Update RFC8226 to use ComponentPresentConstraint() instead of the previous work around - Add RFC2631 providing OtherInfo for Diffie-Hellman Key Agreement diff --git a/pyasn1_modules/rfc7508.py b/pyasn1_modules/rfc7508.py new file mode 100644 index 0000000..6646024 --- /dev/null +++ b/pyasn1_modules/rfc7508.py @@ -0,0 +1,90 @@ +# +# This file is part of pyasn1-modules software. +# +# Created by Russ Housley with assistance from asn1ate v.0.6.0. +# +# Copyright (c) 2019, Vigil Security, LLC +# License: http://snmplabs.com/pyasn1/license.html +# +# Securing Header Fields with S/MIME +# +# ASN.1 source from: +# https://www.rfc-editor.org/rfc/rfc7508.txt +# https://www.rfc-editor.org/errata/eid5875 +# + +from pyasn1.type import char +from pyasn1.type import constraint +from pyasn1.type import namedtype +from pyasn1.type import namedval +from pyasn1.type import univ + +from pyasn1_modules import rfc5652 + +import string + +MAX = float('inf') + + +class Algorithm(univ.Enumerated): + namedValues = namedval.NamedValues( + ('canonAlgorithmSimple', 0), + ('canonAlgorithmRelaxed', 1) + ) + + +class HeaderFieldStatus(univ.Integer): + namedValues = namedval.NamedValues( + ('duplicated', 0), + ('deleted', 1), + ('modified', 2) + ) + + +class HeaderFieldName(char.VisibleString): + subtypeSpec = ( + constraint.PermittedAlphabetConstraint(*string.printable) - + constraint.PermittedAlphabetConstraint(':') + ) + + +class HeaderFieldValue(char.UTF8String): + pass + + +class HeaderField(univ.Sequence): + componentType = namedtype.NamedTypes( + namedtype.NamedType('field-Name', HeaderFieldName()), + namedtype.NamedType('field-Value', HeaderFieldValue()), + namedtype.DefaultedNamedType('field-Status', + HeaderFieldStatus().subtype(value='duplicated')) + ) + + +class HeaderFields(univ.SequenceOf): + componentType = HeaderField() + subtypeSpec = constraint.ValueSizeConstraint(1, MAX) + + +class SecureHeaderFields(univ.Set): + componentType = namedtype.NamedTypes( + namedtype.NamedType('canonAlgorithm', Algorithm()), + namedtype.NamedType('secHeaderFields', HeaderFields()) + ) + + +id_aa = univ.ObjectIdentifier((1, 2, 840, 113549, 1, 9, 16, 2, )) + +id_aa_secureHeaderFieldsIdentifier = id_aa + (55, ) + + + +# Map of Attribute Type OIDs to Attributes added to the +# ones that are in rfc5652.py + +_cmsAttributesMapUpdate = { + id_aa_secureHeaderFieldsIdentifier: SecureHeaderFields(), +} + +rfc5652.cmsAttributesMap.update(_cmsAttributesMapUpdate) + diff --git a/tests/__main__.py b/tests/__main__.py index 0af458c..c1fa4f7 100644 --- a/tests/__main__.py +++ b/tests/__main__.py @@ -76,6 +76,7 @@ suite = unittest.TestLoader().loadTestsFromNames( 'tests.test_rfc7229.suite', 'tests.test_rfc7292.suite', 'tests.test_rfc7296.suite', + 'tests.test_rfc7508.suite', 'tests.test_rfc7633.suite', 'tests.test_rfc7773.suite', 'tests.test_rfc7894.suite', diff --git a/tests/test_rfc7508.py b/tests/test_rfc7508.py new file mode 100644 index 0000000..07f0d86 --- /dev/null +++ b/tests/test_rfc7508.py @@ -0,0 +1,132 @@ +# +# This file is part of pyasn1-modules software. +# +# Created by Russ Housley +# Copyright (c) 2019, Vigil Security, LLC +# License: http://snmplabs.com/pyasn1/license.html +# + +import sys + +from pyasn1.type import univ + +from pyasn1.codec.der.decoder import decode as der_decode +from pyasn1.codec.der.encoder import encode as der_encode + +from pyasn1_modules import pem +from pyasn1_modules import rfc5652 +from pyasn1_modules import rfc7508 + +try: + import unittest2 as unittest +except ImportError: + import unittest + + +class SignedMessageTestCase(unittest.TestCase): + signed_message_pem_text = """\ +MIIE/AYJKoZIhvcNAQcCoIIE7TCCBOkCAQExDTALBglghkgBZQMEAgIwUQYJKoZI +hvcNAQcBoEQEQkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbg0KDQpXYXRzb24sIGNv +bWUgaGVyZSAtIEkgd2FudCB0byBzZWUgeW91LqCCAnwwggJ4MIIB/qADAgECAgkA +pbNUKBuwbjswCgYIKoZIzj0EAwMwPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZB +MRAwDgYDVQQHDAdIZXJuZG9uMREwDwYDVQQKDAhCb2d1cyBDQTAeFw0xOTA1Mjkx +NDQ1NDFaFw0yMDA1MjgxNDQ1NDFaMHAxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJW +QTEQMA4GA1UEBxMHSGVybmRvbjEQMA4GA1UEChMHRXhhbXBsZTEOMAwGA1UEAxMF +QWxpY2UxIDAeBgkqhkiG9w0BCQEWEWFsaWNlQGV4YW1wbGUuY29tMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAE+M2fBy/sRA6V1pKFqecRTE8+LuAHtZxes1wmJZrBBg+b +z7uYZfYQxI3dVB0YCSD6Mt3yXFlnmfBRwoqyArbjIBYrDbHBv2k8Csg2DhQ7qs/w +to8hMKoFgkcscqIbiV7Zo4GUMIGRMAsGA1UdDwQEAwIHgDBCBglghkgBhvhCAQ0E +NRYzVGhpcyBjZXJ0aWZpY2F0ZSBjYW5ub3QgYmUgdHJ1c3RlZCBmb3IgYW55IHB1 +cnBvc2UuMB0GA1UdDgQWBBTEuloOPnrjPIGw9AKqaLsW4JYONTAfBgNVHSMEGDAW +gBTyNds0BNqlVfK9aQOZsGLs4hUIwTAKBggqhkjOPQQDAwNoADBlAjBjuR/RNbgL +3kRhmn+PJTeKaL9sh/oQgHOYTgLmSnv3+NDCkhfKuMNoo/tHrkmihYgCMQC94Mae +rDIrQpi0IDh+v0QSAv9rMife8tClafXWtDwwL8MS7oAh0ymT446Uizxx3PUxggIA +MIIB/AIBATBMMD8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJWQTEQMA4GA1UEBwwH +SGVybmRvbjERMA8GA1UECgwIQm9ndXMgQ0ECCQCls1QoG7BuOzALBglghkgBZQME +AgKgggElMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X +DTE5MDUyOTE4MjMxOVowKAYJKoZIhvcNAQk0MRswGTALBglghkgBZQMEAgKhCgYI +KoZIzj0EAwMwMQYLKoZIhvcNAQkQAjcxIjEgCgEBMBswGRoERnJvbQwRYWxpY2VA +ZXhhbXBsZS5jb20wPwYJKoZIhvcNAQkEMTIEMLbkIqT9gmce1Peqxm1E9OiwuY1R +WHHGVufwmjb6XKzj4goQ5tryN5uJN9NM+ZkmbDBNBgsqhkiG9w0BCRACATE+MDwE +IMdPIQ9kJ1cI9Q6HkRCzbXWdD331uAUCL3MMFXP4KFOjgAEBMBUwE4ERYWxpY2VA +ZXhhbXBsZS5jb20wCgYIKoZIzj0EAwMEZzBlAjEAuZ8SebvwMRvLPn9+s3VHFUNU +bEtkkWCao1uNm5TOzphK0NbxzOsD854aC5ReKPSDAjAm1U0siLQw5p4qzGwyxDw9 +5AI5J8Mvy+icNubmfsd4ofvxdaECdhr4rvsSMwbOsFk= +""" + + def setUp(self): + self.asn1Spec = rfc5652.ContentInfo() + + def testDerCodec(self): + substrate = pem.readBase64fromText(self.signed_message_pem_text) + asn1Object, rest = der_decode (substrate, asn1Spec=self.asn1Spec) + assert not rest + assert asn1Object.prettyPrint() + assert der_encode(asn1Object) == substrate + + secure_header_field_attr_found = False + assert asn1Object['contentType'] == rfc5652.id_signedData + sd, rest = der_decode (asn1Object['content'], asn1Spec=rfc5652.SignedData()) + for sa in sd['signerInfos'][0]['signedAttrs']: + sat = sa['attrType'] + sav0 = sa['attrValues'][0] + + if sat == rfc7508.id_aa_secureHeaderFieldsIdentifier: + assert sat in rfc5652.cmsAttributesMap.keys() + sav, rest = der_decode(sav0, asn1Spec=rfc5652.cmsAttributesMap[sat]) + assert not rest + assert sav.prettyPrint() + assert der_encode(sav) == sav0 + + from_field = rfc7508.HeaderFieldName('From') + alice_email = rfc7508.HeaderFieldValue('alice@example.com') + for shf in sav['secHeaderFields']: + if shf['field-Name'] == from_field: + assert shf['field-Value'] == alice_email + secure_header_field_attr_found = True + + assert secure_header_field_attr_found + + def testOpenTypes(self): + substrate = pem.readBase64fromText(self.signed_message_pem_text) + asn1Object, rest = der_decode(substrate, + asn1Spec=self.asn1Spec, decodeOpenTypes=True) + assert not rest + assert asn1Object.prettyPrint() + assert der_encode(asn1Object) == substrate + + assert asn1Object['contentType'] in rfc5652.cmsContentTypesMap.keys() + assert asn1Object['contentType'] == rfc5652.id_signedData + + sd = asn1Object['content'] + assert sd['version'] == rfc5652.CMSVersion().subtype(value='v1') + + ect = sd['encapContentInfo']['eContentType'] + assert ect in rfc5652.cmsContentTypesMap.keys() + assert ect == rfc5652.id_data + + for sa in sd['signerInfos'][0]['signedAttrs']: + if sa['attrType'] == rfc7508.id_aa_secureHeaderFieldsIdentifier: + assert sa['attrType'] in rfc5652.cmsAttributesMap.keys() + + secure_header_field_attr_found = False + for sa in sd['signerInfos'][0]['signedAttrs']: + if sa['attrType'] == rfc7508.id_aa_secureHeaderFieldsIdentifier: + assert sa['attrType'] in rfc5652.cmsAttributesMap.keys() + from_field = rfc7508.HeaderFieldName('From') + alice_email = rfc7508.HeaderFieldValue('alice@example.com') + for shf in sa['attrValues'][0]['secHeaderFields']: + if shf['field-Name'] == from_field: + assert shf['field-Value'] == alice_email + secure_header_field_attr_found = True + + assert secure_header_field_attr_found + + +suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + +if __name__ == '__main__': + import sys + + result = unittest.TextTestRunner(verbosity=2).run(suite) + sys.exit(not result.wasSuccessful()) -- cgit v1.2.3