Skip to content

Commit 75bfe8b

Browse files
committed
Add support for RFC 9763
1 parent 0303805 commit 75bfe8b

File tree

4 files changed

+255
-1
lines changed

4 files changed

+255
-1
lines changed

CHANGES.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
Revision 0.4.7, released DD-MMM-2025
2+
------------------------------------
3+
- Cleanup setup.cfg and setup.py
4+
- Added RFC9763 for Related Certificates for Use in Multiple Authentications
5+
within a Protocol
6+
17
Revision 0.4.6, released 25-MAR-2025
28
------------------------------------
39
- Added RFC9688 for SHA3 One-way Hash Functions in the CMS

pyasn1_alt_modules/rfc9763.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
#
2+
# This file is part of pyasn1-alt-modules software.
3+
#
4+
# Created by Russ Housley.
5+
#
6+
# Copyright (c) 2025, Vigil Security, LLC
7+
# License: http://vigilsec.com/pyasn1-alt-modules-license.txt
8+
#
9+
# Related Certificates for Use in Multiple Authentications within a Protocol
10+
#
11+
# ASN.1 source from:
12+
# https://www.rfc-editor.org/rfc/rfc9763.txt
13+
#
14+
15+
from pyasn1.type import char
16+
from pyasn1.type import constraint
17+
from pyasn1.type import namedtype
18+
from pyasn1.type import univ
19+
20+
from pyasn1_alt_modules import rfc5280
21+
from pyasn1_alt_modules import rfc5652
22+
from pyasn1_alt_modules import rfc5751
23+
from pyasn1_alt_modules import rfc6019
24+
from pyasn1_alt_modules import opentypemap
25+
26+
certificateExtensionsMap = opentypemap.get('certificateExtensionsMap')
27+
28+
certificateAttributesMap = opentypemap.get('certificateAttributesMap')
29+
30+
MAX = float('inf')
31+
32+
33+
# Imports from RFC 5280
34+
35+
id_pe = rfc5280.id_pe
36+
37+
Attribute = rfc5280.Attribute
38+
39+
AlgorithmIdentifier = rfc5280.AlgorithmIdentifier
40+
41+
42+
43+
# Imports from RFC 5652
44+
45+
IssuerAndSerialNumber = rfc5652.IssuerAndSerialNumber
46+
47+
48+
# Imports from RFC 5751
49+
50+
id_aa = rfc5751.id_aa
51+
52+
53+
# Imports from RFC 6019
54+
55+
BinaryTime = rfc6019.BinaryTime
56+
57+
58+
# relatedCertificate Extension
59+
60+
id_pe_relatedCert = id_pe + (36, )
61+
62+
63+
class DigestAlgorithmIdentifier(AlgorithmIdentifier):
64+
pass
65+
66+
67+
class RelatedCertificate(univ.Sequence):
68+
componentType = namedtype.NamedTypes(
69+
namedtype.NamedType('hashAlgorithm', DigestAlgorithmIdentifier()),
70+
namedtype.NamedType('hashValue', univ.OctetString())
71+
)
72+
73+
74+
# relatedCertRequest Attribute
75+
76+
id_aa_relatedCertRequest = id_aa + (60, )
77+
78+
79+
class URI(char.IA5String):
80+
pass
81+
82+
83+
class UniformResourceIdentifiers(univ.SequenceOf):
84+
componentType = URI()
85+
subtypeSpec=constraint.ValueSizeConstraint(1, MAX)
86+
87+
88+
class RequesterCertificate(univ.Sequence):
89+
componentType = namedtype.NamedTypes(
90+
namedtype.NamedType('certID', IssuerAndSerialNumber()),
91+
namedtype.NamedType('requestTime', BinaryTime()),
92+
namedtype.NamedType('locationInfo', UniformResourceIdentifiers()),
93+
namedtype.NamedType('signature', univ.BitString())
94+
)
95+
96+
97+
# Update the Certificate Extension Map and the Certificate Attribute Map
98+
99+
_certificateAttributesMapUpdate = {
100+
id_aa_relatedCertRequest: RequesterCertificate(),
101+
}
102+
103+
certificateAttributesMap.update(_certificateAttributesMapUpdate)
104+
105+
106+
_certificateExtensionsMapUpdate = {
107+
id_pe_relatedCert: RelatedCertificate(),
108+
}
109+
110+
certificateExtensionsMap.update(_certificateExtensionsMapUpdate)

tests/__main__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,8 @@
192192
'tests.test_rfc9691.suite',
193193
'tests.test_rfc9708.suite',
194194
'tests.test_rfc9709.suite',
195-
'tests.test_rfc9734.suite']
195+
'tests.test_rfc9734.suite',
196+
'tests.test_rfc9763.suite']
196197
)
197198

198199

tests/test_rfc9763.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#
2+
# This file is part of pyasn1-alt-modules software.
3+
#
4+
# Created by Russ Housley
5+
# Copyright (c) 2025, Vigil Security, LLC
6+
# License: http://vigilsec.com/pyasn1-alt-modules-license.txt
7+
#
8+
import sys
9+
import unittest
10+
11+
from pyasn1.codec.der.decoder import decode as der_decoder
12+
from pyasn1.codec.der.encoder import encode as der_encoder
13+
14+
from pyasn1_alt_modules import pem
15+
from pyasn1_alt_modules import rfc2986
16+
from pyasn1_alt_modules import rfc4055
17+
from pyasn1_alt_modules import rfc5280
18+
from pyasn1_alt_modules import rfc9763
19+
from pyasn1_alt_modules import opentypemap
20+
21+
22+
class CertificationRequestTestCase(unittest.TestCase):
23+
pem_text = """\
24+
MIICcTCCAfcCAQAwcDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlZBMRAwDgYDVQQH
25+
EwdIZXJuZG9uMRAwDgYDVQQKEwdFeGFtcGxlMQ4wDAYDVQQDEwVBbGljZTEgMB4G
26+
CSqGSIb3DQEJARYRYWxpY2VAZXhhbXBsZS5jb20wdjAQBgcqhkjOPQIBBgUrgQQA
27+
IgNiAAT4zZ8HL+xEDpXWkoWp5xFMTz4u4Ae1nF6zXCYlmsEGD5vPu5hl9hDEjd1U
28+
HRgJIPoy3fJcWWeZ8FHCirICtuMgFisNscG/aTwKyDYOFDuqz/C2jyEwqgWCRyxy
29+
ohuJXtmgggEGMIIBAgYLKoZIhvcNAQkQAjwxgfIwge8wVzBRMQswCQYDVQQGEwJV
30+
UzELMAkGA1UECBMCVkExEDAOBgNVBAcTB0hlcm5kb24xEDAOBgNVBAoTB0V4YW1w
31+
bGUxETAPBgNVBAMTCEJvZ3VzIENBAgICmgIEZ+2IIzAlFiNodHRwczovL3JlcG8u
32+
ZXhhbXBsZS5jb20vbXljZXJ0LnA3YwNnADBkAjBhJnt/AN5PKuERkhLNg04UEDkM
33+
90wSayFvM6/dkvHvATT5EXEA0fgu9kxIS/6qDwkCMBqRmguBEYlMqRE5Ar6DT9Dg
34+
xYYZaRaOpcFR3fBrvuoUoQtjynQH7OEBQ0vWSoEHmTAKBggqhkjOPQQDAwNoADBl
35+
AjEAikPcXx+k7i2rRhd0sC+U4nRvoWbmaNqouskMTMFLhZR8lqy+Ue25oxjLKB80
36+
cL9BAjAA8qyl4itjffQIbI2cXBP2h5yZJ91/5lNf5Qp7ogi+1M1uIMVJaRQ++sHC
37+
/N8yKhw=
38+
"""
39+
40+
def setUp(self):
41+
self.asn1Spec = rfc2986.CertificationRequest()
42+
43+
def testDerCodec(self):
44+
substrate = pem.readBase64fromText(self.pem_text)
45+
asn1Object, rest = der_decoder(substrate, asn1Spec=self.asn1Spec)
46+
self.assertFalse(rest)
47+
self.assertTrue(asn1Object.prettyPrint())
48+
self.assertEqual(substrate, der_encoder(asn1Object))
49+
50+
certificateAttributesMap = opentypemap.get('certificateAttributesMap')
51+
self.assertIn(rfc9763.id_aa_relatedCertRequest, certificateAttributesMap)
52+
53+
found = False
54+
for attr in asn1Object['certificationRequestInfo']['attributes']:
55+
if attr['type'] == rfc9763.id_aa_relatedCertRequest:
56+
rc, rest = der_decoder(attr['values'][0],
57+
asn1Spec=certificateAttributesMap[attr['type']])
58+
self.assertFalse(rest)
59+
self.assertTrue(rc.prettyPrint())
60+
self.assertEqual(attr['values'][0], der_encoder(rc))
61+
self.assertIn("p7c", rc['locationInfo'][0])
62+
found = True
63+
64+
self.assertTrue(found)
65+
66+
def testOpenTypes(self):
67+
substrate = pem.readBase64fromText(self.pem_text)
68+
asn1Object, rest = der_decoder(substrate,
69+
asn1Spec=self.asn1Spec, decodeOpenTypes=True)
70+
self.assertFalse(rest)
71+
self.assertTrue(asn1Object.prettyPrint())
72+
self.assertEqual(substrate, der_encoder(asn1Object))
73+
74+
found = False
75+
for attr in asn1Object['certificationRequestInfo']['attributes']:
76+
if attr['type'] == rfc9763.id_aa_relatedCertRequest:
77+
self.assertEqual(0x67ED8823, attr['values'][0]['requestTime'])
78+
found = True
79+
80+
self.assertTrue(found)
81+
82+
83+
class CertificationExtensionTestCase(unittest.TestCase):
84+
pem_text = """\
85+
MIIDWTCCAt+gAwIBAgIJAKWzVCgbsG5VMAoGCCqGSM49BAMDMD8xCzAJBgNVBAYT
86+
AlVTMQswCQYDVQQIDAJWQTEQMA4GA1UEBwwHSGVybmRvbjERMA8GA1UECgwIQm9n
87+
dXMgQ0EwHhcNMjUwNDA3MjE0OTA3WhcNMjYwNDA3MjE0OTA3WjBOMQswCQYDVQQG
88+
EwJVUzELMAkGA1UECBMCVkExEDAOBgNVBAcTB0hlcm5kb24xEDAOBgNVBAoTB0V4
89+
YW1wbGUxDjAMBgNVBAMTBUtlaXRoMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAErg7y
90+
rZBUSfy3PIIjWV1+A/UOuVYyp3jTIKnaayr2HsKA+Go46023Hgm6fgZJC9YfGXRj
91+
6qjerTOkLzuKNYBmJVzfdz6abYMdHGzzJKrTscmQsLrVGn+r5A0c1Bz+Socco4IB
92+
ljCCAZIwHQYDVR0OBBYEFNGhVu4MQu5yVBtg7oXFoxu4dYfvMG8GA1UdIwRoMGaA
93+
FPI12zQE2qVV8r1pA5mwYuziFQjBoUOkQTA/MQswCQYDVQQGEwJVUzELMAkGA1UE
94+
CAwCVkExEDAOBgNVBAcMB0hlcm5kb24xETAPBgNVBAoMCEJvZ3VzIENBggkA6JHW
95+
BpFPzvIwDAYDVR0TAQH/BAIwADALBgNVHQ8EBAMCBsAwHAYDVR0RBBUwE4ERa2Vp
96+
dGhAZXhhbXBsZS5jb20wNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRw
97+
Oi8vb2NzcC5leGFtcGxlLmNvbS8wTQYIKwYBBQUHASQEQTA/MAsGCWCGSAFlAwQC
98+
AgQw1waLIBpBI+G8TFWN/QwSgRtbm+vLYR3LDeqXla8BDhO/zGp9QJE+9I4SGIvF
99+
1oE0MEIGCWCGSAGG+EIBDQQ1FjNUaGlzIGNlcnRpZmljYXRlIGNhbm5vdCBiZSB0
100+
cnVzdGVkIGZvciBhbnkgcHVycG9zZS4wCgYIKoZIzj0EAwMDaAAwZQIxAIWf14uO
101+
eX39ND1Bx+HDMy40GPZSMASoe6dnxVFgphdzvUnPARM2Qpm/9xvILLj0EgIwIt44
102+
0y/25Xe4wVX+6kR45V8TbByxs4bYCQcgWfeiw9Bu15yi8BYDhmgAK7LgZd6l
103+
"""
104+
105+
def setUp(self):
106+
self.asn1Spec = rfc5280.Certificate()
107+
108+
def testDerCodec(self):
109+
substrate = pem.readBase64fromText(self.pem_text)
110+
asn1Object, rest = der_decoder(substrate, asn1Spec=self.asn1Spec)
111+
self.assertFalse(rest)
112+
self.assertTrue(asn1Object.prettyPrint())
113+
self.assertEqual(substrate, der_encoder(asn1Object))
114+
115+
certificateExtensionsMap = opentypemap.get('certificateExtensionsMap')
116+
self.assertIn(rfc9763.id_pe_relatedCert, certificateExtensionsMap)
117+
118+
found = False
119+
for extn in asn1Object['tbsCertificate']['extensions']:
120+
if extn['extnID'] == rfc9763.id_pe_relatedCert:
121+
extnValue, rest = der_decoder(extn['extnValue'],
122+
asn1Spec=certificateExtensionsMap[extn['extnID']])
123+
self.assertFalse(rest)
124+
self.assertTrue(extnValue.prettyPrint())
125+
self.assertEqual(extn['extnValue'], der_encoder(extnValue))
126+
self.assertEqual(rfc4055.id_sha384,
127+
extnValue['hashAlgorithm']['algorithm'])
128+
found = True
129+
130+
self.assertTrue(found)
131+
132+
133+
suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
134+
135+
if __name__ == '__main__':
136+
result = unittest.TextTestRunner(verbosity=2).run(suite)
137+
sys.exit(not result.wasSuccessful())

0 commit comments

Comments
 (0)