[Freeipa-devel] [PATCH] 0090, 0092..0094 cert-show: show subject alternative names

Fraser Tweedale ftweedal at redhat.com
Tue Aug 23 09:46:57 UTC 2016


Thanks for review; rebased and updated patch attached.  Only 0090
has substantive changes.

Cheers,
Fraser

On Mon, Aug 22, 2016 at 09:22:08AM +0200, Jan Cholasta wrote:
> On 19.8.2016 13:11, Fraser Tweedale wrote:
> > Bump for review.
> > 
> > On Mon, Aug 15, 2016 at 05:15:16PM +1000, Fraser Tweedale wrote:
> > > Thanks for reviews.  Rebased and updated patches attached (and one
> > > new patch).  No substantive changes to 92..94.  Patch order is:
> > > 
> > > 92-2, 93-2, 94-2, 98, 90-3
> > > 
> > > Other comments inline.
> > > 
> > > Thanks,
> > > Fraser
> > > 
> > > On Fri, Aug 12, 2016 at 11:33:28AM +0200, Jan Cholasta wrote:
> > > > Patch 0092: ACK
> > > > 
> > > > Patch 0093: ACK
> > > > 
> > > > Patch 0094: ACK
> 
> Please fix this PEP8 issue before pushing:
> 
> ./ipaserver/plugins/cert.py:597:17: W503 line break before binary operator
> 
> 
> Patch 0098: ACK
> 
> > > > 
> > > > Patch 0090:
> > > > 
> > > > 1) Generic otherNames (san_other) do not work correctly. The OID is not
> > > > included in the value and names with complex type other than
> > > > KerberosPrincipal are not parsed correctly. The value should include the OID
> > > > and DER blob of the name.
> > > > 
> > > Updated to use "OID:b64(DER)" as the attribute value.
> 
> OK.
> 
> > > 
> > > > 2) With --all, san_other should be included in the result for all
> > > > otherNames, even the known ones, to provide (limited) forward compatibility.
> > > > 
> > > Done; when --all given, known otherName kinds are included in
> > > 'san_other' attribute in addition to their own attribute.
> 
> OK.
> 
> > > 
> > > > 3) Do we have to support *all* the name types? I mean we could, for the sake
> > > > of completeness, but it might be easier to just keep the few ones we
> > > > actually care about (email, DNS name, principal name, UPN and directory name
> > > > in your patch 0095).
> > > > 
> > > Yeah, why not support them all?  See also Petr's comments in other
> > > branch of thread.
> 
> Works for me, but see Lukáš's reply, I think he has a point. Maybe we can
> make a compromise and show only supported name types by default and
> everything with --all?
> 
Now only showing DNSName, RFC822Name, DirectoryName, UPN and
KRBPrincipalName unless --all is given.

> > > 
> > > > 4)
> > > > 
> > > > +            obj.setdefault(attr_name, []).append(unicode(name))
> > > > 
> > > > The value should not (always) be unicode, but of the type declared by the
> > > > param (unicode or ipapython.kerberos.Principal or
> > > > ipapython.dnsutil.DNSName).
> > > > 
> > > I now pass the value to the constructor of whatever type the
> > > parameter uses:
> > > 
> > >         attr_value = self.params[attr_name].type(name_formatted)
> > >         obj.setdefault(attr_name, []).append(attr_value)
> 
> OK.
> 
> 
> 5) san_directoryname should be a DNParam rather than Str.
> 
Fixed, thanks.

> 
> 6) Could we use "Subject <name type>" instead of "Subject Alternative Name
> (<name type>)" for labels? Or something else which is shorter and has the
> name type more "visible" than the current form.
> 
No worries.

> 
> 7) The patch needs a rebase.
> 
> 
> -- 
> Jan Cholasta
-------------- next part --------------
From 9dbe4c3cebb1279aefefe3dae9f3da2232d9c12f Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Thu, 21 Jul 2016 16:27:49 +1000
Subject: [PATCH 92/94] Move GeneralName parsing code to ipalib.x509

GeneralName parsing code is primarily relevant to X.509.  An
upcoming change will add SAN parsing to the cert-show command, so
first move the GeneralName parsing code from ipalib.pkcs10 to
ipalib.x509.

Part of: https://fedorahosted.org/freeipa/ticket/6022
---
 ipalib/pkcs10.py          |  93 ++-----------------------------------
 ipalib/x509.py            | 114 +++++++++++++++++++++++++++++++++++++++++++++-
 ipaserver/plugins/cert.py |   8 ++--
 3 files changed, 120 insertions(+), 95 deletions(-)

diff --git a/ipalib/pkcs10.py b/ipalib/pkcs10.py
index e340c1a2005ad781542a32a0a76753e80364dacf..158ebb3a25be2bd292f3883545fe8afe49b7be8c 100644
--- a/ipalib/pkcs10.py
+++ b/ipalib/pkcs10.py
@@ -22,9 +22,10 @@ from __future__ import print_function
 import sys
 import base64
 import nss.nss as nss
-from pyasn1.type import univ, char, namedtype, tag
+from pyasn1.type import univ, namedtype, tag
 from pyasn1.codec.der import decoder
 import six
+from ipalib import x509
 
 if six.PY3:
     unicode = str
@@ -32,11 +33,6 @@ if six.PY3:
 PEM = 0
 DER = 1
 
-SAN_DNSNAME = 'DNS name'
-SAN_RFC822NAME = 'RFC822 Name'
-SAN_OTHERNAME_UPN = 'Other Name (OID.1.3.6.1.4.1.311.20.2.3)'
-SAN_OTHERNAME_KRB5PRINCIPALNAME = 'Other Name (OID.1.3.6.1.5.2.2)'
-
 def get_subject(csr, datatype=PEM):
     """
     Given a CSR return the subject value.
@@ -72,78 +68,6 @@ def get_extensions(csr, datatype=PEM):
     return tuple(get_prefixed_oid_str(ext)[4:]
                  for ext in request.extensions)
 
-class _PrincipalName(univ.Sequence):
-    componentType = namedtype.NamedTypes(
-        namedtype.NamedType('name-type', univ.Integer().subtype(
-            explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))
-        ),
-        namedtype.NamedType('name-string', univ.SequenceOf(char.GeneralString()).subtype(
-            explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))
-        ),
-    )
-
-class _KRB5PrincipalName(univ.Sequence):
-    componentType = namedtype.NamedTypes(
-        namedtype.NamedType('realm', char.GeneralString().subtype(
-            explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))
-        ),
-        namedtype.NamedType('principalName', _PrincipalName().subtype(
-            explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))
-        ),
-    )
-
-def _decode_krb5principalname(data):
-    principal = decoder.decode(data, asn1Spec=_KRB5PrincipalName())[0]
-    realm = (str(principal['realm']).replace('\\', '\\\\')
-                                    .replace('@', '\\@'))
-    name = principal['principalName']['name-string']
-    name = '/'.join(str(n).replace('\\', '\\\\')
-                          .replace('/', '\\/')
-                          .replace('@', '\\@') for n in name)
-    name = '%s@%s' % (name, realm)
-    return name
-
-class _AnotherName(univ.Sequence):
-    componentType = namedtype.NamedTypes(
-        namedtype.NamedType('type-id', univ.ObjectIdentifier()),
-        namedtype.NamedType('value', univ.Any().subtype(
-            explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))
-        ),
-    )
-
-class _GeneralName(univ.Choice):
-    componentType = namedtype.NamedTypes(
-        namedtype.NamedType('otherName', _AnotherName().subtype(
-            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))
-        ),
-        namedtype.NamedType('rfc822Name', char.IA5String().subtype(
-            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))
-        ),
-        namedtype.NamedType('dNSName', char.IA5String().subtype(
-            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))
-        ),
-        namedtype.NamedType('x400Address', univ.Sequence().subtype(
-            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))
-        ),
-        namedtype.NamedType('directoryName', univ.Choice().subtype(
-            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 4))
-        ),
-        namedtype.NamedType('ediPartyName', univ.Sequence().subtype(
-            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 5))
-        ),
-        namedtype.NamedType('uniformResourceIdentifier', char.IA5String().subtype(
-            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 6))
-        ),
-        namedtype.NamedType('iPAddress', univ.OctetString().subtype(
-            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 7))
-        ),
-        namedtype.NamedType('registeredID', univ.ObjectIdentifier().subtype(
-            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 8))
-        ),
-    )
-
-class _SubjectAltName(univ.SequenceOf):
-    componentType = _GeneralName()
 
 def get_subjectaltname(csr, datatype=PEM):
     """
@@ -159,19 +83,8 @@ def get_subjectaltname(csr, datatype=PEM):
         return None
     del request
 
-    nss_names = nss.x509_alt_name(extension.value, nss.AsObject)
-    asn1_names = decoder.decode(extension.value.data,
-                                asn1Spec=_SubjectAltName())[0]
-    names = []
-    for nss_name, asn1_name in zip(nss_names, asn1_names):
-        name_type = nss_name.type_string
-        if name_type == SAN_OTHERNAME_KRB5PRINCIPALNAME:
-            name = _decode_krb5principalname(asn1_name['otherName']['value'])
-        else:
-            name = nss_name.name
-        names.append((name_type, name))
+    return x509.decode_generalnames(extension.value)
 
-    return tuple(names)
 
 # Unfortunately, NSS can only parse the extension request attribute, so
 # we have to parse friendly name ourselves (see RFC 2986)
diff --git a/ipalib/x509.py b/ipalib/x509.py
index 82194922d151a1b0f2df03df3578ad45b43b71c9..15168de08240a84794efef409d022eaa983291c9 100644
--- a/ipalib/x509.py
+++ b/ipalib/x509.py
@@ -40,7 +40,7 @@ import re
 
 import nss.nss as nss
 from nss.error import NSPRError
-from pyasn1.type import univ, namedtype, tag
+from pyasn1.type import univ, char, namedtype, tag
 from pyasn1.codec.der import decoder, encoder
 import six
 
@@ -63,6 +63,11 @@ EKU_EMAIL_PROTECTION = '1.3.6.1.5.5.7.3.4'
 EKU_ANY = '2.5.29.37.0'
 EKU_PLACEHOLDER = '1.3.6.1.4.1.3319.6.10.16'
 
+SAN_DNSNAME = 'DNS name'
+SAN_RFC822NAME = 'RFC822 Name'
+SAN_OTHERNAME_UPN = 'Other Name (OID.1.3.6.1.4.1.311.20.2.3)'
+SAN_OTHERNAME_KRB5PRINCIPALNAME = 'Other Name (OID.1.3.6.1.5.2.2)'
+
 _subject_base = None
 
 def subject_base():
@@ -374,6 +379,113 @@ def encode_ext_key_usage(ext_key_usage):
     eku = encoder.encode(eku)
     return _encode_extension('2.5.29.37', EKU_ANY not in ext_key_usage, eku)
 
+
+class _AnotherName(univ.Sequence):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('type-id', univ.ObjectIdentifier()),
+        namedtype.NamedType('value', univ.Any().subtype(
+            explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))
+        ),
+    )
+
+
+class _GeneralName(univ.Choice):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('otherName', _AnotherName().subtype(
+            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))
+        ),
+        namedtype.NamedType('rfc822Name', char.IA5String().subtype(
+            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))
+        ),
+        namedtype.NamedType('dNSName', char.IA5String().subtype(
+            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))
+        ),
+        namedtype.NamedType('x400Address', univ.Sequence().subtype(
+            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))
+        ),
+        namedtype.NamedType('directoryName', univ.Choice().subtype(
+            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 4))
+        ),
+        namedtype.NamedType('ediPartyName', univ.Sequence().subtype(
+            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 5))
+        ),
+        namedtype.NamedType('uniformResourceIdentifier', char.IA5String().subtype(
+            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 6))
+        ),
+        namedtype.NamedType('iPAddress', univ.OctetString().subtype(
+            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 7))
+        ),
+        namedtype.NamedType('registeredID', univ.ObjectIdentifier().subtype(
+            implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 8))
+        ),
+    )
+
+
+class _SubjectAltName(univ.SequenceOf):
+    componentType = _GeneralName()
+
+
+class _PrincipalName(univ.Sequence):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('name-type', univ.Integer().subtype(
+            explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))
+        ),
+        namedtype.NamedType('name-string', univ.SequenceOf(char.GeneralString()).subtype(
+            explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))
+        ),
+    )
+
+
+class _KRB5PrincipalName(univ.Sequence):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('realm', char.GeneralString().subtype(
+            explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))
+        ),
+        namedtype.NamedType('principalName', _PrincipalName().subtype(
+            explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))
+        ),
+    )
+
+
+def _decode_krb5principalname(data):
+    principal = decoder.decode(data, asn1Spec=_KRB5PrincipalName())[0]
+    realm = (str(principal['realm']).replace('\\', '\\\\')
+                                    .replace('@', '\\@'))
+    name = principal['principalName']['name-string']
+    name = '/'.join(str(n).replace('\\', '\\\\')
+                          .replace('/', '\\/')
+                          .replace('@', '\\@') for n in name)
+    name = '%s@%s' % (name, realm)
+    return name
+
+
+def decode_generalnames(secitem):
+    """
+    Decode a GeneralNames object (this the data for the Subject
+    Alt Name and Issuer Alt Name extensions, among others).
+
+    ``secitem``
+      The input is the DER-encoded extension data, without the
+      OCTET STRING header, as an nss SecItem object.
+
+    Return a list of tuples of name types (as string, suitable for
+    presentation) and names (as string, suitable for presentation).
+
+    """
+    nss_names = nss.x509_alt_name(secitem, repr_kind=nss.AsObject)
+    asn1_names = decoder.decode(secitem.data, asn1Spec=_SubjectAltName())[0]
+    names = []
+    for nss_name, asn1_name in zip(nss_names, asn1_names):
+        name_type = nss_name.type_string
+        if name_type == SAN_OTHERNAME_KRB5PRINCIPALNAME:
+            name = _decode_krb5principalname(asn1_name['otherName']['value'])
+        else:
+            name = nss_name.name
+        names.append((name_type, name))
+
+    return names
+
+
 if __name__ == '__main__':
     # this can be run with:
     # python ipalib/x509.py < /etc/ipa/ca.crt
diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
index 6dd9f6ffcdcd9d051d50d912996fea2104d71dff..c25965080e40dcbcaa21ab140509eaf2e29d7c61 100644
--- a/ipaserver/plugins/cert.py
+++ b/ipaserver/plugins/cert.py
@@ -560,7 +560,7 @@ class cert_request(Create, BaseCertMethod, VirtualCommand):
 
         # Validate the subject alt name, if any
         for name_type, name in subjectaltname:
-            if name_type == pkcs10.SAN_DNSNAME:
+            if name_type == x509.SAN_DNSNAME:
                 name = unicode(name)
                 alt_principal_obj = None
                 alt_principal_string = unicode(principal)
@@ -591,13 +591,13 @@ class cert_request(Create, BaseCertMethod, VirtualCommand):
                             "with subject alt name '%s'.") % name)
                 if alt_principal_string is not None and not bypass_caacl:
                     caacl_check(principal_type, principal, ca, profile_id)
-            elif name_type in (pkcs10.SAN_OTHERNAME_KRB5PRINCIPALNAME,
-                               pkcs10.SAN_OTHERNAME_UPN):
+            elif name_type in (x509.SAN_OTHERNAME_KRB5PRINCIPALNAME,
+                               x509.SAN_OTHERNAME_UPN):
                 if name != principal_string:
                     raise errors.ACIError(
                         info=_("Principal '%s' in subject alt name does not "
                                "match requested principal") % name)
-            elif name_type == pkcs10.SAN_RFC822NAME:
+            elif name_type == x509.SAN_RFC822NAME:
                 if principal_type == USER:
                     if name not in principal_obj.get('mail', []):
                         raise errors.ValidationError(
-- 
2.5.5

-------------- next part --------------
From 4c72105829ffa02bde5e11669440ca2e095c35e9 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Fri, 22 Jul 2016 12:05:13 +1000
Subject: [PATCH 93/94] x509: fix SAN directoryName parsing

The subjectAltName extension parsing code in ipalib.x509 fails on
directoryName values because the Choice structure is not endowed
with an inner type.  Implement the Name structure, whose inner type
is a CHOICE { SEQUENCE OF RelativeDistinguishedName }, to resolve.

Note that the structure still does not get fully parsed; only enough
to recognise the SequenceOf tag and not fail.

Part of: https://fedorahosted.org/freeipa/ticket/6022
---
 ipalib/x509.py | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/ipalib/x509.py b/ipalib/x509.py
index 15168de08240a84794efef409d022eaa983291c9..2dc67441c92686826dd24f00a5ad30566cd032da 100644
--- a/ipalib/x509.py
+++ b/ipalib/x509.py
@@ -196,6 +196,12 @@ def is_self_signed(certificate, datatype=PEM, dbdir=None):
     del nsscert
     return self_signed
 
+class _Name(univ.Choice):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('rdnSequence',
+            univ.SequenceOf()),
+    )
+
 class _TBSCertificate(univ.Sequence):
     componentType = namedtype.NamedTypes(
         namedtype.NamedType(
@@ -204,9 +210,9 @@ class _TBSCertificate(univ.Sequence):
                 tag.tagClassContext, tag.tagFormatSimple, 0))),
         namedtype.NamedType('serialNumber', univ.Integer()),
         namedtype.NamedType('signature', univ.Sequence()),
-        namedtype.NamedType('issuer', univ.Sequence()),
+        namedtype.NamedType('issuer', _Name()),
         namedtype.NamedType('validity', univ.Sequence()),
-        namedtype.NamedType('subject', univ.Sequence()),
+        namedtype.NamedType('subject', _Name()),
         namedtype.NamedType('subjectPublicKeyInfo', univ.Sequence()),
         namedtype.OptionalNamedType(
             'issuerUniquedID',
@@ -403,7 +409,7 @@ class _GeneralName(univ.Choice):
         namedtype.NamedType('x400Address', univ.Sequence().subtype(
             implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3))
         ),
-        namedtype.NamedType('directoryName', univ.Choice().subtype(
+        namedtype.NamedType('directoryName', _Name().subtype(
             implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 4))
         ),
         namedtype.NamedType('ediPartyName', univ.Sequence().subtype(
-- 
2.5.5

-------------- next part --------------
From 80172d2ebc4d5b1f40206bf527eb3e3f13a0c4ab Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Fri, 22 Jul 2016 12:11:59 +1000
Subject: [PATCH 94/94] x509: use NSS enums and OIDs to identify SAN types

GeneralName parsing currently relies heavily on strings from NSS.
Make the code hopefully less brittle by identifying GeneralName
types by NSS enums and, for otherName, the name-type OID also.

Part of: https://fedorahosted.org/freeipa/ticket/6022
---
 ipalib/x509.py            | 30 +++++++++++++++++++++++-------
 ipaserver/plugins/cert.py | 19 ++++++++++---------
 2 files changed, 33 insertions(+), 16 deletions(-)

diff --git a/ipalib/x509.py b/ipalib/x509.py
index 2dc67441c92686826dd24f00a5ad30566cd032da..541609fbc1a53a73eafcff2327e53a292c2d9a3c 100644
--- a/ipalib/x509.py
+++ b/ipalib/x509.py
@@ -33,6 +33,7 @@
 
 from __future__ import print_function
 
+import collections
 import os
 import sys
 import base64
@@ -63,10 +64,8 @@ EKU_EMAIL_PROTECTION = '1.3.6.1.5.5.7.3.4'
 EKU_ANY = '2.5.29.37.0'
 EKU_PLACEHOLDER = '1.3.6.1.4.1.3319.6.10.16'
 
-SAN_DNSNAME = 'DNS name'
-SAN_RFC822NAME = 'RFC822 Name'
-SAN_OTHERNAME_UPN = 'Other Name (OID.1.3.6.1.4.1.311.20.2.3)'
-SAN_OTHERNAME_KRB5PRINCIPALNAME = 'Other Name (OID.1.3.6.1.5.2.2)'
+SAN_UPN = '1.3.6.1.4.1.311.20.2.3'
+SAN_KRB5PRINCIPALNAME = '1.3.6.1.5.2.2'
 
 _subject_base = None
 
@@ -465,6 +464,10 @@ def _decode_krb5principalname(data):
     return name
 
 
+GeneralNameInfo = collections.namedtuple(
+        'GeneralNameInfo', ('type', 'desc', 'value'))
+
+
 def decode_generalnames(secitem):
     """
     Decode a GeneralNames object (this the data for the Subject
@@ -482,12 +485,25 @@ def decode_generalnames(secitem):
     asn1_names = decoder.decode(secitem.data, asn1Spec=_SubjectAltName())[0]
     names = []
     for nss_name, asn1_name in zip(nss_names, asn1_names):
-        name_type = nss_name.type_string
-        if name_type == SAN_OTHERNAME_KRB5PRINCIPALNAME:
+        # NOTE: we use the NSS enum to identify the name type.
+        # (For otherName we also tuple it up with the type-id OID).
+        # The enum does not correspond exactly to the ASN.1 tags.
+        # If we ever want to switch to using the true tag numbers,
+        # the expression to get the tag is:
+        #
+        #   asn1_name.getComponent().getTagSet()[0].asTuple()[2]
+        #
+        if nss_name.type_enum == nss.certOtherName:
+            oid = str(asn1_name['otherName']['type-id'])
+            nametype = (nss_name.type_enum, oid)
+        else:
+            nametype = nss_name.type_enum
+
+        if nametype == (nss.certOtherName, SAN_KRB5PRINCIPALNAME):
             name = _decode_krb5principalname(asn1_name['otherName']['value'])
         else:
             name = nss_name.name
-        names.append((name_type, name))
+        names.append(GeneralNameInfo(nametype, nss_name.type_string, name))
 
     return names
 
diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
index c25965080e40dcbcaa21ab140509eaf2e29d7c61..3e9eda5047b839171e6a0bc96f92927fbb191824 100644
--- a/ipaserver/plugins/cert.py
+++ b/ipaserver/plugins/cert.py
@@ -559,8 +559,8 @@ class cert_request(Create, BaseCertMethod, VirtualCommand):
                 "to the 'userCertificate' attribute of entry '%s'.") % dn)
 
         # Validate the subject alt name, if any
-        for name_type, name in subjectaltname:
-            if name_type == x509.SAN_DNSNAME:
+        for name_type, desc, name in subjectaltname:
+            if name_type == nss.certDNSName:
                 name = unicode(name)
                 alt_principal_obj = None
                 alt_principal_string = unicode(principal)
@@ -574,7 +574,7 @@ class cert_request(Create, BaseCertMethod, VirtualCommand):
                         raise errors.ValidationError(
                             name='csr',
                             error=_("subject alt name type %s is forbidden "
-                                "for user principals") % name_type
+                                "for user principals") % desc
                         )
                 except errors.NotFound:
                     # We don't want to issue any certificates referencing
@@ -591,13 +591,15 @@ class cert_request(Create, BaseCertMethod, VirtualCommand):
                             "with subject alt name '%s'.") % name)
                 if alt_principal_string is not None and not bypass_caacl:
                     caacl_check(principal_type, principal, ca, profile_id)
-            elif name_type in (x509.SAN_OTHERNAME_KRB5PRINCIPALNAME,
-                               x509.SAN_OTHERNAME_UPN):
+            elif name_type in [
+                (nss.certOtherName, x509.SAN_UPN),
+                (nss.certOtherName, x509.SAN_KRB5PRINCIPALNAME),
+            ]:
                 if name != principal_string:
                     raise errors.ACIError(
                         info=_("Principal '%s' in subject alt name does not "
                                "match requested principal") % name)
-            elif name_type == x509.SAN_RFC822NAME:
+            elif name_type == nss.certRFC822Name:
                 if principal_type == USER:
                     if name not in principal_obj.get('mail', []):
                         raise errors.ValidationError(
@@ -610,12 +612,11 @@ class cert_request(Create, BaseCertMethod, VirtualCommand):
                     raise errors.ValidationError(
                         name='csr',
                         error=_("subject alt name type %s is forbidden "
-                            "for non-user principals") % name_type
+                            "for non-user principals") % desc
                     )
             else:
                 raise errors.ACIError(
-                    info=_("Subject alt name type %s is forbidden") %
-                         name_type)
+                    info=_("Subject alt name type %s is forbidden") % desc)
 
         # Request the certificate
         result = self.Backend.ra.request_certificate(
-- 
2.5.5

-------------- next part --------------
From ffde58d7e6fad175049e5ef5c9760978769c49fe Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Mon, 15 Aug 2016 15:39:49 +1000
Subject: [PATCH] x509: include otherName DER value in GeneralNameInfo

We want to include the whole DER value when we pretty-print
unrecognised otherNames, so add a field to the GeneralNameInfo
namedtuple and populate it for otherNames.

Part of: https://fedorahosted.org/freeipa/ticket/6022
---
 ipalib/x509.py            | 13 +++++++++----
 ipaserver/plugins/cert.py |  2 +-
 2 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/ipalib/x509.py b/ipalib/x509.py
index 541609fbc1a53a73eafcff2327e53a292c2d9a3c..e986a97a58aafd3aeab08765a397edbf67c7841a 100644
--- a/ipalib/x509.py
+++ b/ipalib/x509.py
@@ -465,7 +465,7 @@ def _decode_krb5principalname(data):
 
 
 GeneralNameInfo = collections.namedtuple(
-        'GeneralNameInfo', ('type', 'desc', 'value'))
+        'GeneralNameInfo', ('type', 'desc', 'value', 'der_value'))
 
 
 def decode_generalnames(secitem):
@@ -477,8 +477,9 @@ def decode_generalnames(secitem):
       The input is the DER-encoded extension data, without the
       OCTET STRING header, as an nss SecItem object.
 
-    Return a list of tuples of name types (as string, suitable for
-    presentation) and names (as string, suitable for presentation).
+    Return a list of ``GeneralNameInfo`` namedtuples.  The
+    ``der_value`` field is set for otherNames, otherwise it is
+    ``None``.
 
     """
     nss_names = nss.x509_alt_name(secitem, repr_kind=nss.AsObject)
@@ -496,14 +497,18 @@ def decode_generalnames(secitem):
         if nss_name.type_enum == nss.certOtherName:
             oid = str(asn1_name['otherName']['type-id'])
             nametype = (nss_name.type_enum, oid)
+            der_value = asn1_name['otherName']['value'].asOctets()
         else:
             nametype = nss_name.type_enum
+            der_value = None
 
         if nametype == (nss.certOtherName, SAN_KRB5PRINCIPALNAME):
             name = _decode_krb5principalname(asn1_name['otherName']['value'])
         else:
             name = nss_name.name
-        names.append(GeneralNameInfo(nametype, nss_name.type_string, name))
+
+        gni = GeneralNameInfo(nametype, nss_name.type_string, name, der_value)
+        names.append(gni)
 
     return names
 
diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
index 3e9eda5047b839171e6a0bc96f92927fbb191824..9ee0b38c0aeadf2041b7d322417bbeab417b23a6 100644
--- a/ipaserver/plugins/cert.py
+++ b/ipaserver/plugins/cert.py
@@ -559,7 +559,7 @@ class cert_request(Create, BaseCertMethod, VirtualCommand):
                 "to the 'userCertificate' attribute of entry '%s'.") % dn)
 
         # Validate the subject alt name, if any
-        for name_type, desc, name in subjectaltname:
+        for name_type, desc, name, der_name in subjectaltname:
             if name_type == nss.certDNSName:
                 name = unicode(name)
                 alt_principal_obj = None
-- 
2.5.5

-------------- next part --------------
From d81c6c4cd7432783aaee511f2426f72a9cb1bddf Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Thu, 14 Jul 2016 21:36:33 +1000
Subject: [PATCH] cert-show: show subject alternative names

Enhance the cert-show command to return subject alternative name
values.

Fixes: https://fedorahosted.org/freeipa/ticket/6022
---
 ipaserver/plugins/cert.py | 130 ++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 127 insertions(+), 3 deletions(-)

diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
index 9ee0b38c0aeadf2041b7d322417bbeab417b23a6..d4c27a4dd80b2d047cf520524ced6724e5ec329e 100644
--- a/ipaserver/plugins/cert.py
+++ b/ipaserver/plugins/cert.py
@@ -39,7 +39,7 @@ from ipalib import ngettext
 from ipalib.constants import IPA_CA_CN
 from ipalib.crud import Create, PKQuery, Retrieve, Search
 from ipalib.frontend import Method, Object
-from ipalib.parameters import Bytes, DateTime, DNParam, Principal
+from ipalib.parameters import Bytes, DateTime, DNParam, DNSNameParam, Principal
 from ipalib.plugable import Registry
 from .virtual import VirtualCommand
 from .baseldap import pkey_to_value
@@ -50,6 +50,7 @@ from ipalib.request import context
 from ipalib import output
 from ipapython import kerberos
 from ipapython.dn import DN
+from ipapython.ipa_log_manager import root_logger
 from ipaserver.plugins.service import normalize_principal, validate_realm
 
 if six.PY3:
@@ -312,9 +313,77 @@ class BaseCertObject(Object):
             label=_('Serial number (hex)'),
             flags={'no_create', 'no_update', 'no_search'},
         ),
+        Str(
+            'san_rfc822name*',
+            label=_('Subject email address'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
+        DNSNameParam(
+            'san_dnsname*',
+            label=_('Subject DNS name'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
+        Str(
+            'san_x400address*',
+            label=_('Subject X.400 address'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
+        DNParam(
+            'san_directoryname*',
+            label=_('Subject directory name'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
+        Str(
+            'san_edipartyname*',
+            label=_('Subject EDI Party name'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
+        Str(
+            'san_uri*',
+            label=_('Subject URI'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
+        Str(
+            'san_ipaddress*',
+            label=_('Subject IP Address'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
+        Str(
+            'san_oid*',
+            label=_('Subject OID'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
+        Principal(
+            'san_other_upn*',
+            label=_('Subject UPN'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
+        Principal(
+            'san_other_kpn*',
+            label=_('Subject Kerberos principal name'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
+        Str(
+            'san_other*',
+            label=_('Subject Other Name'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
     )
 
     def _parse(self, obj, full=True):
+        """Extract certificate-specific data into a result object.
+
+        ``obj``
+            Result object containing certificate, into which extracted
+            data will be inserted.
+        ``full``
+            Whether to include all fields, or only the ones we guess
+            people want to see most of the time.  Also add
+            recognised otherNames to the generic ``san_other``
+            attribute when ``True`` in addition to the specialised
+            attribute.
+
+        """
         cert = obj.get('certificate')
         if cert is not None:
             cert = x509.load_certificate(cert)
@@ -329,11 +398,66 @@ class BaseCertObject(Object):
                 obj['sha1_fingerprint'] = unicode(
                     nss.data_to_hex(nss.sha1_digest(cert.der_data), 64)[0])
 
+            try:
+                ext_san = cert.get_extension(nss.SEC_OID_X509_SUBJECT_ALT_NAME)
+                general_names = x509.decode_generalnames(ext_san.value)
+            except KeyError:
+                general_names = []
+
+            for name_type, desc, name, der_name in general_names:
+                try:
+                    self._add_san_attribute(
+                        obj, full, name_type, name, der_name)
+                except Exception as e:
+                    # Invalid GeneralName (i.e. not a valid X.509 cert);
+                    # don't fail but log something about it
+                    root_logger.warning(
+                        "Encountered bad GeneralName; skipping", exc_info=True)
+
         serial_number = obj.get('serial_number')
         if serial_number is not None:
             obj['serial_number_hex'] = u'0x%X' % serial_number
 
 
+    def _add_san_attribute(
+            self, obj, full, name_type, name, der_name):
+        name_type_map = {
+            nss.certRFC822Name: 'san_rfc822name',
+            nss.certDNSName: 'san_dnsname',
+            nss.certX400Address: 'san_x400address',
+            nss.certDirectoryName: 'san_directoryname',
+            nss.certEDIPartyName: 'san_edipartyname',
+            nss.certURI: 'san_uri',
+            nss.certIPAddress: 'san_ipaddress',
+            nss.certRegisterID: 'san_oid',
+            (nss.certOtherName, x509.SAN_UPN): 'san_other_upn',
+            (nss.certOtherName, x509.SAN_KRB5PRINCIPALNAME): 'san_other_kpn',
+        }
+        default_attrs = {
+            'san_rfc822name', 'san_dnsname', 'san_directoryname',
+            'san_other_upn', 'san_other_kpn',
+        }
+
+        attr_name = name_type_map.get(name_type, 'san_other')
+
+        if full or attr_name in default_attrs:
+            if attr_name != 'san_other':
+                name_formatted = name
+            else:
+                # display as "OID : b64(DER)"
+                name_formatted = u'{}:{}'.format(
+                    name_type[1], base64.b64encode(der_name))
+            attr_value = self.params[attr_name].type(name_formatted)
+            obj.setdefault(attr_name, []).append(attr_value)
+
+        if full and attr_name.startswith('san_other_'):
+            # also include known otherName in generic otherName attribute
+            name_formatted = u'{}:{}'.format(
+                name_type[1], base64.b64encode(der_name))
+            attr_value = self.params['san_other'].type(name_formatted)
+            obj.setdefault('san_other', []).append(attr_value)
+
+
 class BaseCertMethod(Method):
     def get_options(self):
         yield Str('cacn?',
@@ -622,7 +746,7 @@ class cert_request(Create, BaseCertMethod, VirtualCommand):
         result = self.Backend.ra.request_certificate(
             csr, profile_id, ca_id, request_type=request_type)
         if not raw:
-            self.obj._parse(result)
+            self.obj._parse(result, all)
             result['request_id'] = int(result['request_id'])
 
         # Success? Then add it to the principal's entry
@@ -800,7 +924,7 @@ class cert_show(Retrieve, CertMethod, VirtualCommand):
 
         if not raw:
             result['certificate'] = result['certificate'].replace('\r\n', '')
-            self.obj._parse(result)
+            self.obj._parse(result, all)
             result['revoked'] = ('revocation_reason' in result)
             self.obj._fill_owners(result)
 
-- 
2.5.5



More information about the Freeipa-devel mailing list