For one of my project I was required to encode and decode CA certificate "CA Version" extension. The biggest problem is that there are no .NET or CryptoAPI interfaces that can do it.

CA Version abstract

CA Version extension is private Microsoft certificate extension and used in Windows PKI only. In addition, this extension exist only in CA certificate (where CA Type property of Basic Constraints extension is set to CA = True). End entity certificates never contains this extension. Main purpose of this certificate is to simplify CA server lifecycle. Sometimes CA certificate become expired and there are two choices to continue the work: build new CA from scratch or renew CA certificate (see related discussion about CA certificate renewal: Root CA certificate renewal). Though there are certain cases when CA requires to renew its own certificate prior to expiration. As the result CA server will maintain two signing certificates. Windows CA MUST publish CRLs for each valid signing key (not a certificate!). Since CA certificate can be renewed with existing key pair it is possible when CA server maintains multiple CA certificates and single CRL. Or CA server can maintain four CA certificates. Here is a great article that describes how CA Version extension works — Certification Authority Renewal.

Extension value has the following form: V<CA certificate index>.<CRL and key index>. For example: V0.0 (initial certificate), V1.0 (renewed with existing key pair), V1.1 (renewed with new key pair), V7.6 (the last string in the table of mentioned article) and so on. It is not possible to determine current Key Index from CA certificate index value. CA certificate index can be retrieved by using various methods, but key index only from CA Version extension. Therefore here is the following logic:

  • Create certificate object (X509Certificate2 object)
  • Explore CA Version extension (OID = 1.3.6.1.4.1.311.21.1)
  • Decode ASN.1 DER encoded extension value.

Also here might be another scenario — you may want to add CA Version support for your 3rd party CA server. In that case you will have to add to the certificate (or certificate request) encoded CA Version extension.

CA Version encoding rules

Here we will discuss about CA Version extension encoding rules by using Abstract Syntax Notation v1 Distinguished Encoding Rules (or simply ASN.1 DER). Here is a structure of raw data:

  • CA Version extension tag. MUST be 0x02;
  • Actual value length in bytes (calculated from remaining string)
  • Encoded CA Version extension value in little-endian encoding.

There might be several encoding forms:

Short form

Short form is applicable only when key index is zero. For example, V1.0, V3.0, V8.0, etc.:

V0.0 — 0x02, 0x01, 0x00
V2.0 — 0x02, 0x01, 0x02
V10.0 — 0x02, 0x01, 0x0a
V255.0 — 0x02, 0x01, 0xff
<…>

in a given examples we see that only last byte is changed that represents CA certificate index value. You may notice that the last byte can set value up to 255. What if we need higher value? In that case transitional byte is used (similarly as described in this article: How to encode Object Identifier to an ASN.1 DER encoded string). This byte acts as a multiplier to 256. For example:

V256.0 — 0x02, 0x02, 0x01, 0x00
V300.0 — 0x02, 0x02, 0x01, 0x2c
V3000.0 — 0x02, 0x02, 0x0b, 0xb8

you see that second byte is increased to 0x02 because encoded string is extended for one byte. 3rd byte is a multiplier to 256. Let's see how the last value is produced:

As 3000 is larger than 256 we add transitional byte. Divide 3000 by 256: 3000 / 256 = 11,71875 and round result to lesser integer = 11 (0x0b). Multiply 256 to 11 and get transitional byte base: 256 * 11 = 2816. Subtract this base value from original: 3000 – 2816 = 184 (0xb8).

Pretty simply! by using this encoding rules we can encode CA certificate index up to 65535 (imagine the CA certificate that was renewed 65535 times Plats smaids). Short notation don't support encoding for a values that are larger than 65535. Instead, long form must be used.

Long form

Long form is preferred form for any scenario. Long form allows to encode both CA certificate index and key index values as follows:

V0.0 — 0x02, 0x03, 0x00, 0x00, 0x00
V1.0 — 0x02, 0x03, 0x00, 0x00, 0x01
V2.2 — 0x02, 0x03, 0x02, 0x00, 0x02
V255.127 — 0x02, 0x03, 0x7f, 0x00, 0xff

Note: remember that actual data is encoded in little-endian encoding.

though here is a little difference: key index is encoded by a single byte only up to 127 (but not to 255 as CA Certificate index). If key index value is larger than 127 additional empty byte is used:

V255.128 — 0x02, 0x04, 0x00, 0x80, 0x00, 0xff
V255.255 — 0x02, 0x04, 0x00, 0xff, 0x00, 0xff

if key index value is larger than 255, empty byte become as a transitional byte and the same logic as for CA certificate index value is used:

V256.256 — 0x02, 0x04, 0x01, 0x00, 0x01, 0x00

you see that transitional bytes are set to 1 and remaining value is set to 0. This means that we just multiply 256 to transitional byte value (256 * 1 = 256). Here is an example for the V300.300. Take key index value and divide it to 256. 300 / 256 = 1,171875. Round resulting value to lesser integer = 1. Set transitional byte to 1 and multiply 256 to this value: 256 * 1 = 256. This is our new base. Subtract this base value from initial value: 300 – 256 = 44 (0x2c). Set this value as a remaining value. Repeat this process for CA certificate index value and you should get the following string:

V300.300 — 0x02, 0x04, 0x01, 0x2c, 0x01, 0x2c

And finally take another custom example: V1000.750:

Take key index value (750) and divide it by 256: 750 / 256 = 2,9296875. Round resulting value to lesser integer = 2. Set this value as a transitional byte. Multiply this transitional byte base to 256: 256 * 2 = 512. Subtract this value from original key index value: 750 – 512 = 238. Set resulting value as a resulting byte. First part of encoded string is: 0x02, 0x04, 0x02, 0xee. Now we need to encode CA certificate index value. Divide 1000 by 256: 1000 / 256 = 3,90625. Round resulting value to lesser integer = 3. Set this value as a transitional byte. Multiply this value to 256 and get transitional byte base: 256 * 3 = 768. Subtract this value from original CA certificate index value: 1000 – 768 = 232 (0xe8). Set resulting value as a remaining byte. And here is a full encoded string:

V1000.750 — 0x02, 0x04, 0x02, 0xee, 0x03, 0xe8

Summary

As you see here (and in the previous such article: How to encode Object Identifier to an ASN.1 DER encoded string) we can get several general encoded string structure:

  • String type (or type tag)
  • Whole data length
  • Data type (if certain tag support multiply data types with different encoding rules)
  • Current data part length (if certain extension support multiple data sets)
  • actual data that is encoded by using encoding rules defined for current data type.

Hope this helps.


Share this article:

Comments:

Johannes

I implemented and tested your logic, but it doesn't seem to work for me for following example: V256.127 - 0x02, 0x03, 0x7f, 0x00, 0x01, 0x00 Any idea what I am doing wrong?

Johannes

Typo in the data length, the one bellow is what my algorithm gets and does not work: V256.127 - 0x02, 0x04, 0x7f, 0x00, 0x01, 0x00

Vadims Podans

I believe, that zero byte in the middle is the reason. The byte sequence should be: 0x02, 0x03, 0x7f, 0x01, 0x00 only 3 bytes are necessary. Look for this example: V255.128 � 0x02, 0x04, 0x00, 0x80, 0x00, 0xff since your first token (127) is less than 128, there are no extra byte and you just put your first token value: 0x7f. Since key index is less than 127, no extra byte is necessary. In other word, decoder expects the following sequence for long form (3 bytes or longer): if the first byte is zero and the second is non-zero, then the second byte is actual value and it is large than 127 and less than 256. If the first byte is non-zero and the second is zero, then first byte is treated as a transitional byte it is multiplied by 256. Second byte is appended to previous operation result. Try other examples. I agree, the logic is not very simple and you should train yourself to completely understand the subject.

Johannes

I figured out what the error was, but from your description above I did not see at all that the appearance of some additional zeros depends on the caIndex. Here is a simple algo that works for me: Create new byte array Add 0x02 as tag Add 0x00 as placeholder for length if (keyIndex > 127 && keyIndex < 256 && caIndex < 256) Add 0x00 as transitional Add byte representation of keyIndex as Int without leading zeros if (caIndex < 256) Add 0x00 as separator Add byte representation of caIndex as Int without leading zeros Works for me so far. Anyways, without your blog I wouldn't have figured out how to encode the CA Version at all. Do you know by any chance how I could call CryptEncodeObject in the Crypt32.dll so that I don't have to do that manually? Thanks!

Vadims Podans

I'll try to provide you some PoC in next few days. BTW, CryptEncodeObject do not support CA version extension directly.


Post your comment:

Please, solve this little equation and enter result below. Captcha