Skip to content

Commit

Permalink
Add conversion filters for serial numbers (ansible-collections#713)
Browse files Browse the repository at this point in the history
* Refactoring.

* Add parse_filter and to_filter plugins.

* Mention filters when serial numbers are accepted or returned.
  • Loading branch information
felixfontein authored Feb 18, 2024
1 parent 5159189 commit 6b1a3d6
Show file tree
Hide file tree
Showing 27 changed files with 500 additions and 55 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ If you use the Ansible package and do not update collections independently, use
- crypto_info module
- get_certificate module
- luks_device module
- parse_serial and to_serial filters

You can also find a list of all modules and plugins with documentation on the [Ansible docs site](https://docs.ansible.com/ansible/latest/collections/community/crypto/), or the [latest commit collection documentation](https://ansible-collections.github.io/community.crypto/branch/main/).

Expand Down
4 changes: 4 additions & 0 deletions plugins/doc_fragments/module_csr.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ class ModuleDocFragment(object):
or for own CAs."
- The C(AuthorityKeyIdentifier) extension will only be added if at least one of O(authority_key_identifier),
O(authority_cert_issuer) and O(authority_cert_serial_number) is specified.
- This option accepts an B(integer). If you want to provide serial numbers as colon-separated hex strings,
such as C(11:22:33), you need to convert them to an integer with P(community.crypto.parse_serial#filter).
type: int
crl_distribution_points:
description:
Expand Down Expand Up @@ -322,4 +324,6 @@ class ModuleDocFragment(object):
- module: community.crypto.openssl_privatekey_pipe
- module: community.crypto.openssl_publickey
- module: community.crypto.openssl_csr_info
- plugin: community.crypto.parse_serial
plugin_type: filter
'''
4 changes: 4 additions & 0 deletions plugins/filter/openssl_csr_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
- community.crypto.name_encoding
seealso:
- module: community.crypto.openssl_csr_info
- plugin: community.crypto.to_serial
plugin_type: filter
'''

EXAMPLES = '''
Expand Down Expand Up @@ -268,6 +270,8 @@
description:
- The CSR's authority cert serial number.
- Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string,
such as C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
returned: success
type: int
sample: 12345
Expand Down
66 changes: 66 additions & 0 deletions plugins/filter/parse_serial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024, Felix Fontein <[email protected]>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = """
name: parse_serial
short_description: Convert a serial number as a colon-separated list of hex numbers to an integer
author: Felix Fontein (@felixfontein)
version_added: 2.18.0
description:
- "Parses a colon-separated list of hex numbers of the form C(00:11:22:33) and returns the corresponding integer."
options:
_input:
description:
- A serial number represented as a colon-separated list of hex numbers between 0 and 255.
- These numbers are interpreted as the byte presentation of an unsigned integer in network byte order.
That is, C(01:00) is interpreted as the integer 256.
type: string
required: true
seealso:
- plugin: community.crypto.to_serial
plugin_type: filter
"""

EXAMPLES = """
- name: Parse serial number
ansible.builtin.debug:
msg: "{{ '11:22:33' | community.crypto.parse_serial }}"
"""

RETURN = """
_value:
description:
- The serial number as an integer.
type: int
"""

from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six import string_types

from ansible_collections.community.crypto.plugins.module_utils.serial import parse_serial


def parse_serial_filter(input):
if not isinstance(input, string_types):
raise AnsibleFilterError(
'The input for the community.crypto.parse_serial filter must be a string; got {type} instead'.format(type=type(input))
)
try:
return parse_serial(to_native(input))
except ValueError as exc:
raise AnsibleFilterError(to_native(exc))


class FilterModule(object):
'''Ansible jinja2 filters'''

def filters(self):
return {
'parse_serial': parse_serial_filter,
}
68 changes: 68 additions & 0 deletions plugins/filter/to_serial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024, Felix Fontein <[email protected]>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = """
name: to_serial
short_description: Convert an integer to a colon-separated list of hex numbers
author: Felix Fontein (@felixfontein)
version_added: 2.18.0
description:
- "Converts an integer to a colon-separated list of hex numbers of the form C(00:11:22:33)."
options:
_input:
description:
- The non-negative integer to convert.
type: int
required: true
seealso:
- plugin: community.crypto.to_serial
plugin_type: filter
"""

EXAMPLES = """
- name: Convert integer to serial number
ansible.builtin.debug:
msg: "{{ 1234567 | community.crypto.to_serial }}"
"""

RETURN = """
_value:
description:
- A colon-separated list of hexadecimal numbers.
- Letters are upper-case, and all numbers have exactly two digits.
- The string is never empty. The representation of C(0) is C("00").
type: string
"""

from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six import integer_types

from ansible_collections.community.crypto.plugins.module_utils.serial import to_serial


def to_serial_filter(input):
if not isinstance(input, integer_types):
raise AnsibleFilterError(
'The input for the community.crypto.to_serial filter must be an integer; got {type} instead'.format(type=type(input))
)
if input < 0:
raise AnsibleFilterError('The input for the community.crypto.to_serial filter must not be negative')
try:
return to_serial(input)
except ValueError as exc:
raise AnsibleFilterError(to_native(exc))


class FilterModule(object):
'''Ansible jinja2 filters'''

def filters(self):
return {
'to_serial': to_serial_filter,
}
9 changes: 8 additions & 1 deletion plugins/filter/x509_certificate_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
- community.crypto.name_encoding
seealso:
- module: community.crypto.x509_certificate_info
- plugin: community.crypto.to_serial
plugin_type: filter
'''

EXAMPLES = '''
Expand Down Expand Up @@ -253,7 +255,10 @@
type: str
sample: sha256WithRSAEncryption
serial_number:
description: The certificate's serial number.
description:
- The certificate's serial number.
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string,
such as C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
returned: success
type: int
sample: 1234
Expand Down Expand Up @@ -291,6 +296,8 @@
description:
- The certificate's authority cert serial number.
- Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string,
such as C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
returned: success
type: int
sample: 12345
Expand Down
7 changes: 6 additions & 1 deletion plugins/filter/x509_crl_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
- community.crypto.name_encoding
seealso:
- module: community.crypto.x509_crl_info
- plugin: community.crypto.to_serial
plugin_type: filter
'''

EXAMPLES = '''
Expand Down Expand Up @@ -100,7 +102,10 @@
elements: dict
contains:
serial_number:
description: Serial number of the certificate.
description:
- Serial number of the certificate.
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string,
such as C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
type: int
sample: 1234
revocation_date:
Expand Down
52 changes: 11 additions & 41 deletions plugins/module_utils/acme/backend_cryptography.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import binascii
import datetime
import os
import sys
import traceback

from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
Expand All @@ -37,6 +36,11 @@

from ansible_collections.community.crypto.plugins.module_utils.acme.utils import nopad_b64

from ansible_collections.community.crypto.plugins.module_utils.crypto.math import (
convert_int_to_bytes,
convert_int_to_hex,
)

from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
parse_name_field,
)
Expand Down Expand Up @@ -78,40 +82,6 @@
CRYPTOGRAPHY_ERROR = traceback.format_exc()


if sys.version_info[0] >= 3:
# Python 3 (and newer)
def _count_bytes(n):
return (n.bit_length() + 7) // 8 if n > 0 else 0

def _convert_int_to_bytes(count, no):
return no.to_bytes(count, byteorder='big')

def _pad_hex(n, digits):
res = hex(n)[2:]
if len(res) < digits:
res = '0' * (digits - len(res)) + res
return res
else:
# Python 2
def _count_bytes(n):
if n <= 0:
return 0
h = '%x' % n
return (len(h) + 1) // 2

def _convert_int_to_bytes(count, n):
h = '%x' % n
if len(h) > 2 * count:
raise Exception('Number {1} needs more than {0} bytes!'.format(count, n))
return ('0' * (2 * count - len(h)) + h).decode('hex')

def _pad_hex(n, digits):
h = '%x' % n
if len(h) < digits:
h = '0' * (digits - len(h)) + h
return h


class CryptographyChainMatcher(ChainMatcher):
@staticmethod
def _parse_key_identifier(key_identifier, name, criterium_idx, module):
Expand Down Expand Up @@ -223,8 +193,8 @@ def parse_key(self, key_file=None, key_content=None, passphrase=None):
'alg': 'RS256',
'jwk': {
"kty": "RSA",
"e": nopad_b64(_convert_int_to_bytes(_count_bytes(pk.e), pk.e)),
"n": nopad_b64(_convert_int_to_bytes(_count_bytes(pk.n), pk.n)),
"e": nopad_b64(convert_int_to_bytes(pk.e)),
"n": nopad_b64(convert_int_to_bytes(pk.n)),
},
'hash': 'sha256',
}
Expand Down Expand Up @@ -260,8 +230,8 @@ def parse_key(self, key_file=None, key_content=None, passphrase=None):
'jwk': {
"kty": "EC",
"crv": curve,
"x": nopad_b64(_convert_int_to_bytes(num_bytes, pk.x)),
"y": nopad_b64(_convert_int_to_bytes(num_bytes, pk.y)),
"x": nopad_b64(convert_int_to_bytes(pk.x, count=num_bytes)),
"y": nopad_b64(convert_int_to_bytes(pk.y, count=num_bytes)),
},
'hash': hashalg,
'point_size': point_size,
Expand All @@ -288,8 +258,8 @@ def sign(self, payload64, protected64, key_data):
hashalg = cryptography.hazmat.primitives.hashes.SHA512
ecdsa = cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hashalg())
r, s = cryptography.hazmat.primitives.asymmetric.utils.decode_dss_signature(key_data['key_obj'].sign(sign_payload, ecdsa))
rr = _pad_hex(r, 2 * key_data['point_size'])
ss = _pad_hex(s, 2 * key_data['point_size'])
rr = convert_int_to_hex(r, 2 * key_data['point_size'])
ss = convert_int_to_hex(s, 2 * key_data['point_size'])
signature = binascii.unhexlify(rr) + binascii.unhexlify(ss)

return {
Expand Down
Loading

0 comments on commit 6b1a3d6

Please sign in to comment.