Novell is now a part of Micro Focus

ASN.1, OIDs, and NDS The Common Fit

Articles and Tips: article

KEVIN BURNETT
Software Engineer
Strategic Partner Engineering Group

01 Nov 1999


Explains how to use ASN.1 to uniquely identify NDS schema extensions in NetWare 5. Includes information on the necessary BER and DER encoding. Contains sample code.

Introduction

In the July 1999 issue of Novell Developer Notes, Nancy and Steve McLain gave you a comprehensive discussion of designing NDS Schema Extensions. I also published a small piece about extending the NDS schema via the proprietary Novell NDS C/C++ interface called DSAPI in the July issue.

These articles presented an overview of what the NDS schema is, and what you, as a developer, can do to an extensible schema. My article outlined how to extend the NDS schema via C/C++ APIs.

With the introduction of NetWare 4.0, NDS was introduced to the world. With the NDS extensible schema, two main and controlled ways of uniquely extending the NDS schema without trespassing on another developer's/application's extension, have been provided. The first was implemented with the release of NetWare 4, the second with the release of NetWare 5. The first is a unique prefix made of up to eight alpha characters, registered through Novell's DeveloperNet Labs, the second, a unique number called an ASN.1 ID. ASN.1 ID is the subject of this article.

Note: To get the most out of this article, you should have a sound understanding of the NDS schema. You will need to understand the components that make up the NDS schema such as Objects, Attributes and Attribute values, along with the basic components used to extend the NDS schema; object classes and attribute definitions.

Traditional Naming Of A New NDS Schema Entry

Beginning with NetWare 4, when a developer wanted to extend the NDS schema, a name would need to be chosen for doing this. The name would identify the Class definition or Attribute definition. This name would be passed into the actual API to extend the NDS schema. The following examples show how this works:

For a new Attribute:

NWDSDefineAttr(context, "Name_Of_New_Attr", buffer);

For a new Class Definition:

NWDSDefineClass(context, "Name_Of_New_Class", buffer);

With the increasing number of developers writing software that extends the NDS schema, the chance of having two products that extend the NDS schema with the same name is becoming a real possibility. Novell realized this and created the Novell Schema Registry. Novell's DeveloperNet Labs (http://developer.novell.com/support/schreg2c.htm) provide this service. This registry allows you to register a name prefix with Novell. Name prefix registration allows you to immediately design and implement NDS Schema extensions that are unique in NDS with the current schema calls. A name prefix may be up to eight alpha-numeric ASCII characters. Name prefixes are requested by the registrar upon application, but must be applicable to the name of an associated product, business or technology. To use a name prefix with the current schema calls, you need to create an attribute or class definition named by the name prefix, immediately followed by a colon delimiter immediately followed by a descriptive identifier for the attribute or class definition. An example may look like the following:

NDS:Widget

Where "NDS" is the name prefix, the colon ":" is the delimiter, and "Widget" is the descriptive identifier.

The actual name prefix is what is registered with Novell. Novell will guarantee that this prefix is unique in Novell's database. Novell charges third-party developers to register name prefixes, while encouraging in-house developers to use the name prefix convention to aid in the certification process.

Name Prefix OIDs

Beginning with NetWare 5, an increased effort has been initiated to provide a unique OID with each name prefix registration. These specific OIDs are associated with the defined NDS Schema. OIDs are hierarchical in nature. Every schema extension will have a unique OID and an associated parent OID. For example, in the above example NDS name prefix might be associated with an OID of the following:

Joint-iso-ccitt(2).country(16).us(840).organization(1).Novell(113719).subregistry(2).NDS(1)

The created "NDS:Widget" could be created as follows:

Joint-iso-ccitt(2).country(16).us(840).organization(1).Novell(113719).subregistry(2).NDS(1).classes(1).Widget(1)

or

2.16.840.1.113719.2.1.1.1

assuming that NDS:Widget is a class definition.

Name prefixes will aid in associating existing schema extensions with future OID definitions making migration to the use of OIDs easier.

OIDs may be obtained from American National Standards Institute (ANSI), Internet Assigned Numbers Authority (IANA), or the NDS Schema Registry. If you decide to get an OID from an organization other than Novell, then that OID can be associated with the name prefix when registering with Novell. Otherwise Novell will provide an OID with each name prefix registered.

Identifying A Schema Entry

You can extend the NDS Schema with a variety of programming languages. For this article, I will use the C programming language as a reference. To create an object class or an attribute definition with Novell's DSAPI, you must fill out the following structures:

Object Class Info structure:

NWCLASS_INFO structure, which is defined as a type Class_Info_T.

typedef struct

{

   nuint32            classFlags;

   Asn1ID_T           asn1ID;

} Class_Info_T, N_FAR *pClass_Info_T;

Attribute Info structure:

NWATTR_INFO structure, which is defined as a type Attr_Info_T.

typedef struct

{

   nuint32  attrFlags;

   nint32   attrSyntaxID;

   nint32   attrLower;

   nint32   attrUpper;

   Asn1ID_T asn1ID;

} Attr_Info_T, N_FAR *pAttr_Info_T;

Both of these structures allow you to set the appropriate flags needed to create either a class definition or attribute. They also allow you to enter an ASN1ID. This ASN1ID contains a unique object ID (OID) for identifying the new schema entry. The structure for this entry is as follows:

typedef struct

{

   nuint32  length;

   nuint8   data[MAX_ASN1_NAME];

} Asn1ID_T, N_FAR *pAsn1ID_T;

Note that there is a length and the actual data. The length is the length of the actual data. This is used in the encoding process needed before the data can actually be stored in the data array. We'll discuss encoding later in the article.

With the introduction of NetWare 4, these ASN1ID fields could be populated with an OID and the length of the OID, but NDS did not use the data. NDS would store the data for you, but not interact with it.

Beginning with NetWare 5, NDS functionality was designed to use OIDs. There was a big push at Novell to have all of our internal name prefixes associated with OIDs. Research was also conducted to determine how this OID information should be stored in the NDS schema. The ASN.1 standard is a part of the X.500 standard on which NDS is based. The X.500 standard includes provisions to use the ASN.1 standard to store OIDs. NDS has adopted this standard.

ASN.1

Abstract Syntax Notation One, or ASN.1 for short, is a language for describing information that is structured. Typically this information is sent over some sort of communication medium. This information is typically abstract, much like an OID. It is widely used and employed in the Open Systems Interconnection (OSI)'s application layer. ASN.1 is defined fully in X.208.

With ASN.1, you can view and describe information and its organization at a high level without being concerned with the format in which it is sent.

ASN.1 is a flexible notation that allows you to define a variety of data types, from simple types (integers and strings) to structured types (sets and sequences). Complex data can be defined too.

Typically ASN.1 data is encoded using one of two encoding standards: Basic Encoding Rules (BER) and Distinguished Encoding Rules (DER). BER, defined in X.209, is used to describe how to represent or encode values of each ASN.1 type as a string of 8-bit octets. DER, a subset of BER, can be used to give a unique encoding to each ASN.1 value.

BER Encoding

In order to store an OID in the NDS Schema, the OID value must be BER encoded. BER encoding is accomplished by doing the following:

Let's say that you have an OID as follows:

2.16.840.1.113719.1.1.4.1.2

The first thing that BER encoding does is to look at the first two elements of the OID, 2.16. The 2 is multiplied by 40 and added to the 16 giving us 96 or 60hex. This is the third element of the BER encoded string. The first element is always the type, 06 indicates an OID. The second element is the length of the OID, elements three to the end of the OID. At this point our BER encoded value looks like this:

06 0C 60

The remaining data is encoded by acting on each distinct element of the OID, starting with the third element. In this case, the value would be 840. If you look at 840(decimal) which equals 348hex and


011

100

1000

(binary)

3

4

8

(hex)

With BER encoding you start with the least significant bit(lsb) of the least significant byte and work towards the most significant bit(msb) of the most significant byte. Basically, right to left, in the diagram above. You take the first seven bits, which would be:

1001000

and then determine if there are more bits that make up the OID element. If there are not, then you place a `0' in the eighth position of the octet. If there are more bits, then you place a `1' in the eighth bit position of prior octets to signify that there are more octets to follow and place a `0' in the last octet to signify the end. With BER encoding, the eighth bit, per octet, is reserved for this purpose. In this case, the final encoding will look like this:


10000110

01001000


More octetsTo follow

Last octet

The final value can be represented in hex as 86 48.

Note: In this example we have divided each OID element by 128 saving that resultant value and the remainder as elements of the BER encoded ASN.1 ID.

Converting the whole OID to ASN.1 using BER encoding results in:

06 0C 60 86 48 01 86 F8 37 01 01 04 01 02

The BER encoding compression of the original OID is 28:14 bytes. The OID was originally comprised of 28 bytes, the compressed BER encoded value is 14 bytes.

BER Decoding

Decoding a BER encoded string of data to the original OID is the reverse of the encoding process. The first octet contains the type and can be skipped. The second octet contains the length and is needed for the decoding of the actual data. The first element after the length contains two OID values. The first value can be obtained by dividing this element by 40. The second data element can be obtained by taking the remainder from the previous division. At this point, in our example, we have the first two elements of the OID:

96 / 40 = 2 with 16 left over.

The OID looks like this so far:

2 16

To retrieve the remaining data, we need to look at the fourth element of the BER encoded octet string. We need to check bit 8 to see of there are more octets that make up this element of the OID. If bit 8 is set, then multiply the octet by 128 and then add the lower bits to the result. Hence for the fourth element, 86:

86hex = 10000110binary.

Since bit 8 is set, we know that the next BER encoded element is a part of this OID element. First we strip off bit 8 and then multiply the remaining 7 bits by 128 (80hex) as shown by:

10000110

Strip off bit 8 and drop leading zeros:

110 or 6hex/decimal

multiply by 128:

and we get 768 decimal.

Next we go to the next BER encoded element:

48hex or 01001000binary

Since bit eight is not set, we know that this is the last BER element of this OID element. We just add this value to the previous value:

(48hex = 72decimal) + 768 = 840decminal

So 840 is our next OID element. Continuing this process for the remaining BER elements and adding `dot' notation between the OID elements gives the resulting OID:

2.16.840.1.113791.1.1.4.1.2

Which is the OID we started with before it was ASN.1 BER encoded.

Sample Code

Sample code has been provided in the form of a program that will encode NDS based OIDs to the BER format suitable for storing in NDS's Schema ANS.1 ID fields. This code is self-supporting. If executed as is, it will allow you to enter an NDS OID. It will convert this OID to BER format. It will also convert the BER encoded string back to an NDS OID. The compression ratio will be displayed as a ratio of bytes that make up the original NDS OID compared to the bytes that make up the BER encoded OID.

Note: The two routines, LdapOid2Asn1ID and Asn1Id2LdapOid, can be used as stand-alone routines to encode NDS OIDs to BER octets and decode BER octets to NDS OIDs.

#include <stdio.h>

#define MAX_UINT64_EXPANSION            9
#define MAX_LDAP_OID_BYTES              128
#define MIN_ASN1_BUF_LEN                3
#define OIT_TOP_FACTOR                  40L
#define MAX_OIT_TOP                     2L
#define MAX_ULONG                       0xFFFFFFFFL
#define LBER_OID                        6
#define BASE_TEN                        10
#define ERR_INVALID_IDENTITY            -677
#define ERR_INSUFFICIENT_BUFFER         -649

typedef unsigned long uint32;

int LdapOid2Asn1Id(char *ldapOid, int asn1BufLen, char *asn1Buf);
int Asn1Id2LdapOid(uint32 asn1Len, char *asn1Id, int oidBufLen, char *oidBuf);
int GetKeyboardInput(int inputBufLen, char *inputBuf);

int main(void)
{
    int err, i, encodedLen;
    char inputBuf[80];
    char buf1[128];
    char buf2[MAX_LDAP_OID_BYTES + 1];

    printf("This program encodes and decodes LDAP OIDs to and from "
            "ASN.1 syntax.\nAn LDAP OID is a dot delimited integer string "
            "such as listed below:\n\t2.5.6.17\n\t2.16.840.1.113730.3.2.4\n"
            "\t0.9.2342.192000.100.1.3\n\n");
    printf("Enter an LDAP OID or press  to quit:\n");
    while (((err = GetKeyboardInput(sizeof(inputBuf), inputBuf)) == 0) &&
            (inputBuf[0] != '\0'))
    {
        if ((err = LdapOid2Asn1Id(inputBuf, sizeof(buf1), buf1)) != 0)
        {
            printf("LdapOid2Asn1Id failure:%d.\n", err);
            goto _mainResume;
        }

        printf("The ASN.1 encoded OID is:");
        encodedLen = buf1[1] + 2;            /* Add for the beginning two fields. */
        for (i = 0; i < encodedLen; i++)
        {
            printf(" 0x%02x", (unsigned char)buf1[i]);
            if (((i + 1) % 8) == 0)
            {
                printf("\n                         ");
            }
        }

        printf("\n");
        if ((err = Asn1Id2LdapOid(encodedLen, buf1, sizeof(buf2), buf2)) != 0)
        {
            printf("Asn1Id2LdapOid failure:%d.\n", err);
            goto _mainResume;
        }

        printf("The decoded ASN.1 OID is: %s\n", buf2);
        printf("The original LDAP OID is: %s\n", inputBuf);
        if (strcmp(inputBuf, buf2) != 0)
        {
            printf("WARNING! Failed to exactly encode/decode LDAP OID.\n");
        }
        else
        {
            printf("ASN.1 compression ratio %d:%d bytes.\n",
                    strlen(inputBuf) + 1, encodedLen);
        }


_mainResume:
        printf("\nEnter another LDAP OID or press  to quit:\n");
    }

    return err;
}


int LdapOid2Asn1Id(char *ldapOid, int asn1BufLen, char *asn1Buf)
{
    char *startp, *endp;
    unsigned long value, mod, oitTop;
    int err, offset, i, j;
    char tempBuf[MAX_UINT64_EXPANSION];

    endp = ldapOid;
    err = 0;
    i = 0;

    if (ldapOid == NULL)
    {
        printf("LdapOid2Asn1Id: passed NULL ldapOid.\n");
        return ERR_INVALID_IDENTITY;
    }

    if ((asn1BufLen < MIN_ASN1_BUF_LEN) || (asn1Buf == NULL))
    {
        return ERR_INSUFFICIENT_BUFFER;
    }

    do
    {
        startp = endp;
        if (isdigit(*startp) == 0)
        {
            offset = startp - ldapOid;                 /* Numerals only. */
        }
        else if ((*startp == '0')  &&
                ((*(startp + 1) != '.') && (*(startp + 1) != '\0')))
        {
            offset = (startp + 1) - ldapOid;           /* Single 0s only. */
        }
        else
        {
            offset = -1;                               /* So far, so good. */
        }

        if (offset >= 0)
        {
            printf("LdapOid2Asn1Id: invalid OID syntax '%c' at offset %d.\n",
                    ldapOid[offset], offset);
            err = ERR_INVALID_IDENTITY;
            break;
        }

        value = strtoul(startp, &endp, BASE_TEN);
        if (value == MAX_ULONG)
        {

		
            printf("LdapOid2Asn1Id: "
                    "overflow failure on huge integer element at offset %d.\n",
                    startp - ldapOid);
            err = ERR_INVALID_IDENTITY;
            break;
        }

        if ((*endp == '.') && (*(endp + 1) != '\0'))
        {
            endp++;
        }

        if (i > 2)
        {
            /* Do this the 3rd-nth time through the main
             * loop for every integer element in the OID.
             */

            /* First, unravel the element backwards. */
            j = 0;
            while (value >= 0x80L)
            {
                mod = value % 0x80L;
                tempBuf[j++] = (char)mod;
                value = (value - mod) / 0x80L;
            }
            tempBuf[j] = (char)value;

            if ((i + j) >= asn1BufLen)
            {
                printf("LdapOid2Asn1Id: output buffer len %d is too small.\n",
                        asn1BufLen);
                err = ERR_INSUFFICIENT_BUFFER;
                break;
            }

            /* Then, put the unraveled octets in the proper ASN.1 order. */
            while (j > 0)
            {
                asn1Buf[i++] = tempBuf[j--] | 0x80;
            }
            asn1Buf[i++] = tempBuf[j];
        }
        else if (i == 2)
        {
            /* Only do this the 2nd time through the main loop. */
            if (value >= OIT_TOP_FACTOR)
            {
                printf("LdapOid2Asn1Id: "
                        "2nd OID integer '%d' must be less than %d.\n",
                        value, OIT_TOP_FACTOR);
                err = ERR_INVALID_IDENTITY;
                break;
            }
            asn1Buf[i++] = (char)((oitTop * OIT_TOP_FACTOR) + value);
        }
        else
        {


            /* Only do this the 1st time through the main loop. */
            i = 2;
            oitTop = value;
            if (oitTop > MAX_OIT_TOP)
            {
                printf("LdapOid2Asn1Id: 1st OID integer '%d' must be either "
                        "0, 1 or 2.\n", oitTop);
                err = ERR_INVALID_IDENTITY;
                break;
            }
        }
    } while (*endp != '\0');

    if ((i <= 3) && (err == 0))
    {
        err = ERR_INVALID_IDENTITY;
        printf("LdapOid2Asn1Id: LDAP OIDs must contain at least 3 elements.\n");
    }

    if (err == 0)
    {
        asn1Buf[0] = LBER_OID;
        asn1Buf[1] = i - 2;
    }
    else
    {
        asn1Buf[0] = '\0';
    }

    return err;
}


/* This handles Asn1Ids with a length of 127 or less.
 * NW4&5 Asn1Ids seem to be limited to 32 bytes max.
 */
int Asn1Id2LdapOid(uint32 asn1Len, char *asn1Id, int oidBufLen, char *oidBuf)
{
    unsigned long value;
    int err, length, i, index;
    char *cur;

    err = 0;
    i = 0;
    index = 0;

    if ((oidBufLen <= MAX_LDAP_OID_BYTES) || (oidBuf == NULL))
    {
        return ERR_INSUFFICIENT_BUFFER;
    }

    /* NW stores a 32 byte array of zeros when there is no OID ??? */
    if ((asn1Len == 0) || (asn1Id == NULL) || (asn1Id[0] == '\0'))
    {
        goto _Asn1Id2LdapOidExit;
    }

    value = (unsigned long)(asn1Id[i++]);
    if (value != LBER_OID)
    {

        printf("Asn1Id2LdapOid: 1st ASN.1 value %d must be %d.\n",
                value, LBER_OID);
        err = ERR_INVALID_IDENTITY;
        goto _Asn1Id2LdapOidExit;
    }

    length = (int)(asn1Id[i++]);
    if ((length > 127) || (length < 2))
    {
        printf("Asn1Id2LdapOid: ASN.1 length %d must be between 2 and 127.\n",
                length);
        err = ERR_INVALID_IDENTITY;
        goto _Asn1Id2LdapOidExit;
    }

    /* The first element after the length contains 2 oid values. */
    value = ((unsigned long)(asn1Id[i])) / 40;        /* First value. */
    index += sprintf(&(oidBuf[index]), "%lu", value);

    value = ((unsigned long)(asn1Id[i++])) % 40;        /* Secound value. */
    index += sprintf(&(oidBuf[index]), ".%lu", value);

    /* Now adjust everything for the rest of the asn1Id. */
    cur = &asn1Id[i];
    length--;
    i = 0;

    /* This code converts base 128 numbers to decimal. */
    while (i < length)
    {
        value  = 0L;

        /* Make sure we have enough space for the next series of:
         * a delimeter char, an unsigned long decimal, and a null terminator.
         */
        if ((index + 1 + (sizeof(unsigned long) + 2) + 1) > oidBufLen)
        {
            printf("Asn1Id2LdapOid: output buffer len %d is too small.\n",
                    oidBufLen);
            index = 0;
            err = ERR_INSUFFICIENT_BUFFER;
            goto _Asn1Id2LdapOidExit;
        }

        /* High bit means this is not the last part of this element. */
        while ((cur[i] & 0x80) == 0x80)
        {
            /* Multiply the result of last time through the 
             * loop by 128, and then add the low bits to it.
             */
            value = (0x80L * value) + (cur[i++] & (0x7F));
        }

        /* This is the last part of this element,
         * so multiply and add one final time.
         */
        value = (0x80L * value) + cur[i++];

        index += sprintf(&(oidBuf[index]), ".%lu", value);
    }

_Asn1Id2LdapOidExit:
    oidBuf[index] = '\0';
    return err;
}


int GetKeyboardInput(int inputBufLen, char *inputBuf)
{
    int ch, index;

    index = 0;
    inputBufLen--;

#ifdef _WIN32
    /* Handle the raw key strokes and gain more control. */
    while ((ch = getch()) != EOF)
    {
        if (ch == 0x08)                     /* Backspace */
        {
            if (index > 0)
            {
                index--;
                putchar('\b');
                putchar(' ');
                putchar('\b');
            }
        }
        else if (ch == 0x0D)                    /* Enter */
        {
            ch = 0;
            inputBuf[index] = (char)ch;
            putchar('\n');
            putchar('\r');
            break;
        }
        else if (ch == 0x03)                    /* Ctrl-C */
        {
            putchar('\n');
            putchar('\r');
            exit(1);
        }
        else if (index == inputBufLen)
        {
            putchar(0x07);
        }
        else if (ch == 0xE0)                  /* Arrow-Key */
        {
            if ((ch = getch()) == EOF)
            {
                break;
            }
            /*printf(":%d:0x%x ", ch, ch);*/
            if ((ch == 0x48) && (index == 0))   /* Up-Arrow */
            {
                while (inputBuf[index] != '\0')
                {
                    putchar(inputBuf[index++]);
                }


            }
        }
        else if (ch == 0x00)                    /* Function-Key */
        {
            ch = getch();
        }
        else
        {
            inputBuf[index++] = (char)ch;
            putchar(ch);
            /*printf(":%d:0x%x ", ch, ch);*/
        }
    }
#else /*_WIN32*/
#ifdef unix
    /* Would have to use curses to handle the raw key strokes. */
    while ((ch = getchar()) != EOF)
    {
        if (ch == 0x0A)                            /* Enter-Key */
        {
            ch = 0;
            inputBuf[index] = (char)ch;
            break;
        }

        if (index < inputBufLen)
        {
            inputBuf[index] = (char)ch;
        }
        else
        {
            inputBuf[inputBufLen] = '\0';
        }
        index++;
    }

    if (index > inputBufLen)
    {
        printf("WARNING! Input buffer overflow truncated at %d characters.\n",
                inputBufLen);
    }
#else /*unix*/
#error Need to define an OS.
#endif /*unix*/
#endif /*_WIN32*/
    return ch;
}

Conclusion

NetWare 5's version of NDS allows you to uniquely identify NDS schema extensions by using ASN.1. The type of encoding used by the ASN.1 specification is BER and DER. All ASN.1 entries in the NDS schema must be BER encoded.

When you register a NDS Schema extension, you can register a name and get an ASN.1 ID directly from Novell, or you can associate an ASN.1 ID, obtained from another standards organization, with your NDS schema prefix.

Questions/comments about this article can be sent to kburnett@novell.com.

References

"A Layman's Guide to a Subset of ASN.1, BER, and DER"An RSA Laboratories Technical Note," Burton S. Kaliski Jr. Revised November 1, 1993. Copyright 1991-1993 RSA Data Security, Inc. Public-Key Cryptography Standards (PKCS).

Abstract Syntax Notation One The Tutorial Reference By Douglas Steedman. Copyright 1990, 1993, Douglas Steedman.

* Originally published in Novell AppNotes


Disclaimer

The origin of this information may be internal or external to Novell. While Novell makes all reasonable efforts to verify this information, Novell does not make explicit or implied claims to its validity.

© Copyright Micro Focus or one of its affiliates