def test_demo_plugin(requests_mock): with_plugin_cfg = CertomancerConfig.from_file('tests/data/with-plugin.yml', 'tests/data') arch = with_plugin_cfg.get_pki_arch(ArchLabel('testing-ca')) illusionist.Illusionist(pki_arch=arch).register(requests_mock) importlib.import_module('example_plugin.encrypt_echo') # make the endpoint encrypt something endpoint = 'http://test.test/testing-ca/plugin/encrypt-echo/test-endpoint' payload = b'test test test' response = requests.post(endpoint, data=payload) # decrypt it env_data = cms.ContentInfo.load(response.content)['content'] key = arch.key_set.get_private_key(KeyLabel('signer1')) ktri = env_data['recipient_infos'][0].chosen encrypted_key = ktri['encrypted_key'].native decrypted_key = asymmetric.rsa_pkcs1v15_decrypt( asymmetric.load_private_key(key.dump()), encrypted_key) eci = env_data['encrypted_content_info'] cea = eci['content_encryption_algorithm'] assert cea['algorithm'].native == 'aes256_cbc' iv = cea['parameters'].native encrypted_content_bytes = eci['encrypted_content'].native decrypted_payload = symmetric.aes_cbc_pkcs7_decrypt( decrypted_key, encrypted_content_bytes, iv) assert decrypted_payload == payload
def _setup(cfgfile) -> ServiceSetup: cfg = CertomancerConfig.from_file(cfgfile, 'tests/data') arch = cfg.get_pki_arch(ArchLabel('testing-ca')) return ServiceSetup(cfg, arch, illusionist.Illusionist(pki_arch=arch))
def test_self_signed(label): cfg = f''' {label}: subject: root subject-key: root issuer: root authority-key: root validity: valid-from: "2000-01-01T00:00:00+0000" valid-to: "2500-01-01T00:00:00+0000" extensions: - id: basic_constraints critical: true value: ca: true - id: key_usage critical: true smart-value: schema: key-usage params: [digital_signature, key_cert_sign, crl_sign] ''' arch = PKIArchitecture( arch_label=ArchLabel('test'), key_set=RSA_KEYS, entities=ENTITIES, cert_spec_config=yaml.safe_load(cfg), service_config={}, external_url_prefix='http://test.test', ) root_cert = arch.get_cert(CertLabel(label)) assert root_cert.subject == ENTITIES[EntityLabel('root')] assert root_cert.issuer == ENTITIES[EntityLabel('root')] assert root_cert.not_valid_before == datetime(2000, 1, 1, tzinfo=pytz.utc)
def test_demo_plugin(): with_plugin_cfg = CertomancerConfig.from_file('tests/data/with-plugin.yml', 'tests/data') with_plugin_app = Animator(AnimatorArchStore(with_plugin_cfg.pki_archs), with_web_ui=False) client = Client(with_plugin_app, Response) # make the endpoint encrypt something endpoint = '/testing-ca/plugin/encrypt-echo/test-endpoint' payload = b'test test test' response = client.post(endpoint, data=payload) # decrypt it env_data = cms.ContentInfo.load(response.data)['content'] arch = with_plugin_cfg.get_pki_arch(ArchLabel('testing-ca')) key = arch.key_set.get_private_key(KeyLabel('signer1')) ktri = env_data['recipient_infos'][0].chosen encrypted_key = ktri['encrypted_key'].native decrypted_key = asymmetric.rsa_pkcs1v15_decrypt( asymmetric.load_private_key(key.dump()), encrypted_key) eci = env_data['encrypted_content_info'] cea = eci['content_encryption_algorithm'] assert cea['algorithm'].native == 'aes256_cbc' iv = cea['parameters'].native encrypted_content_bytes = eci['encrypted_content'].native decrypted_payload = symmetric.aes_cbc_pkcs7_decrypt( decrypted_key, encrypted_content_bytes, iv) assert decrypted_payload == payload
def test_serial_order_indep(order): arch = CONFIG.get_pki_arch(ArchLabel('testing-ca')) for lbl in order: arch.get_cert(CertLabel(lbl)) assert arch.get_cert(CertLabel('root')).serial_number == 4096 assert arch.get_cert(CertLabel('interm')).serial_number == 4097 assert arch.get_cert(CertLabel('signer1')).serial_number == 4097
def test_pss_exclusive(): cfg = CertomancerConfig.from_file('tests/data/with-external-config.yml', 'tests/data') arch = cfg.get_pki_arch(ArchLabel('testing-ca-pss-exclusive')) certs = ['root', 'interm', 'signer1', 'signer2'] for c in certs: assert arch.get_cert(CertLabel(c)).signature_algo == 'rsassa_pss' assert arch.get_cert(CertLabel(c)).public_key.algorithm == 'rsassa_pss'
async def test_embed_ac_revinfo_adobe_style(requests_mock): signer = get_ac_aware_signer() w = IncrementalPdfFileWriter(BytesIO(MINIMAL)) pki_arch = CERTOMANCER.get_pki_arch(ArchLabel('testing-ca-with-aa')) dummy_ts = timestamps.DummyTimeStamper( tsa_cert=pki_arch.get_cert(CertLabel('tsa')), tsa_key=pki_arch.key_set.get_private_key(KeyLabel('tsa')), certs_to_embed=SimpleCertificateStore.from_certs( [pki_arch.get_cert('root')] ) ) from certomancer.integrations.illusionist import Illusionist from pyhanko_certvalidator.fetchers.requests_fetchers import ( RequestsFetcherBackend, ) fetchers = RequestsFetcherBackend().get_fetchers() main_vc = ValidationContext( trust_roots=[pki_arch.get_cert('root')], allow_fetching=True, other_certs=signer.cert_registry, fetchers=fetchers, revocation_mode='require' ) ac_vc = ValidationContext( trust_roots=[pki_arch.get_cert('root-aa')], allow_fetching=True, other_certs=signer.cert_registry, fetchers=fetchers, revocation_mode='require' ) Illusionist(pki_arch).register(requests_mock) out = await signers.async_sign_pdf( w, signers.PdfSignatureMetadata( field_name='Sig1', embed_validation_info=True, validation_context=main_vc, ac_validation_context=ac_vc ), timestamper=dummy_ts, signer=signer ) r = PdfFileReader(out) s = r.embedded_signatures[0] # 4 CA certs, 1 AA certs, 1 AC, 1 signer cert -> 7 certs assert len(s.other_embedded_certs) == 5 # signer cert is excluded assert len(s.embedded_attr_certs) == 1 from pyhanko.sign.validation import RevocationInfoValidationType status = await async_validate_pdf_ltv_signature( s, RevocationInfoValidationType.ADOBE_STYLE, validation_context_kwargs={ 'trust_roots': [pki_arch.get_cert('root')] }, ac_validation_context_kwargs={ 'trust_roots': [pki_arch.get_cert('root-aa')] } ) assert status.bottom_line roles = list(status.ac_attrs['role'].attr_values) role = roles[0] assert isinstance(role, cms.RoleSyntax) assert role['role_name'].native == '*****@*****.**'
def serve_ocsp_response(self, request: Request, *, label: str, arch: str): pki_arch = self.architectures[ArchLabel(arch)] ocsp_resp = pki_arch.service_registry.summon_responder( ServiceLabel(label), self.at_time(request) ) data = request.stream.read() req: ocsp.OCSPRequest = ocsp.OCSPRequest.load(data) response = ocsp_resp.build_ocsp_response(req) return Response(response.dump(), mimetype='application/ocsp-response')
def serve_any_cert(self, _request: Request, *, arch: str, label: str, use_pem): mime = 'application/x-pem-file' if use_pem else 'application/pkix-cert' pki_arch = self.architectures[ArchLabel(arch)] cert = pki_arch.get_cert(CertLabel(label)) data = cert.dump() if use_pem: data = pem.armor('certificate', data) return Response(data, mimetype=mime)
def serve_timestamp_response(self, request: Request, *, label: str, arch: str): pki_arch = self.architectures[ArchLabel(arch)] tsa = pki_arch.service_registry.summon_timestamper( ServiceLabel(label), self.at_time(request) ) data = request.stream.read() req: tsp.TimeStampReq = tsp.TimeStampReq.load(data) response = tsa.request_tsa_response(req) return Response(response.dump(), mimetype='application/timestamp-reply')
def serve_zip(self, _request: Request, *, arch): try: pki_arch = self.architectures[ArchLabel(arch)] except KeyError: raise NotFound() zip_buffer = BytesIO() pki_arch.zip_certs(zip_buffer) zip_buffer.seek(0) data = zip_buffer.read() cd_header = f'attachment; filename="{arch}-certificates.zip"' return Response(data, mimetype='application/zip', headers={'Content-Disposition': cd_header})
def test_subject_alt_names(): # test whether SAN extensions are reassigned properly when using # cert specs as templates for other cert specs arch = CONFIG.get_pki_arch(ArchLabel('testing-ca')) signer1 = arch.get_cert(CertLabel('signer1')) signer2 = arch.get_cert(CertLabel('signer2')) signer1_long = arch.get_cert(CertLabel('signer1-long')) assert signer1.subject_alt_name_value[ 0].chosen.native == '*****@*****.**' assert signer2.subject_alt_name_value[0].chosen.native \ == '*****@*****.**' assert signer1_long.subject_alt_name_value is None
def test_dump_flat_no_pfx(tmp_path): arch = CONFIG.get_pki_arch(ArchLabel('testing-ca')) arch.dump_certs(str(tmp_path), include_pkcs12=False, flat=True) dumped = set(_collect_files(str(tmp_path))) assert dumped == { 'signer1-long.cert.pem', 'signer1.cert.pem', 'signer2.cert.pem', 'interm-ocsp.cert.pem', 'interm.cert.pem', 'tsa.cert.pem', 'tsa2.cert.pem', 'root.cert.pem', }
def _parse_credential_id(self, cred_id): splits = cred_id.split(sep='/', maxsplit=1) arch_label = ArchLabel(splits[0]) try: cert_label = CertLabel(splits[1]) except IndexError: raise web.HTTPNotFound() config = self.certomancer_config try: pki_arch = config.get_pki_arch(arch_label) cert_spec = pki_arch.get_cert_spec(cert_label) except CertomancerObjectNotFoundError: raise web.HTTPNotFound() return pki_arch, cert_spec
def test_pkcs12(pw): arch = CONFIG.get_pki_arch(ArchLabel('testing-ca')) package = arch.package_pkcs12(CertLabel('signer1'), password=pw) if pw: # there's something about passwordless PKCS#12 files that doesn't quite # jive between oscrypto and pyca/cryptography key, cert, chain = oskeys.parse_pkcs12(package, password=pw) assert cert.dump() == arch.get_cert(CertLabel('signer1')).dump() assert len(chain) == 2 assert key is not None from cryptography.hazmat.primitives.serialization import pkcs12 key, cert, chain = pkcs12.load_key_and_certificates(package, password=pw) assert key is not None assert len(chain) == 2
def serve_crl(self, request: Request, *, label: ServiceLabel, arch: str, crl_no, use_pem): pki_arch = self.architectures[ArchLabel(arch)] mime = 'application/x-pem-file' if use_pem else 'application/pkix-crl' if crl_no is not None: crl = pki_arch.service_registry.get_crl(label, number=crl_no) else: crl = pki_arch.service_registry.get_crl( label, self.at_time(request) ) data = crl.dump() if use_pem: data = pem.armor('X509 CRL', data) return Response(data, mimetype=mime)
def test_sign_public_only(): cfg = ''' root-ca: subject: root subject-key: root issuer: root authority-key: root validity: valid-from: "2000-01-01T00:00:00+0000" valid-to: "2500-01-01T00:00:00+0000" extensions: - id: basic_constraints critical: true value: ca: true - id: key_usage critical: true smart-value: schema: key-usage params: [digital_signature, key_cert_sign, crl_sign] leaf: subject: pub-only subject-key: split-key-pub issuer: root authority-key: root validity: valid-from: "2020-01-01T00:00:00+0000" valid-to: "2050-01-01T00:00:00+0000" extensions: - id: key_usage critical: true smart-value: schema: key-usage params: [digital_signature] ''' arch = PKIArchitecture( arch_label=ArchLabel('test'), key_set=RSA_KEYS, entities=ENTITIES, cert_spec_config=yaml.safe_load(cfg), service_config={}, external_url_prefix='http://test.test', ) pubkey = arch.get_cert(CertLabel('leaf')).public_key with open('tests/data/keys-rsa/split-key-pub.key.pem', 'rb') as inf: pubkey_actual = oskeys.parse_public(inf.read()) assert pubkey.native == pubkey_actual.native
def serve_cert(self, _request: Request, *, label: str, arch: str, cert_label: Optional[str], use_pem): mime = 'application/x-pem-file' if use_pem else 'application/pkix-cert' pki_arch = self.architectures[ArchLabel(arch)] cert_label = CertLabel(cert_label) if cert_label is not None else None cert = pki_arch.service_registry.get_cert_from_repo( ServiceLabel(label), cert_label ) if cert is None: raise NotFound() data = cert.dump() if use_pem: data = pem.armor('certificate', data) return Response(data, mimetype=mime)
def serve_pfx(self, request: Request, *, arch): pki_arch = self.architectures[ArchLabel(arch)] try: cert = request.form['cert'] except KeyError: raise BadRequest() cert = CertLabel(cert) if not (pyca_cryptography_present() and pki_arch.is_subject_key_available(cert)): raise NotFound() pass_bytes = request.form.get('passphrase', '').encode('utf8') data = pki_arch.package_pkcs12(cert, password=pass_bytes or None) cd_header = f'attachment; filename="{cert}.pfx"' return Response(data, mimetype='application/x-pkcs12', headers={'Content-Disposition': cd_header})
def get_ac_aware_signer(actual_signer='signer1'): pki_arch = CERTOMANCER.get_pki_arch(ArchLabel('testing-ca-with-aa')) signer = signers.SimpleSigner( signing_cert=pki_arch.get_cert(CertLabel(actual_signer)), signing_key=pki_arch.key_set.get_private_key(KeyLabel(actual_signer)), cert_registry=SimpleCertificateStore.from_certs( [ pki_arch.get_cert('root'), pki_arch.get_cert('interm'), pki_arch.get_cert('root-aa'), pki_arch.get_cert('interm-aa'), pki_arch.get_cert('leaf-aa') ] ), attribute_certs=[ pki_arch.get_attr_cert(CertLabel('alice-role-with-rev')) ] ) return signer
def test_dump_zip(): out = BytesIO() arch = CONFIG.get_pki_arch(ArchLabel('testing-ca')) arch.zip_certs(out) out.seek(0) z = ZipFile(out) dumped = set(z.namelist()) assert dumped == set( map( lambda n: 'testing-ca/' + n, { 'interm/signer1-long.cert.pem', 'interm/signer1.cert.pem', 'interm/signer2.cert.pem', 'interm/interm-ocsp.cert.pem', 'root/interm.cert.pem', 'root/tsa.cert.pem', 'root/tsa2.cert.pem', 'root/root.cert.pem', }))
def serve_plugin(self, request: Request, plugin_label: str, *, label: str, arch: str): pki_arch = self.architectures[ArchLabel(arch)] services = pki_arch.service_registry plugin_label = PluginLabel(plugin_label) label = ServiceLabel(label) try: plugin_info = services.get_plugin_info(plugin_label, label) except ConfigurationError: raise NotFound() content_type = plugin_info.content_type req_content = request.stream.read() try: response_bytes = services.invoke_plugin( plugin_label, label, req_content, at_time=self.at_time(request) ) except PluginServiceRequestError as e: raise BadRequest(e.user_msg) return Response(response_bytes, mimetype=content_type)
def live_ac_vcs(requests_mock, with_authorities=False): pki_arch = CERTOMANCER.get_pki_arch(ArchLabel('testing-ca-with-aa')) if with_authorities: other_certs = [ pki_arch.get_cert('interm'), pki_arch.get_cert('interm-aa'), pki_arch.get_cert('leaf-aa') ] else: other_certs = [] main_vc = ValidationContext( trust_roots=[pki_arch.get_cert(CertLabel('root'))], allow_fetching=True, other_certs=other_certs, ) ac_vc = ValidationContext( trust_roots=[pki_arch.get_cert(CertLabel('root-aa'))], allow_fetching=True, other_certs=other_certs, ) Illusionist(pki_arch).register(requests_mock) return main_vc, ac_vc
def test_raw_extension(): cfg = ''' root: subject: root subject-key: root issuer: root authority-key: root validity: valid-from: "2000-01-01T00:00:00+0000" valid-to: "2500-01-01T00:00:00+0000" extensions: - id: basic_constraints critical: true value: ca: true - id: key_usage critical: true smart-value: schema: key-usage params: [digital_signature, key_cert_sign, crl_sign] - id: "2.16.840.1.113730.1.1" # this is netscape_certificate_type smart-value: schema: der-bytes params: "03020520" ''' arch = PKIArchitecture( arch_label=ArchLabel('test'), key_set=RSA_KEYS, entities=ENTITIES, cert_spec_config=yaml.safe_load(cfg), service_config={}, external_url_prefix='http://test.test', ) ext: x509.Extension = next( filter( lambda x: x['extn_id'].native == 'netscape_certificate_type', arch.get_cert(CertLabel('root'))['tbs_certificate']['extensions'])) assert ext['extn_value'].parsed.native == {'email'}
def test_dump_with_pfx(tmp_path): arch = CONFIG.get_pki_arch(ArchLabel('testing-ca')) arch.dump_certs(str(tmp_path), include_pkcs12=True) dumped = set(_collect_files(str(tmp_path))) assert dumped == { 'interm/signer1-long.cert.pem', 'interm/signer1-long.pfx', 'interm/signer1.cert.pem', 'interm/signer1.pfx', 'interm/signer2.cert.pem', 'interm/signer2.pfx', 'interm/interm-ocsp.cert.pem', 'interm/interm-ocsp.pfx', 'root/interm.cert.pem', 'root/interm.pfx', 'root/tsa.cert.pem', 'root/tsa.pfx', 'root/tsa2.cert.pem', 'root/tsa2.pfx', 'root/root.cert.pem', 'root/root.pfx', }
def test_raw_extension(wrong_value): cfg = f''' root: subject: root subject-key: root issuer: root authority-key: root validity: valid-from: "2000-01-01T00:00:00+0000" valid-to: "2500-01-01T00:00:00+0000" extensions: - id: basic_constraints critical: true value: ca: true - id: key_usage critical: true smart-value: schema: key-usage params: [digital_signature, key_cert_sign, crl_sign] - id: "2.16.840.1.113730.1.1" # this is netscape_certificate_type smart-value: schema: der-bytes params: {wrong_value} ''' arch = PKIArchitecture( arch_label=ArchLabel('test'), key_set=RSA_KEYS, entities=ENTITIES, cert_spec_config=yaml.safe_load(cfg), service_config={}, external_url_prefix='http://test.test', ) with pytest.raises(ConfigurationError): arch.get_cert(CertLabel('root'))
PUBKEY_TEST_DECRYPTER = SimpleEnvelopeKeyDecrypter.load( f"{CRYPTO_DATA_DIR}/keys-rsa/signer.key.pem", f"{CRYPTO_DATA_DIR}/testing-ca/interm/decrypter1.cert.pem", b'secret') # no keyEncipherment bit on this one PUBKEY_SELFSIGNED_DECRYPTER = SimpleEnvelopeKeyDecrypter.load( "pyhanko_tests/data/crypto/selfsigned.key.pem", "pyhanko_tests/data/crypto/selfsigned.cert.pem", b'secret') CERTOMANCER_CONFIG_PATH = CRYPTO_DATA_DIR + '/certomancer.yml' def _configure_certomancer(): with open(CERTOMANCER_CONFIG_PATH, 'r') as inf: cfg_text = inf.read() cfg = yaml.safe_load(cfg_text) pki_archs = cfg['pki-architectures'] # Clone RSA config to get equivalent ECDSA setup clone = dict(pki_archs['testing-ca']) clone['keyset'] = 'testing-ca-ecdsa' pki_archs['testing-ca-ecdsa'] = clone return CertomancerConfig(cfg, key_search_dir=CRYPTO_DATA_DIR) CERTOMANCER = _configure_certomancer() TESTING_CA = CERTOMANCER.get_pki_arch(ArchLabel('testing-ca')) TESTING_CA_ECDSA = CERTOMANCER.get_pki_arch(ArchLabel('testing-ca-ecdsa')) TESTING_CA_DIR = CRYPTO_DATA_DIR + '/testing-ca'
async def test_parse_ac_with_malformed_attribute(requests_mock): attr_cert_cfg = f''' test-ac: holder: name: signer1 # this needs to match against something from a totally different PKI # arch, so make the coupling as loose as possible include-base-cert-id: false include-entity-name: true issuer: root attributes: - id: charging_identity smart-value: schema: ietf-attribute params: ["Big Corp Inc."] validity: valid-from: "2000-01-01T00:00:00+0000" valid-to: "2100-01-01T00:00:00+0000" ''' pki_arch = PKIArchitecture( arch_label=ArchLabel('test'), key_set=TESTING_CA.key_set, entities=TESTING_CA.entities, cert_spec_config=yaml.safe_load(BASIC_AC_ISSUER_SETUP), ac_spec_config=yaml.safe_load(attr_cert_cfg), service_config={}, external_url_prefix='http://test.test', ) spec = pki_arch.get_attr_cert_spec(CertLabel('test-ac')) # we have to get a bit creative to get Certomancer to output invalid asn1 # in exactly the way we want class FakeAttrSpec: # noinspection PyUnusedLocal def to_asn1(self, arch): return NONSENSICAL_ATTR # noinspection PyTypeChecker spec.attributes.append(FakeAttrSpec()) cms_sig = await FROM_CA.async_sign_general_data( b'Hello world', digest_algorithm='sha256', signed_attr_settings=PdfCMSSignedAttributes( cades_signed_attrs=CAdESSignedAttrSpec( signer_attributes=SignerAttrSpec( certified_attrs=[ pki_arch.get_attr_cert(CertLabel('test-ac')) ], claimed_attrs=[] ) ) ) ) vc = live_testing_vc(requests_mock) ac_vc = ValidationContext( trust_roots=[pki_arch.get_cert(CertLabel('ac-issuer'))], allow_fetching=False, ) status = await async_validate_detached_cms( input_data=b'Hello world', signed_data=cms_sig['content'], signer_validation_context=vc, ac_validation_context=ac_vc ) assert isinstance(status, StandardCMSSignatureStatus) # The malformed attribute shouldn't have been processed, # but the other attrs should've assert len(status.cades_signer_attrs.certified_attrs) == 1
# no keyEncipherment bit on this one PUBKEY_SELFSIGNED_DECRYPTER = SimpleEnvelopeKeyDecrypter.load( "pyhanko_tests/data/crypto/selfsigned.key.pem", "pyhanko_tests/data/crypto/selfsigned.cert.pem", b'secret') CERTOMANCER_CONFIG_PATH = CRYPTO_DATA_DIR + '/certomancer.yml' def _configure_certomancer(): with open(CERTOMANCER_CONFIG_PATH, 'r') as inf: cfg_text = inf.read() cfg = yaml.safe_load(cfg_text) pki_archs = cfg['pki-architectures'] # Clone RSA config to get equivalent ECDSA & DSA setup ecdsa_clone = dict(pki_archs['testing-ca']) ecdsa_clone['keyset'] = 'testing-ca-ecdsa' pki_archs['testing-ca-ecdsa'] = ecdsa_clone dsa_clone = dict(pki_archs['testing-ca']) dsa_clone['keyset'] = 'testing-ca-dsa' pki_archs['testing-ca-dsa'] = dsa_clone return CertomancerConfig(cfg, key_search_dir=CRYPTO_DATA_DIR) CERTOMANCER = _configure_certomancer() TESTING_CA = CERTOMANCER.get_pki_arch(ArchLabel('testing-ca')) UNRELATED_TSA = CERTOMANCER.get_pki_arch(ArchLabel('unrelated-tsa')) TESTING_CA_ECDSA = CERTOMANCER.get_pki_arch(ArchLabel('testing-ca-ecdsa')) TESTING_CA_DSA = CERTOMANCER.get_pki_arch(ArchLabel('testing-ca-dsa')) TESTING_CA_DIR = CRYPTO_DATA_DIR + '/testing-ca'