forge icon indicating copy to clipboard operation
forge copied to clipboard

support for x509 name constraints extension

Open joudinet opened this issue 3 years ago • 2 comments

According the rfc, one can (and should) define a name constraints for CA certificates, to indicate a name space within which all subject names in subsequent certificates in a certification path MUST be located.

I failed to find out how to define such extension with forge. Did I miss something or the support for this extension is currently missing in the forge library?

joudinet avatar Mar 03 '22 10:03 joudinet

It's currently not implemented. However, I ran into the same requirement and painstakingly made some code that implements it. The code I wrote only supports name constraints for IPs but I will look into implementing the rest of the section when I have the time. For the time being, you could modify the following code to fit your needs:

import {asn1, md, pki, util} from 'node-forge';
import * as forge from 'node-forge';

interface GenerateNameConstraintsInput {
    /**
     * Array of excluded IPs with their respective subnet mask (e.g. 1.1.1.1.255.255.255.255).
     */
    excluded?: Array<string>

    /**
     * Array of permitted IPs with their respective subnet mask (e.g. 1.1.1.1.255.255.255.255).
     */
    permitted?: Array<string>
}

/**
 * Convert a variable octet length string to bytes. e.g. `1.2.3.4.255.255.255.0`
 */
function ipv4ToBytes(ip: string): forge.Bytes {
    return ip.split('.').reduce((buffer, part) => {
        buffer.putByte(parseInt(part, 10))
        return buffer;
    }, util.createBuffer()).getBytes()
}

/**
 * Generate name constraints in conformance with
 * [RFC 5280 § 4.2.1.10](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10), limited to
 * IPs only.
 */
function generateNameConstraints(input: GenerateNameConstraintsInput): asn1.Asn1 {
    const ipsToSequence = (ips: Array<string>) => ips.map(permitted => {
        return asn1.create(
            asn1.Class.UNIVERSAL,
            asn1.Type.SEQUENCE,
            true,
            [asn1.create(asn1.Class.CONTEXT_SPECIFIC, 7, false, ipv4ToBytes(permitted))],
        )
    });

    const permittedAndExcluded: Array<asn1.Asn1> = []

    if (input.permitted !== undefined) {
        permittedAndExcluded.push(asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, ipsToSequence(input.permitted)))
    }

    if (input.excluded !== undefined) {
        permittedAndExcluded.push(asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, ipsToSequence(input.excluded)))
    }

    return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, permittedAndExcluded)
}

const cert = forge.pki.createCertificate();
cert.setExtensions([
    {
        id: '2.5.29.30', // https://oidref.com/2.5.29.30, https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10
        critical: true,
        value: generateNameConstraints({
            permitted: [`192.168.5.245.255.255.255.255`),
        }),
        name: 'nameConstraints',
    },
]);

A PR will follow.

MatthiasKunnen avatar Mar 04 '22 15:03 MatthiasKunnen

Thanks, using your code I manage to add DNS name restrictions to a root CA. Instead of asn1.create(asn1.Class.CONTEXT_SPECIFIC, 7, false, ipv4ToBytes(permitted)), I simply did: asn1.create(asn1.Class.CONTEXT_SPECIFIC, 2, false, util.encodeUtf8("example.com"))

joudinet avatar Mar 10 '22 10:03 joudinet