Add a new parameter type, SerialNumber, as a subclass of Str

Transmitting a big integer like a random serial number over
either xmlrpc or JSON is problematic because they only support
32-bit integers at best. A random serial number can be as big
as 128 bits (theoretically 160 but dogtag limits it).

Treat as a string instead. Internally the value can be treated
as an Integer to conversions to/from hex as needed but for
transmission purposes handle it as a string.

Fixes: https://pagure.io/freeipa/issue/2016

Signed-off-by: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: Francisco Trivino <ftrivino@redhat.com>
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
This commit is contained in:
Rob Crittenden 2022-02-24 12:53:35 -05:00 committed by Florence Blanc-Renaud
parent d3481449ee
commit 83be923ac5
5 changed files with 107 additions and 19 deletions

10
API.txt
View File

@ -762,8 +762,8 @@ option: Str('host*', cli_name='hosts')
option: DateTime('issuedon_from?', autofill=False)
option: DateTime('issuedon_to?', autofill=False)
option: DNParam('issuer?', autofill=False)
option: Int('max_serial_number?', autofill=False)
option: Int('min_serial_number?', autofill=False)
option: SerialNumber('max_serial_number?', autofill=False)
option: SerialNumber('min_serial_number?', autofill=False)
option: Str('no_host*', cli_name='no_hosts')
option: Flag('no_members', autofill=True, default=True)
option: Principal('no_service*', cli_name='no_services')
@ -790,7 +790,7 @@ output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
output: Output('truncated', type=[<type 'bool'>])
command: cert_remove_hold/1
args: 1,2,1
arg: Int('serial_number')
arg: SerialNumber('serial_number')
option: Str('cacn?', autofill=True, cli_name='ca', default=u'ipa')
option: Str('version?')
output: Output('result')
@ -811,14 +811,14 @@ output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
output: PrimaryKey('value')
command: cert_revoke/1
args: 1,3,1
arg: Int('serial_number')
arg: SerialNumber('serial_number')
option: Str('cacn?', autofill=True, cli_name='ca', default=u'ipa')
option: Int('revocation_reason', autofill=True, default=0)
option: Str('version?')
output: Output('result')
command: cert_show/1
args: 1,7,3
arg: Int('serial_number')
arg: SerialNumber('serial_number')
option: Flag('all', autofill=True, cli_name='all', default=False)
option: Str('cacn?', autofill=True, cli_name='ca', default=u'ipa')
option: Flag('chain', autofill=True, default=False)

View File

@ -921,7 +921,10 @@ from ipalib.backend import Backend
from ipalib.frontend import Command, LocalOrRemote, Updater
from ipalib.frontend import Object, Method
from ipalib.crud import Create, Retrieve, Update, Delete, Search
from ipalib.parameters import DefaultFrom, Bool, Flag, Int, Decimal, Bytes, Str, IA5Str, Password, DNParam
from ipalib.parameters import (
DefaultFrom, Bool, Flag, Int, Decimal, Bytes, Str, IA5Str,
Password, DNParam, SerialNumber
)
from ipalib.parameters import (BytesEnum, StrEnum, IntEnum, AccessTime, File,
DateTime, DNSNameParam)
from ipalib.errors import SkipPluginModule

View File

@ -2254,3 +2254,47 @@ def create_signature(command):
)
return signature
class SerialNumber(Str):
"""Certificate serial number parameter type
"""
type = str
allowed_types = (str,)
# FIXME: currently unused, perhaps drop it
MAX_VALUE = 340282366920938463463374607431768211456 # 2^128
kwargs = Param.kwargs + (
('minlength', int, 1),
('maxlength', int, 40), # Up to 128-bit values
('length', int, None),
)
def _validate_scalar(self, value, index=None):
super(SerialNumber, self)._validate_scalar(value)
if value.startswith('-'):
raise ValidationError(
name=self.name, error=_('must be at least 0')
)
if not value.isdigit():
if value.lower().startswith('0x'):
try:
int(value[2:], 16)
except ValueError:
raise ValidationError(
name=self.name, error=_(
_('invalid valid hex'),
)
)
else:
raise ValidationError(
name=self.name, error=_(
_('must be an integer'),
)
)
if value == '0':
raise ValidationError(
name=self.name, error=_('invalid serial number 0')
)

View File

@ -31,7 +31,7 @@ from cryptography.hazmat.primitives import hashes, serialization
from dns import resolver, reversename
import six
from ipalib import Command, Str, Int, Flag, StrEnum
from ipalib import Command, Str, Int, Flag, StrEnum, SerialNumber
from ipalib import api
from ipalib import errors, messages
from ipalib import x509
@ -446,7 +446,7 @@ class BaseCertObject(Object):
label=_('Fingerprint (SHA256)'),
flags={'no_create', 'no_update', 'no_search'},
),
Int(
SerialNumber(
'serial_number',
label=_('Serial number'),
doc=_('Serial number in decimal or if prefixed with 0x in hexadecimal'),
@ -1370,7 +1370,7 @@ class cert_show(Retrieve, CertMethod, VirtualCommand):
# Dogtag lightweight CAs have shared serial number domain, so
# we don't tell Dogtag the issuer (but we check the cert after).
#
result = self.Backend.ra.get_certificate(str(serial_number))
result = self.Backend.ra.get_certificate(serial_number)
cert = x509.load_der_x509_certificate(
base64.b64decode(result['certificate']))
@ -1443,7 +1443,7 @@ class cert_revoke(PKQuery, CertMethod, VirtualCommand):
# Make sure that the cert specified by issuer+serial exists.
# Will raise NotFound if it does not.
resp = api.Command.cert_show(unicode(serial_number), cacn=kw['cacn'])
resp = api.Command.cert_show(serial_number, cacn=kw['cacn'])
try:
self.check_access()
@ -1465,7 +1465,8 @@ class cert_revoke(PKQuery, CertMethod, VirtualCommand):
# we don't tell Dogtag the issuer (but we already checked that
# the given serial was issued by the named ca).
result=self.Backend.ra.revoke_certificate(
str(serial_number), revocation_reason=revocation_reason)
serial_number,
revocation_reason=revocation_reason)
)
@ -1489,7 +1490,8 @@ class cert_remove_hold(PKQuery, CertMethod, VirtualCommand):
# we don't tell Dogtag the issuer (but we already checked that
# the given serial was issued by the named ca).
result=self.Backend.ra.take_certificate_off_hold(
str(serial_number))
serial_number
)
)
@ -1503,17 +1505,13 @@ class cert_find(Search, CertMethod):
doc=_('Match cn attribute in subject'),
autofill=False,
),
Int('min_serial_number?',
SerialNumber('min_serial_number?',
doc=_("minimum serial number"),
autofill=False,
minvalue=0,
maxvalue=2147483647,
),
Int('max_serial_number?',
SerialNumber('max_serial_number?',
doc=_("maximum serial number"),
autofill=False,
minvalue=0,
maxvalue=2147483647,
),
Flag('exactly?',
doc=_('match the common name exactly'),
@ -1896,7 +1894,9 @@ class cert_find(Search, CertMethod):
ca_obj = ca_objs[cacn] = (
self.api.Command.ca_show(cacn, all=True)['result'])
obj.update(ra.get_certificate(str(serial_number)))
obj.update(
ra.get_certificate(serial_number)
)
if not raw:
obj['certificate'] = (
obj['certificate'].replace('\r\n', ''))

View File

@ -1858,3 +1858,44 @@ class test_DNParam(ClassChecker):
for value in good:
assert mthd(value) == tuple(DN(oneval) for oneval in value)
assert o.convert(None) is None
class test_SerialNumber(ClassChecker):
"""
Test the `ipalib.parameters.SerialNumber` class.
"""
_cls = parameters.SerialNumber
def test_init(self):
"""
Test the `ipalib.parameters.SerialNumber.__init__` method.
"""
o = self.cls('my_serial')
assert o.type is str
assert o.length is None
def test_validate_scalar(self):
"""
Test the `ipalib.parameters.SerialNumber._convert_scalar` method.
"""
o = self.cls('my_serial')
mthd = o._validate_scalar
for value in ('1234', '0xabcd', '0xABCD'):
assert mthd(value) is None
bad = ['Hello', '123A']
for value in bad:
e = raises(errors.ValidationError, mthd, value)
assert e.name == 'my_serial'
assert_equal(e.error, 'must be an integer')
bad = ['-1234', '-0xAFF']
for value in bad:
e = raises(errors.ValidationError, mthd, value)
assert e.name == 'my_serial'
assert_equal(e.error, 'must be at least 0')
bad = ['0xGAH', '0x',]
for value in bad:
e = raises(errors.ValidationError, mthd, value)
assert e.name == 'my_serial'
assert_equal(
e.error, 'invalid valid hex'
)