laforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/35635?usp=email )
Change subject: osmo-smdpp: Implement eUICC + EUM certificate signature chain validation
......................................................................
osmo-smdpp: Implement eUICC + EUM certificate signature chain validation
Change-Id: I961827c50ed5e34c6507bfdf853952ece5b0d121
---
M osmo-smdpp.py
M pySim/esim/rsp.py
2 files changed, 43 insertions(+), 20 deletions(-)
Approvals:
laforge: Looks good to me, approved
Jenkins Builder: Verified
diff --git a/osmo-smdpp.py b/osmo-smdpp.py
index 7e0db27..58b83ff 100755
--- a/osmo-smdpp.py
+++ b/osmo-smdpp.py
@@ -36,7 +36,8 @@
import pySim.esim.rsp as rsp
from pySim.esim.es8p import *
-from pySim.esim.x509_cert import oid, cert_policy_has_oid, CertAndPrivkey
+from pySim.esim.x509_cert import oid, cert_policy_has_oid, cert_get_auth_key_id
+from pySim.esim.x509_cert import CertAndPrivkey, CertificateSet, cert_get_subject_key_id, VerifyError
# HACK: make this configurable
DATA_DIR = './smdpp-data'
@@ -214,7 +215,12 @@
if 'euiccCiPKIdListForSigningV3' in euiccInfo1:
pkid_list = pkid_list + euiccInfo1['euiccCiPKIdListForSigningV3']
# verify it supports one of the keys indicated by euiccCiPKIdListForSigning
- if not any(self.ci_get_cert_for_pkid(x) for x in pkid_list):
+ ci_cert = None
+ for x in pkid_list:
+ ci_cert = self.ci_get_cert_for_pkid(x)
+ if ci_cert:
+ break
+ if not ci_cert:
raise ApiError('8.8.2', '3.1', 'None of the proposed Public Key Identifiers is supported by the SM-DP+')
# TODO: Determine the set of CERT.DPauth.SIG that satisfy the following criteria:
@@ -257,7 +263,8 @@
#output['otherCertsInChain'] = b64encode2str()
# create SessionState and store it in rss
- self.rss[transactionId] = rsp.RspSessionState(transactionId, serverChallenge)
+ self.rss[transactionId] = rsp.RspSessionState(transactionId, serverChallenge,
+ cert_get_subject_key_id(ci_cert))
return output
@@ -292,29 +299,35 @@
euicc_cert = x509.load_der_x509_certificate(euiccCertificate_bin)
eum_cert = x509.load_der_x509_certificate(eumCertificate_bin)
- # TODO: Verify the validity of the eUICC certificate chain
- # raise ApiError('8.1.3', '6.1', 'Verification failed')
- # raise ApiError('8.1.3', '6.3', 'Expired')
-
- # TODO: Verify that the Root Certificate of the eUICC certificate chain corresponds to the
- # euiccCiPKIdToBeUsed or euiccCiPKIdToBeUsedV3
- # raise ApiError('8.11.1', '3.9', 'Unknown')
-
- # Verify euiccSignature1 over euiccSigned1 using pubkey from euiccCertificate.
- # Otherwise, the SM-DP+ SHALL return a status code "eUICC - Verification failed"
- if not self._ecdsa_verify(euicc_cert, euiccSignature1_bin, euiccSigned1_bin):
- raise ApiError('8.1', '6.1', 'Verification failed')
-
# Verify that the transactionId is known and relates to an ongoing RSP session. Otherwise, the SM-DP+
# SHALL return a status code "TransactionId - Unknown"
ss = self.rss.get(transactionId, None)
if ss is None:
raise ApiError('8.10.1', '3.9', 'Unknown')
ss.euicc_cert = euicc_cert
- ss.eum_cert = eum_cert # do we need this in the state?
+ ss.eum_cert = eum_cert # TODO: do we need this in the state?
- # TODO: verify eUICC cert is signed by EUM cert
- # TODO: verify EUM cert is signed by CI cert
+ # Verify that the Root Certificate of the eUICC certificate chain corresponds to the
+ # euiccCiPKIdToBeUsed or TODO: euiccCiPKIdToBeUsedV3
+ if cert_get_auth_key_id(eum_cert) != ss.ci_cert_id:
+ raise ApiError('8.11.1', '3.9', 'Unknown')
+
+ # Verify the validity of the eUICC certificate chain
+ cs = CertificateSet(self.ci_get_cert_for_pkid(ss.ci_cert_id))
+ cs.add_intermediate_cert(eum_cert)
+ # TODO v3: otherCertsInChain
+ try:
+ cs.verify_cert_chain(euicc_cert)
+ except VerifyError:
+ raise ApiError('8.1.3', '6.1', 'Verification failed')
+ # raise ApiError('8.1.3', '6.3', 'Expired')
+
+
+ # Verify euiccSignature1 over euiccSigned1 using pubkey from euiccCertificate.
+ # Otherwise, the SM-DP+ SHALL return a status code "eUICC - Verification failed"
+ if not self._ecdsa_verify(euicc_cert, euiccSignature1_bin, euiccSigned1_bin):
+ raise ApiError('8.1', '6.1', 'Verification failed')
+
# TODO: verify EID of eUICC cert is within permitted range of EUM cert
ss.eid = ss.euicc_cert.subject.get_attributes_for_oid(x509.oid.NameOID.SERIAL_NUMBER)[0].value
diff --git a/pySim/esim/rsp.py b/pySim/esim/rsp.py
index b5289be..1f8e989 100644
--- a/pySim/esim/rsp.py
+++ b/pySim/esim/rsp.py
@@ -35,10 +35,11 @@
and subsequently used by further API calls using the same transactionId. The session state
is removed either after cancelSession or after notification.
TODO: add some kind of time based expiration / garbage collection."""
- def __init__(self, transactionId: str, serverChallenge: bytes):
+ def __init__(self, transactionId: str, serverChallenge: bytes, ci_cert_id: bytes):
self.transactionId = transactionId
self.serverChallenge = serverChallenge
# used at a later point between API calsl
+ self.ci_cert_id = ci_cert_id
self.euicc_cert: Optional[x509.Certificate] = None
self.eum_cert: Optional[x509.Certificate] = None
self.eid: Optional[bytes] = None
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/35635?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I961827c50ed5e34c6507bfdf853952ece5b0d121
Gerrit-Change-Number: 35635
Gerrit-PatchSet: 4
Gerrit-Owner: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-MessageType: merged
laforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/35633?usp=email )
Change subject: New pySim.esim.x509_cert module for X.509 certificate handling
......................................................................
New pySim.esim.x509_cert module for X.509 certificate handling
Change-Id: Ia8cc2dac02fcd96624dc6d9348f103373eeeb614
---
A pySim/esim/x509_cert.py
1 file changed, 146 insertions(+), 0 deletions(-)
Approvals:
Jenkins Builder: Verified
laforge: Looks good to me, approved
diff --git a/pySim/esim/x509_cert.py b/pySim/esim/x509_cert.py
new file mode 100644
index 0000000..f6c6d43
--- /dev/null
+++ b/pySim/esim/x509_cert.py
@@ -0,0 +1,137 @@
+# Implementation of X.509 certificate handling in GSMA eSIM
+# as per SGP22 v3.0
+#
+# (C) 2024 by Harald Welte <laforge(a)osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import requests
+from typing import Optional, List
+
+from cryptography.hazmat.primitives.asymmetric import ec, padding
+from cryptography.hazmat.primitives import hashes
+from cryptography.exceptions import InvalidSignature
+from cryptography import x509
+
+from pySim.utils import b2h
+
+def check_signed(signed: x509.Certificate, signer: x509.Certificate) -> bool:
+ """Verify if 'signed' certificate was signed using 'signer'."""
+ # this code only works for ECDSA, but this is all we need for GSMA eSIM
+ pkey = signer.public_key()
+ # this 'signed.signature_algorithm_parameters' below requires cryptopgraphy 41.0.0 :(
+ pkey.verify(signed.signature, signed.tbs_certificate_bytes, signed.signature_algorithm_parameters)
+
+def cert_get_subject_key_id(cert: x509.Certificate) -> bytes:
+ """Obtain the subject key identifier of the given cert object (as raw bytes)."""
+ ski_ext = cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
+ return ski_ext.key_identifier
+
+def cert_get_auth_key_id(cert: x509.Certificate) -> bytes:
+ """Obtain the authority key identifier of the given cert object (as raw bytes)."""
+ aki_ext = cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier).value
+ return aki_ext.key_identifier
+
+class VerifyError(Exception):
+ """An error during certificate verification,"""
+ pass
+
+class CertificateSet:
+ """A set of certificates consisting of a trusted [self-signed] CA root certificate,
+ and an optional number of intermediate certificates. Can be used to verify the certificate chain
+ of any given other certificate."""
+ def __init__(self, root_cert: x509.Certificate):
+ check_signed(root_cert, root_cert)
+ # TODO: check other mandatory attributes for CA Cert
+ usage_ext = root_cert.extensions.get_extension_for_class(x509.KeyUsage).value
+ if not usage_ext.key_cert_sign:
+ raise ValueError('Given root certificate key usage does not permit signing of certificates')
+ if not usage_ext.crl_sign:
+ raise ValueError('Given root certificate key usage does not permit signing of CRLs')
+ self.root_cert = root_cert
+ self.intermediate_certs = {}
+ self.crl = None
+
+ def load_crl(self, urls: Optional[List[str]] = None):
+ if urls and type(urls) is str:
+ urls = [urls]
+ if not urls:
+ # generate list of CRL URLs from root CA certificate
+ crl_ext = self.root_cert.extensions.get_extension_for_class(x509.CRLDistributionPoints).value
+ name_list = [x.full_name for x in crl_ext]
+ merged_list = []
+ for n in name_list:
+ merged_list += n
+ uri_list = filter(lambda x: isinstance(x, x509.UniformResourceIdentifier), merged_list)
+ urls = [x.value for x in uri_list]
+
+ for url in urls:
+ try:
+ crl_bytes = requests.get(url)
+ except requests.exceptions.ConnectionError:
+ continue
+ crl = x509.load_der_x509_crl(crl_bytes)
+ if not crl.is_signature_valid(self.root_cert.public_key()):
+ raise ValueError('Given CRL has incorrect signature and cannot be trusted')
+ # FIXME: various other checks
+ self.crl = crl
+ # FIXME: should we support multiple CRLs? we only support a single CRL right now
+ return
+ # FIXME: report on success/failure
+
+ @property
+ def root_cert_id(self) -> bytes:
+ return cert_get_subject_key_id(self.root_cert)
+
+ def add_intermediate_cert(self, cert: x509.Certificate):
+ """Add a potential intermediate certificate to the CertificateSet."""
+ # TODO: check mandatory attributes for intermediate cert
+ usage_ext = cert.extensions.get_extension_for_class(x509.KeyUsage).value
+ if not usage_ext.key_cert_sign:
+ raise ValueError('Given intermediate certificate key usage does not permit signing of certificates')
+ aki = cert_get_auth_key_id(cert)
+ ski = cert_get_subject_key_id(cert)
+ if aki == ski:
+ raise ValueError('Cannot add self-signed cert as intermediate cert')
+ self.intermediate_certs[ski] = cert
+ # TODO: we could test if this cert verifies against the root, and mark it as pre-verified
+ # so we don't need to verify again and again the chain of intermediate certificates
+
+ def verify_cert_crl(self, cert: x509.Certificate):
+ if not self.crl:
+ # we cannot check if there's no CRL
+ return
+ if self.crl.get_revoked_certificate_by_serial_number(cert.serial_nr):
+ raise VerifyError('Certificate is present in CRL, verification failed')
+
+ def verify_cert_chain(self, cert: x509.Certificate, max_depth: int = 100):
+ """Verify if a given certificate's signature chain can be traced back to the root CA of this
+ CertificateSet."""
+ depth = 1
+ c = cert
+ while True:
+ aki = cert_get_auth_key_id(c)
+ if aki == self.root_cert_id:
+ # last step:
+ check_signed(c, self.root_cert)
+ return
+ parent_cert = self.intermediate_certs.get(aki, None)
+ if not aki:
+ raise VerifyError('Could not find intermediate certificate for AuthKeyId %s' % b2h(aki))
+ check_signed(c, parent_cert)
+ # if we reach here, we passed (no exception raised)
+ c = parent_cert
+ depth += 1
+ if depth > max_depth:
+ raise VerifyError('Maximum depth %u exceeded while verifying certificate chain' % max_depth)
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/35633?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: Ia8cc2dac02fcd96624dc6d9348f103373eeeb614
Gerrit-Change-Number: 35633
Gerrit-PatchSet: 2
Gerrit-Owner: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-MessageType: merged
laforge has posted comments on this change. ( https://gerrit.osmocom.org/c/pysim/+/35634?usp=email )
Change subject: Move X.509 related code from osmo-smdpp to pySim.esim.x509_cert
......................................................................
Patch Set 2: Code-Review+2
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/35634?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I230ba0537b702b0bf6e9da9a430908ed2a21ca61
Gerrit-Change-Number: 35634
Gerrit-PatchSet: 2
Gerrit-Owner: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-Comment-Date: Wed, 24 Jan 2024 08:45:39 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
Gerrit-MessageType: comment