node-opcua/node-opcua-crypto

View on GitHub
packages/node-opcua-crypto/source/subject.ts

Summary

Maintainability
A
35 mins
Test Coverage
// ---------------------------------------------------------------------------------------------------------------------
// node-opcua-crypto
// ---------------------------------------------------------------------------------------------------------------------
// Copyright (c) 2014-2022 - Etienne Rossignon - etienne.rossignon (at) gadz.org
// Copyright (c) 2022-2024 - Sterfive.com
// ---------------------------------------------------------------------------------------------------------------------
//
// This  project is licensed under the terms of the MIT license.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so,  subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
// Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ---------------------------------------------------------------------------------------------------------------------

export interface SubjectOptions {
    commonName?: string;
    organization?: string;
    organizationalUnit?: string;
    locality?: string;
    state?: string;
    country?: string;
    domainComponent?: string;
}

const _keys = {
    C: "country",
    CN: "commonName",
    DC: "domainComponent",
    L: "locality",
    O: "organization",
    OU: "organizationalUnit",
    ST: "state",
};

const enquoteIfNecessary = (str: string) => {
    str = str.replace(/"/g, "”");
    return str.match(/\/|=/) ? `"${str}"` : str;
};
const unquote = (str: string) => str.replace(/"/gm, "");
const unquote2 = (str?: string | undefined) => {
    if (!str) return str;
    const m = str.match(/^"(.*)"$/);
    return m ? m[1] : str;
};
/**
 * subjectName    The subject name to use for the Certificate.
 * If not specified the ApplicationName and/or domainNames are used to create a suitable default value.
 */
export class Subject implements SubjectOptions {
    public readonly commonName?: string;
    public readonly organization?: string;
    public readonly organizationalUnit?: string;
    public readonly locality?: string;
    public readonly state?: string;
    public readonly country?: string;
    public readonly domainComponent?: string;

    constructor(options: SubjectOptions | string) {
        if (typeof options === "string") {
            options = Subject.parse(options);
        }
        this.commonName = unquote2(options.commonName);
        this.organization = unquote2(options.organization);
        this.organizationalUnit = unquote2(options.organizationalUnit);
        this.locality = unquote2(options.locality);
        this.state = unquote2(options.state);
        this.country = unquote2(options.country);
        this.domainComponent = unquote2(options.domainComponent);
    }

    public static parse(str: string): SubjectOptions {
        const elements = str.split(/\/(?=[^/]*?=)/);
        const options: Record<string, unknown> = {};

        elements.forEach((element: string) => {
            if (element.length === 0) {
                return;
            }
            const s: string[] = element.split("=");

            if (s.length !== 2) {
                throw new Error("invalid format for " + element);
            }
            const longName = (_keys as Record<string, string>)[s[0]];
            if (!longName) {
                throw new Error("Invalid field found in subject name " + s[0]);
            }
            const value = s[1];
            options[longName] = unquote(Buffer.from(value, "ascii").toString("utf8"));
        });
        return options as SubjectOptions;
    }

    public toStringInternal(sep: string): string {
        // https://reference.opcfoundation.org/v104/GDS/docs/7.6.4/
        // The format of the subject name is a sequence of name value pairs separated by a ‘/’.
        // The name shall be one of ‘CN’, ‘O’, ‘OU’, ‘DC’, ‘L’, ‘S’ or ‘C’ and
        // shall be followed by a ‘=’ and then followed by the value.
        // The value may be any printable character except for ‘”’.
        // If the value contains a ‘/’ or a ‘=’ then it shall be enclosed in double quotes (‘”’).

        const tmp: string[] = [];
        if (this.country) {
            tmp.push("C=" + enquoteIfNecessary(this.country));
        }
        if (this.state) {
            tmp.push("ST=" + enquoteIfNecessary(this.state));
        }
        if (this.locality) {
            tmp.push("L=" + enquoteIfNecessary(this.locality));
        }
        if (this.organization) {
            tmp.push("O=" + enquoteIfNecessary(this.organization));
        }
        if (this.organizationalUnit) {
            tmp.push("OU=" + enquoteIfNecessary(this.organizationalUnit));
        }
        if (this.commonName) {
            tmp.push("CN=" + enquoteIfNecessary(this.commonName));
        }
        if (this.domainComponent) {
            tmp.push("DC=" + enquoteIfNecessary(this.domainComponent));
        }
        return tmp.join(sep);
    }
    public toStringForOPCUA(): string {
        return this.toStringInternal("/");
    }
    public toString(): string {
        // standard for SSL is to have a / in front of each Field
        // see https://www.digicert.com/kb/ssl-support/openssl-quick-reference-guide.htm
        const t = this.toStringForOPCUA();
        return t ? "/" + t : t;
    }
}