Skip to content

certified.encode — Certificate Encoding

Helpers for building x509 names, subject alternative names, and certificate fields. Also defines PrivIface, the key-type abstraction used throughout the library.

Name builders

person_name

Build and return an x509.Name suitable for an individual.

Source code in certified/encode.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def person_name(
    name : str,
    uname : Optional[str] = None,
    domain : List[str] = [],
    #email : Optional[str] = None, # deprecated.
    location: Location = (None, None, None),
    pseudonym: Optional[str] = None,
) -> x509.Name:
    """ Build and return an x509.Name suitable for an individual.
    """
    #   (NameOID.EMAIL_ADDRESS, email)

    country, state, city = location

    name_pieces = []
    for oid, val in [
                (NameOID.COMMON_NAME, name),
                (NameOID.USER_ID, uname),
                (NameOID.PSEUDONYM, pseudonym),
                (NameOID.COUNTRY_NAME, country),
                (NameOID.STATE_OR_PROVINCE_NAME, state),
                (NameOID.LOCALITY_NAME, city)
            ] + [
                (NameOID.DOMAIN_COMPONENT, dn) for dn in domain
            ]:
        if val:
            name_pieces.append(x509.NameAttribute(oid, val))

    return x509.Name(name_pieces)

org_name

Build and return an x509.Name suitable for an organization.

Parameters:

Name Type Description Default
organization_name str

Sets the "Organization Name" (O) attribute on the certificate.

required
unit_name str

Sets the "Organization Unit Name" (OU) attribute on the certificate.

required
common_name Optional[str]

Sets the "Common Name" of the certificate. This is a legacy field that used to be used to check identity. It's an arbitrary string with poorly-defined semantics, so modern programs are supposed to ignore it. But it might be useful if you need to test how your software handles legacy or buggy certificates.

None
location Location

Optionally a tuple containing: (country_code, state_or_province, city_or_locality)

(None, None, None)
pseudonym Optional[str]

Used here to denote whether this is a signing key.

None
Source code in certified/encode.py
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def org_name(
    organization_name: str,
    unit_name: str,
    common_name: Optional[str] = None,
    domain: List[str] = [],
    location: Location = (None,None,None),
    pseudonym: Optional[str] = None,
) -> x509.Name:
    """ Build and return an x509.Name suitable for an organization.

    Args:
       organization_name: Sets the "Organization Name" (O)
           attribute on the certificate.

       unit_name: Sets the "Organization Unit Name" (OU)
           attribute on the certificate.

       common_name: Sets the "Common Name" of the certificate. This is a
           legacy field that used to be used to check identity. It's an
           arbitrary string with poorly-defined semantics, so
           [modern programs are supposed to ignore it](https://developers.google.com/web/updates/2017/03/chrome-58-deprecations#remove_support_for_commonname_matching_in_certificates).
           But it might be useful if you need to test how your software
           handles legacy or buggy certificates.

       location: Optionally a tuple containing:
           (country_code, state_or_province, city_or_locality)

       pseudonym: Used here to denote whether this is a signing key.
    """

    country, state, city = location

    name_pieces = []
    for oid, val in [
                (NameOID.ORGANIZATION_NAME, organization_name),
                (NameOID.ORGANIZATIONAL_UNIT_NAME, unit_name),
                (NameOID.COMMON_NAME, common_name),
                (NameOID.PSEUDONYM, pseudonym),
                (NameOID.COUNTRY_NAME, country),
                (NameOID.STATE_OR_PROVINCE_NAME, state),
                (NameOID.LOCALITY_NAME, city)
            ] + [
                (NameOID.DOMAIN_COMPONENT, dn) for dn in domain
            ]:
        if val:
            name_pieces.append(x509.NameAttribute(oid, val))

    return x509.Name(name_pieces)

append_pseudonym

Append a NameOID.PSEUDONYM field to the name with the given value.

Used by certified to create a unique name for the signing certificate by appending ps = "Signing Certificate"

Parameters:

Name Type Description Default
name Name

the base name

required
ps str

appended pseudonym

required
Source code in certified/encode.py
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
def append_pseudonym(name : x509.Name, ps : str) -> x509.Name:
    """
    Append a NameOID.PSEUDONYM field to the name
    with the given value.

    Used by certified to create a unique name for the
    signing certificate by appending ps = "Signing Certificate"

    Args:
      name: the base name
      ps: appended pseudonym
    """
    parts = [n for n in name]
    parts.append( x509.NameAttribute(NameOID.PSEUDONYM, ps) )
    return x509.Name(parts)

SAN

Build a subject alternative name field. Examples include:

  • emails: The emails that this certificate will be valid for.

    • Email address: example@example.com
  • hosts:

    • Regular hostname: example.com
    • Wildcard hostname: *.example.com
    • International Domain Name (IDN): café.example.com
    • IDN in A-label form: xn--caf-dma.example.com
    • IPv4 address: 127.0.0.1
    • IPv6 address: ::1
    • IPv4 network: 10.0.0.0/8
    • IPv6 network: 2001::/16
  • uris:

    • "https://dx.doi.org/10.1.1.1"
Source code in certified/encode.py
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
def SAN(emails=[], hosts=[], uris=[]) -> x509.SubjectAlternativeName:
    """ Build a subject alternative name field.
        Examples include:

        * emails: The emails that this certificate will be valid for.

            - Email address: ``example@example.com``

        * hosts:
            - Regular hostname: ``example.com``
            - Wildcard hostname: ``*.example.com``
            - International Domain Name (IDN): ``café.example.com``
            - IDN in A-label form: ``xn--caf-dma.example.com``
            - IPv4 address: ``127.0.0.1``
            - IPv6 address: ``::1``
            - IPv4 network: ``10.0.0.0/8``
            - IPv6 network: ``2001::/16``

        * uris:
            - "https://dx.doi.org/10.1.1.1"
    """
    assert sum(map(len, [emails, hosts, uris])) > 0, "No identities provided."
    return x509.SubjectAlternativeName(
                [x509.RFC822Name(e) for e in emails]
              + [_host(ip) for ip in hosts] 
              + [x509.UniformResourceIdentifier(u) for u in uris]
           )

Key type interface

PrivIface

Source code in certified/encode.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
class PrivIface:
    def __init__(self, keytype: KeyType) -> None:
        self.ed : Optional[type[Union[
            ed25519.Ed25519PrivateKey,ed448.Ed448PrivateKey
                ]]] = None
        self.ec : Optional[ Any ] = None
        if keytype == KeyType.ed25519:
            self.ed = ed25519.Ed25519PrivateKey
        elif keytype == KeyType.ed448:
            self.ed = ed448.Ed448PrivateKey
        elif keytype == KeyType.secp256r1:
            self.ec = ec.SECP256R1
        elif keytype == KeyType.secp384r1:
            self.ec = ec.SECP384R1
        elif keytype == KeyType.secp521r1:
            self.ec = ec.SECP521R1
        elif keytype == KeyType.secp256k1:
            self.ec = ec.SECP256K1
        else:
            raise KeyError(keytype)

    def hash_alg(self) -> Optional[hashes.SHA256]:
        if self.ed:
            return None
        return hashes.SHA256()
        #return hashes.BLAKE2b(64)
        # cryptography.exceptions.UnsupportedAlgorithm: Hash algorithm "blake2b" not supported for signatures

    def generate(self) -> CertificateIssuerPrivateKeyTypes:
        if self.ec:
            return ec.generate_private_key(self.ec())
        elif self.ed:
            return self.ed.generate()
        raise KeyError("Invalid key type.")

    def __eq__(a, b):
        return a.ed == b.ed and a.ec == b.ec

Certificate field helpers

cert_builder_common

Common part of the certificate building process.

Factored into some re-usable code that automatically sets up valid date ranges and checks that your name won't collide with what you're signing.

Source code in certified/encode.py
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
def cert_builder_common(
        subject: x509.Name,
        issuer: x509.Name,
        public_key: CertificatePublicKeyTypes,
        not_before: Optional[datetime.datetime] = None,
        not_after: Optional[datetime.datetime] = None,
        self_signed: bool = False,
    ) -> x509.CertificateBuilder:
    """
    Common part of the certificate building process.

    Factored into some re-usable code that automatically
    sets up valid date ranges and checks that your
    name won't collide with what you're signing.
    """
    not_before = not_before if not_before else datetime.datetime.now(datetime.timezone.utc)
    # default valid for ~1 years
    not_after = not_after if not_after else (
            not_before + datetime.timedelta(days=365)
    )
    if self_signed:
        assert subject == issuer, "Self-signed certificate, but subject != issuer"
    else:
       assert subject != issuer, "Cannot have subject == issuer for normal certificate."

    return (
        x509.CertificateBuilder()
            . subject_name(subject)
            . issuer_name(issuer)
            . public_key(public_key)
            . not_valid_before(not_before)
            . not_valid_after(not_after)
            . serial_number(x509.random_serial_number())
            . add_extension(
                x509.SubjectKeyIdentifier.from_public_key(public_key),
                critical=False,
            )
    )

hash_for_pubkey

Source code in certified/encode.py
89
90
91
92
93
def hash_for_pubkey(pkey : CertificatePublicKeyTypes
                   ) -> Optional[hashes.SHA256]:
    if isinstance(pkey, (ed25519.Ed25519PublicKey, ed448.Ed448PublicKey)):
        return None
    return hashes.SHA256()

rfc4514name

Source code in certified/encode.py
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
def rfc4514name(subject: x509.Name):
    # By default, attributes CN, L, ST, O, OU, C, STREET, DC, UID are represented by their short name
    # CN      commonName (2.5.4.3)
    # L       localityName (2.5.4.7)
    # ST      stateOrProvinceName (2.5.4.8)
    # O       organizationName (2.5.4.10)
    # OU      organizationalUnitName (2.5.4.11)
    # C       countryName (2.5.4.6)
    # STREET  streetAddress (2.5.4.9)
    # DC      domainComponent (0.9.2342.19200300.100.1.25)
    # UID     userId (0.9.2342.19200300.100.1.1)
    #
    # but, rfc5280 says we MUST be prepared for
    #   x country
    #   x organization
    #   x organizational unit
    #   - distinguished name qualifier,
    #   x state or province name,
    #   x common name (e.g., "Susan Housley"), and
    #   - serial number.
    # and SHOULD be prepared for
    #   x locality,
    #   - title,
    #   - surname,
    #   - given name,
    #   - initials,
    #   - pseudonym, and
    #   - generation qualifier (e.g., "Jr.", "3rd", or "IV").
    #
    # apparently, STREET, UID, and DC are "free"

    return subject.rfc4514_string({
                NameOID.PSEUDONYM: "P",
                NameOID.DN_QUALIFIER: "DQ",
                NameOID.SERIAL_NUMBER: "S",
                NameOID.EMAIL_ADDRESS: "E",
                NameOID.TITLE: "T",
                NameOID.SURNAME: "SUR",
                NameOID.GIVEN_NAME: "NAME",
                NameOID.INITIALS: "IN",
                NameOID.GENERATION_QUALIFIER: "GEN"
           })

get_is_ca

Source code in certified/encode.py
455
456
457
458
459
460
461
462
463
def get_is_ca(cert: Union[x509.CertificateSigningRequest,
                          x509.Certificate]) -> bool:
    try:
        basic = cert.extensions \
                    .get_extension_for_class(x509.BasicConstraints)
        return basic.value.ca
    except x509.ExtensionNotFound:
        return False
        raise ValueError("BasicConstraints not found.")

get_path_length

Source code in certified/encode.py
447
448
449
450
451
452
453
def get_path_length(cert: x509.Certificate) -> Optional[int]:
    try:
        basic = cert.extensions \
                    .get_extension_for_class(x509.BasicConstraints)
        return basic.value.path_length
    except x509.ExtensionNotFound:
        raise ValueError("BasicConstraints not found.")

get_aki

Collect the SubjectKeyIdentifier from a certificate and return it as an AuthorityKeyIdentifier. The content should be the same, but they have different header / wrappers.

Source code in certified/encode.py
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
def get_aki(cert : x509.Certificate) -> x509.AuthorityKeyIdentifier:
    """ Collect the SubjectKeyIdentifier from a certificate
        and return it as an AuthorityKeyIdentifier.
        The content should be the same, but they have different
        header / wrappers.
    """
    try:
        ski_ext = cert.extensions.get_extension_for_class(
            x509.SubjectKeyIdentifier
        )
    except x509.ExtensionNotFound:
        raise
        # we want the pubkey to match, so skip this.
        return x509.AuthorityKeyIdentifier.from_issuer_public_key(
                cert.public_key()
        )
    return x509.AuthorityKeyIdentifier \
               .from_issuer_subject_key_identifier(ski_ext.value)

get_urls

Source code in certified/encode.py
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
def get_urls(cert : x509.Certificate) -> List[str]:
    try:
        san = cert.extensions.get_extension_for_class(
                x509.SubjectAlternativeName
            )
    except x509.ExtensionNotFound:
        return []

    urls = []
    for n in san.value:
        if isinstance(n, x509.DNSName):
            urls.append(n.value)
        elif isinstance(n, x509.IPAddress):
            urls.append(str(n.value))
    return urls